From cd95c479e91fea35894d764a7bc1d82e95236860 Mon Sep 17 00:00:00 2001 From: Mike Bayer Date: Mon, 26 May 2008 23:01:05 +0000 Subject: [PATCH] added string argument resolution to relation() in conjunction with declarative for: order_by, primaryjoin, secondaryjoin, secondary, foreign_keys, and remote_side. --- doc/build/content/ormtutorial.txt | 15 +++++-- doc/build/content/plugins.txt | 23 ++++++++--- lib/sqlalchemy/ext/declarative.py | 69 ++++++++++++++++++++++++------- lib/sqlalchemy/orm/properties.py | 12 +++++- test/ext/declarative.py | 30 +++++++++++++- 5 files changed, 122 insertions(+), 27 deletions(-) diff --git a/doc/build/content/ormtutorial.txt b/doc/build/content/ormtutorial.txt index 411bf64ca1..85c9ba84af 100644 --- a/doc/build/content/ormtutorial.txt +++ b/doc/build/content/ormtutorial.txt @@ -538,9 +538,18 @@ The `relation()` function is extremely flexible, and could just have easily been {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: diff --git a/doc/build/content/plugins.txt b/doc/build/content/plugins.txt index b0eb4ad425..9dc33d3e22 100644 --- a/doc/build/content/plugins.txt +++ b/doc/build/content/plugins.txt @@ -92,12 +92,23 @@ using them: 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) diff --git a/lib/sqlalchemy/ext/declarative.py b/lib/sqlalchemy/ext/declarative.py index b29f051b15..9493281411 100644 --- a/lib/sqlalchemy/ext/declarative.py +++ b/lib/sqlalchemy/ext/declarative.py @@ -95,11 +95,22 @@ where we define a primary join condition on the ``Address`` class using them:: 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) @@ -181,7 +192,7 @@ Mapped instances then make usage of ``Session`` in the usual way. """ 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 @@ -290,20 +301,48 @@ class DeclarativeMeta(type): 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 diff --git a/lib/sqlalchemy/orm/properties.py b/lib/sqlalchemy/orm/properties.py index 08264cd172..98f19131ba 100644 --- a/lib/sqlalchemy/orm/properties.py +++ b/lib/sqlalchemy/orm/properties.py @@ -246,11 +246,11 @@ class PropertyLoader(StrategizedProperty): 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 @@ -518,6 +518,14 @@ class PropertyLoader(StrategizedProperty): 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): diff --git a/test/ext/declarative.py b/test/ext/declarative.py index 984bf0ce97..ebe608c321 100644 --- a/test/ext/declarative.py +++ b/test/ext/declarative.py @@ -81,7 +81,35 @@ class DeclarativeTest(testing.TestBase, testing.AssertsExecutionResults): 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' -- 2.47.3