SQLAlchemy ORM: Invalidated Collection

Question:

I don’t understand why SQLAlchemy gives me the following warning:

SAWarning: This collection has been invalidated.
  util.warn("This collection has been invalidated.")

Here is a minimal example:

import sqlalchemy
print(sqlalchemy.__version__)

from sqlalchemy import create_engine
from sqlalchemy import Column, Integer, ForeignKey

from sqlalchemy.exc import IntegrityError
from sqlalchemy.ext.declarative import declarative_base

from sqlalchemy.orm import relationship, sessionmaker

Base = declarative_base()

class ModelBase:

    @classmethod
    def create(cls, session, **kwargs):
        obj = cls(**kwargs)
        session.add(obj)
        session.commit()

        return obj

class User(ModelBase, Base):
    __tablename__ = 'users'

    id = Column(Integer, primary_key=True)

    addresses = relationship("Address", order_by="Address.id", back_populates="user")

class Address(ModelBase, Base):
    __tablename__ = 'addresses'

    id = Column(Integer, primary_key=True)

    user_id = Column(Integer, ForeignKey('users.id'))

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


engine = create_engine('sqlite:///:memory:', echo=False)
Base.metadata.create_all(engine)


Session = sessionmaker(bind=engine)
session = Session()

jack = User.create(session)
jack.addresses.append(Address.create(session))

The warning is thrown in the last line. When I rewrite the code a little the warning disappears.

For instance:

jack = User.create(session)
a = Address.create(session)
jack.addresses.append(a)

or

jack = User.create(session)
jack.addresses = [Address.create(session)]

I can reproduce this warning with SQLAlchemy 1.3.18 and 1.4.0b1.

Asked By: chhenning

||

Answers:

The warning was added to SQLAlchemy in this commit. The commit message is

A warning is emitted when a reference
to an instrumented collection is no longer
associated with the parent class due to
expiration/attribute refresh/collection
replacement, but an append
or remove operation is received on the
now-detached collection.

I think what happens here is jack.addresses is valid when initially accessed, but the commit inside creates expires all the objects in the session including jack.addresses, and this causes the warning.

If the code is split

a = Address.create(session)
jack.addresses.append(a)

jack.addresses is accessed after the commit in create and so the session is able to refresh it and no warning is emitted.

It’s worth noting that committing in create-type methods like in the question goes against the grain of SQLAlchemy’s unit of work model, in which changes are generally accumulated in the session and finally sent to the database in a single commit. Removing obj.commit() from the create method and committing once both objects are created will work just as well, is more efficient and avoids partial updates, for example the user instance is created and committed but there is an error creating the address.

Answered By: snakecharmerb
Categories: questions Tags: ,
Answers are sorted by their score. The answer accepted by the question owner as the best is marked with
at the top-right corner.