Skip to content

Django ORM vs SQLAlchemy: Which Migrations Tool Wins?

The Problem with Database Migrations

Database migrations are unavoidable in web development, and choosing the wrong migration tool leads to lost productivity, team conflicts, and production disasters. I’ve worked with both Django ORM migrations and SQLAlchemy/Alembic, and each has its place depending on your project needs.

When you’re building a web application, your database schema will change. New features require new tables, existing tables need new columns, and relationships evolve over time. Without a reliable migration system, you’re stuck with manual schema management, which is error-prone and doesn’t scale across teams.

I’ve seen teams struggle with:

  • Lost productivity from manual schema management
  • Team conflicts from uncoordinated migrations
  • Production disasters from misapplied changes

The good news is that both Django and SQLAlchemy have mature migration solutions. Let me show you how each works and help you decide which is right for your project.

Django ORM Migrations: Automatic and Integrated

If you’re already using Django, the built-in migration system is incredibly convenient. I love how zero-configuration it is - it works immediately after creating a Django project.

The typical workflow is straightforward:

Terminal
python manage.py makemigrations # Auto-detects model changes
python manage.py migrate # Applies changes to database

Let me show you a practical example. I’ll create a simple Django model and see how migrations work:

models.py
from django.db import models
class Article(models.Model):
title = models.CharField(max_length=200)
content = models.TextField()
published_at = models.DateTimeField(auto_now_add=True)
def __str__(self):
return self.title

When I run makemigrations, Django automatically detects that I’ve created a new model:

Terminal
$ python manage.py makemigrations
Migrations for 'blog':
blog/migrations/0001_initial.py
- Create model Article

The generated migration file is Python code that I can review:

migrations/0001_initial.py
from django.db import migrations, models
class Migration(migrations.Migration):
initial = True
dependencies = []
operations = [
migrations.CreateModel(
name='Article',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('title', models.CharField(max_length=200)),
('content', models.TextField()),
('published_at', models.DateTimeField(auto_now_add=True)),
],
),
]

One thing I appreciate about Django migrations is how they handle team conflicts. When two developers create migrations independently, Django detects the conflict and provides clear guidance:

Terminal
$ python manage.py migrate
CommandError: Conflicting migrations detected; multiple leaf nodes in the migration graph.
To fix this, run 'python manage.py makemigrations --merge'

The --merge flag automatically creates a migration that combines both changes. I think this is one of Django’s strongest features - it makes collaboration almost effortless.

For rollbacks, I can simply migrate to a previous version:

Terminal
$ python manage.py showmigrations blog
blog
[X] 0001_initial
[X] 0002_add_author
[X] 0003_add_tags
$ python manage.py migrate blog 0002_add_author
Operations to perform:
Target specific migration: 0002_add_author, from blog
Running migrations:
Rendering model states... DONE
Unapplying blog.0003_add_tags... OK

SQLAlchemy/Alembic: Flexible and Framework-Agnostic

When I’m working with Flask or FastAPI, or when I need database-agnostic migrations, SQLAlchemy with Alembic is my go-to choice. It requires more setup but offers greater flexibility.

The initial setup looks like this:

Terminal
alembic init alembic # Create migration environment
# Edit alembic.ini and env.py # Configure database connection
alembic revision --autogenerate # Generate migration (requires setup)
alembic upgrade head # Apply migrations

Let me show you how I set up Alembic for a FastAPI project. First, I initialize the environment:

Terminal
$ alembic init alembic
Creating directory /path/to/alembic...done
Creating directory /path/to/alembic/versions...done
Generating /path/to/alembic.ini...done
Generating /path/to/alembic/env.py...done
Generating /path/to/alembic/script.py.mako...done
Generating /path/to/alembic/versions/README...done
Please edit configuration/connection/logging settings in
'/path/to/alembic.ini' before proceeding.

The most important file to configure is env.py. I need to import my SQLAlchemy models and set up the metadata:

alembic/env.py
from logging.config import fileConfig
from sqlalchemy import engine_from_config
from sqlalchemy import pool
from alembic import context
import sys
import os
# Add parent directory to path
sys.path.insert(0, os.path.dirname(os.path.dirname(__file__)))
# Import your models here
from models import Base # This is your SQLAlchemy declarative base
config = context.config
if config.config_file_name is not None:
fileConfig(config.config_file_name)
target_metadata = Base.metadata # This is crucial for autogenerate to work

I also need to configure the database URL in alembic.ini:

alembic.ini
[alembic]
script_location = alembic
sqlalchemy.url = postgresql://user:password@localhost/mydb
[post_write_hooks]
[loggers]
keys = root,sqlalchemy,alembic
[handlers]
keys = console
[formatters]
keys = generic

Now I can define my SQLAlchemy model:

models.py
from sqlalchemy import Column, Integer, String, DateTime, create_engine
from sqlalchemy.ext.declarative import declarative_base
from sqlalchemy.sql import func
Base = declarative_base()
class Article(Base):
__tablename__ = 'articles'
id = Column(Integer, primary_key=True)
title = Column(String(200))
content = Column(String)
published_at = Column(DateTime, server_default=func.now())
def __repr__(self):
return f"<Article(title='{self.title}')>"

When I run alembic revision --autogenerate, Alembic detects the changes:

Terminal
$ alembic revision --autogenerate -m "Initial migration"
INFO [alembic.runtime.migration] Context impl PostgresqlImpl.
INFO [alembic.runtime.migration] Context impl PostgresqlImpl.
INFO [alembic.autogenerate.compare] Detected added table 'articles'
Generating /path/to/alembic/versions/c3f8d1e2a4b6_initial_migration.py ... done

The generated migration file gives me full control:

alembic/versions/c3f8d1e2a4b6_initial_migration.py
"""Initial migration
Revision ID: c3f8d1e2a4b6
Revises:
Create Date: 2026-04-09 10:00:00.000000
"""
from alembic import op
import sqlalchemy as sa
# revision identifiers, used by Alembic.
revision = 'c3f8d1e2a4b6'
down_revision = None
branch_labels = None
depends_on = None
def upgrade():
op.create_table('articles',
sa.Column('id', sa.Integer(), nullable=False),
sa.Column('title', sa.String(length=200), nullable=True),
sa.Column('content', sa.String(), nullable=True),
sa.Column('published_at', sa.DateTime(), server_default=sa.func.now(), nullable=True),
sa.PrimaryKeyConstraint('id')
)
def downgrade():
op.drop_table('articles')

What I really like about Alembic is the revision chain concept. Each migration has a down_revision that links it to the previous migration:

alembic/versions/next_migration.py
revision = 'a1b2c3d4e5f6'
down_revision = 'c3f8d1e2a4b6' # Points to the previous migration
branch_labels = None
depends_on = None

This allows for branching migrations in complex deployment scenarios - something that Django doesn’t easily support.

Common Mistakes I’ve Made

Django Mistakes

The biggest mistake I’ve made with Django is running makemigrations without reviewing what was detected:

Terminal
$ python manage.py makemigrations
Migrations for 'blog':
blog/migrations/0004_auto_20260409.py
- Alter field title on article
- Remove field author from article

I should always review the generated file before applying it, especially in production. Another mistake is creating unmergeable conflicts by running migrations in the wrong order across development and production environments.

Alembic Mistakes

The most common Alembic error I’ve seen is skipping the env.py configuration. When target_metadata isn’t set to your model’s metadata, --autogenerate won’t detect any changes:

alembic/env.py
# WRONG - autogenerate won't detect changes
target_metadata = None
# CORRECT - imports your models
from models import Base
target_metadata = Base.metadata

Another mistake is not understanding the revision chain. If you manually edit down_revision, you can break the migration graph and make rollbacks impossible.

Comparison Summary

FeatureDjango ORMSQLAlchemy/Alembic
Setup RequiredNonealembic init, config files
Auto-detectionAutomatic from modelsRequires --autogenerate + env.py setup
Framework Lock-inDjango onlyAny SQLAlchemy project
Conflict HandlingAutomatic detectionManual merge commands
Learning CurveLow (2 commands)Medium (config + revision pattern)
Branching SupportLimitedFull support
Multiple DatabasesPer-database configMultiple configurations

When to Choose What

I recommend Django ORM migrations when:

  • You’re building a Django application
  • Your team values simplicity and consistency
  • You want zero configuration overhead
  • You need automatic conflict resolution

I recommend SQLAlchemy/Alembic when:

  • You’re using Flask, FastAPI, or other non-Django frameworks
  • You need framework independence
  • Your migration scenarios are complex (branching, multiple databases)
  • You want full control over migration operations

One Reddit user I found summarized it well: “Django ORM and how easy to handle DB migrations are is why I’m having troubles finding a good reason to switch away from it.” This resonates with my experience - Django’s integration is hard to beat if you’re already in the Django ecosystem.

But for projects where flexibility matters, Alembic gives you the control you need. The setup cost is worth it when you’re working across multiple frameworks or need advanced migration features.

Summary

In this post, I compared Django ORM migrations and SQLAlchemy/Alembic. The key point is: if you’re already using Django, stick with Django migrations for their simplicity and automatic conflict resolution. If you’re using Flask, FastAPI, or need framework-independent migrations, Alembic gives you the control and flexibility you need.

Final Words + More Resources

My intention with this article was to help others share my knowledge and experience. If you want to contact me, you can contact by email: Email me

Here are also the most important links from this article along with some further resources that will help you in this scope:

Oh, and if you found these resources useful, don’t forget to support me by starring the repo on GitHub!

Comments