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

Who Is The Sky? by David Byrne

Seeing David Byrne last night for the first-ever performance of Who Is The Sky was one of the best and most impactful concerts/experiences of my life.

via Blog – Brad Frost September 17, 2025

Pluralistic: Conspiratorialism's causal chain (17 Sep 2025)

Today's links Conspiratorialism's causal chain: A four-part begat. Hey look at this: Delights to delectate. Object permanence: Legal threats over HDCP leaks; Print your own TSA luggage keys; "A Natural History of Empty Lots." Upcoming appe…

via Pluralistic: Daily links from Cory Doctorow September 17, 2025

Theatre Review: Interview (Understudy Performance) ★★★☆☆

One of the best things about London theatre is that once in a while a show will give its understudies a chance to break out of the dressing room and soar on the stage. It's a chance to see talented performers at a discount price. What's not to lik…

via Terence Eden’s Blog September 17, 2025

Generated by openring