Zodb - Object database

онлайн тренажер по питону
Online Python Trainer for Beginners

Learn Python easily without overwhelming theory. Solve practical tasks with automatic checking, get hints in Russian, and write code directly in your browser — no installation required.

Start Course

Introduction to ZODB: Python Object Database

Most modern databases force you to serialize Python objects into strings, tables, or JSON before they can be saved. This adds architectural overhead and extra code. ZODB — the Zope Object Database — eliminates this problem by allowing you to store Python objects “as‑is” directly.

ZODB (Zope Object Database) is a fully transparent, native object database written in pure Python. It is schema‑free, SQL‑free, provides ACID‑compliant transactions, and lets you work with objects exactly as you would with regular variables.

History and Origin of ZODB

ZODB was created in the 1990s as part of the Zope project to serve the Zope web framework. The library was designed from the start to persist Python objects without mapping them to relational structures. Over more than 25 years of continuous development, ZODB has proven itself as a reliable, production‑ready solution for Python object persistence.

Key Benefits of ZODB

ZODB delivers several unique advantages compared with traditional relational databases:

Transparent Object Persistence

Objects are automatically saved and retrieved without writing custom serialization code.

Schema‑Free Architecture

ZODB requires no predefined schema, allowing object structures to evolve dynamically.

Full ACID Transaction Support

Provides reliable, atomic transactions that guarantee data consistency.

Automatic Memory Management

ZODB lazily loads and unloads objects, making it suitable for large data sets.

Installation and Configuration

Basic Installation

Install the core package with pip:

pip install ZODB

Extended Installation with Extras

For additional features, install the package with the persistent extra:

pip install ZODB[persistent]

Optional Packages

Multi‑user support via ZEO:

pip install ZEO

Efficient BTree data structures:

pip install BTrees

Architecture Overview

Core Components

ZODB is built from several essential parts:

  • Storage — low‑level data stores such as FileStorage, DemoStorage, etc.
  • DB — the main database interface.
  • Connection — object‑level connection manager.
  • Transaction — module that handles transaction lifecycle.
  • Persistent — base class for all persistable objects.

How an Object Database Works

ZODB follows these core principles:

  1. Object serialization: uses a customized pickle implementation.
  2. Change tracking: automatically monitors attribute modifications.
  3. Lazy loading: objects are loaded only when accessed.
  4. Reference management: maintains links between related objects.

Creating and Using Persistent Objects

Base Class Persistent

All objects that need to be stored must inherit from Persistent:

from persistent import Persistent
import datetime

class Person(Persistent):
    def __init__(self, name, age=None):
        self.name = name
        self.age = age
        self.created_at = datetime.datetime.now()
    
    def set_age(self, age):
        self.age = age
        self._p_changed = True  # Explicitly mark the object as changed

Composite Objects

ZODB seamlessly handles nested objects:

class Address(Persistent):
    def __init__(self, city, street, house):
        self.city = city
        self.street = street
        self.house = house

class Employee(Persistent):
    def __init__(self, name, position):
        self.name = name
        self.position = position
        self.address = None
        self.projects = []

Working with the Database

Creating a Database and Opening a Connection

from ZODB import FileStorage, DB
import transaction

# Create a file‑based storage
storage = FileStorage.FileStorage('mydata.fs')
db = DB(storage)

# Open a connection
conn = db.open()
root = conn.root()

Writing and Reading Data

Persisting and retrieving objects is straightforward:

# Create an object
person = Person("Ivan", 30)

# Store it in the root mapping
root['person'] = person
transaction.commit()

# Retrieve the object
loaded_person = root['person']
print(f"Name: {loaded_person.name}, Age: {loaded_person.age}")

Properly Closing Resources

# Close connection and storage
conn.close()
db.close()
storage.close()

Database Structure and Root Object

Root Object

Each ZODB instance provides a root object (root()), typically a persistent.mapping.PersistentMapping dictionary that serves as the entry point for all persisted data:

# Use the root as a dictionary
root['users'] = {}
root['settings'] = {}
root['data'] = {}

Using PersistentMapping

For persistent dictionaries, import PersistentMapping:

from persistent.mapping import PersistentMapping

root['users'] = PersistentMapping()
root['users']['admin'] = Person("Administrator")
root['users']['user1'] = Person("User 1")

Using PersistentList

For persistent lists, import PersistentList:

from persistent.list import PersistentList

root['employees'] = PersistentList()
root['employees'].append(Person("Employee 1"))
root['employees'].append(Person("Employee 2"))

Modifying and Deleting Data

Updating Objects

# Change attributes
root['person'].name = "Anna"
root['person'].age = 25

# Commit the changes
transaction.commit()

Explicit Change Notification for Mutable Containers

When modifying mutable containers (lists, dicts) you may need to flag the change manually:

# Append to a mutable attribute
root['person'].skills.append("Python")
root['person']._p_changed = True

transaction.commit()

Deleting Objects

# Delete a top‑level object
del root['person']
transaction.commit()

# Delete from a nested collection
del root['users']['admin']
transaction.commit()

Transaction Management and Rollback

Basic Transaction Operations

ZODB provides full transaction control:

# Save changes
transaction.commit()

# Revert changes
transaction.abort()

Context Manager Usage

# Automatic commit/abort with a context manager
with transaction.manager:
    root['x'] = 42
    root['y'] = Person("Test")
    # Commit occurs automatically when exiting the block

Error Handling

try:
    root['person'].name = "New Name"
    transaction.commit()
except Exception as e:
    transaction.abort()  # Roll back on error
    print(f"Error: {e}")

Working with Multiple Objects and References

Creating Object Relationships

ZODB automatically persists linked objects:

class Group(Persistent):
    def __init__(self, name):
        self.name = name
        self.members = PersistentList()
    
    def add_member(self, person):
        self.members.append(person)

# Build related objects
group = Group("Developers")
person1 = Person("Ivan")
person2 = Person("Anna")

group.add_member(person1)
group.add_member(person2)

root['group'] = group
transaction.commit()

Bidirectional Links

class Project(Persistent):
    def __init__(self, name):
        self.name = name
        self.employees = PersistentList()

class Employee(Persistent):
    def __init__(self, name):
        self.name = name
        self.projects = PersistentList()

# Create two‑way connections
project = Project("Website")
employee = Employee("Developer")

project.employees.append(employee)
employee.projects.append(project)

Storage Types in ZODB

FileStorage

Standard file‑based storage implementation:

from ZODB.FileStorage import FileStorage
from ZODB import DB

storage = FileStorage('database.fs')
db = DB(storage)

DemoStorage (In‑Memory)

Ideal for testing and temporary data:

from ZODB.DemoStorage import DemoStorage

storage = DemoStorage()
db = DB(storage)

MappingStorage

Simple in‑memory mapping storage:

from ZODB.MappingStorage import MappingStorage

storage = MappingStorage()
db = DB(storage)

BlobStorage

Optimized for large binary objects (blobs):

from ZODB.blob import BlobStorage
from ZODB.FileStorage import FileStorage

storage = BlobStorage('blobs', FileStorage('data.fs'))
db = DB(storage)

ZEO – Client/Server Mode for ZODB

Installing ZEO

pip install ZEO

Server Configuration

Create a zeo.conf configuration file:

%define INSTANCE /path/to/instance

address 8100
read-only false
invalidation-queue-size 100
pid-filename $INSTANCE/var/ZEO.pid

path $INSTANCE/var/Data.fs

log-level info
log-file $INSTANCE/log/ZEO.log

Starting the Server

runzeo -C zeo.conf

Client Connection

from ZEO import ClientStorage
from ZODB import DB

# Connect to a ZEO server
storage = ClientStorage.ClientStorage(('localhost', 8100))
db = DB(storage)

Advantages of ZEO

  • Multi‑user access
  • Client‑side caching
  • Data replication
  • Monitoring and management tools

In‑Memory and Temporary Databases

Creating a Temporary Database

from ZODB.DemoStorage import DemoStorage
import transaction

storage = DemoStorage()
db = DB(storage)
conn = db.open()
root = conn.root()

# Work with transient data
root['temp_data'] = Person("Temporary User")
transaction.commit()

Using ZODB in Unit Tests

import unittest
from ZODB.DemoStorage import DemoStorage
from ZODB import DB
import transaction

class TestZODB(unittest.TestCase):
    def setUp(self):
        self.storage = DemoStorage()
        self.db = DB(self.storage)
        self.conn = self.db.open()
        self.root = self.conn.root()
    
    def tearDown(self):
        self.conn.close()
        self.db.close()
    
    def test_person_creation(self):
        person = Person("Test")
        self.root['person'] = person
        transaction.commit()
        self.assertEqual(self.root['person'].name, "Test")

Compatibility with Other Libraries

BTrees

BTrees provide fast indexing for large collections:

from BTrees.OOBTree import OOBTree
from BTrees.IOBTree import IOBTree

# Create indexes
name_index = OOBTree()
id_index = IOBTree()

# Populate indexes
person = Person("Ivan")
name_index["Ivan"] = person
id_index[1] = person

root['name_index'] = name_index
root['id_index'] = id_index

ZCatalog

Full‑text search integration via ZCatalog:

from Products.ZCatalog.ZCatalog import ZCatalog

catalog = ZCatalog('catalog')
catalog.addIndex('name', 'TextIndex')
catalog.addIndex('age', 'FieldIndex')

# Index an object
catalog.catalog_object(person, uid='/person/1')

Serialization Limitations

Because ZODB relies on pickle, the following objects cannot be persisted:

  • Open file handles
  • Sockets
  • Live threads
  • Lambda functions
  • Generators

Performance Optimization

Database Packing

from ZODB.FileStorage import FileStorage
from ZODB.serialize import referencesf
import time

storage = FileStorage('data.fs')
storage.pack(time.time(), referencesf)

Object Caching

# Increase cache size for better performance
db = DB(storage, cache_size=10000)

Query Optimization with Indexes

# Use BTrees for fast lookups
from BTrees.OOBTree import OOBTree

users_by_email = OOBTree()
users_by_email['user@example.com'] = user_object

# Retrieve quickly
user = users_by_email.get('user@example.com')

Monitoring and Debugging

Inspecting Database Contents

# List all keys in the root mapping
for key in root.keys():
    print(f"Key: {key}, Object: {root[key]}")

Database Statistics

print(f"Database size: {len(storage)}")
print(f"Last transaction ID: {storage.lastTransaction()}")

Transaction Debugging

import transaction

# Enable debug mode
transaction.manager.debug = True

# Iterate over stored transactions
for record in storage.iterator():
    print(f"Transaction: {record.tid}, Time: {record.time}")

Migration and Versioning

Evolving Object Schemas

class Person(Persistent):
    def __init__(self, name):
        self.name = name
        self._version = 1
    
    def migrate_to_v2(self):
        if getattr(self, '_version', 0) < 2:
            self.email = ""
            self._version = 2
            self._p_changed = True

Bulk Migration Script

def migrate_all_persons():
    for key in root.keys():
        obj = root[key]
        if isinstance(obj, Person):
            obj.migrate_to_v2()
    transaction.commit()

Backup and Restore

Creating a Backup

import shutil
import time
from ZODB.FileStorage import FileStorage
from ZODB.serialize import referencesf

# Copy the database file
shutil.copy('data.fs', 'backup_data.fs')

# Create an incremental backup via packing
storage = FileStorage('data.fs')
storage.pack(time.time(), referencesf)

Restoring from Backup

# Replace the current file with the backup
shutil.copy('backup_data.fs', 'data.fs')

# Verify integrity
storage = FileStorage('data.fs')
storage.checkCurrentSerialInTransaction()

Key ZODB Methods and Functions

Class/Module Method/Function Description
FileStorage FileStorage(path) Create a file‑based storage
  pack(time, referencesf) Pack (garbage‑collect) the database
  close() Close the storage
DB DB(storage, cache_size=10000) Instantiate a ZODB database
  open() Open a new connection
  close() Close the database
  pack() Pack the entire database
Connection root() Retrieve the root mapping
  close() Close the connection
  sync() Synchronize with the storage
  transaction_manager Access the transaction manager
transaction commit() Persist all changes
  abort() Rollback uncommitted changes
  savepoint() Create a savepoint within a transaction
  manager Context‑manager interface
Persistent _p_changed Flag indicating object modification
  _p_jar Reference to the connection (storage jar)
  _p_oid Object identifier
  _p_state Internal state flag
PersistentMapping PersistentMapping() Create a persistent dictionary
  keys() Return all keys
  values() Return all values
  items() Return key‑value pairs
PersistentList PersistentList() Create a persistent list
  append(item) Add an element
  extend(items) Add multiple elements
  pop() Remove the last element
DemoStorage DemoStorage() Create an in‑memory temporary storage
ClientStorage ClientStorage((host, port)) Connect to a ZEO server
OOBTree OOBTree() Create a B‑tree for indexing
  get(key, default) Retrieve a value by key
  update(dict) Update from another mapping

Typical ZODB Use Cases

Web Applications

ZODB integrates naturally with Pyramid, Plone, and other Python web frameworks:

# Store user sessions
class UserSession(Persistent):
    def __init__(self, user_id):
        self.user_id = user_id
        self.data = PersistentMapping()
        self.created_at = datetime.datetime.now()

root['sessions'] = PersistentMapping()

Content Management Systems

class Article(Persistent):
    def __init__(self, title, content):
        self.title = title
        self.content = content
        self.created_at = datetime.datetime.now()
        self.tags = PersistentList()
        self.comments = PersistentList()

class Comment(Persistent):
    def __init__(self, author, text):
        self.author = author
        self.text = text
        self.created_at = datetime.datetime.now()

Data Caching

class CacheItem(Persistent):
    def __init__(self, key, value, ttl=3600):
        self.key = key
        self.value = value
        self.created_at = datetime.datetime.now()
        self.ttl = ttl
    
    def is_expired(self):
        return (datetime.datetime.now() - self.created_at).seconds > self.ttl

IoT and Embedded Systems

class SensorData(Persistent):
    def __init__(self, sensor_id, value, timestamp):
        self.sensor_id = sensor_id
        self.value = value
        self.timestamp = timestamp

class Device(Persistent):
    def __init__(self, device_id, name):
        self.device_id = device_id
        self.name = name
        self.sensors = PersistentList()
        self.data_history = PersistentList()

Comparison with Other Database Solutions

Database Type Python‑Object Support Transactions SQL Schema Scalability
ZODB Object‑oriented Full native objects Yes (ACID) No No Medium
SQLite Relational No native objects Yes Yes Yes Low
PostgreSQL Relational Partial (JSON) Yes Yes Yes High
MongoDB Document Partial (dict) Yes No Flexible High
Redis Key‑Value No Partial No No High
PickleDB Key‑Value Partial No No No Low

Testing with ZODB

Basic Test Setup

import unittest
from ZODB.DemoStorage import DemoStorage
from ZODB import DB
import transaction

class TestZODB(unittest.TestCase):
    def setUp(self):
        self.storage = DemoStorage()
        self.db = DB(self.storage)
        self.conn = self.db.open()
        self.root = self.conn.root()
    
    def tearDown(self):
        transaction.abort()
        self.conn.close()
        self.db.close()
    
    def test_person_creation(self):
        person = Person("Test")
        self.root['person'] = person
        transaction.commit()
        self.assertEqual(self.root['person'].name, "Test")
        self.assertIsInstance(self.root['person'], Person)

Using Pytest Fixtures

import pytest
from ZODB.DemoStorage import DemoStorage
from ZODB import DB
import transaction

@pytest.fixture
def zodb_setup():
    storage = DemoStorage()
    db = DB(storage)
    conn = db.open()
    root = conn.root()
    
    yield root, conn, db
    
    transaction.abort()
    conn.close()
    db.close()

def test_person_operations(zodb_setup):
    root, conn, db = zodb_setup
    
    person = Person("Test")
    root['person'] = person
    transaction.commit()
    
    assert root['person'].name == "Test"

Frequently Asked Questions

What is ZODB and when should I use it?

ZODB (Zope Object Database) is an object‑oriented database for Python that lets you persist Python objects directly, without converting them to SQL tables or JSON. It is ideal for web applications, content management systems, and any Python project that benefits from native object persistence.

How does ZODB differ from traditional SQL databases?

ZODB requires no schema, does not use SQL, and stores Python objects with their attributes and methods intact. This simplifies development but limits complex ad‑hoc querying capabilities.

Does ZODB support transactions?

Yes. ZODB provides full ACID transaction support via the transaction module, with commit() and abort() operations.

Can I use ZODB in web applications?

Absolutely. ZODB was originally built for web frameworks and is widely used with Pyramid, Plone, and similar stacks to store sessions, content, and complex object graphs.

Is there asynchronous support for ZODB?

ZODB is synchronous. In async environments you can offload database operations to a thread pool or use compatible async wrappers.

Is ZODB production‑ready?

Yes. It has been deployed in production for over 25 years. For high‑availability setups, combine ZODB with ZEO and implement regular backups.

How do I search data in ZODB?

ZODB lacks a built‑in query language. Searches are performed by iterating over objects or using index structures like BTrees. For full‑text search, integrate ZCatalog.

Can I migrate data out of ZODB?

Yes. Write migration scripts that read objects from ZODB and write them to another storage system (SQL, NoSQL, etc.). The exact approach depends on your target schema.

What limitations does ZODB have?

Key constraints include: no native SQL querying, moderate scalability, reliance on pickle for serialization, and inability to store open files, sockets, threads, lambdas, or generators.

How can I improve ZODB performance?

Best practices: regularly pack the database, tune the cache size, create BTrees for frequent lookups, and keep transactions short.

Best Practices for Using ZODB

Object Design

  1. Inherit from Persistent: All stored objects should subclass Persistent.
  2. Use _p_changed wisely: Explicitly flag changes when automatic detection is insufficient.
  3. Avoid circular references: They can cause memory‑management issues.

Transaction Management

  1. Leverage context managers: with transaction.manager: ensures automatic commit or abort.
  2. Handle exceptions: Always call transaction.abort() inside except blocks.
  3. Keep transactions short: Commit frequently to reduce lock contention.

Performance Tuning

  1. Regular packing: Remove old object revisions with pack().
  2. Adjust cache size: Increase cache_size for workloads with many hot objects.
  3. Build indexes: Use BTrees for fast key‑based retrieval.

Conclusion

ZODB is a unique solution for persisting data in Python applications. It offers transparent object handling, full ACID transaction support, and a schema‑free approach, making it an attractive choice for developers who need native integration with Python objects.

Typical scenarios include web applications, content management systems, data caching, and rapid prototyping. While ZODB may not be the best fit for extremely high‑throughput systems that require complex ad‑hoc queries, it remains a solid option for many Python projects where simplicity and developer productivity are paramount.

When used correctly, ZODB can dramatically simplify application architecture and reduce boilerplate code for data access. Understanding its limitations and following best practices will help you achieve optimal performance and reliability.

News