Plugins
>>> from sqlalchemy_history.plugins import PropertyModTrackerPlugin
>>> versioning_manager.plugins.append(PropertyModTrackerPlugin())
>>> versioning_manager.plugins
<PluginCollection [...]>
>>> del versioning_manager.plugins[0] # You can also remove plugin
Activity
The ActivityPlugin is the most powerful plugin for tracking changes of individual entities.
If you use ActivityPlugin you probably don't need to use TransactionChanges nor TransactionMeta plugins.
You can initalize the ActivityPlugin by adding it to versioning manager.
>>> activity_plugin = ActivityPlugin()
>>> make_versioned(plugins=[activity_plugin])
ActivityPlugin uses single database table for tracking activities. This table
follows the data structure in activity stream specification
_, but it comes
with a nice twist:
|Column |Type |Description
|:-------------: |:---------: |:------------:
|id |BigInteger |The primary key of the activity
|verb |Unicode |Verb defines the action of the activity
|data |JSON |Additional data for the activity in JSON format
|transaction_id |BigInteger |The transaction this activity was associated with
|object_id |BigInteger |The primary key of the object. Object can be any entity which has an integer as primary key.
|object_type |Unicode |The type of the object (class name as string)
|object_tx_id |BigInteger |The last transaction_id associated with the object. This is used for efficiently fetching the object version associated with this activity.
|target_id |BigInteger |The primary key of the target. Target can be any entity which has an integer as primary key.
|target_type |Unicode |The of the target (class name as string)
|target_tx_id |BigInteger |The last transaction_id associated with the target.
Each Activity has relationships to actor, object and target but it also holds
information about the associated transaction and about the last associated
transactions with the target and object. This allows each activity to also have
object_version and target_version relationships for introspecting what those
objects and targets were in given point in time. All these relationship
properties use generic relationships
_ of the SQLAlchemy-Utils package.
Limitations
Currently all changes to parent models must be flushed or committed before creating activities. This is due to a fact that there is still no dependency processors for generic relationships. So when you create activities and assign objects / targets for those please remember to flush the session before creating an activity::
>>> article = Article(name=u'Some article')
>>> session.add(article)
>>> session.flush() # <- IMPORTANT!
>>> first_activity = Activity(verb=u'create', object=article)
>>> session.add(first_activity)
>>> session.commit()
Targets and objects of given activity must have an integer primary key column id.
Create activities
Once your models have been configured you can get the Activity model from the ActivityPlugin class with activity_cls property::
Activity = activity_plugin.activity_cls
Now let's say we have model called Article and Category. Each Article has one Category. Activities should be created along with the changes you make on these models.
>>> article = Article(name=u'Some article')
>>> session.add(article)
>>> session.flush()
>>> first_activity = Activity(verb=u'create', object=article)
>>> session.add(first_activity)
>>> session.commit()
Current transaction gets automatically assigned to activity object::
>>> first_activity.transaction # Transaction object
Update activities
The object property of the Activity object holds the current object and the object_version holds the object version at the time when the activity was created.
>>> article.name = u'Some article updated!'
>>> session.flush()
>>> second_activity = Activity(verb=u'update', object=article)
>>> session.add(second_activity)
>>> session.commit()
>>> second_activity.object.name # u'Some article updated!'
>>> first_activity.object.name # u'Some article updated!'
>>> first_activity.object_version.name # u'Some article'
Delete activities
The version properties are especially useful for delete activities. Once the activity is fetched from the database the object is no longer available ( since its deleted), hence the only way we could show some information about the object the user deleted is by accessing the object_version property.
>>> session.delete(article)
>>> session.flush()
>>> third_activity = Activity(verb=u'delete', object=article)
>>> session.add(third_activity)
>>> session.commit()
>>> third_activity.object_version.name # u'Some article updated!'
Local version histories using targets
The target property of the Activity model offers a way of tracking changes of given related object. In the example below we create a new activity when adding a category for article and then mark the article as the target of this activity.
>>> session.add(Category(name=u'Fist category', article=article))
>>> session.flush()
>>> activity = Activity(
... verb=u'create',
... object=category,
... target=article
... )
>>> session.add(activity)
>>> session.commit()
Now if we wanted to find all the changes that affected given article we could do so by searching through all the activities where either the object or target is the given article.
>>> import sqlalchemy as sa
>>> activities = session.query(Activity).filter(
... sa.or_(
... Activity.object == article,
... Activity.target == article
... )
... )
Also Read
ActivityFactory
Bases: ModelFactory
Source code in sqlalchemy_history/plugins/activity.py
class ActivityFactory(ModelFactory):
model_name = "Activity"
def create_class(self, manager):
"""Create Activity class.
:param manager:
"""
class Activity(manager.declarative_base, ActivityBase):
__tablename__ = "activity"
manager = self
transaction_id = sa.Column(sa.BigInteger, index=True, nullable=False)
data = sa.Column(JSONType)
object_type = sa.Column(sa.String(255))
object_id = sa.Column(sa.BigInteger)
object_tx_id = sa.Column(sa.BigInteger)
target_type = sa.Column(sa.String(255))
target_id = sa.Column(sa.BigInteger)
target_tx_id = sa.Column(sa.BigInteger)
def _calculate_tx_id(self, obj):
session = sa.orm.object_session(self)
if obj:
object_version = version_obj(session, obj)
if object_version:
return object_version.transaction_id
model = obj.__class__
version_cls = version_class(model)
primary_key = inspect(model).primary_key[0].name
return (
session.query(sa.func.max(version_cls.transaction_id))
.filter(getattr(version_cls, primary_key) == getattr(obj, primary_key))
.scalar()
)
def calculate_object_tx_id(self):
self.object_tx_id = self._calculate_tx_id(self.object)
def calculate_target_tx_id(self):
self.target_tx_id = self._calculate_tx_id(self.target)
object = generic_relationship(object_type, object_id)
@hybrid_property
def object_version_type(self):
return self.object_type + "Version"
@object_version_type.expression
def object_version_type(cls):
return sa.func.concat(cls.object_type, "Version")
object_version = generic_relationship(object_version_type, (object_id, object_tx_id))
target = generic_relationship(target_type, target_id)
@hybrid_property
def target_version_type(self):
return self.target_type + "Version"
@target_version_type.expression
def target_version_type(cls):
return sa.func.concat(cls.target_type, "Version")
target_version = generic_relationship(target_version_type, (target_id, target_tx_id))
Activity.transaction = sa.orm.relationship(
manager.transaction_cls,
backref=sa.orm.backref(
"activities",
),
primaryjoin=("%s.id == Activity.transaction_id" % manager.transaction_cls.__name__),
foreign_keys=[Activity.transaction_id],
)
return Activity
create_class(manager)
Create Activity class.
Parameters: |
|
---|
sqlalchemy_history/plugins/activity.py
def create_class(self, manager):
"""Create Activity class.
:param manager:
"""
class Activity(manager.declarative_base, ActivityBase):
__tablename__ = "activity"
manager = self
transaction_id = sa.Column(sa.BigInteger, index=True, nullable=False)
data = sa.Column(JSONType)
object_type = sa.Column(sa.String(255))
object_id = sa.Column(sa.BigInteger)
object_tx_id = sa.Column(sa.BigInteger)
target_type = sa.Column(sa.String(255))
target_id = sa.Column(sa.BigInteger)
target_tx_id = sa.Column(sa.BigInteger)
def _calculate_tx_id(self, obj):
session = sa.orm.object_session(self)
if obj:
object_version = version_obj(session, obj)
if object_version:
return object_version.transaction_id
model = obj.__class__
version_cls = version_class(model)
primary_key = inspect(model).primary_key[0].name
return (
session.query(sa.func.max(version_cls.transaction_id))
.filter(getattr(version_cls, primary_key) == getattr(obj, primary_key))
.scalar()
)
def calculate_object_tx_id(self):
self.object_tx_id = self._calculate_tx_id(self.object)
def calculate_target_tx_id(self):
self.target_tx_id = self._calculate_tx_id(self.target)
object = generic_relationship(object_type, object_id)
@hybrid_property
def object_version_type(self):
return self.object_type + "Version"
@object_version_type.expression
def object_version_type(cls):
return sa.func.concat(cls.object_type, "Version")
object_version = generic_relationship(object_version_type, (object_id, object_tx_id))
target = generic_relationship(target_type, target_id)
@hybrid_property
def target_version_type(self):
return self.target_type + "Version"
@target_version_type.expression
def target_version_type(cls):
return sa.func.concat(cls.target_type, "Version")
target_version = generic_relationship(target_version_type, (target_id, target_tx_id))
Activity.transaction = sa.orm.relationship(
manager.transaction_cls,
backref=sa.orm.backref(
"activities",
),
primaryjoin=("%s.id == Activity.transaction_id" % manager.transaction_cls.__name__),
foreign_keys=[Activity.transaction_id],
)
return Activity
ActivityPlugin
Bases: Plugin
Source code in sqlalchemy_history/plugins/activity.py
class ActivityPlugin(Plugin):
activity_cls = None
def after_build_models(self, manager):
self.activity_cls = ActivityFactory()(manager)
manager.activity_cls = self.activity_cls
def is_session_modified(self, session):
"""Return that the session has been modified if the session contains an
activity class.
:param session: SQLAlchemy session object
"""
return any(isinstance(obj, self.activity_cls) for obj in session)
def before_flush(self, uow, session):
for obj in session:
if isinstance(obj, self.activity_cls):
obj.transaction = uow.current_transaction
obj.calculate_target_tx_id()
obj.calculate_object_tx_id()
def after_version_class_built(self, parent_cls, version_cls):
pass
is_session_modified(session)
Return that the session has been modified if the session contains an activity class.
Parameters: |
|
---|
sqlalchemy_history/plugins/activity.py
def is_session_modified(self, session):
"""Return that the session has been modified if the session contains an
activity class.
:param session: SQLAlchemy session object
"""
return any(isinstance(obj, self.activity_cls) for obj in session)
PropertyModTracker
The PropertyModTrackerPlugin offers a way of efficiently tracking individual property modifications. With PropertyModTrackerPlugin you can make efficient queries such as:
Find all versions of model X where user updated the property A or property B.
Find all versions of model X where user didn't update property A.
PropertyModTrackerPlugin adds separate modified tracking column for each
versioned column. So for example if you have versioned model called Article
with columns name
and content
, this plugin would add two additional boolean
columns name_mod
and content_mod
for the version model. When user commits
transactions the plugin automatically updates these boolean columns.
TransactionChanges
TransactionChanges provides way of keeping track efficiently which declarative models were changed in given transaction. This can be useful when transactions need to be queried afterwards for problems such as:
-
Find all transactions which affected
User
model. -
Find all transactions which didn't affect models
Entity
andEvent
.
The plugin works in two ways. On class instrumentation phase this plugin
creates a special transaction model called TransactionChanges
. This model is
associated with table called transaction_changes
, which has only only two
fields: transaction_id and entity_name. If for example transaction consisted
of saving 5 new User entities and 1 Article entity, two new rows would be
inserted into transaction_changes table.
================ ================= transaction_id entity_name ---------------- ----------------- 233678 User 233678 Article ================ =================
TransactionChangesFactory
Bases: ModelFactory
Source code in sqlalchemy_history/plugins/transaction_changes.py
class TransactionChangesFactory(ModelFactory):
model_name = "TransactionChanges"
def create_class(self, manager):
"""Create TransactionChanges class.
:param manager:
"""
class TransactionChanges(manager.declarative_base, TransactionChangesBase):
__tablename__ = "transaction_changes"
TransactionChanges.transaction = sa.orm.relationship(
manager.transaction_cls,
backref=sa.orm.backref(
"changes",
),
primaryjoin=("%s.id == TransactionChanges.transaction_id" % manager.transaction_cls.__name__),
foreign_keys=[TransactionChanges.transaction_id],
)
return TransactionChanges
create_class(manager)
Create TransactionChanges class.
Parameters: |
|
---|
sqlalchemy_history/plugins/transaction_changes.py
def create_class(self, manager):
"""Create TransactionChanges class.
:param manager:
"""
class TransactionChanges(manager.declarative_base, TransactionChangesBase):
__tablename__ = "transaction_changes"
TransactionChanges.transaction = sa.orm.relationship(
manager.transaction_cls,
backref=sa.orm.backref(
"changes",
),
primaryjoin=("%s.id == TransactionChanges.transaction_id" % manager.transaction_cls.__name__),
foreign_keys=[TransactionChanges.transaction_id],
)
return TransactionChanges
TransactionMeta
TransactionMetaPlugin offers a way of saving key-value data for transations. You can use the plugin in same way as other plugins::
meta_plugin = TransactionMetaPlugin()
versioning_manager.plugins.add(meta_plugin)
TransactionMetaPlugin creates a simple model called TransactionMeta. This class has three columns: transaction_id, key and value. TransactionMeta plugin also creates an association proxy between TransactionMeta and Transaction classes for easy dictionary based access of key-value pairs.
You can easily 'tag' transactions with certain key value pairs by giving these keys and values to the meta property of Transaction class.
::
from sqlalchemy_history import versioning_manager
article = Article()
session.add(article)
uow = versioning_manager.unit_of_work(session)
tx = uow.create_transaction(session)
tx.meta = {u'some_key': u'some value'}
session.commit()
TransactionMeta = meta_plugin.model_class
Transaction = versioning_manager.transaction_cls
# find all transactions with 'article' tags
query = (
session.query(Transaction)
.join(Transaction.meta_relation)
.filter(
db.and_(
TransactionMeta.key == 'some_key',
TransactionMeta.value == 'some value'
)
)
)
TransactionMetaFactory
Bases: ModelFactory
Source code in sqlalchemy_history/plugins/transaction_meta.py
class TransactionMetaFactory(ModelFactory):
model_name = "TransactionMeta"
def create_class(self, manager):
"""Create TransactionMeta class.
:param manager:
"""
class TransactionMeta(manager.declarative_base, TransactionMetaBase):
__tablename__ = "transaction_meta"
TransactionMeta.transaction = sa.orm.relationship(
manager.transaction_cls,
backref=sa.orm.backref("meta_relation", collection_class=attribute_mapped_collection("key")),
primaryjoin=("%s.id == TransactionMeta.transaction_id" % manager.transaction_cls.__name__),
foreign_keys=[TransactionMeta.transaction_id],
)
manager.transaction_cls.meta = association_proxy(
"meta_relation",
"value",
creator=lambda key, value: TransactionMeta(key=key, value=value),
)
return TransactionMeta
create_class(manager)
Create TransactionMeta class.
Parameters: |
|
---|
sqlalchemy_history/plugins/transaction_meta.py
def create_class(self, manager):
"""Create TransactionMeta class.
:param manager:
"""
class TransactionMeta(manager.declarative_base, TransactionMetaBase):
__tablename__ = "transaction_meta"
TransactionMeta.transaction = sa.orm.relationship(
manager.transaction_cls,
backref=sa.orm.backref("meta_relation", collection_class=attribute_mapped_collection("key")),
primaryjoin=("%s.id == TransactionMeta.transaction_id" % manager.transaction_cls.__name__),
foreign_keys=[TransactionMeta.transaction_id],
)
manager.transaction_cls.meta = association_proxy(
"meta_relation",
"value",
creator=lambda key, value: TransactionMeta(key=key, value=value),
)
return TransactionMeta