SQLAlchemy Relationships

A SQLAlchemy relationship is a Python-side link between two models that lets you navigate from one object to its related rows — like reading a user's posts — without writing SQL joins by hand.

Learn SQLAlchemy Relationships in our free Flask course — a beginner-friendly interactive lesson with worked examples, a practice exercise and a quick…

Part of the free Flask course at LearnCodingFast — hands-on lessons with examples you run in your browser, plus practice exercises and a quick quiz.

In this lesson you'll build one-to-many links with ForeignKey and relationship , model many-to-many with an association table, and learn when to load related data lazily versus eagerly.

The most common relationship is one-to-many : one user writes many posts; each post belongs to one user. You model it with two pieces. The "many" side (Post) gets a ForeignKey column pointing at the "one" side. Then both sides get a relationship() linked by back_populates so you can navigate either way.

The runnable example uses plain SQLAlchemy 2.x with an in-memory SQLite database, so it runs anywhere. After saving a user with two posts, you can read user.posts and post.author .

Output: Ada's posts: ['Hello', 'World'] and Author of 'Hello': Ada — the same relationship read from each end.

When each side can have many of the other — a post has many tags, and each tag labels many posts — you need a many-to-many relationship. The database can't store that directly, so you add a third association table holding two foreign keys. You then pass it to relationship(secondary=...) on both sides.

The runnable example wires up a post_tag association table and demonstrates adding tags to a post and listing the posts for a tag.

Output: Tags on the post: ['python', 'flask'] , and the python tag lists both posts ( 'Flask + SQLAlchemy' and 'Pure Python' ) — many on each side.

By default, relationships load lazily : SQLAlchemy fetches related rows only the moment you touch the attribute. That is convenient, but looping over 100 users and reading user.posts each time fires 100 extra queries — the infamous N+1 problem . To avoid it, load eagerly with selectinload() or joinedload() , which fetch everything up front.

Output: Ada -> ['Ada-1', 'Ada-2'] and Bob -> ['Bob-1', 'Bob-2'] , fetched without firing a separate query per user.

In a Flask app you usually use Flask-SQLAlchemy , where models inherit from db.Model and you use db.relationship . The mapping is direct — here is the same one-to-many in that style:

Complete the one-to-many between an Author and their Books. Replace each ___ .

❌ NoForeignKeysError / could not determine join condition

You added a relationship() but forgot the ForeignKey column. SQLAlchemy needs the FK to know how the two tables connect.

The string in back_populates must be the attribute name on the other class. A typo leaves the two sides out of sync or raises a mapper error.

❌ DetachedInstanceError when reading related data

You closed the session before touching a lazily-loaded attribute. Either keep the session open or load eagerly with selectinload() .

Model a many-to-many between students and the courses they enroll in.

Lesson complete — your data is connected!

You can model one-to-many with ForeignKey and relationship , build many-to-many with an association table, and choose lazy or eager loading to keep queries efficient.

🚀 Up next: Querying with SQLAlchemy — filter, sort, join, and aggregate the data you've just connected.

Practice quiz

What does a ForeignKey column store?

  • The id of a related row
  • A Python list
  • The whole related object
  • A SQL query string

Answer: The id of a related row. A ForeignKey holds the primary-key value of the row it references, e.g. users.id.

What does relationship() add compared to a bare ForeignKey?

  • A new database table
  • Python-side navigation like user.posts
  • An index on the column
  • Automatic encryption

Answer: Python-side navigation like user.posts. relationship() lets you traverse objects in Python without writing the join yourself.

What keeps both sides of a relationship in sync?

  • primary_key=True
  • nullable=False
  • back_populates
  • autoincrement

Answer: back_populates. back_populates links the two relationship() declarations so updates reflect on both sides.

In one-to-many, where does the ForeignKey column live?

  • On the 'one' side
  • On a separate config file
  • Nowhere; it is implicit
  • On the 'many' side

Answer: On the 'many' side. The 'many' side (e.g. Post) carries the ForeignKey pointing at the 'one' side.

How do you model a many-to-many relationship?

  • With an association (secondary) table
  • With two primary keys on one table
  • With a JSON column
  • It is impossible in SQLAlchemy

Answer: With an association (secondary) table. A third association table holds two foreign keys and is passed as relationship(secondary=...).

Which argument attaches the association table to a relationship?

  • join=
  • secondary=
  • through=
  • link=

Answer: secondary=. relationship(secondary=post_tag) wires a many-to-many through the association table.

What problem does lazy loading in a loop commonly cause?

  • A syntax error
  • Duplicate primary keys
  • The N+1 query problem
  • A migration conflict

Answer: The N+1 query problem. Touching a lazy attribute per row fires one extra query each time — the N+1 problem.

Which option loads related rows eagerly to avoid N+1?

  • lazy='dynamic'
  • defer()
  • noload()
  • selectinload()

Answer: selectinload(). selectinload() (or joinedload()) fetches related rows up front in few queries.

In Flask-SQLAlchemy, models inherit from which base?

  • db.Model
  • DeclarativeBase only
  • flask.Model
  • orm.BaseModel

Answer: db.Model. Flask-SQLAlchemy models subclass db.Model and use db.relationship / db.ForeignKey.

What error suggests you added relationship() but forgot the ForeignKey?

  • DetachedInstanceError
  • NoForeignKeysError / could not determine join condition
  • IntegrityError
  • OperationalError: no such table

Answer: NoForeignKeysError / could not determine join condition. Without a ForeignKey, SQLAlchemy cannot determine the join and raises NoForeignKeysError.