Using the legacy ‘backref’ relationship parameter

Note

The relationship.backref keyword should be considered legacy, and use of relationship.back_populates with explicit relationship() constructs should be preferred. Using individual 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.

See also

For general information about bidirectional relationships, see the following sections:

Working with ORM Related Objects - in the SQLAlchemy Unified Tutorial, presents an overview of bi-directional relationship configuration and behaviors using relationship.back_populates

Behavior of save-update cascade with bi-directional relationships - notes on bi-directional relationship() behavior regarding Session cascade behaviors.

relationship.back_populates

The relationship.backref keyword argument on the relationship() construct allows the automatic generation of a new relationship() that will be automatically be added to the ORM mapping for the related class. It will then be placed into a relationship.back_populates configuration against the current relationship() being configured, with both relationship() constructs referring to each other.

Starting with the following example:

from sqlalchemy import Column, ForeignKey, Integer, String
from sqlalchemy.orm import DeclarativeBase, relationship


class Base(DeclarativeBase):
    pass


class User(Base):
    __tablename__ = "user"
    id = mapped_column(Integer, primary_key=True)
    name = mapped_column(String)

    addresses = relationship("Address", backref="user")


class Address(Base):
    __tablename__ = "address"
    id = mapped_column(Integer, primary_key=True)
    email = mapped_column(String)
    user_id = mapped_column(Integer, ForeignKey("user.id"))

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. Using relationship.back_populates it’s equivalent to the following:

from sqlalchemy import Column, ForeignKey, Integer, String
from sqlalchemy.orm import DeclarativeBase, relationship


class Base(DeclarativeBase):
    pass


class User(Base):
    __tablename__ = "user"
    id = mapped_column(Integer, primary_key=True)
    name = mapped_column(String)

    addresses = relationship("Address", back_populates="user")


class Address(Base):
    __tablename__ = "address"
    id = mapped_column(Integer, primary_key=True)
    email = mapped_column(String)
    user_id = mapped_column(Integer, ForeignKey("user.id"))

    user = relationship("User", back_populates="addresses")

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 SQLAlchemy Unified Tutorial at Working with ORM Related Objects.

Backref Default Arguments

Since relationship.backref generates a whole new relationship(), the generation process by default will attempt to include corresponding arguments in the new relationship() that correspond to the original arguments. As an example, below is a relationship() that includes a custom join condition which also includes the relationship.backref keyword:

from sqlalchemy import Column, ForeignKey, Integer, String
from sqlalchemy.orm import DeclarativeBase, relationship


class Base(DeclarativeBase):
    pass


class User(Base):
    __tablename__ = "user"
    id = mapped_column(Integer, primary_key=True)
    name = mapped_column(String)

    addresses = relationship(
        "Address",
        primaryjoin=(
            "and_(User.id==Address.user_id, Address.email.startswith('tony'))"
        ),
        backref="user",
    )


class Address(Base):
    __tablename__ = "address"
    id = mapped_column(Integer, primary_key=True)
    email = mapped_column(String)
    user_id = mapped_column(Integer, ForeignKey("user.id"))

When the “backref” is generated, the relationship.primaryjoin condition is copied to the new relationship() as well:

>>> print(User.addresses.property.primaryjoin)
"user".id = address.user_id AND address.email LIKE :email_1 || '%%'
>>>
>>> print(Address.user.property.primaryjoin)
"user".id = address.user_id AND address.email LIKE :email_1 || '%%'
>>>

Other arguments that are transferrable include the relationship.secondary parameter that refers to a many-to-many association table, as well as the “join” arguments relationship.primaryjoin and relationship.secondaryjoin; “backref” is smart enough to know that these two arguments should also be “reversed” when generating the opposite side.

Specifying Backref Arguments

Lots of other arguments for a “backref” are not implicit, and include arguments like relationship.lazy, relationship.remote_side, relationship.cascade and relationship.cascade_backrefs. For this case we use the backref() function in place of a string; this will store a specific set of arguments that will be transferred to the new relationship() when generated:

# <other imports>
from sqlalchemy.orm import backref


class User(Base):
    __tablename__ = "user"
    id = mapped_column(Integer, primary_key=True)
    name = mapped_column(String)

    addresses = relationship(
        "Address",
        backref=backref("user", lazy="joined"),
    )

Where above, we placed a lazy="joined" directive only on the Address.user side, indicating that when a query against Address is made, a join to the User entity should be made automatically which will populate the .user attribute of each returned Address. The backref() function formatted the arguments we gave it into a form that is interpreted by the receiving relationship() as additional arguments to be applied to the new relationship it creates.

Back to Top