SQLAlchemy
Django
get_or_create
Python
ORM

Does SQLAlchemy have an equivalent of Django's get_or_create?

System Design practice on Codemia

Work through 120+ system design problems with detailed solutions, from rate limiters to multi-region storage.

Practice system design

Introduction

SQLAlchemy does not ship a direct ORM helper that exactly mirrors Django’s get_or_create. You can absolutely implement the same behavior, but the correct version depends on transaction handling and a database-enforced uniqueness rule rather than on a convenience method name alone.

Core Sections

What get_or_create is really trying to guarantee

The Django pattern means: look up a row by a unique key, and if it is not present, create it and tell the caller whether the row was newly created. The subtle part is concurrency. If two requests try to create the same logical row at the same time, the pattern should still result in one row, not two.

That means the real requirement is not merely ergonomic ORM code. It is correctness under concurrent inserts.

Start with a unique constraint in the database

No ORM helper can make get_or_create safe if the database itself allows duplicates for the lookup fields. The first step is therefore a uniqueness guarantee.

python
1from sqlalchemy import String, UniqueConstraint
2from sqlalchemy.orm import DeclarativeBase, Mapped, mapped_column
3
4
5class Base(DeclarativeBase):
6    pass
7
8
9class User(Base):
10    __tablename__ = "users"
11    __table_args__ = (UniqueConstraint("email"),)
12
13    id: Mapped[int] = mapped_column(primary_key=True)
14    email: Mapped[str] = mapped_column(String, nullable=False)
15    name: Mapped[str] = mapped_column(String, nullable=False)

Without that constraint, two processes can both observe that the row does not exist and then both insert it successfully.

Why the naive pattern is not enough

A first attempt often looks like this:

python
1user = session.query(User).filter_by(email=email).one_or_none()
2if user is None:
3    user = User(email=email, name=name)
4    session.add(user)
5    session.commit()

This works in a single-threaded example, but it is race-prone. Two requests can both miss the query and both insert. If the database has a unique constraint, one of them will fail at commit time. If it does not, you get duplicate data.

A practical race-aware helper

A safer SQLAlchemy pattern is to try the insert and recover from a uniqueness violation.

python
1from sqlalchemy import select
2from sqlalchemy.exc import IntegrityError
3
4
5def get_or_create_user(session, email: str, name: str):
6    user = session.scalar(select(User).where(User.email == email))
7    if user is not None:
8        return user, False
9
10    user = User(email=email, name=name)
11    session.add(user)
12
13    try:
14        session.commit()
15        return user, True
16    except IntegrityError:
17        session.rollback()
18        existing = session.scalar(select(User).where(User.email == email))
19        return existing, False

This pattern assumes the unique constraint exists. If another transaction inserts the row first, your commit fails, the session rolls back, and you fetch the row that now exists.

flush() versus commit() depends on transaction ownership

If the helper lives inside a larger unit of work, calling commit() inside it may be too aggressive. In that case, use flush() instead so the outer caller keeps control of the transaction boundary.

python
1from sqlalchemy import select
2from sqlalchemy.exc import IntegrityError
3
4
5def get_or_create_user(session, email: str, name: str):
6    user = session.scalar(select(User).where(User.email == email))
7    if user is not None:
8        return user, False
9
10    user = User(email=email, name=name)
11    session.add(user)
12
13    try:
14        session.flush()
15        return user, True
16    except IntegrityError:
17        session.rollback()
18        existing = session.scalar(select(User).where(User.email == email))
19        return existing, False

The better choice depends on who owns the transaction in your application architecture.

Database-native upsert can be even better

For high-throughput or contention-heavy paths, dialect-specific upsert support is often cleaner than a generic ORM helper. PostgreSQL, for example, supports ON CONFLICT, and SQLAlchemy can express that through dialect-specific insert helpers.

That route is less portable, but it is often the strongest option when performance and concurrency matter more than ORM abstraction symmetry with Django.

Why SQLAlchemy leaves this to application code

SQLAlchemy tends to provide building blocks rather than impose a single high-level ORM workflow. That gives you freedom around transaction boundaries, retries, and database-specific behavior, but it also means you must think more carefully about correctness than you would with a single convenience method.

So the right answer is not "SQLAlchemy forgot this feature." The right answer is that the framework expects you to choose the exact tradeoff that fits your database and transaction model.

Common Pitfalls

  • Implementing a select-then-insert helper without a unique constraint is not race-safe and can create duplicate rows.
  • Catching IntegrityError without rolling back leaves the SQLAlchemy session in a failed state.
  • Calling commit() inside a helper can conflict with application code that expects to manage transactions at a higher level.
  • Focusing on API similarity to Django instead of concurrency semantics misses the most important part of get_or_create behavior.
  • Ignoring dialect-specific upsert features can leave performance on the table for hot insert paths.

Summary

  • SQLAlchemy has no single built-in ORM helper identical to Django’s get_or_create.
  • A correct implementation starts with a database uniqueness guarantee.
  • The standard safe pattern is to attempt the insert, catch IntegrityError, roll back, and re-query.
  • 'flush() may be preferable to commit() when the caller owns the transaction.'
  • For high-contention paths, a database-native upsert can be a better solution than a generic ORM helper.

Related reading
Course
Beginner
27 lessons
10 hours
System Design Fundamentals

Build a strong foundation in designing scalable, reliable distributed systems.

View the course
Track what you have practised

A free account saves your progress, solutions and study plan across every problem on Codemia.

System Design practice on Codemia

Work through 120+ system design problems with detailed solutions, from rate limiters to multi-region storage.

Practice system design

All Rights Reserved.