relationship() and one-to-many

SQLAlchemy · Practical · 12 min

Relationships let you work with user.posts instead of hand-joining ids, and their cascade settings decide what happens on save and delete.

A database link needs a foreign key column on the "many" side: user_id: Mapped[int] = mapped_column(ForeignKey("users.id")). The ORM adds `relationship()` attributes on top so you can navigate between objects:

  • On User: posts: Mapped[list["Post"]] = relationship(back_populates="author").
  • On Post: author: Mapped["User"] = relationship(back_populates="posts").

The Mapped[list[...]] annotation makes it a collection (one-to-many); a plain class makes it a single object (many-to-one). back_populates names the attribute on the other side and keeps both sides in sync in Python: appending a post to user.posts sets post.author, before anything is flushed.

When you add the user to the session, related posts are added automatically through the default save-update cascade, and the foreign key values are filled in at flush time. You never set post.user_id by hand.

Deletion needs a decision. By default, deleting a user sets user_id to NULL on its posts (which fails if the column is NOT NULL). Use cascade="all, delete-orphan" on the parent's collection when children can't exist without their parent: deleting the user deletes its posts, and removing a post from user.posts deletes it.

Accessing user.posts on a loaded object runs a SELECT the first time: relationships are lazy-loaded by default. Loading strategies are covered in their own lesson.

Example

from sqlalchemy import ForeignKey, create_engine, select
from sqlalchemy.orm import DeclarativeBase, Mapped, mapped_column, relationship, Session

class Base(DeclarativeBase): pass
class User(Base):
    __tablename__ = "users"
    id: Mapped[int] = mapped_column(primary_key=True)
    name: Mapped[str]
    posts: Mapped[list["Post"]] = relationship(back_populates="author",
                                               cascade="all, delete-orphan")
class Post(Base):
    __tablename__ = "posts"
    id: Mapped[int] = mapped_column(primary_key=True)
    title: Mapped[str]
    user_id: Mapped[int] = mapped_column(ForeignKey("users.id"))
    author: Mapped[User] = relationship(back_populates="posts")

engine = create_engine("sqlite://"); Base.metadata.create_all(engine)
with Session(engine) as s:
    ada = User(name="ada", posts=[Post(title="Engines"), Post(title="Notes")])
    s.add(ada)                       # posts cascade in automatically
    s.commit()
    print(ada.posts[0].author.name, ada.posts[0].user_id)   # ada 1
    s.delete(ada); s.commit()
    print(s.scalars(select(Post)).all())                   # []

Remember

  • ForeignKey column on the many side, relationship() on both sides
  • back_populates keeps both directions in sync in memory
  • Related objects are added via save-update cascade; FK values are set at flush
  • cascade='all, delete-orphan' when children belong to the parent

Watch out for

  • Setting the foreign key id by hand and the relationship as well, then getting confused when they disagree.

Check yourself

  1. Without delete cascade, what does deleting a User do to its Posts by default?

Sign in to answer, see the explanations, and get this lesson back as a quick review later.

Official docs: docs.sqlalchemy.org/en/20/orm/basic_relationships.html