The Revision object is used to encapsulate changes to the domain model/database. It also allows changes to multiple objects/rows to be part of a single ‘revision’:
A Revision to the Database/Domain Model.
All versioned objects have an associated Revision which can be accessed via the revision attribute.
SQLAlchemy MapperExtension which implements revisioning of sqlalchemy mapped objects.
In essence it implements copy on write.
However various additional features such as:
- Checking for ‘real’ changes – often sqlalchemy objects are marked as changed when not (just a related attribute has changed).
- support for ignored attributes (these attributes will be ignored when checking for changes and creating new revisions of the object)
Attach active and deleted stateful lists along with the association proxy based on the active list to original object (object_to_alter).
To illustrate if one has:
class Package(object):
# package_licenses is the basic_m2m_name attribute
#
# it should come from a simple relation pointing to PackageLicense
# and returns PackageLicense objects (so do *not* use secondary
# keyword)
#
# It will usually not be defined here but in the Package mapper:
#
# 'package_licenses':relation(License) ...
package_licenses = ... from_mapper ...
Then after running:
add_stateful_m2m(Package, PackageLicense, 'licenses', 'license',
'package_licenses')
there will be additional properties:
# NB: licenses_active and licenses_deleted are lists of PackageLicense # objects while licenses (being an assoc proxy) is a list of Licenses licenses_active # these are active PackageLicenses licenses_deleted # these are deleted PackageLicenses licenses # these are active Licenses
@arg **kwargs: these are passed on to the DeferredProperty.
Here is a full demonstration of using vdm which can be found in vdm/sqlalchemy/demo.py:
'''Demo of vdm for SQLAlchemy.
This module sets up a small domain model with some versioned objects. Code
that then uses these objects can be found in demo_test.py.
'''
from datetime import datetime
import logging
logger = logging.getLogger('vdm')
from sqlalchemy import *
from sqlalchemy import __version__ as sqla_version
# from sqlalchemy import create_engine
import vdm.sqlalchemy
# engine = create_engine('sqlite:///:memory:')
engine = create_engine('postgres://tester:pass@localhost/vdmtest')
metadata = MetaData(bind=engine)
## VDM-specific tables
revision_table = vdm.sqlalchemy.make_revision_table(metadata)
## Demo tables
license_table = Table('license', metadata,
Column('id', Integer, primary_key=True),
Column('name', String(100)),
Column('open', Boolean),
)
import uuid
def uuidstr(): return str(uuid.uuid4())
package_table = Table('package', metadata,
# Column('id', Integer, primary_key=True),
Column('id', String(36), default=uuidstr, primary_key=True),
Column('name', String(100)),
Column('title', String(100)),
Column('license_id', Integer, ForeignKey('license.id')),
Column('notes', UnicodeText),
)
tag_table = Table('tag', metadata,
Column('id', Integer, primary_key=True),
Column('name', String(100)),
)
package_tag_table = Table('package_tag', metadata,
Column('id', Integer, primary_key=True),
# Column('package_id', Integer, ForeignKey('package.id')),
Column('package_id', String(36), ForeignKey('package.id')),
Column('tag_id', Integer, ForeignKey('tag.id')),
)
vdm.sqlalchemy.make_table_stateful(license_table)
vdm.sqlalchemy.make_table_stateful(package_table)
vdm.sqlalchemy.make_table_stateful(tag_table)
vdm.sqlalchemy.make_table_stateful(package_tag_table)
license_revision_table = vdm.sqlalchemy.make_revisioned_table(license_table)
package_revision_table = vdm.sqlalchemy.make_revisioned_table(package_table)
# TODO: this has a composite primary key ...
package_tag_revision_table = vdm.sqlalchemy.make_revisioned_table(package_tag_table)
## -------------------
## Mapped classes
class License(vdm.sqlalchemy.RevisionedObjectMixin,
vdm.sqlalchemy.StatefulObjectMixin,
vdm.sqlalchemy.SQLAlchemyMixin
):
def __init__(self, **kwargs):
for k,v in kwargs.items():
setattr(self, k, v)
class Package(vdm.sqlalchemy.RevisionedObjectMixin,
vdm.sqlalchemy.StatefulObjectMixin,
vdm.sqlalchemy.SQLAlchemyMixin
):
def __init__(self, **kwargs):
for k,v in kwargs.items():
setattr(self, k, v)
class Tag(vdm.sqlalchemy.SQLAlchemyMixin):
def __init__(self, name):
self.name = name
class PackageTag(vdm.sqlalchemy.RevisionedObjectMixin,
vdm.sqlalchemy.StatefulObjectMixin,
vdm.sqlalchemy.SQLAlchemyMixin
):
def __init__(self, package=None, tag=None, state=None, **kwargs):
logger.debug('PackageTag.__init__: %s, %s' % (package, tag))
self.package = package
self.tag = tag
self.state = state
for k,v in kwargs.items():
setattr(self, k, v)
## --------------------------------------------------------
## Mapper Stuff
from sqlalchemy.orm import scoped_session, sessionmaker, create_session
from sqlalchemy.orm import relation, backref
# both options now work
# Session = scoped_session(sessionmaker(autoflush=False, transactional=True))
# this is the more testing one ...
if sqla_version <= '0.4.99':
Session = scoped_session(sessionmaker(autoflush=True, transactional=True))
else:
Session = scoped_session(sessionmaker(autoflush=True, autocommit=False))
# mapper = Session.mapper
from sqlalchemy.orm import mapper
# VDM-specific domain objects
State = vdm.sqlalchemy.State
Revision = vdm.sqlalchemy.make_Revision(mapper, revision_table)
mapper(License, license_table, properties={
},
extension=vdm.sqlalchemy.Revisioner(license_revision_table)
)
mapper(Package, package_table, properties={
'license':relation(License),
# delete-orphan on cascade does NOT work!
# Why? Answer: because of way SQLAlchemy/our code works there are points
# where PackageTag object is created *and* flushed but does not yet have
# the package_id set (this cause us other problems ...). Some time later a
# second commit happens in which the package_id is correctly set.
# However after first commit PackageTag does not have Package and
# delete-orphan kicks in to remove it!
#
# do we want lazy=False here? used in:
# <http://www.sqlalchemy.org/trac/browser/sqlalchemy/trunk/examples/association/proxied_association.py>
'package_tags':relation(PackageTag, backref='package', cascade='all'), #, delete-orphan'),
},
extension = vdm.sqlalchemy.Revisioner(package_revision_table)
)
mapper(Tag, tag_table)
mapper(PackageTag, package_tag_table, properties={
'tag':relation(Tag),
},
extension = vdm.sqlalchemy.Revisioner(package_tag_revision_table)
)
vdm.sqlalchemy.modify_base_object_mapper(Package, Revision, State)
vdm.sqlalchemy.modify_base_object_mapper(License, Revision, State)
vdm.sqlalchemy.modify_base_object_mapper(PackageTag, Revision, State)
PackageRevision = vdm.sqlalchemy.create_object_version(mapper, Package,
package_revision_table)
LicenseRevision = vdm.sqlalchemy.create_object_version(mapper, License,
license_revision_table)
PackageTagRevision = vdm.sqlalchemy.create_object_version(mapper, PackageTag,
package_tag_revision_table)
from base import add_stateful_versioned_m2m
vdm.sqlalchemy.add_stateful_versioned_m2m(Package, PackageTag, 'tags', 'tag',
'package_tags')
vdm.sqlalchemy.add_stateful_versioned_m2m_on_version(PackageRevision, 'tags')
## ------------------------
## Repository helper object
from tools import Repository
repo = Repository(metadata, Session,
versioned_objects = [ Package, License, PackageTag ]
)