attribute wouldnt occur. [ticket:360]
- implemented from_obj argument for query.count, improves count function
on selectresults [ticket:325]
+- added an assertion within the "cascade" step of ORM relationships to check
+that the class of object attached to a parent object is appropriate
+(i.e. if A.items stores B objects, raise an error if a C is appended to A.items)
+- new extension sqlalchemy.ext.associationproxy, provides transparent "association object"
+mappings. new example examples/association/proxied_association.py illustrates.
0.3.0
- General:
### Association Object {@name=association}
-Many to Many can also be done with an association object, that adds additional information about how two items are related. In this pattern, the "secondary" option is no longer used; instead, the association object becomes a mapped entity itself, mapped to the association table. If the association table has no explicit primary key columns defined, you also have to tell the mapper what columns will compose its "primary key", which are typically the two (or more) columns involved in the association. Also, the relation between the parent and association mapping is typically set up with a cascade of `all, delete-orphan`. This is to insure that when an association object is removed from its parent collection, it is deleted (otherwise, the unit of work tries to null out one of the foreign key columns, which raises an error condition since that column is also part of its "primary key").
+Many to Many can also be done with an association object, that adds additional information about how two items are related. In this pattern, the "secondary" option to `relation()` is no longer used; instead, the association object becomes a mapped entity itself, mapped to the association table. If the association table has no explicit primary key columns defined, you also have to tell the mapper what columns will compose its "primary key", which are typically the two (or more) columns involved in the association. Also, the relation between the parent and association mapping is typically set up with a cascade of `all, delete-orphan`. This is to insure that when an association object is removed from its parent collection, it is deleted (otherwise, the unit of work tries to null out one of the foreign key columns, which raises an error condition since that column is also part of its "primary key").
{python}
from sqlalchemy import *
session.flush()
+SQLAlchemy includes an extension module which can be used in some cases to decrease the explicitness of the association object pattern; this extension is described in [plugins_associationproxy](rel:plugins_associationproxy).
\ No newline at end of file
# commit all changes
ctx.current.flush()
+### associationproxy
+
+**Author:** Mike Bayer<br/>
+**Version:** 0.3.1 or greater
+
+`associationproxy` is used to create a transparent proxy to the associated object in an association relationship, thereby decreasing the verbosity of the pattern in cases where explicit access to the association object is not required. The association relationship pattern is a richer form of a many-to-many relationship, which is described in [datamapping_association](rel:datamapping_association). It is strongly recommended to fully understand the association object pattern in its explicit form before using this extension; see the examples in the SQLAlchemy distribution under the directory `examples/association/`.
+
+When dealing with association relationships, the **association object** refers to the object that maps to a row in the association table (i.e. the many-to-many table), while the **associated object** refers to the "endpoint" of the association, i.e. the ultimate object referenced by the parent. The proxy can return collections of objects attached to association objects, and can also create new association objects given only the associated object. An example using the Keyword mapping described in the data mapping documentation is as follows:
+
+ {python}
+ from sqlalchemy.ext.associationproxy import AssociationProxy
+
+ class User(object):
+ pass
+
+ class Keyword(object):
+ def __init__(self, name):
+ self.keyword_name = name
+
+ class Article(object):
+ # create "keywords" proxied association.
+ # the collection is called 'keyword_associations', the endpoint
+ # attribute of each association object is called 'keyword'. the
+ # class itself of the association object will be figured out automatically .
+ keywords = AssociationProxy('keyword_associations', 'keyword')
+
+ class KeywordAssociation(object):
+ pass
+
+ # create mappers normally
+ # note that we set up 'keyword_associations' on Article,
+ # and 'keyword' on KeywordAssociation.
+ mapper(Article, articles_table, properties={
+ 'keyword_associations':relation(KeywordAssociation, lazy=False, cascade="all, delete-orphan")
+ }
+ )
+ mapper(KeywordAssociation, itemkeywords_table,
+ primary_key=[itemkeywords_table.c.article_id, itemkeywords_table.c.keyword_id],
+ properties={
+ 'keyword' : relation(Keyword, lazy=False),
+ 'user' : relation(User, lazy=False)
+ }
+ )
+ mapper(User, users_table)
+ mapper(Keyword, keywords_table)
+
+ # now, Keywords can be attached to an Article directly;
+ # KeywordAssociation will be created by the AssociationProxy, and have the
+ # 'keyword' attribute set to the new Keyword.
+ # note that these KeywordAssociation objects will not have a User attached to them.
+ article = Article()
+ article.keywords.append(Keyword('blue'))
+ article.keywords.append(Keyword('red'))
+ session.save(article)
+ session.flush()
+
+ # the "keywords" collection also returns the underlying Keyword objects
+ article = session.query(Article).get_by(id=12)
+ for k in article.keywords:
+ print "Keyword:", k.keyword_name
+
+ # the original 'keyword_associations' relation exists normally with no awareness of the proxy
+ article.keyword_associations.append(KeywordAssociation())
+ print [ka for ka in article.keyword_associations]
+
+Note that the above operations on the `keywords` collection are proxying operations to and from the `keyword_associations` collection, which exists normally and can be accessed directly. `AssociationProxy` will also detect if the collection is list or scalar based and will configure the proxied property to act the same way.
+
+For the common case where the association object's creation needs to be specified by the application, `AssociationProxy` takes an optional callable `creator()` which takes a single associated object as an argument, and returns a new association object.
+
+ {python}
+ def create_keyword_association(keyword):
+ ka = KeywordAssociation()
+ ka.keyword = keyword
+ return ka
+
+ class Article(object):
+ # create "keywords" proxied association
+ keywords = AssociationProxy('keyword_associations', 'keyword', creator=create_keyword_association)
+
+
### threadlocal
**Author:** Mike Bayer and Daniel Miller
--- /dev/null
+"""basic example of using the association object pattern, which is
+a richer form of a many-to-many relationship."""
+
+
+# the model will be an ecommerce example. We will have an
+# Order, which represents a set of Items purchased by a user.
+# each Item has a price. however, the Order must store its own price for
+# each Item, representing the price paid by the user for that particular order, which
+# is independent of the price on each Item (since those can change).
+
+from sqlalchemy import *
+from sqlalchemy.ext.selectresults import SelectResults
+from datetime import datetime
+
+import logging
+logging.basicConfig(format='%(message)s')
+logging.getLogger('sqlalchemy.engine').setLevel(logging.INFO)
+
+engine = create_engine('sqlite://')
+metadata = BoundMetaData(engine)
+
+orders = Table('orders', metadata,
+ Column('order_id', Integer, primary_key=True),
+ Column('customer_name', String(30), nullable=False),
+ Column('order_date', DateTime, nullable=False, default=datetime.now()),
+ )
+
+items = Table('items', metadata,
+ Column('item_id', Integer, primary_key=True),
+ Column('description', String(30), nullable=False),
+ Column('price', Float, nullable=False)
+ )
+
+orderitems = Table('orderitems', metadata,
+ Column('order_id', Integer, ForeignKey('orders.order_id'), primary_key=True),
+ Column('item_id', Integer, ForeignKey('items.item_id'), primary_key=True),
+ Column('price', Float, nullable=False)
+ )
+metadata.create_all()
+
+class Order(object):
+ def __init__(self, customer_name):
+ self.customer_name = customer_name
+
+class Item(object):
+ def __init__(self, description, price):
+ self.description = description
+ self.price = price
+
+class OrderItem(object):
+ def __init__(self, item, price=None):
+ self.item = item
+ self.price = price or item.price
+
+mapper(Order, orders, properties={
+ 'items':relation(OrderItem, cascade="all, delete-orphan", lazy=False)
+})
+mapper(Item, items)
+mapper(OrderItem, orderitems, properties={
+ 'item':relation(Item, lazy=False)
+})
+
+session = create_session()
+
+# create our catalog
+session.save(Item('SA T-Shirt', 10.99))
+session.save(Item('SA Mug', 6.50))
+session.save(Item('SA Hat', 8.99))
+session.save(Item('MySQL Crowbar', 16.99))
+session.flush()
+
+# function to return items from the DB
+def item(name):
+ return session.query(Item).get_by(description=name)
+
+# create an order
+order = Order('john smith')
+
+# add three OrderItem associations to the Order and save
+order.items.append(OrderItem(item('SA Mug')))
+order.items.append(OrderItem(item('MySQL Crowbar'), 10.99))
+order.items.append(OrderItem(item('SA Hat')))
+session.save(order)
+session.flush()
+
+session.clear()
+
+# query the order, print items
+order = session.query(Order).get_by(customer_name='john smith')
+print [(item.item.description, item.price) for item in order.items]
+
+# print customers who bought 'MySQL Crowbar' on sale
+result = SelectResults(session.query(Order)).join_to('item').select(and_(items.c.description=='MySQL Crowbar', items.c.price>orderitems.c.price))
+print [order.customer_name for order in result]
+
+
+
+
+
+
+
+
+
+
--- /dev/null
+"""this is a modified version of the basic association example, which illustrates
+the usage of the associationproxy extension."""
+
+from sqlalchemy import *
+from sqlalchemy.ext.selectresults import SelectResults
+from sqlalchemy.ext.associationproxy import AssociationProxy
+from datetime import datetime
+
+import logging
+logging.basicConfig(format='%(message)s')
+#logging.getLogger('sqlalchemy.engine').setLevel(logging.INFO)
+
+engine = create_engine('sqlite://')
+metadata = BoundMetaData(engine)
+
+orders = Table('orders', metadata,
+ Column('order_id', Integer, primary_key=True),
+ Column('customer_name', String(30), nullable=False),
+ Column('order_date', DateTime, nullable=False, default=datetime.now()),
+ )
+
+items = Table('items', metadata,
+ Column('item_id', Integer, primary_key=True),
+ Column('description', String(30), nullable=False),
+ Column('price', Float, nullable=False)
+ )
+
+orderitems = Table('orderitems', metadata,
+ Column('order_id', Integer, ForeignKey('orders.order_id'), primary_key=True),
+ Column('item_id', Integer, ForeignKey('items.item_id'), primary_key=True),
+ Column('price', Float, nullable=False)
+ )
+metadata.create_all()
+
+class Order(object):
+ def __init__(self, customer_name):
+ self.customer_name = customer_name
+ items = AssociationProxy('itemassociations', 'item', creator=lambda x:OrderItem(x))
+
+class Item(object):
+ def __init__(self, description, price):
+ self.description = description
+ self.price = price
+
+class OrderItem(object):
+ def __init__(self, item, price=None):
+ self.item = item
+ self.price = price or item.price
+
+mapper(Order, orders, properties={
+ 'itemassociations':relation(OrderItem, cascade="all, delete-orphan", lazy=False)
+})
+mapper(Item, items)
+mapper(OrderItem, orderitems, properties={
+ 'item':relation(Item, lazy=False)
+})
+
+session = create_session()
+
+# create our catalog
+session.save(Item('SA T-Shirt', 10.99))
+session.save(Item('SA Mug', 6.50))
+session.save(Item('SA Hat', 8.99))
+session.save(Item('MySQL Crowbar', 16.99))
+session.flush()
+
+# function to return items
+def item(name):
+ return session.query(Item).get_by(description=name)
+
+# create an order
+order = Order('john smith')
+
+# append an OrderItem association via the "itemassociations" collection
+order.itemassociations.append(OrderItem(item('MySQL Crowbar'), 10.99))
+
+# append two more Items via the transparent "items" proxy, which
+# will create OrderItems automatically
+order.items.append(item('SA Mug'))
+order.items.append(item('SA Hat'))
+
+session.save(order)
+session.flush()
+
+session.clear()
+
+# query the order, print items
+order = session.query(Order).get_by(customer_name='john smith')
+
+# print items based on the OrderItem collection directly
+print [(item.item.description, item.price) for item in order.itemassociations]
+
+# print items based on the "proxied" items collection
+print [(item.description, item.price) for item in order.items]
+
+# print customers who bought 'MySQL Crowbar' on sale
+result = SelectResults(session.query(Order)).join_to('item').select(and_(items.c.description=='MySQL Crowbar', items.c.price>orderitems.c.price))
+print [order.customer_name for order in result]
+
+
+
+
+
+
+
+
+
+
from sqlalchemy import *
-import sys, sets
+import sets
# this example illustrates a polymorphic load of two classes, where each class has a very
# different set of properties
print session.new
session.flush()
-#sys.exit()
session.clear()
c = session.query(Company).get(1)
-from sqlalchemy import *
-import datetime
-import sys
-
"""this example illustrates a "vertical table". an object is stored with individual attributes
represented in distinct database rows. This allows objects to be created with dynamically changing
fields that are all persisted in a normalized fashion."""
-
+
+from sqlalchemy import *
+import datetime
+
e = BoundMetaData('sqlite://', echo=True)
# this table represents Entity objects. each Entity gets a row in this table,
--- /dev/null
+"""contains the AssociationProxy class, a Python property object which
+provides transparent proxied access to the endpoint of an association object.
+
+See the example examples/association/proxied_association.py.
+"""
+
+from sqlalchemy.orm import class_mapper
+
+class AssociationProxy(object):
+ """a property object that automatically sets up AssociationLists on a parent object."""
+ def __init__(self, targetcollection, attr, creator=None):
+ """create a new association property.
+
+ targetcollection - the attribute name which stores the collection of Associations
+
+ attr - name of the attribute on the Association in which to get/set target values
+
+ creator - optional callable which is used to create a new association object. this
+ callable is given a single argument which is an instance of the "proxied" object.
+ if creator is not given, the association object is created using the class associated
+ with the targetcollection attribute, using its __init__() constructor and setting
+ the proxied attribute.
+ """
+ self.targetcollection = targetcollection
+ self.attr = attr
+ self.creator = creator
+ def __init_deferred(self):
+ prop = class_mapper(self._owner_class).props[self.targetcollection]
+ self._cls = prop.mapper.class_
+ self._uselist = prop.uselist
+ def _get_class(self):
+ try:
+ return self._cls
+ except AttributeError:
+ self.__init_deferred()
+ return self._cls
+ def _get_uselist(self):
+ try:
+ return self._uselist
+ except AttributeError:
+ self.__init_deferred()
+ return self._uselist
+ cls = property(_get_class)
+ uselist = property(_get_uselist)
+ def create(self, target):
+ if self.creator is not None:
+ return self.creator(target)
+ else:
+ assoc = self.cls()
+ setattr(assoc, self.attr, target)
+ return assoc
+ def __get__(self, obj, owner):
+ self._owner_class = owner
+ if obj is None:
+ return self
+ storage_key = '_AssociationProxy_%s' % self.targetcollection
+ if self.uselist:
+ try:
+ return getattr(obj, storage_key)
+ except AttributeError:
+ a = _AssociationList(self, obj)
+ setattr(obj, storage_key, a)
+ return a
+ else:
+ return getattr(getattr(obj, self.targetcollection), self.attr)
+ def __set__(self, obj, value):
+ if self.uselist:
+ setattr(obj, self.targetcollection, [self.create(x) for x in value])
+ else:
+ setattr(obj, self.targetcollection, self.create(value))
+ def __del__(self, obj):
+ delattr(obj, self.targetcollection)
+
+class _AssociationList(object):
+ """generic proxying list which proxies list operations to a different
+ list-holding attribute of the parent object, converting Association objects
+ to and from a target attribute on each Association object."""
+ def __init__(self, proxy, parent):
+ """create a new AssociationList."""
+ self.proxy = proxy
+ self.parent = parent
+ def append(self, item):
+ a = self.proxy.create(item)
+ getattr(self.parent, self.proxy.targetcollection).append(a)
+ def __iter__(self):
+ return iter([getattr(x, self.proxy.attr) for x in getattr(self.parent, self.proxy.targetcollection)])
+ def __repr__(self):
+ return repr([getattr(x, self.proxy.attr) for x in getattr(self.parent, self.proxy.targetcollection)])
+ def __len__(self):
+ return len(getattr(self.parent, self.proxy.targetcollection))
+ def __getitem__(self, index):
+ return getattr(getattr(self.parent, self.proxy.targetcollection)[index], self.proxy.attr)
+ def __setitem__(self, index, value):
+ a = self.proxy.create(item)
+ getattr(self.parent, self.proxy.targetcollection)[index] = a
+
"""
from sqlalchemy import exceptions
from sqlalchemy.orm.mapper import *
-from sqlalchemy.orm.mapper import mapper_registry, ExtensionOption
+from sqlalchemy.orm import mapper as mapperlib
from sqlalchemy.orm.query import Query
from sqlalchemy.orm.util import polymorphic_union
from sqlalchemy.orm import properties, strategies
from sqlalchemy.orm.interfaces import MapperProperty, MapperOption, OperationContext
import weakref
-__all__ = ['Mapper', 'MapperExtension', 'class_mapper', 'object_mapper', 'EXT_PASS']
+__all__ = ['Mapper', 'MapperExtension', 'class_mapper', 'object_mapper', 'EXT_PASS', 'mapper_registry', 'ExtensionOption']
# a dictionary mapping classes to their primary mappers
mapper_registry = weakref.WeakKeyDictionary()
mapper = self.mapper.primary_mapper()
for c in childlist.added_items() + childlist.deleted_items() + childlist.unchanged_items():
if c is not None and c not in recursive:
+ if not isinstance(c, self.mapper.class_):
+ raise exceptions.AssertionError("Attribute '%s' on class '%s' doesn't handle objects of type '%s'" % (self.key, str(self.parent.class_), str(c.__class__)))
recursive.add(c)
yield c
for c2 in mapper.cascade_iterator(type, c, recursive):
passive = type != 'delete' or self.passive_deletes
for c in sessionlib.attribute_manager.get_as_list(object, self.key, passive=passive):
if c is not None and c not in recursive:
+ if not isinstance(c, self.mapper.class_):
+ raise exceptions.AssertionError("Attribute '%s' on class '%s' doesn't handle objects of type '%s'" % (self.key, str(self.parent.class_), str(c.__class__)))
recursive.add(c)
callable_(c, mapper.entity_name)
mapper.cascade_callable(type, c, callable_, recursive)