SQLAlchemy 1.4 / 2.0 Tutorial

This page is part of the SQLAlchemy 1.4 / 2.0 Tutorial.

Previous: Data Manipulation with the ORM | Next: Further Reading

Working with Related Objects

In this section, we will cover one more essential ORM concept, which is how the ORM interacts with mapped classes that refer to other objects. In the section Declaring Mapped Classes, the mapped class examples made use of a construct called relationship(). This construct defines a linkage between two different mapped classes, or from a mapped class to itself, the latter of which is called a self-referential relationship.

To describe the basic idea of relationship(), first we’ll review the mapping in short form, omitting the Column mappings and other directives:

  1. from sqlalchemy.orm import relationship
  2. class User(Base):
  3. __tablename__ = 'user_account'
  4. # ... Column mappings
  5. addresses = relationship("Address", back_populates="user")
  6. class Address(Base):
  7. __tablename__ = 'address'
  8. # ... Column mappings
  9. user = relationship("User", back_populates="addresses")

Above, the User class now has an attribute User.addresses and the Address class has an attribute Address.user. The relationship() construct will be used to inspect the table relationships between the Table objects that are mapped to the User and Address classes. As the Table object representing the address table has a ForeignKeyConstraint which refers to the user_account table, the relationship() can determine unambiguously that there is as one to many relationship from User.addresses to User; one particular row in the user_account table may be referred towards by many rows in the address table.

All one-to-many relationships naturally correspond to a many to one relationship in the other direction, in this case the one noted by Address.user. The relationship.back_populates parameter, seen above configured on both relationship() objects referring to the other name, establishes that each of these two relationship() constructs should be considered to be complimentary to each other; we will see how this plays out in the next section.

Persisting and Loading Relationships

We can start by illustrating what relationship() does to instances of objects. If we make a new User object, we can note that there is a Python list when we access the .addresses element:

  1. >>> u1 = User(name='pkrabs', fullname='Pearl Krabs')
  2. >>> u1.addresses
  3. []

This object is a SQLAlchemy-specific version of Python list which has the ability to track and respond to changes made to it. The collection also appeared automatically when we accessed the attribute, even though we never assigned it to the object. This is similar to the behavior noted at Inserting Rows with the ORM where it was observed that column-based attributes to which we don’t explicitly assign a value also display as None automatically, rather than raising an AttributeError as would be Python’s usual behavior.

As the u1 object is still transient and the list that we got from u1.addresses has not been mutated (i.e. appended or extended), it’s not actually associated with the object yet, but as we make changes to it, it will become part of the state of the User object.

The collection is specific to the Address class which is the only type of Python object that may be persisted within it. Using the list.append() method we may add an Address object:

  1. >>> a1 = Address(email_address="pearl.krabs@gmail.com")
  2. >>> u1.addresses.append(a1)

At this point, the u1.addresses collection as expected contains the new Address object:

  1. >>> u1.addresses
  2. [Address(id=None, email_address='pearl.krabs@gmail.com')]

As we associated the Address object with the User.addresses collection of the u1 instance, another behavior also occurred, which is that the User.addresses relationship synchronized itself with the Address.user relationship, such that we can navigate not only from the User object to the Address object, we can also navigate from the Address object back to the “parent” User object:

  1. >>> a1.user
  2. User(id=None, name='pkrabs', fullname='Pearl Krabs')

This synchronization occurred as a result of our use of the relationship.back_populates parameter between the two relationship() objects. This parameter names another relationship() for which complementary attribute assignment / list mutation should occur. It will work equally well in the other direction, which is that if we create another Address object and assign to its Address.user attribute, that Address becomes part of the User.addresses collection on that User object:

  1. >>> a2 = Address(email_address="pearl@aol.com", user=u1)
  2. >>> u1.addresses
  3. [Address(id=None, email_address='pearl.krabs@gmail.com'), Address(id=None, email_address='pearl@aol.com')]

We actually made use of the user parameter as a keyword argument in the Address consructor, which is accepted just like any other mapped attribute that was declared on the Address class. It is equivalent to assignment of the Address.user attribute after the fact:

  1. # equivalent effect as a2 = Address(user=u1)
  2. >>> a2.user = u1

Cascading Objects into the Session

We now have a User and two Address objects that are associated in a bidirectional structure in memory, but as noted previously in Inserting Rows with the ORM , these objects are said to be in the transient state until they are associated with a Session object.

We make use of the Session that’s still ongoing, and note that when we apply the Session.add() method to the lead User object, the related Address object also gets added to that same Session:

  1. >>> session.add(u1)
  2. >>> u1 in session
  3. True
  4. >>> a1 in session
  5. True
  6. >>> a2 in session
  7. True

The above behavior, where the Session received a User object, and followed along the User.addresses relationship to locate a related Address object, is known as the save-update cascade and is discussed in detail in the ORM reference documentation at Cascades.

The three objects are now in the pending state; this means they are ready to be the subject of an INSERT operation but this has not yet proceeded; all three objects have no primary key assigned yet, and in addition, the a1 and a2 objects have an attribute called user_id which refers to the Column that has a ForeignKeyConsraint referring to the user_account.id column; these are also None as the objects are not yet associated with a real database row:

  1. >>> print(u1.id)
  2. None
  3. >>> print(a1.user_id)
  4. None

It’s at this stage that we can see the very great utility that the unit of work process provides; recall in the section INSERT usually generates the “values” clause automatically, rows were inserted into the user_account and address tables using some elaborate syntaxes in order to automatically associate the address.user_id columns with those of the user_account rows. Additionally, it was necessary that we emit INSERT for user_account rows first, before those of address, since rows in address are dependent on their parent row in user_account for a value in their user_id column.

When using the Session, all this tedium is handled for us and even the most die-hard SQL purist can benefit from automation of INSERT, UPDATE and DELETE statements. When we Session.commit() the transaction all steps invoke in the correct order, and furthermore the newly generated primary key of the user_account row is applied to the address.user_id column appropriately:

  1. >>> session.commit()
  2. INSERT INTO user_account (name, fullname) VALUES (?, ?)
  3. [...] ('pkrabs', 'Pearl Krabs')
  4. INSERT INTO address (email_address, user_id) VALUES (?, ?)
  5. [...] ('pearl.krabs@gmail.com', 6)
  6. INSERT INTO address (email_address, user_id) VALUES (?, ?)
  7. [...] ('pearl@aol.com', 6)
  8. COMMIT

Loading Relationships

In the last step, we called Session.commit() which emitted a COMMIT for the transaction, and then per Session.commit.expire_on_commit expired all objects so that they refresh for the next transaction.

When we next access an attribute on these objects, we’ll see the SELECT emitted for the primary attributes of the row, such as when we view the newly generated primary key for the u1 object:

  1. >>> u1.id
  2. BEGIN (implicit)
  3. SELECT user_account.id AS user_account_id, user_account.name AS user_account_name,
  4. user_account.fullname AS user_account_fullname
  5. FROM user_account
  6. WHERE user_account.id = ?
  7. [...] (6,)
  8. 6

The u1 User object now has a persistent collection User.addresses that we may also access. As this collection consists of an additional set of rows from the address table, when we access this collection as well we again see a lazy load emitted in order to retrieve the objects:

  1. >>> u1.addresses
  2. SELECT address.id AS address_id, address.email_address AS address_email_address,
  3. address.user_id AS address_user_id
  4. FROM address
  5. WHERE ? = address.user_id
  6. [...] (6,)
  7. [Address(id=4, email_address='pearl.krabs@gmail.com'), Address(id=5, email_address='pearl@aol.com')]

Collections and related attributes in the SQLAlchemy ORM are persistent in memory; once the collection or attribute is populated, SQL is no longer emitted until that collection or attribute is expired. We may access u1.addresses again as well as add or remove items and this will not incur any new SQL calls:

  1. >>> u1.addresses
  2. [Address(id=4, email_address='pearl.krabs@gmail.com'), Address(id=5, email_address='pearl@aol.com')]

While the loading emitted by lazy loading can quickly become expensive if we don’t take explicit steps to optimize it, the network of lazy loading at least is fairly well optimized to not perform redundant work; as the u1.addresses collection was refreshed, per the identity map these are in fact the same Address instances as the a1 and a2 objects we’ve been dealing with already, so we’re done loading all attributes in this particular object graph:

  1. >>> a1
  2. Address(id=4, email_address='pearl.krabs@gmail.com')
  3. >>> a2
  4. Address(id=5, email_address='pearl@aol.com')

The issue of how relationships load, or not, is an entire subject onto itself. Some additional introduction to these concepts is later in this section at Loader Strategies.

Using Relationships in Queries

The previous section introduced the behavior of the relationship() construct when working with instances of a mapped class, above, the u1, a1 and a2 instances of the User and Address classes. In this section, we introduce the behavior of relationship() as it applies to class level behavior of a mapped class, where it serves in several ways to help automate the construction of SQL queries.

Using Relationships to Join

The sections Explicit FROM clauses and JOINs and Setting the ON Clause introduced the usage of the Select.join() and Select.join_from() methods to compose SQL JOIN clauses. In order to describe how to join between tables, these methods either infer the ON clause based on the presence of a single unambiguous ForeignKeyConstraint object within the table metadata structure that links the two tables, or otherwise we may provide an explicit SQL Expression construct that indicates a specific ON clause.

When using ORM entities, an additional mechanism is available to help us set up the ON clause of a join, which is to make use of the relationship() objects that we set up in our user mapping, as was demonstrated at Declaring Mapped Classes. The class-bound attribute corresponding to the relationship() may be passed as the single argument to Select.join(), where it serves to indicate both the right side of the join as well as the ON clause at once:

  1. >>> print(
  2. ... select(Address.email_address).
  3. ... select_from(User).
  4. ... join(User.addresses)
  5. ... )
  6. SELECT address.email_address
  7. FROM user_account JOIN address ON user_account.id = address.user_id

The presence of an ORM relationship() on a mapping is not used by Select.join() or Select.join_from() if we don’t specify it; it is not used for ON clause inference. This means, if we join from User to Address without an ON clause, it works because of the ForeignKeyConstraint between the two mapped Table objects, not because of the relationship() objects on the User and Address classes:

  1. >>> print(
  2. ... select(Address.email_address).
  3. ... join_from(User, Address)
  4. ... )
  5. SELECT address.email_address
  6. FROM user_account JOIN address ON user_account.id = address.user_id

Joining between Aliased targets

In the section ORM Entity Aliases we introduced the aliased() construct, which is used to apply a SQL alias to an ORM entity. When using a relationship() to help construct SQL JOIN, the use case where the target of the join is to be an aliased() is suited by making use of the PropComparator.of_type() modifier. To demonstrate we will construct the same join illustrated at ORM Entity Aliases using the relationship() attributes to join instead:

  1. >>> print(
  2. ... select(User).
  3. ... join(User.addresses.of_type(address_alias_1)).
  4. ... where(address_alias_1.email_address == 'patrick@aol.com').
  5. ... join(User.addresses.of_type(address_alias_2)).
  6. ... where(address_alias_2.email_address == 'patrick@gmail.com')
  7. ... )
  8. SELECT user_account.id, user_account.name, user_account.fullname
  9. FROM user_account
  10. JOIN address AS address_1 ON user_account.id = address_1.user_id
  11. JOIN address AS address_2 ON user_account.id = address_2.user_id
  12. WHERE address_1.email_address = :email_address_1
  13. AND address_2.email_address = :email_address_2

To make use of a relationship() to construct a join from an aliased entity, the attribute is available from the aliased() construct directly:

  1. >>> user_alias_1 = aliased(User)
  2. >>> print(
  3. ... select(user_alias_1.name).
  4. ... join(user_alias_1.addresses)
  5. ... )
  6. SELECT user_account_1.name
  7. FROM user_account AS user_account_1
  8. JOIN address ON user_account_1.id = address.user_id

Augmenting the ON Criteria

The ON clause generated by the relationship() construct may also be augmented with additional criteria. This is useful both for quick ways to limit the scope of a particular join over a relationship path, and also for use cases like configuring loader strategies, introduced below at Loader Strategies. The PropComparator.and_() method accepts a series of SQL expressions positionally that will be joined to the ON clause of the JOIN via AND. For example if we wanted to JOIN from User to Address but also limit the ON criteria to only certain email addresses:

  1. >>> stmt = (
  2. ... select(User.fullname).
  3. ... join(User.addresses.and_(Address.email_address == 'pearl.krabs@gmail.com'))
  4. ... )
  5. >>> session.execute(stmt).all()
  6. SELECT user_account.fullname
  7. FROM user_account
  8. JOIN address ON user_account.id = address.user_id AND address.email_address = ?
  9. [...] ('pearl.krabs@gmail.com',)
  10. [('Pearl Krabs',)]

EXISTS forms: has() / any()

In the section EXISTS subqueries, we introduced the Exists object that provides for the SQL EXISTS keyword in conjunction with a scalar subquery. The relationship() construct provides for some helper methods that may be used to generate some common EXISTS styles of queries in terms of the relationship.

For a one-to-many relationship such as User.addresses, an EXISTS against the address table that correlates back to the user_account table can be produced using PropComparator.any(). This method accepts an optional WHERE criteria to limit the rows matched by the subquery:

  1. >>> stmt = (
  2. ... select(User.fullname).
  3. ... where(User.addresses.any(Address.email_address == 'pearl.krabs@gmail.com'))
  4. ... )
  5. >>> session.execute(stmt).all()
  6. SELECT user_account.fullname
  7. FROM user_account
  8. WHERE EXISTS (SELECT 1
  9. FROM address
  10. WHERE user_account.id = address.user_id AND address.email_address = ?)
  11. [...] ('pearl.krabs@gmail.com',)
  12. [('Pearl Krabs',)]

As EXISTS tends to be more efficient for negative lookups, a common query is to locate entities where there are no related entities present. This is succinct using a phrase such as ~User.addresses.any(), to select for User entities that have no related Address rows:

  1. >>> stmt = (
  2. ... select(User.fullname).
  3. ... where(~User.addresses.any())
  4. ... )
  5. >>> session.execute(stmt).all()
  6. SELECT user_account.fullname
  7. FROM user_account
  8. WHERE NOT (EXISTS (SELECT 1
  9. FROM address
  10. WHERE user_account.id = address.user_id))
  11. [...] ()
  12. [('Patrick McStar',), ('Squidward Tentacles',), ('Eugene H. Krabs',)]

The PropComparator.has() method works in mostly the same way as PropComparator.any(), except that it’s used for many-to-one relationships, such as if we wanted to locate all Address objects which belonged to “pearl”:

  1. >>> stmt = (
  2. ... select(Address.email_address).
  3. ... where(Address.user.has(User.name=="pkrabs"))
  4. ... )
  5. >>> session.execute(stmt).all()
  6. SELECT address.email_address
  7. FROM address
  8. WHERE EXISTS (SELECT 1
  9. FROM user_account
  10. WHERE user_account.id = address.user_id AND user_account.name = ?)
  11. [...] ('pkrabs',)
  12. [('pearl.krabs@gmail.com',), ('pearl@aol.com',)]

Common Relationship Operators

There are some additional varieties of SQL generation helpers that come with relationship(), including:

  • many to one equals comparison - a specific object instance can be compared to many-to-one relationship, to select rows where the foreign key of the target entity matches the primary key value of the object given:

    1. >>> print(select(Address).where(Address.user == u1))
    2. SELECT address.id, address.email_address, address.user_id
    3. FROM address
    4. WHERE :param_1 = address.user_id
  • many to one not equals comparison - the not equals operator may also be used:

    1. >>> print(select(Address).where(Address.user != u1))
    2. SELECT address.id, address.email_address, address.user_id
    3. FROM address
    4. WHERE address.user_id != :user_id_1 OR address.user_id IS NULL
  • object is contained in a one-to-many collection - this is essentially the one-to-many version of the “equals” comparison, select rows where the primary key equals the value of the foreign key in a related object:

    1. >>> print(select(User).where(User.addresses.contains(a1)))
    2. SELECT user_account.id, user_account.name, user_account.fullname
    3. FROM user_account
    4. WHERE user_account.id = :param_1
  • An object has a particular parent from a one-to-many perspective - the with_parent() function produces a comparison that returns rows which are referred towards by a given parent, this is essentially the same as using the == operator with the many-to-one side:

    1. >>> from sqlalchemy.orm import with_parent
    2. >>> print(select(Address).where(with_parent(u1, User.addresses)))
    3. SELECT address.id, address.email_address, address.user_id
    4. FROM address
    5. WHERE :param_1 = address.user_id

Loader Strategies

In the section Loading Relationships we introduced the concept that when we work with instances of mapped objects, accessing the attributes that are mapped using relationship() in the default case will emit a lazy load when the collection is not populated in order to load the objects that should be present in this collection.

Lazy loading is one of the most famous ORM patterns, and is also the one that is most controversial. When several dozen ORM objects in memory each refer to a handful of unloaded attributes, routine manipulation of these objects can spin off many additional queries that can add up (otherwise known as the N plus one problem), and to make matters worse they are emitted implicitly. These implicit queries may not be noticed, may cause errors when they are attempted after there’s no longer a database tranasction available, or when using alternative concurrency patterns such as asyncio, they actually won’t work at all.

At the same time, lazy loading is a vastly popular and useful pattern when it is compatible with the concurrency approach in use and isn’t otherwise causing problems. For these reasons, SQLAlchemy’s ORM places a lot of emphasis on being able to control and optimize this loading behavior.

Above all, the first step in using ORM lazy loading effectively is to test the application, turn on SQL echoing, and watch the SQL that is emitted. If there seem to be lots of redundant SELECT statements that look very much like they could be rolled into one much more efficiently, if there are loads occurring inappropriately for objects that have been detached from their Session, that’s when to look into using loader strategies.

Loader strategies are represented as objects that may be associated with a SELECT statement using the Select.options() method, e.g.:

  1. for user_obj in session.execute(
  2. select(User).options(selectinload(User.addresses))
  3. ).scalars():
  4. user_obj.addresses # access addresses collection already loaded

They may be also configured as defaults for a relationship() using the relationship.lazy option, e.g.:

  1. from sqlalchemy.orm import relationship
  2. class User(Base):
  3. __tablename__ = 'user_account'
  4. addresses = relationship("Address", back_populates="user", lazy="selectin")

Each loader strategy object adds some kind of information to the statement that will be used later by the Session when it is deciding how various attributes should be loaded and/or behave when they are accessed.

The sections below will introduce a few of the most prominently used loader strategies.

See also

Two sections in Relationship Loading Techniques:

Selectin Load

The most useful loader in modern SQLAlchemy is the selectinload() loader option. This option solves the most common form of the “N plus one” problem which is that of a set of objects that refer to related collections. selectinload() will ensure that a particular collection for a full series of objects are loaded up front using a single query. It does this using a SELECT form that in most cases can be emitted against the related table alone, without the introduction of JOINs or subqueries, and only queries for those parent objects for which the collection isn’t already loaded. Below we illustrate selectinload() by loading all of the User objects and all of their related Address objects; while we invoke Session.execute() only once, given a select() construct, when the database is accessed, there are in fact two SELECT statements emitted, the second one being to fetch the related Address objects:

  1. >>> from sqlalchemy.orm import selectinload
  2. >>> stmt = (
  3. ... select(User).options(selectinload(User.addresses)).order_by(User.id)
  4. ... )
  5. >>> for row in session.execute(stmt):
  6. ... print(f"{row.User.name} ({', '.join(a.email_address for a in row.User.addresses)})")
  7. SELECT user_account.id, user_account.name, user_account.fullname
  8. FROM user_account ORDER BY user_account.id
  9. [...] ()
  10. SELECT address.user_id AS address_user_id, address.id AS address_id,
  11. address.email_address AS address_email_address
  12. FROM address
  13. WHERE address.user_id IN (?, ?, ?, ?, ?, ?)
  14. [...] (1, 2, 3, 4, 5, 6)
  15. spongebob (spongebob@sqlalchemy.org)
  16. sandy (sandy@sqlalchemy.org, sandy@squirrelpower.org)
  17. patrick ()
  18. squidward ()
  19. ehkrabs ()
  20. pkrabs (pearl.krabs@gmail.com, pearl@aol.com)

See also

Select IN loading - in Relationship Loading Techniques

Joined Load

The joinedload() eager load strategy is the oldest eager loader in SQLAlchemy, which augments the SELECT statement that’s being passed to the database with a JOIN (which may an outer or an inner join depending on options), which can then load in related objects.

The joinedload() strategy is best suited towards loading related many-to-one objects, as this only requires that additional columns are added to a primary entity row that would be fetched in any case. For greater effiency, it also accepts an option joinedload.innerjoin so that an inner join instead of an outer join may be used for a case such as below where we know that all Address objects have an associated User:

  1. >>> from sqlalchemy.orm import joinedload
  2. >>> stmt = (
  3. ... select(Address).options(joinedload(Address.user, innerjoin=True)).order_by(Address.id)
  4. ... )
  5. >>> for row in session.execute(stmt):
  6. ... print(f"{row.Address.email_address} {row.Address.user.name}")
  7. SELECT address.id, address.email_address, address.user_id, user_account_1.id AS id_1,
  8. user_account_1.name, user_account_1.fullname
  9. FROM address
  10. JOIN user_account AS user_account_1 ON user_account_1.id = address.user_id
  11. ORDER BY address.id
  12. [...] ()
  13. spongebob@sqlalchemy.org spongebob
  14. sandy@sqlalchemy.org sandy
  15. sandy@squirrelpower.org sandy
  16. pearl.krabs@gmail.com pkrabs
  17. pearl@aol.com pkrabs

joinedload() also works for collections, meaning one-to-many relationships, however it has the effect of multiplying out primary rows per related item in a recursive way that grows the amount of data sent for a result set by orders of magnitude for nested collections and/or larger collections, so its use vs. another option such as selectinload() should be evaluated on a per-case basis.

It’s important to note that the WHERE and ORDER BY criteria of the enclosing Select statement do not target the table rendered by joinedload(). Above, it can be seen in the SQL that an anonymous alias is applied to the user_account table such that is not directly addressible in the query. This concept is discussed in more detail in the section The Zen of Joined Eager Loading.

The ON clause rendered by joinedload() may be affected directly by using the PropComparator.and_() method described previously at Augmenting the ON Criteria; examples of this technique with loader strategies are further below at Augmenting Loader Strategy Paths. However, more generally, “joined eager loading” may be applied to a Select that uses Select.join() using the approach described in the next section, Explicit Join + Eager load.

Tip

It’s important to note that many-to-one eager loads are often not necessary, as the “N plus one” problem is much less prevalent in the common case. When many objects all refer to the same related object, such as many Address objects that each refer ot the same User, SQL will be emitted only once for that User object using normal lazy loading. The lazy load routine will look up the related object by primary key in the current Session without emitting any SQL when possible.

See also

Joined Eager Loading - in Relationship Loading Techniques

Explicit Join + Eager load

If we were to load Address rows while joining to the user_account table using a method such as Select.join() to render the JOIN, we could also leverage that JOIN in order to eagerly load the contents of the Address.user attribute on each Address object returned. This is essentially that we are using “joined eager loading” but rendering the JOIN ourselves. This common use case is acheived by using the contains_eager() option. this option is very similar to joinedload(), except that it assumes we have set up the JOIN ourselves, and it instead only indicates that additional columns in the COLUMNS clause should be loaded into related attributes on each returned object, for example:

  1. >>> from sqlalchemy.orm import contains_eager
  2. >>> stmt = (
  3. ... select(Address).
  4. ... join(Address.user).
  5. ... where(User.name == 'pkrabs').
  6. ... options(contains_eager(Address.user)).order_by(Address.id)
  7. ... )
  8. >>> for row in session.execute(stmt):
  9. ... print(f"{row.Address.email_address} {row.Address.user.name}")
  10. SELECT user_account.id, user_account.name, user_account.fullname,
  11. address.id AS id_1, address.email_address, address.user_id
  12. FROM address JOIN user_account ON user_account.id = address.user_id
  13. WHERE user_account.name = ? ORDER BY address.id
  14. [...] ('pkrabs',)
  15. pearl.krabs@gmail.com pkrabs
  16. pearl@aol.com pkrabs

Above, we both filtered the rows on user_account.name and also loaded rows from user_account into the Address.user attribute of the returned rows. If we had applied joinedload() separately, we would get a SQL query that unnecessarily joins twice:

  1. >>> stmt = (
  2. ... select(Address).
  3. ... join(Address.user).
  4. ... where(User.name == 'pkrabs').
  5. ... options(joinedload(Address.user)).order_by(Address.id)
  6. ... )
  7. >>> print(stmt) # SELECT has a JOIN and LEFT OUTER JOIN unnecessarily
  8. SELECT address.id, address.email_address, address.user_id,
  9. user_account_1.id AS id_1, user_account_1.name, user_account_1.fullname
  10. FROM address JOIN user_account ON user_account.id = address.user_id
  11. LEFT OUTER JOIN user_account AS user_account_1 ON user_account_1.id = address.user_id
  12. WHERE user_account.name = :name_1 ORDER BY address.id

See also

Two sections in Relationship Loading Techniques:

Augmenting Loader Strategy Paths

In Augmenting the ON Criteria we illustrated how to add arbitrary criteria to a JOIN rendered with relationship() to also include additional criteria in the ON clause. The PropComparator.and_() method is in fact generally available for most loader options. For example, if we wanted to re-load the names of users and their email addresses, but omitting the email addresses with the sqlalchemy.org domain, we can apply PropComparator.and_() to the argument passed to selectinload() to limit this criteria:

  1. >>> from sqlalchemy.orm import selectinload
  2. >>> stmt = (
  3. ... select(User).
  4. ... options(
  5. ... selectinload(
  6. ... User.addresses.and_(
  7. ... ~Address.email_address.endswith("sqlalchemy.org")
  8. ... )
  9. ... )
  10. ... ).
  11. ... order_by(User.id).
  12. ... execution_options(populate_existing=True)
  13. ... )
  14. >>> for row in session.execute(stmt):
  15. ... print(f"{row.User.name} ({', '.join(a.email_address for a in row.User.addresses)})")
  16. SELECT user_account.id, user_account.name, user_account.fullname
  17. FROM user_account ORDER BY user_account.id
  18. [...] ()
  19. SELECT address.user_id AS address_user_id, address.id AS address_id,
  20. address.email_address AS address_email_address
  21. FROM address
  22. WHERE address.user_id IN (?, ?, ?, ?, ?, ?)
  23. AND (address.email_address NOT LIKE '%' || ?)
  24. [...] (1, 2, 3, 4, 5, 6, 'sqlalchemy.org')
  25. spongebob ()
  26. sandy (sandy@squirrelpower.org)
  27. patrick ()
  28. squidward ()
  29. ehkrabs ()
  30. pkrabs (pearl.krabs@gmail.com, pearl@aol.com)

A very important thing to note above is that a special option is added with .execution_options(populate_existing=True). This option which takes effect when rows are being fetched indicates that the loader option we are using should replace the existing contents of collections on the objects, if they are already loaded. As we are working with a single Session repeatedly, the objects we see being loaded above are the same Python instances as those that were first persisted at the start of the ORM section of this tutorial.

See also

Adding Criteria to loader options - in Relationship Loading Techniques

Populate Existing - in ORM Querying Guide

Raiseload

One additional loader strategy worth mentioning is raiseload(). This option is used to completely block an application from having the N plus one problem at all by causing what would normally be a lazy load to raise an error instead. It has two variants that are controlled via the raiseload.sql_only option to block either lazy loads that require SQL, versus all “load” operations including those which only need to consult the current Session.

One way to use raiseload() is to configure it on relationship() itself, by setting relationship.lazy to the value "raise_on_sql", so that for a particular mapping, a certain relationship will never try to emit SQL:

  1. class User(Base):
  2. __tablename__ = 'user_account'
  3. # ... Column mappings
  4. addresses = relationship("Address", back_populates="user", lazy="raise_on_sql")
  5. class Address(Base):
  6. __tablename__ = 'address'
  7. # ... Column mappings
  8. user = relationship("User", back_populates="addresses", lazy="raise_on_sql")

Using such a mapping, the application is blocked from lazy loading, indicating that a particular query would need to specify a loader strategy:

  1. u1 = s.execute(select(User)).scalars().first()
  2. u1.addresses
  3. sqlalchemy.exc.InvalidRequestError: 'User.addresses' is not available due to lazy='raise_on_sql'

The exception would indicate that this collection should be loaded up front instead:

  1. u1 = s.execute(select(User).options(selectinload(User.addresses))).scalars().first()

The lazy="raise_on_sql" option tries to be smart about many-to-one relationships as well; above, if the Address.user attribute of an Address object were not loaded, but that User object were locally present in the same Session, the “raiseload” strategy would not raise an error.

See also

Preventing unwanted lazy loads using raiseload - in Relationship Loading Techniques

SQLAlchemy 1.4 / 2.0 Tutorial

Next Tutorial Section: Further Reading