From: Mike Bayer Date: Thu, 23 Jan 2014 01:16:47 +0000 (-0500) Subject: - Support is improved for supplying a :func:`.join` construct as the X-Git-Tag: rel_0_9_2~35 X-Git-Url: http://git.ipfire.org/gitweb.cgi?a=commitdiff_plain;h=743ceb045e8be8b606198f4d5c02a72abebb2fbb;p=thirdparty%2Fsqlalchemy%2Fsqlalchemy.git - Support is improved for supplying a :func:`.join` construct as the target of :paramref:`.relationship.secondary` for the purposes of creating very complex :func:`.relationship` join conditions. The change includes adjustments to query joining, joined eager loading to not render a SELECT subquery, changes to lazy loading such that the "secondary" target is properly included in the SELECT, and changes to declarative to better support specification of a join() object with classes as targets. --- diff --git a/doc/build/changelog/changelog_09.rst b/doc/build/changelog/changelog_09.rst index 7683265284..893e3970fe 100644 --- a/doc/build/changelog/changelog_09.rst +++ b/doc/build/changelog/changelog_09.rst @@ -14,6 +14,25 @@ .. changelog:: :version: 0.9.2 + .. change:: + :tags: feature, orm + + Support is improved for supplying a :func:`.join` construct as the + target of :paramref:`.relationship.secondary` for the purposes + of creating very complex :func:`.relationship` join conditions. + The change includes adjustments to query joining, joined eager loading + to not render a SELECT subquery, changes to lazy loading such that + the "secondary" target is properly included in the SELECT, and + changes to declarative to better support specification of a + join() object with classes as targets. + + The new use case is somewhat experimental, but a new documentation section + has been added. + + .. seealso:: + + :ref:`composite_secondary_join` + .. change:: :tags: bug, mysql, sql :tickets: 2917 diff --git a/doc/build/orm/mapper_config.rst b/doc/build/orm/mapper_config.rst index 17bd31a6f4..22f483c7e2 100644 --- a/doc/build/orm/mapper_config.rst +++ b/doc/build/orm/mapper_config.rst @@ -1307,11 +1307,13 @@ table metadata. One potential use case for another mapper to exist at the same time is if we wanted to load instances of our class not just from the immediate :class:`.Table` to which it is mapped, but from another selectable that is a derivation of that -:class:`.Table`. While there technically is a way to create such a :func:`.mapper`, -using the ``non_primary=True`` option, this approach is virtually never needed. -Instead, we use the functionality of the :class:`.Query` object to achieve this, -using a method such as :meth:`.Query.select_from` -or :meth:`.Query.from_statement` to specify a derived selectable. +:class:`.Table`. To create a second mapper that only handles querying +when used explicitly, we can use the :paramref:`.mapper.non_primary` argument. +In practice, this approach is usually not needed, as we +can do this sort of thing at query time using methods such as +:meth:`.Query.select_from`, however it is useful in the rare case that we +wish to build a :func:`.relationship` to such a mapper. An example of this is +at :ref:`relationship_non_primary_mapper`. Another potential use is if we genuinely want instances of our class to be persisted into different tables at different times; certain kinds of diff --git a/doc/build/orm/relationships.rst b/doc/build/orm/relationships.rst index 67a41c8087..cdff640641 100644 --- a/doc/build/orm/relationships.rst +++ b/doc/build/orm/relationships.rst @@ -948,9 +948,10 @@ Specifying Alternate Join Conditions The default behavior of :func:`.relationship` when constructing a join is that it equates the value of primary key columns on one side to that of foreign-key-referring columns on the other. -We can change this criterion to be anything we'd like using the ``primaryjoin`` -argument, as well as the ``secondaryjoin`` argument in the case when -a "secondary" table is used. +We can change this criterion to be anything we'd like using the +:paramref:`.relationship.primaryjoin` +argument, as well as the :paramref:`.relationship.secondaryjoin` +argument in the case when a "secondary" table is used. In the example below, using the ``User`` class as well as an ``Address`` class which stores a street address, we @@ -1149,6 +1150,163 @@ Note that in both examples, the ``backref`` keyword specifies a ``left_nodes`` backref - when :func:`.relationship` creates the second relationship in the reverse direction, it's smart enough to reverse the ``primaryjoin`` and ``secondaryjoin`` arguments. +.. _composite_secondary_join: + +Composite "Secondary" Joins +~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +.. note:: + + This section features some new and experimental features of SQLAlchemy. + +Sometimes, when one seeks to build a :func:`.relationship` between two tables +there is a need for more than just two or three tables to be involved in +order to join them. This is an area of :func:`.relationship` where one seeks +to push the boundaries of what's possible, and often the ultimate solution to +many of these exotic use cases needs to be hammered out on the SQLAlchemy mailing +list. + +In more recent versions of SQLAlchemy, the :paramref:`.relationship.secondary` +parameter can be used in some of these cases in order to provide a composite +target consisting of multiple tables. Below is an example of such a +join condition (requires version 0.9.2 at least to function as is):: + + class A(Base): + __tablename__ = 'a' + + id = Column(Integer, primary_key=True) + b_id = Column(ForeignKey('b.id')) + + d = relationship("D", + secondary="join(B, D, B.d_id == D.id)." + "join(C, C.d_id == D.id)", + primaryjoin="and_(A.b_id == B.id, A.id == C.a_id)", + secondaryjoin="D.id == B.d_id", + uselist=False + ) + + class B(Base): + __tablename__ = 'b' + + id = Column(Integer, primary_key=True) + d_id = Column(ForeignKey('d.id')) + + class C(Base): + __tablename__ = 'c' + + id = Column(Integer, primary_key=True) + a_id = Column(ForeignKey('a.id')) + d_id = Column(ForeignKey('d.id')) + + class D(Base): + __tablename__ = 'd' + + id = Column(Integer, primary_key=True) + +In the above example, we provide all three of :paramref:`.relationship.secondary`, +:paramref:`.relationship.primaryjoin`, and :paramref:`.relationship.secondaryjoin`, +in the declarative style referring to the named tables ``a``, ``b``, ``c``, ``d`` +directly. A query from ``A`` to ``D`` looks like: + +.. sourcecode:: python+sql + + sess.query(A).join(A.d).all() + + {opensql}SELECT a.id AS a_id, a.b_id AS a_b_id + FROM a JOIN ( + b AS b_1 JOIN d AS d_1 ON b_1.d_id = d_1.id + JOIN c AS c_1 ON c_1.d_id = d_1.id) + ON a.b_id = b_1.id AND a.id = c_1.a_id JOIN d ON d.id = b_1.d_id + +In the above example, we take advantage of being able to stuff multiple +tables into a "secondary" container, so that we can join across many +tables while still keeping things "simple" for :func:`.relationship`, in that +there's just "one" table on both the "left" and the "right" side; the +complexity is kept within the middle. + +.. versionadded:: 0.9.2 Support is improved for allowing a :func:`.join()` + construct to be used directly as the target of the :paramref:`.relationship.secondary` + argument, including support for joins, eager joins and lazy loading, + as well as support within declarative to specify complex conditions such + as joins involving class names as targets. + +.. _relationship_non_primary_mapper: + +Relationship to Non Primary Mapper +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +In the previous section, we illustrated a technique where we used +:paramref:`.relationship.secondary` in order to place additional tables +within a join condition. There is one complex join case where even this technique is not +sufficient; when we seek to join from ``A`` to ``B``, making use of any +number of ``C``, ``D``, etc. in between, however there are also join conditions +between ``A`` and ``B`` *directly*. In this case, the join from ``A`` to ``B`` +may be difficult to express with just a complex ``primaryjoin`` condition, as the +intermediary tables may need special handling, and it is also not expressable +with a ``secondary`` object, since the ``A->secondary->B`` pattern does not support +any references between ``A`` and ``B`` directly. When this **extremely advanced** +case arises, we can resort to creating a second mapping as a target for +the relationship. This is where we use :func:`.mapper` in order to make a mapping to +a class that includes all the additional tables we need for this join. +In order to produce this mapper as an "alternative" mapping for our class, +we use the :paramref:`.mapper.non_primary` flag. + +Below illustrates a :func:`.relationship` with a simple join from ``A`` to +``B``, however the primaryjoin condition is augmented with two additional +entities ``C`` and ``D``, which also must have rows that line up with +the rows in both ``A`` and ``B`` simultaneously:: + + class A(Base): + __tablename__ = 'a' + + id = Column(Integer, primary_key=True) + b_id = Column(ForeignKey('b.id')) + + class B(Base): + __tablename__ = 'b' + + id = Column(Integer, primary_key=True) + + class C(Base): + __tablename__ = 'c' + + id = Column(Integer, primary_key=True) + a_id = Column(ForeignKey('a.id')) + + class D(Base): + __tablename__ = 'd' + + id = Column(Integer, primary_key=True) + c_id = Column(ForeignKey('c.id')) + b_id = Column(ForeignKey('b.id')) + + # 1. set up the join() as a variable, so we can refer + # to it in the mapping multiple times. + j = join(B, D, D.b_id == B.id).join(C, C.id == D.c_id) + + # 2. Create a new mapper() to B, with non_primary=True. + # Columns in the join with the same name must be + # disambiguated within the mapping, using named properties. + B_viacd = mapper(B, j, non_primary=True, properties={ + "b_id": [j.c.b_id, j.c.d_b_id], + "d_id": j.c.d_id + }) + + A.b = relationship(B_viacd, primaryjoin=A.b_id == B_viacd.c.b_id) + +In the above case, our non-primary mapper for ``B`` will emit for +additional columns when we query; these can be ignored: + +.. sourcecode:: python+sql + + sess.query(A).join(A.b).all() + + {opensql}SELECT a.id AS a_id, a.b_id AS a_b_id, d_1.id AS d_1_id, + b_1.id AS b_1_id, d_1.b_id AS d_1_b_id, d_1.c_id AS d_1_c_id, + c_1.id AS c_1_id, c_1.a_id AS c_1_a_id + FROM a LEFT OUTER JOIN (b AS b_1 JOIN d AS d_1 ON d_1.b_id = b_1.id + JOIN c AS c_1 ON c_1.id = d_1.c_id) ON a.b_id = b_1.id + Building Query-Enabled Properties ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ diff --git a/lib/sqlalchemy/ext/declarative/clsregistry.py b/lib/sqlalchemy/ext/declarative/clsregistry.py index fda1cffb5c..8b846746f5 100644 --- a/lib/sqlalchemy/ext/declarative/clsregistry.py +++ b/lib/sqlalchemy/ext/declarative/clsregistry.py @@ -14,6 +14,7 @@ from ...orm.properties import ColumnProperty, RelationshipProperty, \ from ...schema import _get_table_key from ...orm import class_mapper, interfaces from ... import util +from ... import inspection from ... import exc import weakref @@ -207,6 +208,9 @@ class _GetColumns(object): " directly to a Column)." % key) return getattr(self.cls, key) +inspection._inspects(_GetColumns)( + lambda target: inspection.inspect(target.cls)) + class _GetTable(object): def __init__(self, key, metadata): diff --git a/lib/sqlalchemy/orm/mapper.py b/lib/sqlalchemy/orm/mapper.py index a853efc3ff..26f105bec4 100644 --- a/lib/sqlalchemy/orm/mapper.py +++ b/lib/sqlalchemy/orm/mapper.py @@ -292,8 +292,12 @@ class Mapper(_InspectionAttr): mapping of the class to an alternate selectable, for loading only. - The ``non_primary`` feature is rarely needed with modern - usage. + :paramref:`.Mapper.non_primary` is not an often used option, but + is useful in some specific :func:`.relationship` cases. + + .. seealso:: + + :ref:`relationship_non_primary_mapper` :param order_by: A single :class:`.Column` or list of :class:`.Column` objects for which selection operations should use as the default diff --git a/lib/sqlalchemy/orm/relationships.py b/lib/sqlalchemy/orm/relationships.py index 6fdedd3828..62d4d6b6c5 100644 --- a/lib/sqlalchemy/orm/relationships.py +++ b/lib/sqlalchemy/orm/relationships.py @@ -111,11 +111,14 @@ class RelationshipProperty(StrategizedProperty): strategy_class=None, _local_remote_pairs=None, query_class=None, info=None): - """Provide a relationship of a primary Mapper to a secondary Mapper. + """Provide a relationship between two mapped classes. This corresponds to a parent-child or associative table relationship. The constructed class is an instance of :class:`.RelationshipProperty`. + For an overview of basic patterns with :func:`.relationship` as + used with Declarative, see :ref:`relationship_patterns`. + A typical :func:`.relationship`, used in a classical mapping:: mapper(Parent, properties={ @@ -165,8 +168,11 @@ class RelationshipProperty(StrategizedProperty): :param secondary: for a many-to-many relationship, specifies the intermediary - table, and is an instance of :class:`.Table`. The ``secondary`` keyword - argument should generally only + table, and is typically an instance of :class:`.Table`. + In less common circumstances, the argument may also be specified + as an :class:`.Alias` construct, or even a :class:`.Join` construct. + + The ``secondary`` keyword argument should generally only be used for a table that is not otherwise expressed in any class mapping, unless this relationship is declared as view only, otherwise conflicting persistence operations can occur. @@ -175,6 +181,13 @@ class RelationshipProperty(StrategizedProperty): also be passed as a callable function which is evaluated at mapper initialization time. + .. seealso:: + + :ref:`relationships_many_to_many` + + .. versionadded:: 0.9.2 :paramref:`.relationship.secondary` works + more effectively when referring to a :class:`.Join` instance. + :param active_history=False: When ``True``, indicates that the "previous" value for a many-to-one reference should be loaded when replaced, if @@ -562,6 +575,10 @@ class RelationshipProperty(StrategizedProperty): which is evaluated at mapper initialization time, and may be passed as a Python-evaluable string when using Declarative. + .. seealso:: + + :ref:`relationship_primaryjoin` + :param remote_side: used for self-referential relationships, indicates the column or list of columns that form the "remote side" of the relationship. @@ -593,6 +610,10 @@ class RelationshipProperty(StrategizedProperty): which is evaluated at mapper initialization time, and may be passed as a Python-evaluable string when using Declarative. + .. seealso:: + + :ref:`relationship_primaryjoin` + :param single_parent=(True|False): when True, installs a validator which will prevent objects from being associated with more than one parent at a time. @@ -2422,7 +2443,7 @@ class JoinCondition(object): if aliased: if secondary is not None: - secondary = secondary.alias() + secondary = secondary.alias(flat=True) primary_aliasizer = ClauseAdapter(secondary) secondary_aliasizer = \ ClauseAdapter(dest_selectable, diff --git a/lib/sqlalchemy/orm/strategies.py b/lib/sqlalchemy/orm/strategies.py index 033e3d0644..bd9b02d24b 100644 --- a/lib/sqlalchemy/orm/strategies.py +++ b/lib/sqlalchemy/orm/strategies.py @@ -527,6 +527,10 @@ class LazyLoader(AbstractRelationshipLoader): def _emit_lazyload(self, strategy_options, session, state, ident_key, passive): q = session.query(self.mapper)._adapt_all_clauses() + + if self.parent_property.secondary is not None: + q = q.select_from(self.mapper, self.parent_property.secondary) + q = q._with_invoke_all_eagers(False) pending = not state.key diff --git a/test/ext/declarative/test_basic.py b/test/ext/declarative/test_basic.py index 1f14d81645..496aad3697 100644 --- a/test/ext/declarative/test_basic.py +++ b/test/ext/declarative/test_basic.py @@ -409,6 +409,46 @@ class DeclarativeTest(DeclarativeTestBase): "user_1.firstname || :firstname_1 || user_1.lastname" ) + def test_string_dependency_resolution_asselectable(self): + class A(Base): + __tablename__ = 'a' + + id = Column(Integer, primary_key=True) + b_id = Column(ForeignKey('b.id')) + + d = relationship("D", + secondary="join(B, D, B.d_id == D.id)." + "join(C, C.d_id == D.id)", + primaryjoin="and_(A.b_id == B.id, A.id == C.a_id)", + secondaryjoin="D.id == B.d_id", + ) + + class B(Base): + __tablename__ = 'b' + + id = Column(Integer, primary_key=True) + d_id = Column(ForeignKey('d.id')) + + class C(Base): + __tablename__ = 'c' + + id = Column(Integer, primary_key=True) + a_id = Column(ForeignKey('a.id')) + d_id = Column(ForeignKey('d.id')) + + class D(Base): + __tablename__ = 'd' + + id = Column(Integer, primary_key=True) + s = Session() + self.assert_compile( + s.query(A).join(A.d), + "SELECT a.id AS a_id, a.b_id AS a_b_id FROM a JOIN " + "(b AS b_1 JOIN d AS d_1 ON b_1.d_id = d_1.id " + "JOIN c AS c_1 ON c_1.d_id = d_1.id) ON a.b_id = b_1.id " + "AND a.id = c_1.a_id JOIN d ON d.id = b_1.d_id", + ) + def test_string_dependency_resolution_no_table(self): class User(Base, fixtures.ComparableEntity): diff --git a/test/orm/inheritance/test_manytomany.py b/test/orm/inheritance/test_manytomany.py index 51b7979408..e3d2c90dea 100644 --- a/test/orm/inheritance/test_manytomany.py +++ b/test/orm/inheritance/test_manytomany.py @@ -216,11 +216,13 @@ class InheritTest3(fixtures.MappedTest): class Blub(Bar): def __repr__(self): - return "Blub id %d, data %s, bars %s, foos %s" % (self.id, self.data, repr([b for b in self.bars]), repr([f for f in self.foos])) + return "Blub id %d, data %s, bars %s, foos %s" % ( + self.id, self.data, repr([b for b in self.bars]), + repr([f for f in self.foos])) mapper(Blub, blub, inherits=Bar, properties={ - 'bars':relationship(Bar, secondary=blub_bar, lazy='joined'), - 'foos':relationship(Foo, secondary=blub_foo, lazy='joined'), + 'bars': relationship(Bar, secondary=blub_bar, lazy='joined'), + 'foos': relationship(Foo, secondary=blub_foo, lazy='joined'), }) sess = create_session() diff --git a/test/orm/test_joins.py b/test/orm/test_joins.py index 5f48b39b14..e0eb7c3e01 100644 --- a/test/orm/test_joins.py +++ b/test/orm/test_joins.py @@ -2376,3 +2376,5 @@ class SelfReferentialM2MTest(fixtures.MappedTest): ])).order_by(Node.id).all(), [Node(data='n1'), Node(data='n2')] ) + + diff --git a/test/orm/test_relationships.py b/test/orm/test_relationships.py index 717f136c0c..8f7e2bd557 100644 --- a/test/orm/test_relationships.py +++ b/test/orm/test_relationships.py @@ -8,7 +8,7 @@ from sqlalchemy.orm import mapper, relationship, relation, \ backref, create_session, configure_mappers, \ clear_mappers, sessionmaker, attributes,\ Session, composite, column_property, foreign,\ - remote, synonym + remote, synonym, joinedload from sqlalchemy.orm.interfaces import ONETOMANY, MANYTOONE, MANYTOMANY from sqlalchemy.testing import eq_, startswith_, AssertsCompiledSQL, is_ from sqlalchemy.testing import fixtures @@ -2523,6 +2523,191 @@ class AmbiguousFKResolutionTest(_RelationshipErrors, fixtures.MappedTest): sa.orm.configure_mappers() +class SecondaryNestedJoinTest(fixtures.MappedTest, AssertsCompiledSQL, + testing.AssertsExecutionResults): + """test support for a relationship where the 'secondary' table is a + compound join(). + + join() and joinedload() should use a "flat" alias, lazyloading needs + to ensure the join renders. + + """ + run_setup_mappers = 'once' + run_inserts = 'once' + run_deletes = None + + @classmethod + def define_tables(cls, metadata): + Table('a', metadata, + Column('id', Integer, primary_key=True, test_needs_autoincrement=True), + Column('name', String(30)), + Column('b_id', ForeignKey('b.id')) + ) + Table('b', metadata, + Column('id', Integer, primary_key=True, test_needs_autoincrement=True), + Column('name', String(30)), + Column('d_id', ForeignKey('d.id')) + ) + Table('c', metadata, + Column('id', Integer, primary_key=True, test_needs_autoincrement=True), + Column('name', String(30)), + Column('a_id', ForeignKey('a.id')), + Column('d_id', ForeignKey('d.id')) + ) + Table('d', metadata, + Column('id', Integer, primary_key=True, test_needs_autoincrement=True), + Column('name', String(30)), + ) + + @classmethod + def setup_classes(cls): + class A(cls.Comparable): + pass + class B(cls.Comparable): + pass + class C(cls.Comparable): + pass + class D(cls.Comparable): + pass + + @classmethod + def setup_mappers(cls): + A, B, C, D = cls.classes.A, cls.classes.B, cls.classes.C, cls.classes.D + a, b, c, d = cls.tables.a, cls.tables.b, cls.tables.c, cls.tables.d + j = sa.join(b, d, b.c.d_id == d.c.id).join(c, c.c.d_id == d.c.id) + #j = join(b, d, b.c.d_id == d.c.id).join(c, c.c.d_id == d.c.id).alias() + mapper(A, a, properties={ + "b": relationship(B), + "d": relationship(D, secondary=j, + primaryjoin=and_(a.c.b_id == b.c.id, a.c.id == c.c.a_id), + secondaryjoin=d.c.id == b.c.d_id, + #primaryjoin=and_(a.c.b_id == j.c.b_id, a.c.id == j.c.c_a_id), + #secondaryjoin=d.c.id == j.c.b_d_id, + uselist=False + ) + }) + mapper(B, b, properties={ + "d": relationship(D) + }) + mapper(C, c, properties={ + "a": relationship(A), + "d": relationship(D) + }) + mapper(D, d) + + @classmethod + def insert_data(cls): + A, B, C, D = cls.classes.A, cls.classes.B, cls.classes.C, cls.classes.D + sess = Session() + a1, a2, a3, a4 = A(name='a1'), A(name='a2'), A(name='a3'), A(name='a4') + b1, b2, b3, b4 = B(name='b1'), B(name='b2'), B(name='b3'), B(name='b4') + c1, c2, c3, c4 = C(name='c1'), C(name='c2'), C(name='c3'), C(name='c4') + d1, d2 = D(name='d1'), D(name='d2') + + a1.b = b1 + a2.b = b2 + a3.b = b3 + a4.b = b4 + + c1.a = a1 + c2.a = a2 + c3.a = a2 + c4.a = a4 + + c1.d = d1 + c2.d = d2 + c3.d = d1 + c4.d = d2 + + b1.d = d1 + b2.d = d1 + b3.d = d2 + b4.d = d2 + + sess.add_all([a1, a2, a3, a4, b1, b2, b3, b4, c1, c2, c4, c4, d1, d2]) + sess.commit() + + def test_render_join(self): + A, D = self.classes.A, self.classes.D + sess = Session() + self.assert_compile( + sess.query(A).join(A.d), + "SELECT a.id AS a_id, a.name AS a_name, a.b_id AS a_b_id " + "FROM a JOIN (b AS b_1 JOIN d AS d_1 ON b_1.d_id = d_1.id " + "JOIN c AS c_1 ON c_1.d_id = d_1.id) ON a.b_id = b_1.id " + "AND a.id = c_1.a_id JOIN d ON d.id = b_1.d_id", + dialect="postgresql" + ) + + def test_render_joinedload(self): + A, D = self.classes.A, self.classes.D + sess = Session() + self.assert_compile( + sess.query(A).options(joinedload(A.d)), + "SELECT a.id AS a_id, a.name AS a_name, a.b_id AS a_b_id, " + "d_1.id AS d_1_id, d_1.name AS d_1_name FROM a LEFT OUTER JOIN " + "(b AS b_1 JOIN d AS d_2 ON b_1.d_id = d_2.id JOIN c AS c_1 " + "ON c_1.d_id = d_2.id JOIN d AS d_1 ON d_1.id = b_1.d_id) " + "ON a.b_id = b_1.id AND a.id = c_1.a_id", + dialect="postgresql" + ) + + def test_render_lazyload(self): + from sqlalchemy.testing.assertsql import CompiledSQL + + A, D = self.classes.A, self.classes.D + sess = Session() + a1 = sess.query(A).filter(A.name == 'a1').first() + + def go(): + a1.d + + # here, the "lazy" strategy has to ensure the "secondary" + # table is part of the "select_from()", since it's a join(). + # referring to just the columns wont actually render all those + # join conditions. + self.assert_sql_execution( + testing.db, + go, + CompiledSQL( + "SELECT d.id AS d_id, d.name AS d_name FROM b " + "JOIN d ON b.d_id = d.id JOIN c ON c.d_id = d.id " + "WHERE :param_1 = b.id AND :param_2 = c.a_id AND d.id = b.d_id", + {'param_1': a1.id, 'param_2': a1.id} + ) + ) + + mapping = { + "a1": "d1", + "a2": None, + "a3": None, + "a4": "d2" + } + + def test_join(self): + A, D = self.classes.A, self.classes.D + sess = Session() + + for a, d in sess.query(A, D).outerjoin(A.d): + eq_(self.mapping[a.name], d.name if d is not None else None) + + + def test_joinedload(self): + A, D = self.classes.A, self.classes.D + sess = Session() + + for a in sess.query(A).options(joinedload(A.d)): + d = a.d + eq_(self.mapping[a.name], d.name if d is not None else None) + + def test_lazyload(self): + A, D = self.classes.A, self.classes.D + sess = Session() + + for a in sess.query(A): + d = a.d + eq_(self.mapping[a.name], d.name if d is not None else None) + class InvalidRelationshipEscalationTest(_RelationshipErrors, fixtures.MappedTest): @classmethod