2.2. ZODB

La ZODB es una base de datos de objetos. Hace que sea muy fácil almacenar diferentes tipos de datos de contenido en un gráfico, lo que admite subclases (algo que SQL a menudo hace mal).

Dado que la base de datos almacena objetos y los objetos están definidos en código Python, siempre necesitará el código fuente Python correspondiente para crear instancias de los objetos almacenados dentro de ZODB. Esto puede parecer incómodo al principio, pero necesita tener MySQL ejecutándose para leer lo que hay dentro de los archivos MySQL almacenados en su disco, etc.

Advertencia

La base de datos ZODB no se puede utilizar sin el código fuente de Python utilizado para crear los datos. Los datos no se pueden leer con ninguna herramienta basada en SQL y existen pequeñas herramientas para manejar los datos sin procesar.

ZODB ofrece una base de datos orientada a objetos para Python que proporciona un alto grado de transparencia.

  • ✅ No hay lenguaje separado para las operaciones de base de datos

  • ✅ Muy poco impacto en su código para hacer objetos persistentes

  • ✅ Ningún mapeador de base de datos que oculte parcialmente la base de datos.

  • ✅ Utilizar un mapeo objeto-relacional no es como utilizar una base de datos orientada a objetos.

  • ✅ Casi ninguna costura entre el código y la base de datos.

  • ✅ Las relaciones entre objetos se gestionan de forma muy natural, lo que permite crear grafos de objetos complejos sin uniones.

../_images/zodb_logo.png

Figura 2.1, Logotipo de ZODB

ZODB es una base de datos transaccional ACID.

ZODB funciona con Python 3.7 y versiones superiores. También funciona con PyPy.

📌 Ventajas de ZODB:

  • ✅ Fácil de usar, sin necesidad de instalar bases de datos.

  • ✅ Útil para almacenar estructuras de datos complejas (listas, diccionarios, objetos).

📌 Desventajas:

  • ❌ No es ideal para grandes volúmenes de datos.

  • ❌ No permite consultas avanzadas como SQL.


2.2.1. ¿Qué es la ZODB?

ZODB es un sistema de persistencia para objetos Python. Los lenguajes de programación que escriben objetos automáticamente en el disco y los vuelven a leer cuando son requeridos por un programa en ejecución. Al instalar el ZODB, añades estas facilidades a Python.

Es ciertamente posible construir tu propio sistema para hacer persistentes los objetos Python. Los puntos de partida habituales son el módulo pickle, para convertir objetos en una representación de cadena, y varios módulos de bases de datos, como los módulos gdbm o bsddb, que proporcionan formas de escribir cadenas en el disco y leerlas de vuelta. Es sencillo combinar el módulo pickle y un módulo de base de datos para almacenar y recuperar objetos, y de hecho el módulo shelve, incluido en la biblioteca estándar de Python, lo hace.

El inconveniente es que el programador tiene que gestionar explícitamente los objetos, leyendo un objeto cuando se necesita y escribiéndolo en el disco cuando el objeto ya no es necesario. La ZODB gestiona los objetos por ti, manteniéndolos en una caché, escribiéndolos en disco cuando se modifican y eliminándolos de la caché si no se han utilizado durante un tiempo.


2.2.1.1. OODBs vs. BD relacionales

Otra forma de verlo es que ZODB es una base de datos orientada a objetos (OODB) específica de Python. Las bases de datos de objetos comerciales para C++ o Java a menudo exigen pasar por el aro, como utilizar un preprocesador especial o evitar determinados tipos de datos. Como veremos, el ZODB tiene que pasar por algunos obstáculos, pero en comparación, la naturalidad del ZODB es asombrosa.

Las bases de datos relacionales (RDB) son mucho más comunes que las OODB. Las bases de datos relacionales almacenan la información en tablas; una tabla consta de cualquier número de filas, cada una de las cuales contiene varias columnas de información. (Las filas se denominan más formalmente relaciones, de donde procede el término «base de datos relacional»).

Veamos un ejemplo concreto. El ejemplo procede de mi trabajo diario en la Bolsa de MEMS, en una versión muy simplificada. El trabajo consiste en hacer un seguimiento de los procesos, que son listas de pasos de fabricación que deben realizarse en una fábrica de semiconductores. Una ejecución pertenece a un usuario concreto, y tiene un nombre y un número de identificación asignados. Las ejecuciones constan de una serie de operaciones; una operación es un único paso a realizar, como depositar algo en una oblea o grabar algo en ella.

Las operaciones pueden tener parámetros, que son información adicional necesaria para realizar una operación. Por ejemplo, si vas a depositar algo en una oblea, necesitas saber dos cosas: 1) qué estás depositando, y 2) cuánto debe depositarse. Puede depositar 100 micras de óxido 2) de silicio o 1 micra de cobre.

El traslado de estas estructuras a una base de datos relacional es sencillo:

CREATE TABLE runs (
  int      run_id,
  varchar  owner,
  varchar  title,
  int      acct_num,
  primary key(run_id)
);

CREATE TABLE operations (
  int      run_id,
  int      step_num,
  varchar  process_id,
  PRIMARY KEY(run_id, step_num),
  FOREIGN KEY(run_id) REFERENCES runs(run_id),
);

CREATE TABLE parameters (
  int      run_id,
  int      step_num,
  varchar  param_name,
  varchar  param_value,
  PRIMARY KEY(run_id, step_num, param_name)
  FOREIGN KEY(run_id, step_num)
     REFERENCES operations(run_id, step_num),
);

En Python, escribiría tres clases llamadas Run, Operation, y Parameter. No presentaré código para definir estas clases, ya que ese código carece de interés en este momento. Cada clase contendría un único método para empezar, un método __init__() que asigna valores por defecto, como 0 o None, a cada atributo de la clase.

No es difícil escribir código Python que cree una instancia Run y la rellene con los datos de las tablas relacionales; con un poco más de esfuerzo, se puede construir una herramienta sencilla, normalmente llamada mapeador objeto-relacional, para hacerlo automáticamente.

(Véase https://legacy.python.org/workshops/1997-10/proceedings/shprentz.html para la implementación más exitosa de Joel Shprentz, el sistema de Shprentz se ha utilizado para trabajo real).

Sin embargo, es difícil hacer que un mapeador objeto-relacional sea razonablemente rápido; una implementación simplona como la mía es bastante lenta porque tiene que hacer varias consultas para acceder a todos los datos de un objeto. Los mapeadores objeto-relacionales de mayor rendimiento almacenan en caché los objetos para mejorar el rendimiento, y sólo realizan consultas SQL cuando realmente lo necesitan.

Eso ayuda si quieres acceder al número de ejecución 123 de repente. Pero, ¿qué ocurre si desea encontrar todas las ejecuciones en las que un paso tiene un parámetro denominado «grosor» con un valor de 2.0? En la versión relacional, tiene dos opciones poco atractivas:

  1. Escriba una consulta SQL especializada para este caso: SELECT run_id FROM operations WHERE param_name = 'thickness' AND param_value = 2.0

    Si este tipo de consultas son habituales, puede acabar teniendo muchas consultas especializadas. Cuando se reorganicen las tablas de la base de datos, habrá que modificar todas estas consultas.

  2. Un mapeador objeto-relacional no ayuda mucho. Escanear a través de las ejecuciones significa que el mapeador realizará las consultas SQL necesarias para leer la ejecución nº 1, y luego un simple bucle de Python puede comprobar si alguno de sus pasos tiene el parámetro que estás buscando. Repite para la carrera #2, 3, y así sucesivamente. Esto hace un gran número de consultas SQL, y por lo tanto es increíblemente lento.

Una base de datos de objetos como ZODB simplemente almacena punteros internos de objeto a objeto, por lo que leer un solo objeto es mucho más rápido que hacer un montón de consultas SQL y ensamblar los resultados. Por lo tanto, escanear todas las ejecuciones sigue siendo ineficiente, pero no extremadamente ineficiente.

2.2.1.2. ¿Qué es ZEO?

ZODB incluye varias clases diferentes que implementan la interfaz Storage. Tales clases manejan el trabajo de escribir objetos Python a un medio de almacenamiento físico, que puede ser un archivo de disco (la clase FileStorage), un archivo BerkeleyDB (BDBFullStorage), una base de datos relacional (DCOracleStorage), o algún otro medio. ZEO añade ClientStorage, una nueva Storage que no escribe en soportes físicos, sino que simplemente reenvía todas las peticiones a través de una red a un servidor.

El servidor, que está ejecutando una instancia de la clase StorageServer, simplemente actúa como un front-end para alguna clase física Storage. Es una idea bastante simple, pero como veremos más adelante en este documento, abre muchas posibilidades.


2.2.2. Instalación

Para conectarte hacia una ZODB necesita la librería ZODB. Esto significa que debe instalar ZODB ejecutando el siguiente comando correspondiente a cada sistema operativo, el cual se presentan a continuación:

pip3 install ZODB==6.0

Puede probar si la instalación se realizo correctamente, ejecutando el siguiente comando correspondiente a tu sistema operativo:

python3 -c "import ZODB ; print(ZODB.__package__)"

Si muestra el nombre del paquete ZODB en la terminal, tiene correctamente instalada la librería. Con esto, ya tiene todo listo para continuar.

Adicionalmente puedes instalar un cliente de base de datos ZODB, a continuación se presentan alternativas:

2.2.2.1. ZODB browser

El ZODB browser le permite inspeccionar objetos persistentes almacenados en ZODB, ver sus atributos y los cambios históricos realizados en ellos.

Es un paquete de herramientas de línea de comandos para administrar archivos de base de datos ZODB, incluido el programa zodbbrowser.exe para la shell de línea de comandos.

Para instalar el administrador de base de datos ZODB ZODB browser de forma nativa para sistemas operativos Linux y Windows, a continuación se presentan los modos de instalación:

pip install zodbbrowser==0.17.1

Puede probar si la instalación se realizo correctamente, ejecutando el siguiente comando:

Puede probar si la instalación se realizo correctamente, ejecutando el siguiente comando:

zodbbrowser --help

Si ejecuto el comando anterior, este da como resultado lo siguiente:

Usage: zodbbrowser [options] [FILENAME | --zeo ADDRESS]

Open a ZODB database and start a web-based browser app.

Options:
  -h, --help        show this help message and exit
  --zeo=ADDRESS     connect to ZEO server instead (host:port or socket name)
  --storage=NAME    connect to given ZEO storage
  --listen=ADDRESS  specify port (or host:port) to listen on
  -q, --quiet       be quiet
  --debug           enable debug logging
  --rw              open the database read-write (default: read-only)

Si tiene disponibles el comando zodbbrowser, tiene correctamente instalado el cliente de base de datos gráfico nativo de ZODB.

Nota

Más información consulte https://pypi.org/project/zodbbrowser/

Ejecute el comando zodbrowser especificando un nombre de archivo ZODB, ejecutando el siguiente comando:

zodbbrowser /ruta/al/archivo/Data.fs

El comando anterior muestra el siguiente mensaje:

Listening on http://localhost:8070/

Abra http://localhost:8070 en un navegador web. Tenga en cuenta que no hay controles de acceso; todos los demás usuarios de la máquina local podrán acceder al contenido de la base de datos.

Al abrir la dirección anterior debería mostrar la interfaz gráfica de zodbbrowser, como se muestra a continuación:

../_images/zodbbrowser.png

Figura 2.2, ZODB browser

Si muestra la interfaz gráfica de zodbbrowser, tiene correctamente instalado el cliente de base de datos gráfico nativo de ZODB.


2.2.3. Cadenas de conexión

Para definir el método connect debe definir las cadenas de conexión con ZODB como se describe a continuación:

DB_PATH

Ruta absoluta o relativa del archivo de base de datos ZODB.

DB_FILE

Nombre del archivo de base de datos ZODB.

A continuación presento un ejemplo en Python implementando una cadena de conexión para una base de datos ZODB:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
import os
import persistent
import transaction
import ZODB, ZODB.FileStorage
from pathlib import Path

DB_PATH = os.path.dirname(os.path.abspath(__file__)) + os.sep + "filestorage/"
Path(DB_PATH).mkdir(parents=True, exist_ok=True)
DB_FILE = ZODB.FileStorage.FileStorage(DB_PATH + "Data.fs")
DB = ZODB.DB(DB_FILE)

connection = DB.open()

El ejemplo anterior se describe a continuación:

  • En la línea 1, se importa el módulo os de la librería estándar de Python.

  • En la línea 2, se importa el módulo ZODB.

  • En la línea 4, se define en la constante DB_PATH la ruta absoluta usada para guardar la base de datos.

  • En la línea 5, se define en la constante DB_FILE el nombre de la base de datos.

  • En la línea 6, se define en la constante DB la ruta completa usada para leer la base de datos.

De esta forma se crea una cadena de conexión para ZODB para ser usada por el método open.


2.2.4. Insertar registros

Si requiere insertar registro en un nodo, a continuación tiene un ejemplo:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
"""Módulo de Clases"""

import persistent


class Producto(persistent.Persistent):
    """Clase Producto"""

    def __init__(self, id, descripcion):
        """Método constructor de clase de Producto

        Args:
            id (int): ID del producto
            descripcion (str): Descripción del producto
        """
        self.id = id
        self.descripcion = descripcion

    def __str__(self):
        """Método de representación de informal del objeto,
        usado para crear la salida que se le mostrará al usuario"""
        return f"({self.__class__.__name__}) Id: {self.id}, Descripción: {self.descripcion}."

    def __repr__(self):
        """Método de representación de formal del objeto,
        usado para depuración y desarrollo"""
        return f"<{self.__class__.__name__}:(id={repr(self.id)}, descripcion={repr(self.descripcion)})>"

El módulo classes.py anterior, muestra las clases de tipo persistent.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
"""Programa para la inserción de registro(s) de la ZODB"""

import logging
import os
import transaction
import ZODB, ZODB.FileStorage
from ZODB.POSException import StorageError
from pathlib import Path
from classes import Producto

logging.basicConfig(level=logging.INFO)

DB_PATH = os.path.dirname(os.path.abspath(__file__)) + os.sep + "filestorage/"
Path(DB_PATH).mkdir(parents=True, exist_ok=True)
DB_FILE = ZODB.FileStorage.FileStorage(DB_PATH + "Data.fs")
DB = ZODB.DB(DB_FILE)


def insertar_registro():
    """Función para la inserción de registro(s) de la ZODB"""
    conexion = None
    try:
        # Crear la instancia de DB y pasar el nombre del archivo
        conexion = DB.open()
        # Crear la instancia de conexion y llamar al método open de DB
        nodo = conexion.root()
        logging.info(
            f"✅ ¡Conectado a la base de datos '{os.path.basename(DB_FILE.getName())}!'\n"
        )
        # Crear una instancia de la clase Producto
        producto1 = Producto(1, "Carro")
        nodo["producto1"] = producto1
        # Crear una instancia de la clase Producto
        producto2 = Producto(2, "Moto")
        nodo["producto2"] = producto2
        # Crear una instancia de la clase Producto
        producto3 = Producto(3, "Bicicleta")
        nodo["producto3"] = producto3
        # Crear una lista de instancia de objetos Producto
        nodo["productos"] = [producto1, producto2, producto3]
        # Confirmar la inserción del registro
        transaction.commit()
        logging.info(
            "✅ ¡Fueron insertado(s) los registro(s) correctamente en la ZODB!\n"
        )
    except StorageError as error:
        logging.error(f"❌ ¡Fallo la inserción de registro(s) en la ZODB!: {error}")
    finally:
        if conexion:
            # Cerrar la conexión a la base de datos
            conexion.close()
            logging.info(
                f"✅ ¡La conexión ZODB a la base de datos '{os.path.basename(DB_FILE.getName())}' fue cerrada!"
            )


if __name__ == "__main__":
    insertar_registro()

El módulo zodb_record_insert.py anterior, muestra el script principal de ejecución del programa.


Importante

Usted puede descargar el código usado en esta sección haciendo clic en los siguientes enlaces:

Truco

Para ejecutar el código zodb_record_insert.py abra una consola de comando, acceda al directorio donde se encuentra el programa:

proyectos/
└── zodb/
    ├── classes.py
    └── zodb_record_insert.py

Si tiene la estructura de archivo previa, entonces ejecute el siguiente comando:

python3 zodb_record_insert.py

El anterior código al ejecutar debe mostrar el siguiente mensaje:

INFO:root:✅ ¡Conectado a la base de datos 'Data.fs!'

INFO:root:✅ ¡Fueron insertado(s) los registro(s) correctamente en la ZODB!

INFO:root:✅ ¡La conexión ZODB a la base de datos 'Data.fs' fue cerrada!

2.2.5. Consultar registros

Si requiere consultar registros de un nodo, a continuación tiene un ejemplo:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
"""Módulo de Clases"""

import persistent


class Producto(persistent.Persistent):
    """Clase Producto"""

    def __init__(self, id, descripcion):
        """Método constructor de clase de Producto

        Args:
            id (int): ID del producto
            descripcion (str): Descripción del producto
        """
        self.id = id
        self.descripcion = descripcion

    def __str__(self):
        """Método de representación de informal del objeto,
        usado para crear la salida que se le mostrará al usuario"""
        return f"({self.__class__.__name__}) Id: {self.id}, Descripción: {self.descripcion}."

    def __repr__(self):
        """Método de representación de formal del objeto,
        usado para depuración y desarrollo"""
        return f"<{self.__class__.__name__}:(id={repr(self.id)}, descripcion={repr(self.descripcion)})>"

El módulo classes.py anterior, muestra las clases de tipo persistent.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
"""Programa para la buscar de registro(s) de la ZODB"""

import logging
import os
import ZODB, ZODB.FileStorage
from ZODB.POSException import StorageError
from pathlib import Path
from classes import Producto

logging.basicConfig(level=logging.INFO)

DB_PATH = os.path.dirname(os.path.abspath(__file__)) + os.sep + "filestorage/"
Path(DB_PATH).mkdir(parents=True, exist_ok=True)
DB_FILE = ZODB.FileStorage.FileStorage(DB_PATH + "Data.fs")
DB = ZODB.DB(DB_FILE)


def consultar_registro():
    """Función para la consulta de registro(s) de la ZODB"""
    conexion = None
    try:
        # Crear la instancia de DB y pasar el nombre del archivo
        conexion = DB.open()
        # Crear la instancia de conexion y llamar al método open de DB
        nodo = conexion.root()
        logging.info(
            f"✅ ¡Conectado a la base de datos '{os.path.basename(DB_FILE.getName())}!'\n"
        )
        # Mostrar los nodos de la DB
        print("📜 Lista de registros: ")
        # Mostrar los elementos de nodos
        for clave, valor in nodo.items():
            print(f"'{clave}', Valor: {valor}")
        # Mostrar los valores de nodos
        print("\n📜 Detalles del nodo 'producto1': ")
        # Verificar si existe el nodo 'producto1' y mostrarlo
        if "producto1" in nodo:
            producto = nodo["producto1"]
            # Mostrar detalle de nodo 'producto1'
            print(f"Nodo: {producto}\n")
        else:
            print("\n❌ ¡No existe el registro 'producto1' en la ZODB!")
    except StorageError as error:
        logging.error(f"❌ ¡Fallo la consulta de registro(s) en la ZODB!: {error}")
    finally:
        if conexion:
            # Cerrar la conexión a la base de datos
            conexion.close()
            logging.info(
                f"✅ ¡La conexión ZODB a la base de datos '{os.path.basename(DB_FILE.getName())}' fue cerrada!"
            )


if __name__ == "__main__":
    consultar_registro()

El módulo zodb_record_read.py anterior, muestra el script principal de ejecución del programa.


Importante

Usted puede descargar el código usado en esta sección haciendo clic en los siguientes enlaces:

Truco

Para ejecutar el código zodb_record_read.py abra una consola de comando, acceda al directorio donde se encuentra el programa:

proyectos/
└── zodb/
    ├── classes.py
    └── zodb_record_read.py

Si tiene la estructura de archivo previa, entonces ejecute el siguiente comando:

python3 zodb_record_read.py

El anterior código al ejecutar debe mostrar el siguiente mensaje:

INFO:root:✅ ¡Conectado a la base de datos 'Data.fs!'

📜 Lista de registros:
'producto1', Valor: (Producto) Id: 1, Descripción: Carro.
'producto2', Valor: (Producto) Id: 2, Descripción: Moto.
'producto3', Valor: (Producto) Id: 3, Descripción: Bicicleta.
'productos', Valor: [<Producto:(id=1, descripcion='Carro')>, <Producto:(id=2, descripcion='Moto')>, <Producto:(id=3, descripcion='Bicicleta')>]

📜 Detalles del nodo 'producto1':
Nodo: (Producto) Id: 1, Descripción: Carro.

INFO:root:✅ ¡La conexión ZODB a la base de datos 'Data.fs' fue cerrada!

2.2.6. Actualizar registros

Si requiere actualizar un nodo, a continuación tiene un ejemplo:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
"""Módulo de Clases"""

import persistent


class Producto(persistent.Persistent):
    """Clase Producto"""

    def __init__(self, id, descripcion):
        """Método constructor de clase de Producto

        Args:
            id (int): ID del producto
            descripcion (str): Descripción del producto
        """
        self.id = id
        self.descripcion = descripcion

    def __str__(self):
        """Método de representación de informal del objeto,
        usado para crear la salida que se le mostrará al usuario"""
        return f"({self.__class__.__name__}) Id: {self.id}, Descripción: {self.descripcion}."

    def __repr__(self):
        """Método de representación de formal del objeto,
        usado para depuración y desarrollo"""
        return f"<{self.__class__.__name__}:(id={repr(self.id)}, descripcion={repr(self.descripcion)})>"

El módulo classes.py anterior, muestra las clases de tipo persistent.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
"""Programa para la actualización de registro(s) en la ZODB"""

import logging
import os
import transaction
import ZODB, ZODB.FileStorage
from ZODB.POSException import StorageError
from pathlib import Path
from classes import Producto

logging.basicConfig(level=logging.INFO)

DB_PATH = os.path.dirname(os.path.abspath(__file__)) + os.sep + "filestorage/"
Path(DB_PATH).mkdir(parents=True, exist_ok=True)
DB_FILE = ZODB.FileStorage.FileStorage(DB_PATH + "Data.fs")
DB = ZODB.DB(DB_FILE)


def actualizar_registro():
    """Función para la actualización de registro de la tabla"""
    conexion = None
    try:
        # Crear la instancia de DB y pasar el nombre del archivo
        conexion = DB.open()
        # Crear la instancia de conexion y llamar al método open de db
        nodo = conexion.root()
        logging.info(
            f"✅ ¡Conectado a la base de datos '{os.path.basename(DB_FILE.getName())}!'\n"
        )
        # Mostrar nodo "producto1"
        print(f"{nodo['producto1']}")
        # Actualizar el nodo 'producto1'
        nodo["producto1"].descripcion = "Vehiculo"
        print(f"\tDescripción nueva: {nodo['producto1'].descripcion}")
        # Mostrar nodo "producto2"
        print(f"{nodo['producto2']}")
        # Actualizar el nodo 'producto2'
        nodo["producto2"].descripcion = "Motocicleta"
        print(f"\tDescripción nueva: {nodo['producto2'].descripcion}")
        # Mostrar nodo "producto3"
        print(f"{nodo['producto3']}")
        # Actualizar el nodo 'producto3'
        nodo["producto3"].descripcion = "Bici"
        print(f"\tDescripción nueva: {nodo['producto3'].descripcion}\n")
        # Guardar los cambios en la base de datos
        transaction.commit()
        logging.info(f"✅ ¡Fueron actualizados los nodos correctamente!")
    except StorageError as error:
        logging.error(f"❌ ¡Fallo la actualización de registro(s) en la ZODB!: {error}")
    finally:
        if conexion:
            # Cerrar la conexión a la base de datos
            conexion.close()
            logging.info(
                f"✅ ¡La conexión ZODB a la base de datos '{os.path.basename(DB_FILE.getName())}' fue cerrada!"
            )


if __name__ == "__main__":
    actualizar_registro()

El módulo zodb_record_update.py anterior, muestra el script principal de ejecución del programa.


Importante

Usted puede descargar el código usado en esta sección haciendo clic en los siguientes enlaces:

Truco

Para ejecutar el código zodb_record_update.py abra una consola de comando, acceda al directorio donde se encuentra el programa:

proyectos/
└── zodb/
    ├── classes.py
    └── zodb_record_update.py

Si tiene la estructura de archivo previa, entonces ejecute el siguiente comando:

python3 zodb_record_update.py

El anterior código al ejecutar debe mostrar el siguiente mensaje:

INFO:root:✅ ¡Conectado a la base de datos 'Data.fs!'

(Producto) Id: 1, Descripción: Carro.
        Descripción nueva: Vehiculo
(Producto) Id: 2, Descripción: Moto.
        Descripción nueva: Motocicleta
(Producto) Id: 3, Descripción: Bicicleta.
        Descripción nueva: Bici

INFO:root:✅ ¡Fueron actualizados los nodos correctamente!
INFO:root:✅ ¡La conexión ZODB a la base de datos 'Data.fs' fue cerrada!

2.2.7. Eliminar registros

Si requiere eliminar un nodo, a continuación tiene un ejemplo:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
"""Módulo de Clases"""

import persistent


class Producto(persistent.Persistent):
    """Clase Producto"""

    def __init__(self, id, descripcion):
        """Método constructor de clase de Producto

        Args:
            id (int): ID del producto
            descripcion (str): Descripción del producto
        """
        self.id = id
        self.descripcion = descripcion

    def __str__(self):
        """Método de representación de informal del objeto,
        usado para crear la salida que se le mostrará al usuario"""
        return f"({self.__class__.__name__}) Id: {self.id}, Descripción: {self.descripcion}."

    def __repr__(self):
        """Método de representación de formal del objeto,
        usado para depuración y desarrollo"""
        return f"<{self.__class__.__name__}:(id={repr(self.id)}, descripcion={repr(self.descripcion)})>"

El módulo classes.py anterior, muestra las clases de tipo persistent.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
"""Programa para la eliminación de registro(s) en la ZODB"""

import logging
import os
import transaction
import ZODB, ZODB.FileStorage
from ZODB.POSException import StorageError
from pathlib import Path
from classes import Producto

logging.basicConfig(level=logging.INFO)

DB_PATH = os.path.dirname(os.path.abspath(__file__)) + os.sep + "filestorage/"
Path(DB_PATH).mkdir(parents=True, exist_ok=True)
DB_FILE = ZODB.FileStorage.FileStorage(DB_PATH + "Data.fs")
DB = ZODB.DB(DB_FILE)


def eliminar_registro():
    """Función para la eliminación de registro de la ZODB"""

    conexion = None
    try:
        # Crear la instancia de DB y pasar el nombre del archivo
        conexion = DB.open()
        # Crear la instancia de conexion y llamar al método open de db
        nodo = conexion.root()
        logging.info(
            f"✅ ¡Conectado a la base de datos '{os.path.basename(DB_FILE.getName())}!'\n"
        )
        # Mostrar el objeto a eliminar
        print(f"📜 Descripción del nodo: {nodo['producto1'].descripcion}\n")
        # Eliminar el objeto 'producto1'
        if "producto1" in nodo:
            # Eliminar el objeto de la raíz
            nodo.pop("producto1")
            # Guardar los cambios en la base de datos
            transaction.commit()
            logging.info("✅ ¡Registro eliminado correctamente!")
        else:
            logging.info("❌ ¡No tiene ningún registro en la ZODB!\n")
    except StorageError as error:
        logging.error(f"❌ ¡Fallo la eliminación de registro(s) en la ZODB!: {error}")
    finally:
        if conexion:
            # Cerrar la conexión a la base de datos
            conexion.close()
            logging.info(
                f"✅ ¡La conexión ZODB a la base de datos '{os.path.basename(DB_FILE.getName())}' fue cerrada!"
            )


if __name__ == "__main__":
    eliminar_registro()

El módulo zodb_record_delete.py anterior, muestra el script principal de ejecución del programa.


Importante

Usted puede descargar el código usado en esta sección haciendo clic en los siguientes enlaces:

Truco

Para ejecutar el código zodb_record_delete.py abra una consola de comando, acceda al directorio donde se encuentra el programa:

proyectos/
└── zodb/
    ├── classes.py
    └── zodb_record_delete.py

Si tiene la estructura de archivo previa, entonces ejecute el siguiente comando:

python3 zodb_record_delete.py

El anterior código al ejecutar debe mostrar el siguiente mensaje:

INFO:root:✅ ¡Conectado a la base de datos 'Data.fs!'

📜 Descripción del nodo: Vehiculo

INFO:root:✅ ¡Registro eliminado correctamente!
INFO:root:✅ ¡La conexión ZODB a la base de datos 'Data.fs' fue cerrada!

Así de esta forma puede ingresar, consultar, actualizar y eliminar registro en una tabla en una base de datos ZODB.


2.2.8. Práctica - Caso real

A continuación se presenta una práctica más real de implementar el uso de proyectos con ZODB, a continuación la estructura de proyecto llamado sistema:

proyectos/
└── zodb/
    └── sistema/
        ├── classes/
        │   ├── __init__.py
        │   ├── cliente.py
        │   └── producto.py
        ├── __init__.py
        ├── main.py
        ├── requirements.txt
        └── settings.py

A continuación se presenta y explica el uso de cada archivo para este proyecto:

Archivo producto.py

Módulo producto.py, muestra la clases de tipo persistent.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
"""Módulo de clase Producto"""

import persistent


class Producto(persistent.Persistent):
    """Clase Producto"""

    def __init__(self, id, descripcion):
        """Método constructor de clase de Producto

        Args:
            id (int): ID del producto
            descripcion (str): Descripción del producto
        """
        self.id = id
        self.descripcion = descripcion

    def __str__(self):
        """Método de representación de informal del objeto,
        usado para crear la salida que se le mostrará al usuario"""
        return f"({self.__class__.__name__}) Id: {self.id}, Descripción: {self.descripcion}."

    def __repr__(self):
        """Método de representación de formal del objeto,
        usado para depuración y desarrollo"""
        return f"<{self.__class__.__name__}:(id={repr(self.id)}, descripcion={repr(self.descripcion)})>"

Archivo main.py

Módulo de principal del programa.

  1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
"""Programa para operaciones CRUD de registros de productos de un Inventario con ZODB"""

import logging
import transaction
from settings import (
    DB,
    DB_FILE,
    DB_FILE_NAME,
    INSERT_MULTIPLE_COLUMNS,
    UPDATE_MULTIPLE_COLUMNS,
    DELETE_MULTIPLE_COLUMNS,
)
from classes.producto import Producto
from ZODB.POSException import StorageError

logging.basicConfig(level=logging.INFO)


def insertar_registro(conn, nodo_raiz, registros):
    """Función para la inserción dek nodo de la base de datos

    Args:
        conn (Connection): Representación conexión a la base de datos ZODB
        nodo_raiz (list): Nodo raiz de la base de datos
        registros (list): Lista de filas a ingresar
    """
    try:
        if "productos" not in nodo_raiz:
            nodo_raiz["productos"] = []
        for registro in registros:
            producto = Producto(registro[0], registro[1])
            nodo_raiz["productos"].append(producto)
        # Guardar los cambios en la base de datos
        transaction.commit()
        # print(nodo_raiz["productos"])
        logging.info(
            f"✅ ¡Fueron insertado(s) {len(registros)} registro(s) correctamente en la ZODB!\n"
        )
    except StorageError as error:
        logging.error(f"❌ ¡Fallo la inserción de registro(s) en la ZODB!: {error}")


def consultar_registro(conn, nodo_raiz):
    """Muestra todos los productos de la base de datos

    Args:
        conn (Connection): Representación conexión a la base de datos ZODB
        nodo_raiz (list): Nodo raiz de la base de datos
    """
    # Mostrar los nodos de la DB
    print("📜 Lista de registros:\n")
    # Mostrar los elementos de nodos
    cantidad_nodo = 0
    # Si es el nodo 'productos' existe
    if "productos" in nodo_raiz and nodo_raiz["productos"]:
        for i, producto in enumerate(nodo_raiz["productos"]):
            cantidad_nodo = i + 1
            print(f"  Producto {i + 1}:")
            print(f"    ID: {producto.id}")
            print(f"    Descripción: {producto.descripcion}")
    else:
        print("❌ No hay productos registrados en el Inventario")
    print(f"\n📜 Total de producto(s) en Inventario: {cantidad_nodo}.\n")
    logging.info(
        f"✅ ¡Fueron consultados {cantidad_nodo} registro(s) correctamente en la ZODB!\n"
    )


def actualizar_registro(conn, nodo_raiz, registros):
    """Función para la actualización dek nodo de la base de datos

    Args:
        conn (Connection): Representación conexión a la base de datos ZODB
        nodo_raiz (list): Nodo raiz de la base de datos
        registros (list): Lista de filas a actualizar
    """
    try:
        actualizados = 0
        for i, registro in enumerate(registros):
            if (
                i < len(nodo_raiz["productos"])
                and registro[0] == nodo_raiz["productos"][i].id
            ):
                descripcion_anterior = nodo_raiz["productos"][i].descripcion
                nodo_raiz["productos"][i].descripcion = registro[1]
                actualizados += 1
                print(
                    f"📜 El producto '{descripcion_anterior}' fue actualizado con '{registro[1]}'.\n"
                )
        # Guardar los cambios en la base de datos
        transaction.commit()
        # print(f"{nodo_raiz['productos']}\n")
        logging.info(
            f"✅ ¡Fueron actualizados {actualizados} registro(s) correctamente en la ZODB!\n"
        )
    except StorageError as error:
        logging.error(f"❌ ¡Fallo la actualización de registro(s) en la ZODB!: {error}")


def eliminar_registro(conn, nodo_raiz, registros):
    """Función para la eliminación dek nodo de la base de datos

    Args:
        conn (Connection): Representación conexión a la base de datos ZODB
        nodo_raiz (list): Nodo raiz de la base de datos
        registros (list): Lista de filas a eliminar
    """
    try:
        eliminados = []
        # Ordenar en reversa para eliminar desde el final
        for id_eliminar in sorted(registros, reverse=True):
            if "productos" in nodo_raiz and 0 <= id_eliminar - 1 < len(
                nodo_raiz["productos"]
            ):
                producto_eliminado = nodo_raiz["productos"][id_eliminar - 1].descripcion
                eliminados.append(nodo_raiz["productos"].pop(id_eliminar - 1))
                print(
                    f"📜 El producto '{producto_eliminado}' fue eliminado correctamente.\n"
                )

        # Guardar los cambios en la base de datos
        transaction.commit()
        logging.info(
            f"✅ ¡Fueron eliminados {len(eliminados)} registro(s) correctamente en la ZODB!\n"
        )
    except StorageError as error:
        logging.error(f"❌ ¡Fallo la eliminación de registro(s) en la ZODB!: {error}")


if __name__ == "__main__":
    conexion = None
    try:
        conexion = DB.open()
        nodo_principal = conexion.root()
        logging.info(f"✅ ¡Conectado a la base de datos '{DB_FILE_NAME}!'\n")
        # import pdb; pdb.set_trace()
        insertar_registro(conexion, nodo_principal, INSERT_MULTIPLE_COLUMNS)
        consultar_registro(conexion, nodo_principal)
        actualizar_registro(conexion, nodo_principal, UPDATE_MULTIPLE_COLUMNS)
        eliminar_registro(conexion, nodo_principal, DELETE_MULTIPLE_COLUMNS)
    except StorageError as e:
        logging.error(
            f"❌ ERROR: ¡Se produjo un falla al establecer la conexión a la base de datos '{DB_FILE_NAME}': '{e}'!"
        )
    finally:
        if conexion:
            # Cerrar la conexión a la base de datos
            conexion.close()
            logging.info(
                f"✅ ¡La conexión ZODB a la base de datos '{DB_FILE_NAME}' fue cerrada!"
            )

Archivo settings.py

Módulo de configuraciones del programa.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
"""Módulo de configuraciones"""

import os
from pathlib import Path
import ZODB, ZODB.FileStorage

DB_PATH = os.path.dirname(os.path.abspath(__file__)) + os.sep + "filestorage/"
Path(DB_PATH).mkdir(parents=True, exist_ok=True)
DB_FILE = ZODB.FileStorage.FileStorage(DB_PATH + "inventario.fs")
DB_FILE_NAME = os.path.basename(DB_FILE.getName())
DB = ZODB.DB(DB_FILE)

# Lista de nodos a ingresar
INSERT_MULTIPLE_COLUMNS = [
    (1, "Carro"),
    (2, "Bici"),
    (3, "Motocicleta"),
]

# Lista de nodos a actualizar
UPDATE_MULTIPLE_COLUMNS = [
    (1, "Vehiculo"),
    (2, "Bicicleta"),
]

# Lista de nodos a eliminar
DELETE_MULTIPLE_COLUMNS = [1, 2]

Archivo inventario.fs

Archivo de base de datos de ZODB llamado inventario.fs la cual no se incluye ya que cada vez que se inicia el programa main.py se elimina y crea nuevamente, para cuidar la creación de los datos iniciales.

Archivo requirements.txt

Archivo de requirements.txt de la herramienta de gestión de paquetes pip.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
# Install the packages from PyPi repository.
BTrees==6.1
ZConfig==4.2
ZODB==6.0
persistent==6.1
transaction==5.0
zc.lockfile==3.0.post1
zodbpickle==4.2
zope.deferredimport==5.0
zope.interface==7.2
zope.proxy==6.1
zodbbrowser==0.17.1

Teniendo creada la anterior estructura de proyecto, vuelva a ejecutar ahora el Módulo con el siguiente comando, el cual a continuación se presentan el correspondiente comando de tu sistema operativo:

Antes de ejecutar debes instalar sus dependencias, con el siguiente comando:

pip3 install -r requirements.txt

Truco

Para ejecutar el código fuente de esta práctica debe invocar al módulo main.py, abra una consola de comando, acceda al directorio donde se encuentra la estructura previa y ejecute el siguiente comando:

python3 main.py

El anterior código al ejecutar debe mostrar el siguiente mensaje:

INFO:root:✅ ¡Conectado a la base de datos 'inventario.fs!'

INFO:root:✅ ¡Fueron insertado(s) 3 registro(s) correctamente en la ZODB!

📜 Lista de registros:

Producto 1:
    ID: 1
    Descripción: Carro
Producto 2:
    ID: 2
    Descripción: Bici
Producto 3:
    ID: 3
    Descripción: Motocicleta

📜 Total de producto(s) en Inventario: 3.

INFO:root:✅ ¡Fueron consultados 3 registro(s) correctamente en la ZODB!

📜 El producto 'Carro' fue actualizado con 'Vehiculo'.

📜 El producto 'Bici' fue actualizado con 'Bicicleta'.

INFO:root:✅ ¡Fueron actualizados 2 registro(s) correctamente en la ZODB!

📜 El producto 'Bicicleta' fue eliminado correctamente.

📜 El producto 'Vehiculo' fue eliminado correctamente.

INFO:root:✅ ¡Fueron eliminados 2 registro(s) correctamente en la ZODB!

INFO:root:✅ ¡La conexión ZODB a la base de datos 'inventario.fs' fue cerrada!

La ejecución anterior generar la siguiente estructura:

proyectos/
└── zodb/
    └── sistema/
        ├── classes/
        │   ├── __init__.py
        │   ├── cliente.py
        │   └── producto.py
        ├── filestorage/
        │   ├── inventario.fs
        │   ├── inventario.fs.index
        │   ├── inventario.fs.lock
        │   └── inventario.fs.tmp
        ├── __init__.py
        ├── main.py
        ├── requirements.txt
        └── settings.py

Así de esta forma puede ingresar, consultar, actualizar y eliminar registro en un archivo serializado de objetos python ZODB.


Ver también

Consulte la sección de lecturas suplementarias del entrenamiento para ampliar su conocimiento en esta temática.


¿Cómo puedo ayudar?

¡Mi soporte está aquí para ayudar!

Mi horario de oficina es de lunes a sábado, de 9 AM a 5 PM. UTM - Madrid, España.

La hora aquí es actualmente 7:35 PM UTM.

Mi objetivo es responder a todos los mensajes dentro de un día hábil.

Contrata mi increíble soporte profesional