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