Add docs for observers
This commit is contained in:
@@ -13,6 +13,7 @@ SQLAlchemy-Utils provides custom data types and various utility functions for SQ
|
||||
data_types
|
||||
range_data_types
|
||||
aggregates
|
||||
observers
|
||||
decorators
|
||||
generic_relationship
|
||||
database_helpers
|
||||
|
6
docs/observers.rst
Normal file
6
docs/observers.rst
Normal file
@@ -0,0 +1,6 @@
|
||||
Observers
|
||||
=========
|
||||
|
||||
.. automodule:: sqlalchemy_utils.observer
|
||||
|
||||
.. autofunction:: observes
|
@@ -79,6 +79,9 @@ generator = AttributeValueGenerator()
|
||||
|
||||
def generates(attr, source=None, generator=generator):
|
||||
"""
|
||||
.. deprecated:: 0.28.0
|
||||
Use :meth:`.observer.observes` instead.
|
||||
|
||||
Decorator that marks given function as attribute value generator.
|
||||
|
||||
Many times you may have generated property values. Usual cases include
|
||||
|
@@ -1,3 +1,154 @@
|
||||
"""
|
||||
This module provides a decorator function for observing changes in given
|
||||
property. Internally the decorator is implemented using SQLAlchemy event
|
||||
listeners. Both column properties and relationship properties can be observed.
|
||||
|
||||
Property observers can be used for pre-calculating aggregates and automatic
|
||||
real-time data denormalization.
|
||||
|
||||
Simple observers
|
||||
----------------
|
||||
|
||||
At the heart of the observer extension is the :func:`observes` decorator. You
|
||||
mark some property path as being observed and the marked method will get
|
||||
notified when any changes are made to given path.
|
||||
|
||||
Consider the following model structure:
|
||||
|
||||
::
|
||||
|
||||
class Director(Base):
|
||||
__tablename__ = 'director'
|
||||
id = sa.Column(sa.Integer, primary_key=True)
|
||||
name = sa.Column(sa.String)
|
||||
date_of_birth = sa.Column(sa.Date)
|
||||
|
||||
class Movie(Base):
|
||||
__tablename__ = 'movie'
|
||||
id = sa.Column(sa.Integer, primary_key=True)
|
||||
name = sa.Column(sa.String)
|
||||
director_id = sa.Column(sa.Integer, sa.ForeignKey(Director.id))
|
||||
director = sa.orm.relationship(Director, backref='movies')
|
||||
|
||||
|
||||
Now consider we want to show movies in some listing ordered by director id
|
||||
first and movie id secondly. If we have many movies then using joins and
|
||||
ordering by Director.name will be very slow. Here is where denormalization
|
||||
and :func:`observes` comes to rescue the day. Let's add a new column called
|
||||
director_name to Movie which will get automatically copied from associated
|
||||
Director.
|
||||
|
||||
|
||||
::
|
||||
|
||||
from sqlalchemy_utils import observes
|
||||
|
||||
|
||||
class Movie(Base):
|
||||
# same as before..
|
||||
director_name = sa.Column(sa.String)
|
||||
|
||||
@observes('director')
|
||||
def director_observer(self, director):
|
||||
self.director_name = director.name
|
||||
|
||||
.. note::
|
||||
|
||||
This example could be done much more efficiently using a compound foreing
|
||||
key from direcor_name, director_id to Director.name, Director.id but for
|
||||
the sake of simplicity we added this as an example.
|
||||
|
||||
|
||||
Observes vs aggregated
|
||||
----------------------
|
||||
|
||||
:func:`observes` and :func:`.aggregates.aggregated` can be used for similar
|
||||
things. However performance wise you should take the following things into
|
||||
consideration:
|
||||
|
||||
* :func:`observes` works always inside transaction and deals with objects. If
|
||||
the relationship observer is observing has large number of objects its better
|
||||
to use :func:`.aggregates.aggregated`.
|
||||
* :func:`.aggregates.aggregated` always executes one additional query per
|
||||
aggregate so in scenarios where the observed relationship has only handful of
|
||||
objects its better to use :func:`observes` instead.
|
||||
|
||||
|
||||
Example 1. Movie with many ratings
|
||||
|
||||
Let's say we have a Movie object with potentially thousands of ratings. In this
|
||||
case we should always use :func:`.aggregates.aggregated` since iterating
|
||||
through thousands of objects is slow and very memory consuming.
|
||||
|
||||
Example 2. Product with denormalized catalog name
|
||||
|
||||
Each product belongs to one catalog. Here it is natural to use :func:`observes`
|
||||
for data denormalization.
|
||||
|
||||
|
||||
Deeply nested observing
|
||||
-----------------------
|
||||
|
||||
Consider the following model structure where Catalog has many Categories and
|
||||
Category has many Products.
|
||||
|
||||
::
|
||||
|
||||
class Catalog(Base):
|
||||
__tablename__ = 'catalog'
|
||||
id = sa.Column(sa.Integer, primary_key=True)
|
||||
product_count = sa.Column(sa.Integer, default=0)
|
||||
|
||||
@observes('categories.products')
|
||||
def product_observer(self, products):
|
||||
self.product_count = len(products)
|
||||
|
||||
categories = sa.orm.relationship('Category', backref='catalog')
|
||||
|
||||
class Category(Base):
|
||||
__tablename__ = 'category'
|
||||
id = sa.Column(sa.Integer, primary_key=True)
|
||||
catalog_id = sa.Column(sa.Integer, sa.ForeignKey('catalog.id'))
|
||||
|
||||
products = sa.orm.relationship('Product', backref='category')
|
||||
|
||||
class Product(Base):
|
||||
__tablename__ = 'product'
|
||||
id = sa.Column(sa.Integer, primary_key=True)
|
||||
price = sa.Column(sa.Numeric)
|
||||
|
||||
category_id = sa.Column(sa.Integer, sa.ForeignKey('category.id'))
|
||||
|
||||
|
||||
:func:`observes` is smart enough to:
|
||||
|
||||
* Notify catalog objects of any changes in associated Product objects
|
||||
* Notify catalog objects of any changes in Category objects that affect
|
||||
products (for example if Category gets deleted, or a new Category is added to
|
||||
Catalog with any number of Products)
|
||||
|
||||
|
||||
::
|
||||
|
||||
category = Category(
|
||||
products=[Product(), Product()]
|
||||
)
|
||||
category2 = Category(
|
||||
product=[Product()]
|
||||
)
|
||||
|
||||
catalog = Catalog(
|
||||
categories=[category, category2]
|
||||
)
|
||||
session.add(catalog)
|
||||
session.commit()
|
||||
catalog.product_count # 2
|
||||
|
||||
session.delete(category)
|
||||
session.commit()
|
||||
catalog.product_count # 1
|
||||
|
||||
"""
|
||||
import sqlalchemy as sa
|
||||
|
||||
from collections import defaultdict, namedtuple, Iterable
|
||||
@@ -42,6 +193,9 @@ class PropertyObserver(object):
|
||||
if not sa.event.contains(*args):
|
||||
sa.event.listen(*args)
|
||||
|
||||
def __repr__(self):
|
||||
return '<PropertyObserver>'
|
||||
|
||||
def update_generator_registry(self, mapper, class_):
|
||||
"""
|
||||
Adds generator functions to generator_registry.
|
||||
@@ -129,6 +283,44 @@ observer = PropertyObserver()
|
||||
|
||||
|
||||
def observes(path, observer=observer):
|
||||
"""
|
||||
Mark method as property observer for given property path. Inside
|
||||
transaction observer gathers all changes made in given property path and
|
||||
feeds the changed objects to observer-marked method at the before flush
|
||||
phase.
|
||||
|
||||
::
|
||||
|
||||
from sqlalchemy_utils import observes
|
||||
|
||||
|
||||
class Catalog(Base):
|
||||
__tablename__ = 'catalog'
|
||||
id = sa.Column(sa.Integer, primary_key=True)
|
||||
category_count = sa.Column(sa.Integer, default=0)
|
||||
|
||||
@observes('categories')
|
||||
def category_observer(self, categories):
|
||||
self.category_count = len(categories)
|
||||
|
||||
class Category(Base):
|
||||
__tablename__ = 'category'
|
||||
id = sa.Column(sa.Integer, primary_key=True)
|
||||
catalog_id = sa.Column(sa.Integer, sa.ForeignKey('catalog.id'))
|
||||
|
||||
|
||||
catalog = Catalog(categories=[Category(), Category()])
|
||||
session.add(catalog)
|
||||
session.commit()
|
||||
|
||||
catalog.category_count # 2
|
||||
|
||||
|
||||
.. versionadded: 0.28.0
|
||||
|
||||
:param path: Dot-notated property path, eg. 'categories.products.price'
|
||||
:param observer: :meth:`PropertyObserver` object
|
||||
"""
|
||||
observer.register_listeners()
|
||||
|
||||
def wraps(func):
|
||||
|
Reference in New Issue
Block a user