primaryjoin, secondaryjoin, secondary, foreign_keys, and remote_side.
{python}
class User(Base):
....
- addresses = relation("Address", backref="user")
-
-Where above we used the string name `"Addresses"` in the event that the `Address` class was not yet defined. We are also free to not define a backref, and to define the `relation()` only on one class and not the other. It is also possible to define two separate `relation()`s for either direction, which is generally safe for many-to-one and one-to-many relations, but not for many-to-many relations.
+ addresses = relation(Address, order_by=Address.id, backref="user")
+
+We are also free to not define a backref, and to define the `relation()` only on one class and not the other. It is also possible to define two separate `relation()`s for either direction, which is generally safe for many-to-one and one-to-many relations, but not for many-to-many relations.
+
+When using the `declarative` extension, `relation()` gives us the option to use strings for most arguments that concern the target class, in the case that the target class has not yet been defined. This **only** works in conjunction with `declarative`:
+
+ {python}
+ class User(Base):
+ ....
+ addresses = relation("Address", order_by="Address.id", backref="user")
+
+When `declarative` is not in use, you typically define your `mapper()` well after the target classes and `Table` objects have been defined, so string expressions are not needed.
We'll need to create the `addresses` table in the database, so we will issue another CREATE from our metadata, which will skip over tables which have already been created:
user_id = Column('user_id', Integer, ForeignKey('users.id'))
user = relation(User, primaryjoin=user_id==User.id)
-When an explicit join condition or other configuration which depends
-on multiple classes cannot be defined immediately due to some classes
-not yet being available, these can be defined after all classes have
-been created. Attributes which are added to the class after
-its creation are associated with the Table/mapping in the same
-way as if they had been defined inline:
+In addition to the main argument for `relation`, other arguments
+which depend upon the columns present on an as-yet undefined class
+may also be specified as strings. These strings are evaluated as
+Python expressions. The full namespace available within this
+evaluation includes all classes mapped for this declarative base,
+as well as the contents of the `sqlalchemy` package, including
+expression functions like `desc` and `func`:
+
+ {python}
+ class User(Base):
+ # ....
+ addresses = relation("Address", order_by="desc(Address.email)",
+ primaryjoin="Address.user_id==User.id")
+
+As an alternative to string-based attributes, attributes may also be
+defined after all classes have been created. Just add them to the target
+class after the fact:
{python}
User.addresses = relation(Address, primaryjoin=Address.user_id==User.id)
user_id = Column(Integer, ForeignKey('users.id'))
user = relation(User, primaryjoin=user_id == User.id)
-When an explicit join condition or other configuration which depends on
-multiple classes cannot be defined immediately due to some classes not yet
-being available, these can be defined after all classes have been created.
-Attributes which are added to the class after its creation are associated with
-the Table/mapping in the same way as if they had been defined inline::
+In addition to the main argument for ``relation``, other arguments
+which depend upon the columns present on an as-yet undefined class
+may also be specified as strings. These strings are evaluated as
+Python expressions. The full namespace available within this
+evaluation includes all classes mapped for this declarative base,
+as well as the contents of the ``sqlalchemy`` package, including
+expression functions like ``desc`` and ``func``::
+
+ class User(Base):
+ # ....
+ addresses = relation("Address", order_by="desc(Address.email)",
+ primaryjoin="Address.user_id==User.id")
+
+As an alternative to string-based attributes, attributes may also be
+defined after all classes have been created. Just add them to the target
+class after the fact::
User.addresses = relation(Address, primaryjoin=Address.user_id == User.id)
"""
from sqlalchemy.schema import Table, Column, MetaData
-from sqlalchemy.orm import synonym as _orm_synonym, mapper, comparable_property
+from sqlalchemy.orm import synonym as _orm_synonym, mapper, comparable_property, class_mapper
from sqlalchemy.orm.interfaces import MapperProperty
from sqlalchemy.orm.properties import PropertyLoader, ColumnProperty
from sqlalchemy import util, exceptions
else:
type.__setattr__(cls, key, value)
+class _GetColumns(object):
+ def __init__(self, cls):
+ self.cls = cls
+ def __getattr__(self, key):
+ mapper = class_mapper(self.cls, compile=False)
+ if not mapper:
+ return getattr(self.cls, key)
+ else:
+ return mapper.get_property(key).columns[0]
+
def _deferred_relation(cls, prop):
- if (isinstance(prop, PropertyLoader) and
- isinstance(prop.argument, basestring)):
- arg = prop.argument
- def return_cls():
+ def resolve_arg(arg):
+ import sqlalchemy
+
+ def access_cls(key):
try:
- return cls._decl_class_registry[arg]
+ return _GetColumns(cls._decl_class_registry[key])
except KeyError:
+ return sqlalchemy.__dict__[key]
+
+ d = util.PopulateDict(access_cls)
+ def return_cls():
+ try:
+ x = eval(arg, globals(), d)
+
+ if isinstance(x, _GetColumns):
+ return x.cls
+ else:
+ return x
+ except NameError, n:
raise exceptions.InvalidRequestError(
- "When compiling mapper %s, could not locate a declarative "
- "class named %r. Consider adding this property to the %r "
+ "When compiling mapper %s, expression %r failed to locate a name (%r). "
+ "If this is a class name, consider adding this relation() to the %r "
"class after both dependent classes have been defined." % (
- prop.parent, arg, prop.parent.class_))
- prop.argument = return_cls
+ prop.parent, arg, n.message, cls))
+ return return_cls
+
+ if isinstance(prop, PropertyLoader):
+ for attr in ('argument', 'order_by', 'primaryjoin', 'secondaryjoin', 'secondary', '_foreign_keys', 'remote_side'):
+ v = getattr(prop, attr)
+ if isinstance(v, basestring):
+ setattr(prop, attr, resolve_arg(v))
return prop
self.direction = None
self.viewonly = viewonly
self.lazy = lazy
- self._foreign_keys = util.to_set(foreign_keys)
+ self._foreign_keys = foreign_keys
self.collection_class = collection_class
self.passive_deletes = passive_deletes
self.passive_updates = passive_updates
- self.remote_side = util.to_set(remote_side)
+ self.remote_side = remote_side
self.enable_typechecks = enable_typechecks
self.comparator = PropertyLoader.Comparator(self)
self.join_depth = join_depth
raise sa_exc.ArgumentError("relation '%s' expects a class or a mapper argument (received: %s)" % (self.key, type(self.argument)))
assert isinstance(self.mapper, mapper.Mapper), self.mapper
+ # accept callables for other attributes which may require deferred initialization
+ for attr in ('order_by', 'primaryjoin', 'secondaryjoin', 'secondary', '_foreign_keys', 'remote_side'):
+ if callable(getattr(self, attr)):
+ setattr(self, attr, getattr(self, attr)())
+
+ self._foreign_keys = util.to_set(self._foreign_keys)
+ self.remote_side = util.to_set(self.remote_side)
+
if not self.parent.concrete:
for inheriting in self.parent.iterate_to_root():
if inheriting is not self.parent and inheriting._get_property(self.key, raiseerr=False):
u = User()
assert User.addresses
assert mapperlib._new_mappers is False
-
+
+ def test_string_dependency_resolution(self):
+ from sqlalchemy.sql import desc
+
+ class User(Base, ComparableEntity):
+ __tablename__ = 'users'
+ id = Column(Integer, primary_key=True)
+ name = Column(String(50))
+ addresses = relation("Address", order_by="desc(Address.email)",
+ primaryjoin="User.id==Address.user_id", foreign_keys="[Address.user_id]")
+
+ class Address(Base, ComparableEntity):
+ __tablename__ = 'addresses'
+ id = Column(Integer, primary_key=True)
+ email = Column(String(50))
+ user_id = Column(Integer) # note no foreign key
+
+ Base.metadata.create_all()
+
+ sess = create_session()
+ u1 = User(name='ed', addresses=[Address(email='abc'), Address(email='def'), Address(email='xyz')])
+ sess.add(u1)
+ sess.flush()
+ sess.clear()
+ self.assertEquals(sess.query(User).filter(User.name == 'ed').one(),
+ User(name='ed', addresses=[Address(email='xyz'), Address(email='def'), Address(email='abc')])
+ )
+
+
def test_nice_dependency_error(self):
class User(Base):
__tablename__ = 'users'