From: Mike Bayer Date: Mon, 4 Jul 2022 17:05:37 +0000 (-0400) Subject: move backref to "legacy" X-Git-Tag: rel_2_0_0b1~189^2 X-Git-Url: http://git.ipfire.org/cgi-bin/gitweb.cgi?a=commitdiff_plain;h=d49dfa74e86778eb5c581470405131ed9f9d0206;p=thirdparty%2Fsqlalchemy%2Fsqlalchemy.git 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 --- diff --git a/doc/build/orm/backref.rst b/doc/build/orm/backref.rst index 24587003ea..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 +-------------------------------------------------- -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:: +.. 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. + +.. 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,116 +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. - -Cascade behavior for backrefs -~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ - -It's important to note that even though a bi-directional relationship -may be manipulated from either direction, having the same end effect on the -object structure produced, there is a significant difference in how the -:ref:`save-update cascade ` behaves for two objects -where one is attached and the other is unattached to a :class:`_orm.Session`, -depending on the direction in which the relationships are manipulated. - -The ``save-update`` cascade will only take effect **uni-directionally** -in the direction from a parent object that is already associated with a -:class:`_orm.Session`, towards an object that is being associated with that -parent directly via an attribute or collection on that parent. It won't -take effect if the parent object is instead assigned to an attribute or -collection on the child, in which case the unattached parent object should be -added to the :class:`_orm.Session` explicitly using :meth:`_orm.Session.add`. - -For a complete example of how this looks in practice, see the section -: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 8d8e1f7763..90d12b3cf6 100644 --- a/doc/build/orm/cascades.rst +++ b/doc/build/orm/cascades.rst @@ -122,6 +122,8 @@ for granted; it simplifies code by allowing a single call to that :class:`.Session` at once. While it can be disabled, there is usually not a need to do so. +.. _back_populates_cascade: + .. _backref_cascade: Behavior of save-update cascade with bi-directional relationships @@ -129,7 +131,8 @@ Behavior of save-update cascade with bi-directional relationships The ``save-update`` cascade takes place **uni-directionally** in the context of a bi-directional relationship, i.e. when using -:ref:`backref / back_populates ` to create two separate +the :paramref:`_orm.relationship.back_populates` or :paramref:`_orm.relationship.backref` +parameters to create two separate :func:`_orm.relationship` objects which refer to each other. An object that's not associated with a :class:`_orm.Session`, when assigned to 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/_orm_constructors.py b/lib/sqlalchemy/orm/_orm_constructors.py index bafad09f22..ea95f14202 100644 --- a/lib/sqlalchemy/orm/_orm_constructors.py +++ b/lib/sqlalchemy/orm/_orm_constructors.py @@ -978,41 +978,50 @@ def relationship( 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`. :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 @@ -1876,18 +1885,24 @@ def dynamic_loader( def backref(name: str, **kwargs: Any) -> _ORMBackrefArgument: - """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 """