Commits

Mike Bayer committed c5d8823

added string argument resolution to relation() in conjunction with declarative for: order_by,
primaryjoin, secondaryjoin, secondary, foreign_keys, and remote_side.

  • Participants
  • Parent commits 89d2368

Comments (0)

Files changed (5)

File doc/build/content/ormtutorial.txt

     {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:
 

File doc/build/content/plugins.txt

         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)

File lib/sqlalchemy/ext/declarative.py

         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 resolve_arg(arg):
+        import sqlalchemy
+        
+        def access_cls(key):
+            try:
+                return _GetColumns(cls._decl_class_registry[key])
+            except KeyError:
+                return sqlalchemy.__dict__[key]
+
+        d = util.PopulateDict(access_cls)
         def return_cls():
             try:
-                return cls._decl_class_registry[arg]
-            except KeyError:
+                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
 

File lib/sqlalchemy/orm/properties.py

         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):

File test/ext/declarative.py

         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'