1.. _relationship_configure_joins:
2
3Configuring how Relationship Joins
4----------------------------------
5
6:func:`.relationship` will normally create a join between two tables
7by examining the foreign key relationship between the two tables
8to determine which columns should be compared.  There are a variety
9of situations where this behavior needs to be customized.
10
11.. _relationship_foreign_keys:
12
13Handling Multiple Join Paths
14~~~~~~~~~~~~~~~~~~~~~~~~~~~~
15
16One of the most common situations to deal with is when
17there are more than one foreign key path between two tables.
18
19Consider a ``Customer`` class that contains two foreign keys to an ``Address``
20class::
21
22    from sqlalchemy import Integer, ForeignKey, String, Column
23    from sqlalchemy.ext.declarative import declarative_base
24    from sqlalchemy.orm import relationship
25
26    Base = declarative_base()
27
28    class Customer(Base):
29        __tablename__ = 'customer'
30        id = Column(Integer, primary_key=True)
31        name = Column(String)
32
33        billing_address_id = Column(Integer, ForeignKey("address.id"))
34        shipping_address_id = Column(Integer, ForeignKey("address.id"))
35
36        billing_address = relationship("Address")
37        shipping_address = relationship("Address")
38
39    class Address(Base):
40        __tablename__ = 'address'
41        id = Column(Integer, primary_key=True)
42        street = Column(String)
43        city = Column(String)
44        state = Column(String)
45        zip = Column(String)
46
47The above mapping, when we attempt to use it, will produce the error::
48
49    sqlalchemy.exc.AmbiguousForeignKeysError: Could not determine join
50    condition between parent/child tables on relationship
51    Customer.billing_address - there are multiple foreign key
52    paths linking the tables.  Specify the 'foreign_keys' argument,
53    providing a list of those columns which should be
54    counted as containing a foreign key reference to the parent table.
55
56The above message is pretty long.  There are many potential messages
57that :func:`.relationship` can return, which have been carefully tailored
58to detect a variety of common configurational issues; most will suggest
59the additional configuration that's needed to resolve the ambiguity
60or other missing information.
61
62In this case, the message wants us to qualify each :func:`.relationship`
63by instructing for each one which foreign key column should be considered, and
64the appropriate form is as follows::
65
66    class Customer(Base):
67        __tablename__ = 'customer'
68        id = Column(Integer, primary_key=True)
69        name = Column(String)
70
71        billing_address_id = Column(Integer, ForeignKey("address.id"))
72        shipping_address_id = Column(Integer, ForeignKey("address.id"))
73
74        billing_address = relationship("Address", foreign_keys=[billing_address_id])
75        shipping_address = relationship("Address", foreign_keys=[shipping_address_id])
76
77Above, we specify the ``foreign_keys`` argument, which is a :class:`.Column` or list
78of :class:`.Column` objects which indicate those columns to be considered "foreign",
79or in other words, the columns that contain a value referring to a parent table.
80Loading the ``Customer.billing_address`` relationship from a ``Customer``
81object will use the value present in ``billing_address_id`` in order to
82identify the row in ``Address`` to be loaded; similarly, ``shipping_address_id``
83is used for the ``shipping_address`` relationship.   The linkage of the two
84columns also plays a role during persistence; the newly generated primary key
85of a just-inserted ``Address`` object will be copied into the appropriate
86foreign key column of an associated ``Customer`` object during a flush.
87
88When specifying ``foreign_keys`` with Declarative, we can also use string
89names to specify, however it is important that if using a list, the **list
90is part of the string**::
91
92        billing_address = relationship("Address", foreign_keys="[Customer.billing_address_id]")
93
94In this specific example, the list is not necessary in any case as there's only
95one :class:`.Column` we need::
96
97        billing_address = relationship("Address", foreign_keys="Customer.billing_address_id")
98
99.. versionchanged:: 0.8
100    :func:`.relationship` can resolve ambiguity between foreign key targets on the
101    basis of the ``foreign_keys`` argument alone; the :paramref:`~.relationship.primaryjoin`
102    argument is no longer needed in this situation.
103
104.. _relationship_primaryjoin:
105
106Specifying Alternate Join Conditions
107~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
108
109The default behavior of :func:`.relationship` when constructing a join
110is that it equates the value of primary key columns
111on one side to that of foreign-key-referring columns on the other.
112We can change this criterion to be anything we'd like using the
113:paramref:`~.relationship.primaryjoin`
114argument, as well as the :paramref:`~.relationship.secondaryjoin`
115argument in the case when a "secondary" table is used.
116
117In the example below, using the ``User`` class
118as well as an ``Address`` class which stores a street address,  we
119create a relationship ``boston_addresses`` which will only
120load those ``Address`` objects which specify a city of "Boston"::
121
122    from sqlalchemy import Integer, ForeignKey, String, Column
123    from sqlalchemy.ext.declarative import declarative_base
124    from sqlalchemy.orm import relationship
125
126    Base = declarative_base()
127
128    class User(Base):
129        __tablename__ = 'user'
130        id = Column(Integer, primary_key=True)
131        name = Column(String)
132        boston_addresses = relationship("Address",
133                        primaryjoin="and_(User.id==Address.user_id, "
134                            "Address.city=='Boston')")
135
136    class Address(Base):
137        __tablename__ = 'address'
138        id = Column(Integer, primary_key=True)
139        user_id = Column(Integer, ForeignKey('user.id'))
140
141        street = Column(String)
142        city = Column(String)
143        state = Column(String)
144        zip = Column(String)
145
146Within this string SQL expression, we made use of the :func:`.and_` conjunction construct to establish
147two distinct predicates for the join condition - joining both the ``User.id`` and
148``Address.user_id`` columns to each other, as well as limiting rows in ``Address``
149to just ``city='Boston'``.   When using Declarative, rudimentary SQL functions like
150:func:`.and_` are automatically available in the evaluated namespace of a string
151:func:`.relationship` argument.
152
153The custom criteria we use in a :paramref:`~.relationship.primaryjoin`
154is generally only significant when SQLAlchemy is rendering SQL in
155order to load or represent this relationship. That is, it's used in
156the SQL statement that's emitted in order to perform a per-attribute
157lazy load, or when a join is constructed at query time, such as via
158:meth:`.Query.join`, or via the eager "joined" or "subquery" styles of
159loading.   When in-memory objects are being manipulated, we can place
160any ``Address`` object we'd like into the ``boston_addresses``
161collection, regardless of what the value of the ``.city`` attribute
162is.   The objects will remain present in the collection until the
163attribute is expired and re-loaded from the database where the
164criterion is applied.   When a flush occurs, the objects inside of
165``boston_addresses`` will be flushed unconditionally, assigning value
166of the primary key ``user.id`` column onto the foreign-key-holding
167``address.user_id`` column for each row.  The ``city`` criteria has no
168effect here, as the flush process only cares about synchronizing
169primary key values into referencing foreign key values.
170
171.. _relationship_custom_foreign:
172
173Creating Custom Foreign Conditions
174~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
175
176Another element of the primary join condition is how those columns
177considered "foreign" are determined.  Usually, some subset
178of :class:`.Column` objects will specify :class:`.ForeignKey`, or otherwise
179be part of a :class:`.ForeignKeyConstraint` that's relevant to the join condition.
180:func:`.relationship` looks to this foreign key status as it decides
181how it should load and persist data for this relationship.   However, the
182:paramref:`~.relationship.primaryjoin` argument can be used to create a join condition that
183doesn't involve any "schema" level foreign keys.  We can combine :paramref:`~.relationship.primaryjoin`
184along with :paramref:`~.relationship.foreign_keys` and :paramref:`~.relationship.remote_side` explicitly in order to
185establish such a join.
186
187Below, a class ``HostEntry`` joins to itself, equating the string ``content``
188column to the ``ip_address`` column, which is a PostgreSQL type called ``INET``.
189We need to use :func:`.cast` in order to cast one side of the join to the
190type of the other::
191
192    from sqlalchemy import cast, String, Column, Integer
193    from sqlalchemy.orm import relationship
194    from sqlalchemy.dialects.postgresql import INET
195
196    from sqlalchemy.ext.declarative import declarative_base
197
198    Base = declarative_base()
199
200    class HostEntry(Base):
201        __tablename__ = 'host_entry'
202
203        id = Column(Integer, primary_key=True)
204        ip_address = Column(INET)
205        content = Column(String(50))
206
207        # relationship() using explicit foreign_keys, remote_side
208        parent_host = relationship("HostEntry",
209                            primaryjoin=ip_address == cast(content, INET),
210                            foreign_keys=content,
211                            remote_side=ip_address
212                        )
213
214The above relationship will produce a join like::
215
216    SELECT host_entry.id, host_entry.ip_address, host_entry.content
217    FROM host_entry JOIN host_entry AS host_entry_1
218    ON host_entry_1.ip_address = CAST(host_entry.content AS INET)
219
220An alternative syntax to the above is to use the :func:`.foreign` and
221:func:`.remote` :term:`annotations`,
222inline within the :paramref:`~.relationship.primaryjoin` expression.
223This syntax represents the annotations that :func:`.relationship` normally
224applies by itself to the join condition given the :paramref:`~.relationship.foreign_keys` and
225:paramref:`~.relationship.remote_side` arguments.  These functions may
226be more succinct when an explicit join condition is present, and additionally
227serve to mark exactly the column that is "foreign" or "remote" independent
228of whether that column is stated multiple times or within complex
229SQL expressions::
230
231    from sqlalchemy.orm import foreign, remote
232
233    class HostEntry(Base):
234        __tablename__ = 'host_entry'
235
236        id = Column(Integer, primary_key=True)
237        ip_address = Column(INET)
238        content = Column(String(50))
239
240        # relationship() using explicit foreign() and remote() annotations
241        # in lieu of separate arguments
242        parent_host = relationship("HostEntry",
243                            primaryjoin=remote(ip_address) == \
244                                    cast(foreign(content), INET),
245                        )
246
247
248.. _relationship_custom_operator:
249
250Using custom operators in join conditions
251~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
252
253Another use case for relationships is the use of custom operators, such
254as PostgreSQL's "is contained within" ``<<`` operator when joining with
255types such as :class:`.postgresql.INET` and :class:`.postgresql.CIDR`.
256For custom operators we use the :meth:`.Operators.op` function::
257
258    inet_column.op("<<")(cidr_column)
259
260However, if we construct a :paramref:`~.relationship.primaryjoin` using this
261operator, :func:`.relationship` will still need more information.  This is because
262when it examines our primaryjoin condition, it specifically looks for operators
263used for **comparisons**, and this is typically a fixed list containing known
264comparison operators such as ``==``, ``<``, etc.   So for our custom operator
265to participate in this system, we need it to register as a comparison operator
266using the :paramref:`~.Operators.op.is_comparison` parameter::
267
268    inet_column.op("<<", is_comparison=True)(cidr_column)
269
270A complete example::
271
272    class IPA(Base):
273        __tablename__ = 'ip_address'
274
275        id = Column(Integer, primary_key=True)
276        v4address = Column(INET)
277
278        network = relationship("Network",
279                            primaryjoin="IPA.v4address.op('<<', is_comparison=True)"
280                                "(foreign(Network.v4representation))",
281                            viewonly=True
282                        )
283    class Network(Base):
284        __tablename__ = 'network'
285
286        id = Column(Integer, primary_key=True)
287        v4representation = Column(CIDR)
288
289Above, a query such as::
290
291    session.query(IPA).join(IPA.network)
292
293Will render as::
294
295    SELECT ip_address.id AS ip_address_id, ip_address.v4address AS ip_address_v4address
296    FROM ip_address JOIN network ON ip_address.v4address << network.v4representation
297
298.. versionadded:: 0.9.2 - Added the :paramref:`.Operators.op.is_comparison`
299   flag to assist in the creation of :func:`.relationship` constructs using
300   custom operators.
301
302.. _relationship_overlapping_foreignkeys:
303
304Overlapping Foreign Keys
305~~~~~~~~~~~~~~~~~~~~~~~~
306
307A rare scenario can arise when composite foreign keys are used, such that
308a single column may be the subject of more than one column
309referred to via foreign key constraint.
310
311Consider an (admittedly complex) mapping such as the ``Magazine`` object,
312referred to both by the ``Writer`` object and the ``Article`` object
313using a composite primary key scheme that includes ``magazine_id``
314for both; then to make ``Article`` refer to ``Writer`` as well,
315``Article.magazine_id`` is involved in two separate relationships;
316``Article.magazine`` and ``Article.writer``::
317
318    class Magazine(Base):
319        __tablename__ = 'magazine'
320
321        id = Column(Integer, primary_key=True)
322
323
324    class Article(Base):
325        __tablename__ = 'article'
326
327        article_id = Column(Integer)
328        magazine_id = Column(ForeignKey('magazine.id'))
329        writer_id = Column()
330
331        magazine = relationship("Magazine")
332        writer = relationship("Writer")
333
334        __table_args__ = (
335            PrimaryKeyConstraint('article_id', 'magazine_id'),
336            ForeignKeyConstraint(
337                ['writer_id', 'magazine_id'],
338                ['writer.id', 'writer.magazine_id']
339            ),
340        )
341
342
343    class Writer(Base):
344        __tablename__ = 'writer'
345
346        id = Column(Integer, primary_key=True)
347        magazine_id = Column(ForeignKey('magazine.id'), primary_key=True)
348        magazine = relationship("Magazine")
349
350When the above mapping is configured, we will see this warning emitted::
351
352    SAWarning: relationship 'Article.writer' will copy column
353    writer.magazine_id to column article.magazine_id,
354    which conflicts with relationship(s): 'Article.magazine'
355    (copies magazine.id to article.magazine_id). Consider applying
356    viewonly=True to read-only relationships, or provide a primaryjoin
357    condition marking writable columns with the foreign() annotation.
358
359What this refers to originates from the fact that ``Article.magazine_id`` is
360the subject of two different foreign key constraints; it refers to
361``Magazine.id`` directly as a source column, but also refers to
362``Writer.magazine_id`` as a source column in the context of the
363composite key to ``Writer``.   If we associate an ``Article`` with a
364particular ``Magazine``, but then associate the ``Article`` with a
365``Writer`` that's  associated  with a *different* ``Magazine``, the ORM
366will overwrite ``Article.magazine_id`` non-deterministically, silently
367changing which magazine we refer towards; it may
368also attempt to place NULL into this columnn if we de-associate a
369``Writer`` from an ``Article``.  The warning lets us know this is the case.
370
371To solve this, we need to break out the behavior of ``Article`` to include
372all three of the following features:
373
3741. ``Article`` first and foremost writes to
375   ``Article.magazine_id`` based on data persisted in the ``Article.magazine``
376   relationship only, that is a value copied from ``Magazine.id``.
377
3782. ``Article`` can write to ``Article.writer_id`` on behalf of data
379   persisted in the  ``Article.writer`` relationship, but only the
380   ``Writer.id`` column; the ``Writer.magazine_id`` column should not
381   be written into ``Article.magazine_id`` as it ultimately is sourced
382   from ``Magazine.id``.
383
3843. ``Article`` takes ``Article.magazine_id`` into account when loading
385   ``Article.writer``, even though it *doesn't* write to it on behalf
386   of this relationship.
387
388To get just #1 and #2, we could specify only ``Article.writer_id`` as the
389"foreign keys" for ``Article.writer``::
390
391    class Article(Base):
392        # ...
393
394        writer = relationship("Writer", foreign_keys='Article.writer_id')
395
396However, this has the effect of ``Article.writer`` not taking
397``Article.magazine_id`` into account when querying against ``Writer``:
398
399.. sourcecode:: sql
400
401    SELECT article.article_id AS article_article_id,
402        article.magazine_id AS article_magazine_id,
403        article.writer_id AS article_writer_id
404    FROM article
405    JOIN writer ON writer.id = article.writer_id
406
407Therefore, to get at all of #1, #2, and #3, we express the join condition
408as well as which columns to be written by combining
409:paramref:`~.relationship.primaryjoin` fully, along with either the
410:paramref:`~.relationship.foreign_keys` argument, or more succinctly by
411annotating with :func:`~.orm.foreign`::
412
413    class Article(Base):
414        # ...
415
416        writer = relationship(
417            "Writer",
418            primaryjoin="and_(Writer.id == foreign(Article.writer_id), "
419                        "Writer.magazine_id == Article.magazine_id)")
420
421.. versionchanged:: 1.0.0 the ORM will attempt to warn when a column is used
422   as the synchronization target from more than one relationship
423   simultaneously.
424
425
426Non-relational Comparisons / Materialized Path
427~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
428
429.. warning::  this section details an experimental feature.
430
431Using custom expressions means we can produce unorthodox join conditions that
432don't obey the usual primary/foreign key model.  One such example is the
433materialized path pattern, where we compare strings for overlapping path tokens
434in order to produce a tree structure.
435
436Through careful use of :func:`.foreign` and :func:`.remote`, we can build
437a relationship that effectively produces a rudimentary materialized path
438system.   Essentially, when :func:`.foreign` and :func:`.remote` are
439on the *same* side of the comparison expression, the relationship is considered
440to be "one to many"; when they are on *different* sides, the relationship
441is considered to be "many to one".   For the comparison we'll use here,
442we'll be dealing with collections so we keep things configured as "one to many"::
443
444    class Element(Base):
445        __tablename__ = 'element'
446
447        path = Column(String, primary_key=True)
448
449        descendants = relationship('Element',
450                               primaryjoin=
451                                    remote(foreign(path)).like(
452                                            path.concat('/%')),
453                               viewonly=True,
454                               order_by=path)
455
456Above, if given an ``Element`` object with a path attribute of ``"/foo/bar2"``,
457we seek for a load of ``Element.descendants`` to look like::
458
459    SELECT element.path AS element_path
460    FROM element
461    WHERE element.path LIKE ('/foo/bar2' || '/%') ORDER BY element.path
462
463.. versionadded:: 0.9.5 Support has been added to allow a single-column
464   comparison to itself within a primaryjoin condition, as well as for
465   primaryjoin conditions that use :meth:`.ColumnOperators.like` as the comparison
466   operator.
467
468.. _self_referential_many_to_many:
469
470Self-Referential Many-to-Many Relationship
471~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
472
473Many to many relationships can be customized by one or both of :paramref:`~.relationship.primaryjoin`
474and :paramref:`~.relationship.secondaryjoin` - the latter is significant for a relationship that
475specifies a many-to-many reference using the :paramref:`~.relationship.secondary` argument.
476A common situation which involves the usage of :paramref:`~.relationship.primaryjoin` and :paramref:`~.relationship.secondaryjoin`
477is when establishing a many-to-many relationship from a class to itself, as shown below::
478
479    from sqlalchemy import Integer, ForeignKey, String, Column, Table
480    from sqlalchemy.ext.declarative import declarative_base
481    from sqlalchemy.orm import relationship
482
483    Base = declarative_base()
484
485    node_to_node = Table("node_to_node", Base.metadata,
486        Column("left_node_id", Integer, ForeignKey("node.id"), primary_key=True),
487        Column("right_node_id", Integer, ForeignKey("node.id"), primary_key=True)
488    )
489
490    class Node(Base):
491        __tablename__ = 'node'
492        id = Column(Integer, primary_key=True)
493        label = Column(String)
494        right_nodes = relationship("Node",
495                            secondary=node_to_node,
496                            primaryjoin=id==node_to_node.c.left_node_id,
497                            secondaryjoin=id==node_to_node.c.right_node_id,
498                            backref="left_nodes"
499        )
500
501Where above, SQLAlchemy can't know automatically which columns should connect
502to which for the ``right_nodes`` and ``left_nodes`` relationships.   The :paramref:`~.relationship.primaryjoin`
503and :paramref:`~.relationship.secondaryjoin` arguments establish how we'd like to join to the association table.
504In the Declarative form above, as we are declaring these conditions within the Python
505block that corresponds to the ``Node`` class, the ``id`` variable is available directly
506as the :class:`.Column` object we wish to join with.
507
508Alternatively, we can define the :paramref:`~.relationship.primaryjoin`
509and :paramref:`~.relationship.secondaryjoin` arguments using strings, which is suitable
510in the case that our configuration does not have either the ``Node.id`` column
511object available yet or the ``node_to_node`` table perhaps isn't yet available.
512When referring to a plain :class:`.Table` object in a declarative string, we
513use the string name of the table as it is present in the :class:`.MetaData`::
514
515    class Node(Base):
516        __tablename__ = 'node'
517        id = Column(Integer, primary_key=True)
518        label = Column(String)
519        right_nodes = relationship("Node",
520                            secondary="node_to_node",
521                            primaryjoin="Node.id==node_to_node.c.left_node_id",
522                            secondaryjoin="Node.id==node_to_node.c.right_node_id",
523                            backref="left_nodes"
524        )
525
526A classical mapping situation here is similar, where ``node_to_node`` can be joined
527to ``node.c.id``::
528
529    from sqlalchemy import Integer, ForeignKey, String, Column, Table, MetaData
530    from sqlalchemy.orm import relationship, mapper
531
532    metadata = MetaData()
533
534    node_to_node = Table("node_to_node", metadata,
535        Column("left_node_id", Integer, ForeignKey("node.id"), primary_key=True),
536        Column("right_node_id", Integer, ForeignKey("node.id"), primary_key=True)
537    )
538
539    node = Table("node", metadata,
540        Column('id', Integer, primary_key=True),
541        Column('label', String)
542    )
543    class Node(object):
544        pass
545
546    mapper(Node, node, properties={
547        'right_nodes':relationship(Node,
548                            secondary=node_to_node,
549                            primaryjoin=node.c.id==node_to_node.c.left_node_id,
550                            secondaryjoin=node.c.id==node_to_node.c.right_node_id,
551                            backref="left_nodes"
552                        )})
553
554
555Note that in both examples, the :paramref:`~.relationship.backref`
556keyword specifies a ``left_nodes`` backref - when
557:func:`.relationship` creates the second relationship in the reverse
558direction, it's smart enough to reverse the
559:paramref:`~.relationship.primaryjoin` and
560:paramref:`~.relationship.secondaryjoin` arguments.
561
562.. _composite_secondary_join:
563
564Composite "Secondary" Joins
565~~~~~~~~~~~~~~~~~~~~~~~~~~~
566
567.. note::
568
569    This section features some new and experimental features of SQLAlchemy.
570
571Sometimes, when one seeks to build a :func:`.relationship` between two tables
572there is a need for more than just two or three tables to be involved in
573order to join them.  This is an area of :func:`.relationship` where one seeks
574to push the boundaries of what's possible, and often the ultimate solution to
575many of these exotic use cases needs to be hammered out on the SQLAlchemy mailing
576list.
577
578In more recent versions of SQLAlchemy, the :paramref:`~.relationship.secondary`
579parameter can be used in some of these cases in order to provide a composite
580target consisting of multiple tables.   Below is an example of such a
581join condition (requires version 0.9.2 at least to function as is)::
582
583    class A(Base):
584        __tablename__ = 'a'
585
586        id = Column(Integer, primary_key=True)
587        b_id = Column(ForeignKey('b.id'))
588
589        d = relationship("D",
590                    secondary="join(B, D, B.d_id == D.id)."
591                                "join(C, C.d_id == D.id)",
592                    primaryjoin="and_(A.b_id == B.id, A.id == C.a_id)",
593                    secondaryjoin="D.id == B.d_id",
594                    uselist=False
595                    )
596
597    class B(Base):
598        __tablename__ = 'b'
599
600        id = Column(Integer, primary_key=True)
601        d_id = Column(ForeignKey('d.id'))
602
603    class C(Base):
604        __tablename__ = 'c'
605
606        id = Column(Integer, primary_key=True)
607        a_id = Column(ForeignKey('a.id'))
608        d_id = Column(ForeignKey('d.id'))
609
610    class D(Base):
611        __tablename__ = 'd'
612
613        id = Column(Integer, primary_key=True)
614
615In the above example, we provide all three of :paramref:`~.relationship.secondary`,
616:paramref:`~.relationship.primaryjoin`, and :paramref:`~.relationship.secondaryjoin`,
617in the declarative style referring to the named tables ``a``, ``b``, ``c``, ``d``
618directly.  A query from ``A`` to ``D`` looks like:
619
620.. sourcecode:: python+sql
621
622    sess.query(A).join(A.d).all()
623
624    {opensql}SELECT a.id AS a_id, a.b_id AS a_b_id
625    FROM a JOIN (
626        b AS b_1 JOIN d AS d_1 ON b_1.d_id = d_1.id
627            JOIN c AS c_1 ON c_1.d_id = d_1.id)
628        ON a.b_id = b_1.id AND a.id = c_1.a_id JOIN d ON d.id = b_1.d_id
629
630In the above example, we take advantage of being able to stuff multiple
631tables into a "secondary" container, so that we can join across many
632tables while still keeping things "simple" for :func:`.relationship`, in that
633there's just "one" table on both the "left" and the "right" side; the
634complexity is kept within the middle.
635
636.. versionadded:: 0.9.2  Support is improved for allowing a :func:`.join()`
637   construct to be used directly as the target of the :paramref:`~.relationship.secondary`
638   argument, including support for joins, eager joins and lazy loading,
639   as well as support within declarative to specify complex conditions such
640   as joins involving class names as targets.
641
642.. _relationship_non_primary_mapper:
643
644Relationship to Non Primary Mapper
645~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
646
647In the previous section, we illustrated a technique where we used
648:paramref:`~.relationship.secondary` in order to place additional
649tables within a join condition.   There is one complex join case where
650even this technique is not sufficient; when we seek to join from ``A``
651to ``B``, making use of any number of ``C``, ``D``, etc. in between,
652however there are also join conditions between ``A`` and ``B``
653*directly*.  In this case, the join from ``A`` to ``B`` may be
654difficult to express with just a complex
655:paramref:`~.relationship.primaryjoin` condition, as the intermediary
656tables may need special handling, and it is also not expressable with
657a :paramref:`~.relationship.secondary` object, since the
658``A->secondary->B`` pattern does not support any references between
659``A`` and ``B`` directly.  When this **extremely advanced** case
660arises, we can resort to creating a second mapping as a target for the
661relationship.  This is where we use :func:`.mapper` in order to make a
662mapping to a class that includes all the additional tables we need for
663this join. In order to produce this mapper as an "alternative" mapping
664for our class, we use the :paramref:`~.mapper.non_primary` flag.
665
666Below illustrates a :func:`.relationship` with a simple join from ``A`` to
667``B``, however the primaryjoin condition is augmented with two additional
668entities ``C`` and ``D``, which also must have rows that line up with
669the rows in both ``A`` and ``B`` simultaneously::
670
671    class A(Base):
672        __tablename__ = 'a'
673
674        id = Column(Integer, primary_key=True)
675        b_id = Column(ForeignKey('b.id'))
676
677    class B(Base):
678        __tablename__ = 'b'
679
680        id = Column(Integer, primary_key=True)
681
682    class C(Base):
683        __tablename__ = 'c'
684
685        id = Column(Integer, primary_key=True)
686        a_id = Column(ForeignKey('a.id'))
687
688    class D(Base):
689        __tablename__ = 'd'
690
691        id = Column(Integer, primary_key=True)
692        c_id = Column(ForeignKey('c.id'))
693        b_id = Column(ForeignKey('b.id'))
694
695    # 1. set up the join() as a variable, so we can refer
696    # to it in the mapping multiple times.
697    j = join(B, D, D.b_id == B.id).join(C, C.id == D.c_id)
698
699    # 2. Create a new mapper() to B, with non_primary=True.
700    # Columns in the join with the same name must be
701    # disambiguated within the mapping, using named properties.
702    B_viacd = mapper(B, j, non_primary=True, properties={
703        "b_id": [j.c.b_id, j.c.d_b_id],
704        "d_id": j.c.d_id
705        })
706
707    A.b = relationship(B_viacd, primaryjoin=A.b_id == B_viacd.c.b_id)
708
709In the above case, our non-primary mapper for ``B`` will emit for
710additional columns when we query; these can be ignored:
711
712.. sourcecode:: python+sql
713
714    sess.query(A).join(A.b).all()
715
716    {opensql}SELECT a.id AS a_id, a.b_id AS a_b_id
717    FROM a JOIN (b JOIN d ON d.b_id = b.id JOIN c ON c.id = d.c_id) ON a.b_id = b.id
718
719
720Building Query-Enabled Properties
721~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
722
723Very ambitious custom join conditions may fail to be directly persistable, and
724in some cases may not even load correctly. To remove the persistence part of
725the equation, use the flag :paramref:`~.relationship.viewonly` on the
726:func:`~sqlalchemy.orm.relationship`, which establishes it as a read-only
727attribute (data written to the collection will be ignored on flush()).
728However, in extreme cases, consider using a regular Python property in
729conjunction with :class:`.Query` as follows:
730
731.. sourcecode:: python
732
733    class User(Base):
734        __tablename__ = 'user'
735        id = Column(Integer, primary_key=True)
736
737        def _get_addresses(self):
738            return object_session(self).query(Address).with_parent(self).filter(...).all()
739        addresses = property(_get_addresses)
740
741