Recreating Foreign Keys with Alembic

| programming | python | databases |

Alembic is a great tool for keeping track of schema changes in python applications. I am using it to manage DB migrations for braindump along with Flask SQL Alchemy as my ORM. One challenge is managing proper foreign key constraints. By default if you define a foreign key relationship in your schema definition it will not generate the proper migration code. For example, in braindump we have a one to many relationship between users and notes.

class User(UserMixin, db.Model):
    __tablename__ = 'users'
    id = db.Column(db.Integer, primary_key=True)
    email = db.Column(db.String(254), unique=True, index=True)
    password_hash = db.Column(db.String(256))
    confirmed = db.Column(db.Boolean, default=False)
    avatar_hash = db.Column(db.String(32))
    created_date = db.Column(db.DateTime(), default=datetime.utcnow)
    updated_date = db.Column(db.DateTime(), default=datetime.utcnow)
<span class="nx">notes</span> <span class="o">=</span> <span class="nx">db</span><span class="p">.</span><span class="nx">relationship</span><span class="p">(</span>
    <span class="s1">'Note'</span><span class="p">,</span> <span class="nx">backref</span><span class="o">=</span><span class="s1">'author'</span><span class="p">,</span>
    <span class="nx">lazy</span><span class="o">=</span><span class="s1">'dynamic'</span><span class="p">,</span> <span class="nx">cascade</span><span class="o">=</span><span class="s2">"all, delete-orphan"</span><span class="p">)</span>

Even though we define the cascade behavior using SQLAlchemy. When we generate the migration with alembic we get something like this:
sa.ForeignKeyConstraint(['author_id'], ['users.id'], ),
Notice how we are missing the ondelete action. What we actually want is something like this:
sa.ForeignKeyConstraint(['author_id'], ['users.id'], ondelete='CASCADE')
Running the default migration will not create the proper relationship in your database and in our case we are not able to delete a user until we have deleted all of the related records as well. There are two ways to fix this. If you catch this before running your migration addingondelete='CASCADE' will create the proper relationship. If you are like me, and do not catch this, then you will need to run a second migration to remove and recreate these keys. The migration code to do this is shown below:
from alembic import op
import sqlalchemy as sa

def upgrade(): with op.batch_alter_table(“notes”) as batch_op: batch_op.drop_constraint( “notes_author_id_fkey”, type_=“foreignkey”) op.create_foreign_key( “notes_author_id_fkey”, “notes”, “users”, [“author_id”], [“id”], ondelete=“CASCADE”)

Now you have the proper foreign key constraints and the CASCADE action exists in the DB.

Thank you for reading! Share your thoughts with me on bluesky, mastodon, or via email.

Check out some more stuff to read down below.

Most popular posts this month

Recent Favorite Blog Posts

This is a collection of the last 8 posts that I bookmarked.

Articles from blogs I follow around the net

Don’t Let Anyone Take Away Your Big Box of Cables

Speaking of being on the internet and finding things that make you go, “Hey! It’s not just me!” I scrolled across this skeet from Tyler Gaw: I just dug to the bottom of my Big Box of Cables to find two cables that I needed for something. They've been in th...

via Jim Nielsen’s Blog September 9, 2026

Do We Have to Choose Between Rails and Trails?

A Bike-Riding Railfan on the Pros and Cons of "Railbanking"

via High Speed September 9, 2026

Greyscale in Doodle Scan

I added a greyscale colour mode to Doodle Scan

via Robb Knight • Posts • Atom Feed September 9, 2026

Generated by openring