From 78fa5961bc37689209df41f5512ecb9cf31bc2e2 Mon Sep 17 00:00:00 2001 From: Mike Bayer Date: Mon, 4 Jul 2022 13:05:37 -0400 Subject: [PATCH] move backref to "legacy" in the interests of consistency as well as new typing features, backref should be considered legacy and is fully superseded by back_populates. this commit is for 2.0 /1.4, in 2.0 further updates will be made for new ORM syntaxes. Change-Id: Idd3b7a3b07843b73304df69e476dc4239c60b3f8 (cherry picked from commit d49dfa74e86778eb5c581470405131ed9f9d0206) --- doc/build/orm/backref.rst | 298 +++++++--------------------- doc/build/orm/cascades.rst | 2 + doc/build/orm/relationships.rst | 2 +- lib/sqlalchemy/orm/__init__.py | 16 +- lib/sqlalchemy/orm/relationships.py | 56 +++--- 5 files changed, 119 insertions(+), 255 deletions(-) diff --git a/doc/build/orm/backref.rst b/doc/build/orm/backref.rst index f52b868f8d..edc87cd19d 100644 --- a/doc/build/orm/backref.rst +++ b/doc/build/orm/backref.rst @@ -1,11 +1,43 @@ .. _relationships_backref: -Linking Relationships with Backref ----------------------------------- +Using the legacy 'backref' relationship parameter +-------------------------------------------------- + +.. note:: The :paramref:`_orm.relationship.backref` keyword should be considered + legacy, and use of :paramref:`_orm.relationship.back_populates` with explicit + :func:`_orm.relationship` constructs should be preferred. Using + individual :func:`_orm.relationship` constructs provides advantages + including that both ORM mapped classes will include their attributes + up front as the class is constructed, rather than as a deferred step, + and configuration is more straightforward as all arguments are explicit. + New :pep:`484` features in SQLAlchemy 2.0 also take advantage of + attributes being explicitly present in source code rather than + using dynamic attribute generation. -The :paramref:`_orm.relationship.backref` keyword argument was first introduced in :ref:`ormtutorial_toplevel`, and has been -mentioned throughout many of the examples here. What does it actually do ? Let's start -with the canonical ``User`` and ``Address`` scenario:: +.. seealso:: + + For general information about bidirectional relationships, see the + following sections: + + :ref:`tutorial_orm_related_objects` - in the :ref:`unified_tutorial`, + presents an overview of bi-directional relationship configuration + and behaviors using :paramref:`_orm.relationship.back_populates` + + :ref:`back_populates_cascade` - notes on bi-directional :func:`_orm.relationship` + behavior regarding :class:`_orm.Session` cascade behaviors. + + :paramref:`_orm.relationship.back_populates` + + +The :paramref:`_orm.relationship.backref` keyword argument on the +:func:`_orm.relationship` construct allows the +automatic generation of a new :func:`_orm.relationship` that will be automatically +be added to the ORM mapping for the related class. It will then be +placed into a :paramref:`_orm.relationship.back_populates` configuration +against the current :func:`_orm.relationship` being configured, with both +:func:`_orm.relationship` constructs referring to each other. + +Starting with the following example:: from sqlalchemy import Column, ForeignKey, Integer, String from sqlalchemy.orm import declarative_base, relationship @@ -29,12 +61,8 @@ with the canonical ``User`` and ``Address`` scenario:: The above configuration establishes a collection of ``Address`` objects on ``User`` called ``User.addresses``. It also establishes a ``.user`` attribute on ``Address`` which will -refer to the parent ``User`` object. - -In fact, the :paramref:`_orm.relationship.backref` keyword is only a common shortcut for placing a second -:func:`_orm.relationship` onto the ``Address`` mapping, including the establishment -of an event listener on both sides which will mirror attribute operations -in both directions. The above configuration is equivalent to:: +refer to the parent ``User`` object. Using :paramref:`_orm.relationship.back_populates` +it's equivalent to the following:: from sqlalchemy import Column, ForeignKey, Integer, String from sqlalchemy.orm import declarative_base, relationship @@ -58,68 +86,23 @@ in both directions. The above configuration is equivalent to:: user = relationship("User", back_populates="addresses") -Above, we add a ``.user`` relationship to ``Address`` explicitly. On -both relationships, the :paramref:`_orm.relationship.back_populates` directive tells each relationship -about the other one, indicating that they should establish "bidirectional" -behavior between each other. The primary effect of this configuration -is that the relationship adds event handlers to both attributes -which have the behavior of "when an append or set event occurs here, set ourselves -onto the incoming attribute using this particular attribute name". -The behavior is illustrated as follows. Start with a ``User`` and an ``Address`` -instance. The ``.addresses`` collection is empty, and the ``.user`` attribute -is ``None``:: - - >>> u1 = User() - >>> a1 = Address() - >>> u1.addresses - [] - >>> print(a1.user) - None - -However, once the ``Address`` is appended to the ``u1.addresses`` collection, -both the collection and the scalar attribute have been populated:: - - >>> u1.addresses.append(a1) - >>> u1.addresses - [<__main__.Address object at 0x12a6ed0>] - >>> a1.user - <__main__.User object at 0x12a6590> - -This behavior of course works in reverse for removal operations as well, as well -as for equivalent operations on both sides. Such as -when ``.user`` is set again to ``None``, the ``Address`` object is removed -from the reverse collection:: - - >>> a1.user = None - >>> u1.addresses - [] - -The manipulation of the ``.addresses`` collection and the ``.user`` attribute -occurs entirely in Python without any interaction with the SQL database. -Without this behavior, the proper state would be apparent on both sides once the -data has been flushed to the database, and later reloaded after a commit or -expiration operation occurs. The :paramref:`_orm.relationship.backref`/:paramref:`_orm.relationship.back_populates` behavior has the advantage -that common bidirectional operations can reflect the correct state without requiring -a database round trip. - -Remember, when the :paramref:`_orm.relationship.backref` keyword is used on a single relationship, it's -exactly the same as if the above two relationships were created individually -using :paramref:`_orm.relationship.back_populates` on each. - -Backref Arguments -~~~~~~~~~~~~~~~~~ - -We've established that the :paramref:`_orm.relationship.backref` keyword is merely a shortcut for building -two individual :func:`_orm.relationship` constructs that refer to each other. Part of -the behavior of this shortcut is that certain configurational arguments applied to -the :func:`_orm.relationship` -will also be applied to the other direction - namely those arguments that describe -the relationship at a schema level, and are unlikely to be different in the reverse -direction. The usual case -here is a many-to-many :func:`_orm.relationship` that has a :paramref:`_orm.relationship.secondary` argument, -or a one-to-many or many-to-one which has a :paramref:`_orm.relationship.primaryjoin` argument (the -:paramref:`_orm.relationship.primaryjoin` argument is discussed in :ref:`relationship_primaryjoin`). Such -as if we limited the list of ``Address`` objects to those which start with "tony":: +The behavior of the ``User.addresses`` and ``Address.user`` relationships +is that they now behave in a **bi-directional** way, indicating that +changes on one side of the relationship impact the other. An example +and discussion of this behavior is in the :ref:`unified_tutorial` +at :ref:`tutorial_orm_related_objects`. + + +Backref Default Arguments +~~~~~~~~~~~~~~~~~~~~~~~~~ + +Since :paramref:`_orm.relationship.backref` generates a whole new +:func:`_orm.relationship`, the generation process by default +will attempt to include corresponding arguments in the new +:func:`_orm.relationship` that correspond to the original arguments. +As an example, below is a :func:`_orm.relationship` that includes a +:ref:`custom join condition ` +which also includes the :paramref:`_orm.relationship.backref` keyword:: from sqlalchemy import Column, ForeignKey, Integer, String from sqlalchemy.orm import declarative_base, relationship @@ -147,8 +130,8 @@ as if we limited the list of ``Address`` objects to those which start with "tony email = Column(String) user_id = Column(Integer, ForeignKey("user.id")) -We can observe, by inspecting the resulting property, that both sides -of the relationship have this join condition applied:: +When the "backref" is generated, the :paramref:`_orm.relationship.primaryjoin` +condition is copied to the new :func:`_orm.relationship` as well:: >>> print(User.addresses.property.primaryjoin) "user".id = address.user_id AND address.email LIKE :email_1 || '%%' @@ -157,22 +140,26 @@ of the relationship have this join condition applied:: "user".id = address.user_id AND address.email LIKE :email_1 || '%%' >>> -This reuse of arguments should pretty much do the "right thing" - it -uses only arguments that are applicable, and in the case of a many-to- -many relationship, will reverse the usage of +Other arguments that are transferrable include the +:paramref:`_orm.relationship.secondary` parameter that refers to a +many-to-many association table, as well as the "join" arguments :paramref:`_orm.relationship.primaryjoin` and -:paramref:`_orm.relationship.secondaryjoin` to correspond to the other -direction (see the example in :ref:`self_referential_many_to_many` for -this). +:paramref:`_orm.relationship.secondaryjoin`; "backref" is smart enough to know +that these two arguments should also be "reversed" when generating +the opposite side. + +Specifying Backref Arguments +~~~~~~~~~~~~~~~~~~~~~~~~~~~~ -It's very often the case however that we'd like to specify arguments -that are specific to just the side where we happened to place the -"backref". This includes :func:`_orm.relationship` arguments like +Lots of other arguments for a "backref" are not implicit, and +include arguments like :paramref:`_orm.relationship.lazy`, :paramref:`_orm.relationship.remote_side`, :paramref:`_orm.relationship.cascade` and :paramref:`_orm.relationship.cascade_backrefs`. For this case we use -the :func:`.backref` function in place of a string:: +the :func:`.backref` function in place of a string; this will store +a specific set of arguments that will be transferred to the new +:func:`_orm.relationship` when generated:: # from sqlalchemy.orm import backref @@ -195,144 +182,3 @@ returned ``Address``. The :func:`.backref` function formatted the arguments we it into a form that is interpreted by the receiving :func:`_orm.relationship` as additional arguments to be applied to the new relationship it creates. -Setting cascade for backrefs -~~~~~~~~~~~~~~~~~~~~~~~~~~~~ - -A key behavior that occurs in the 1.x series of SQLAlchemy regarding backrefs -is that :ref:`cascades ` will occur bidirectionally by -default. This basically means, if one starts with an ``User`` object -that's been persisted in the :class:`.Session`:: - - user = session.query(User).filter(User.id == 1).first() - -The above ``User`` is :term:`persistent` in the :class:`.Session`. It usually -is intuitive that if we create an ``Address`` object and append to the -``User.addresses`` collection, it is automatically added to the -:class:`.Session` as in the example below:: - - user = session.query(User).filter(User.id == 1).first() - address = Address(email_address='foo') - user.addresses.append(address) - -The above behavior is known as the "save update cascade" and is described -in the section :ref:`unitofwork_cascades`. - -However, if we instead created a new ``Address`` object, and associated the -``User`` object with the ``Address`` as follows:: - - address = Address(email_address='foo', user=user) - -In the above example, it is **not** as intuitive that the ``Address`` would -automatically be added to the :class:`.Session`. However, the backref behavior -of ``Address.user`` indicates that the ``Address`` object is also appended to -the ``User.addresses`` collection. This in turn initiates a **cascade** -operation which indicates that this ``Address`` should be placed into the -:class:`.Session` as a :term:`pending` object. - -Since this behavior has been identified as counter-intuitive to most people, -it can be disabled by setting :paramref:`_orm.relationship.cascade_backrefs` -to False, as in:: - - - class User(Base): - # ... - - addresses = relationship("Address", back_populates="user", cascade_backrefs=False) - -See the example in :ref:`backref_cascade` for further information. - -.. seealso:: - - :ref:`backref_cascade`. - - -One Way Backrefs -~~~~~~~~~~~~~~~~ - -An unusual case is that of the "one way backref". This is where the -"back-populating" behavior of the backref is only desirable in one -direction. An example of this is a collection which contains a -filtering :paramref:`_orm.relationship.primaryjoin` condition. We'd -like to append items to this collection as needed, and have them -populate the "parent" object on the incoming object. However, we'd -also like to have items that are not part of the collection, but still -have the same "parent" association - these items should never be in -the collection. - -Taking our previous example, where we established a -:paramref:`_orm.relationship.primaryjoin` that limited the collection -only to ``Address`` objects whose email address started with the word -``tony``, the usual backref behavior is that all items populate in -both directions. We wouldn't want this behavior for a case like the -following:: - - >>> u1 = User() - >>> a1 = Address(email='mary') - >>> a1.user = u1 - >>> u1.addresses - [<__main__.Address object at 0x1411910>] - -Above, the ``Address`` object that doesn't match the criterion of "starts with 'tony'" -is present in the ``addresses`` collection of ``u1``. After these objects are flushed, -the transaction committed and their attributes expired for a re-load, the ``addresses`` -collection will hit the database on next access and no longer have this ``Address`` object -present, due to the filtering condition. But we can do away with this unwanted side -of the "backref" behavior on the Python side by using two separate :func:`_orm.relationship` constructs, -placing :paramref:`_orm.relationship.back_populates` only on one side:: - - from sqlalchemy import Column, ForeignKey, Integer, String - from sqlalchemy.orm import declarative_base, relationship - - Base = declarative_base() - - - class User(Base): - __tablename__ = "user" - id = Column(Integer, primary_key=True) - name = Column(String) - - addresses = relationship( - "Address", - primaryjoin="and_(User.id==Address.user_id, " - "Address.email.startswith('tony'))", - back_populates="user", - ) - - - class Address(Base): - __tablename__ = "address" - id = Column(Integer, primary_key=True) - email = Column(String) - user_id = Column(Integer, ForeignKey("user.id")) - - user = relationship("User") - -With the above scenario, appending an ``Address`` object to the ``.addresses`` -collection of a ``User`` will always establish the ``.user`` attribute on that -``Address``:: - - >>> u1 = User() - >>> a1 = Address(email='tony') - >>> u1.addresses.append(a1) - >>> a1.user - <__main__.User object at 0x1411850> - -However, applying a ``User`` to the ``.user`` attribute of an ``Address``, -will not append the ``Address`` object to the collection:: - - >>> a2 = Address(email='mary') - >>> a2.user = u1 - >>> a2 in u1.addresses - False - -Of course, we've disabled some of the usefulness of -:paramref:`_orm.relationship.backref` here, in that when we do append an -``Address`` that corresponds to the criteria of -``email.startswith('tony')``, it won't show up in the -``User.addresses`` collection until the session is flushed, and the -attributes reloaded after a commit or expire operation. While we -could consider an attribute event that checks this criterion in -Python, this starts to cross the line of duplicating too much SQL -behavior in Python. The backref behavior itself is only a slight -transgression of this philosophy - SQLAlchemy tries to keep these to a -minimum overall. diff --git a/doc/build/orm/cascades.rst b/doc/build/orm/cascades.rst index 466c1975ce..3c1180404c 100644 --- a/doc/build/orm/cascades.rst +++ b/doc/build/orm/cascades.rst @@ -570,6 +570,8 @@ expunge from the :class:`.Session` using :meth:`.Session.expunge`, the operation should be propagated down to referred objects. +.. _back_populates_cascade: + .. _backref_cascade: Controlling Cascade on Backrefs diff --git a/doc/build/orm/relationships.rst b/doc/build/orm/relationships.rst index b9111741cc..0c12ba1a4b 100644 --- a/doc/build/orm/relationships.rst +++ b/doc/build/orm/relationships.rst @@ -14,9 +14,9 @@ of its usage. For an introduction to relationships, start with the basic_relationships self_referential - backref join_conditions collections relationship_persistence + backref relationship_api diff --git a/lib/sqlalchemy/orm/__init__.py b/lib/sqlalchemy/orm/__init__.py index 78650507ee..6e0de05c6d 100644 --- a/lib/sqlalchemy/orm/__init__.py +++ b/lib/sqlalchemy/orm/__init__.py @@ -172,18 +172,24 @@ composite = public_factory(CompositeProperty, ".orm.composite") def backref(name, **kwargs): - """Create a back reference with explicit keyword arguments, which are the - same arguments one can send to :func:`relationship`. + """When using the :paramref:`_orm.relationship.backref` parameter, + provides specific parameters to be used when the new + :func:`_orm.relationship` is generated. - Used with the ``backref`` keyword argument to :func:`relationship` in - place of a string argument, e.g.:: + E.g.:: 'items':relationship( SomeItem, backref=backref('parent', lazy='subquery')) + The :paramref:`_orm.relationship.backref` parameter is generally + considered to be legacy; for modern applications, using + explicit :func:`_orm.relationship` constructs linked together using + the :paramref:`_orm.relationship.back_populates` parameter should be + preferred. + .. seealso:: - :ref:`relationships_backref` + :ref:`relationships_backref` - background on backrefs """ diff --git a/lib/sqlalchemy/orm/relationships.py b/lib/sqlalchemy/orm/relationships.py index f58277e32e..b51ea0e009 100644 --- a/lib/sqlalchemy/orm/relationships.py +++ b/lib/sqlalchemy/orm/relationships.py @@ -311,41 +311,51 @@ class RelationshipProperty(StrategizedProperty): the "previous" value of the attribute. :param backref: - Indicates the string name of a property to be placed on the related - mapper's class that will handle this relationship in the other - direction. The other property will be created automatically - when the mappers are configured. Can also be passed as a - :func:`.backref` object to control the configuration of the - new relationship. + A reference to a string relationship name, or a :func:`_orm.backref` + construct, which will be used to automatically generate a new + :func:`_orm.relationship` on the related class, which then refers to + this one using a bi-directional + :paramref:`_orm.relationship.back_populates` configuration. + + In modern Python, explicit use of :func:`_orm.relationship` with + :paramref:`_orm.relationship.back_populates` should be preferred, as + it is more robust in terms of mapper configuration as well as more + conceptually straightforward. It also integrates with new :pep:`484` + typing features introduced in SQLAlchemy 2.0 which is not possible + with dynamically generated attributes. .. seealso:: - :ref:`relationships_backref` - Introductory documentation and - examples. + :ref:`relationships_backref` - notes on using + :paramref:`_orm.relationship.backref` - :paramref:`_orm.relationship.back_populates` - alternative form - of backref specification. + :ref:`tutorial_orm_related_objects` - in the + :ref:`unified_tutorial`, presents an overview of bi-directional + relationship configuration and behaviors using + :paramref:`_orm.relationship.back_populates` - :func:`.backref` - allows control over :func:`_orm.relationship` - configuration when using :paramref:`_orm.relationship.backref`. + :func:`.backref` - allows control over :func:`_orm.relationship` + configuration when using :paramref:`_orm.relationship.backref`. :param back_populates: - Takes a string name and has the same meaning as - :paramref:`_orm.relationship.backref`, except the complementing - property is **not** created automatically, and instead must be - configured explicitly on the other mapper. The complementing - property should also indicate - :paramref:`_orm.relationship.back_populates` to this relationship to - ensure proper functioning. + Indicates the name of a :func:`_orm.relationship` on the related + class that will be synchronized with this one. It is usually + expected that the :func:`_orm.relationship` on the related class + also refer to this one. This allows objects on both sides of + each :func:`_orm.relationship` to synchronize in-Python state + changes and also provides directives to the :term:`unit of work` + flush process how changes along these relationships should + be persisted. .. seealso:: - :ref:`relationships_backref` - Introductory documentation and - examples. + :ref:`tutorial_orm_related_objects` - in the + :ref:`unified_tutorial`, presents an overview of bi-directional + relationship configuration and behaviors. - :paramref:`_orm.relationship.backref` - alternative form - of backref specification. + :ref:`relationship_patterns` - includes many examples of + :paramref:`_orm.relationship.back_populates`. :param overlaps: A string name or comma-delimited set of names of other relationships -- 2.47.2