DEV Community

Chris Chen
Chris Chen

Posted on

Database Migration with Python

I think the idea of data migration is similar to git commit. Every migration is a descriptive file that documents changes you've made to the database, such as adding new columns or tables.
Let's dive right into it!


Requirement

alembic documentation

# python version
python==3.10

# requirements.txt
alembic==1.7.6
sqlalchemy==1.4.25
python-dotenv==0.19.0
Enter fullscreen mode Exit fullscreen mode

Folder structure

Folder Structure

backend/

alembic init ./app/migration
This command will generate a alembic.ini file and a migration folder

Folder Structure after alembic init

config alembic.ini

Make sure the path is correct

[alembic]
# path to migration scripts
script_location = ./app/migration
Enter fullscreen mode Exit fullscreen mode

You can set up your sqlalchemy url here, but I prefer doing it in env.py

sqlalchemy.url = mysql+pymysql://user:password@host/database
Enter fullscreen mode Exit fullscreen mode
model/

Define your model class in the model folder

# base.py
from sqlalchemy import Column, DateTime, func
from sqlalchemy.dialects.mysql import INTEGER
from sqlalchemy.ext.declarative import declarative_base


Base = declarative_base()


class BaseMixin:
    def __init__(self, **kwargs):
        for key, value in kwargs.items():
            if key in dir(self):
                exec(f"self.{key} = {value}")

    id = Column(INTEGER(unsigned=True), primary_key=True, autoincrement=True)
    created_at = Column(DateTime(timezone=True), server_default=func.now())
    updated_at = Column(
        DateTime(timezone=True), 
        server_default=func.now(), 
        onupdate=func.now())
Enter fullscreen mode Exit fullscreen mode
# model.py
from .base import Base, BaseMixin
from sqlalchemy import Column, String, Integer


class Users(Base, BaseMixin):
    __tablename__ = 'users'

    username = Column(String(50))
    age = Column(Integer())
Enter fullscreen mode Exit fullscreen mode
# __init__.py
from .base import Base, BaseMixin
from .model import Users
Enter fullscreen mode Exit fullscreen mode
config env.py

I make some changes upon the default env.py

from logging.config import fileConfig
from sqlalchemy import create_engine
from alembic import context
from dotenv import load_dotenv

import os
import sys

sys.path.append(os.path.split(os.getcwd())[0])

load_dotenv()

# this is the Alembic Config object, which provides
# access to the values within the .ini file in use.
config = context.config

# Interpret the config file for Python logging.
# This line sets up loggers basically.
fileConfig(config.config_file_name)

# add your model's MetaData object here
# for 'autogenerate' support
from app.model import Base, Users
target_metadata = Base.metadata

# other values from the config, defined by the needs of env.py,
# can be acquired:
# my_important_option = config.get_main_option("my_important_option")
# ... etc.

def get_url():

    return os.getenv('MYSQL_URL')


def run_migrations_offline():
    """Run migrations in 'offline' mode.

    This configures the context with just a URL
    and not an Engine, though an Engine is acceptable
    here as well.  By skipping the Engine creation
    we don't even need a DBAPI to be available.

    Calls to context.execute() here emit the given string to the
    script output.

    """
    url = get_url()
    context.configure(
        url=url,
        target_metadata=target_metadata,
        literal_binds=True,
        dialect_opts={"paramstyle": "named"},
    )

    with context.begin_transaction():
        context.run_migrations()


def run_migrations_online():
    """Run migrations in 'online' mode.

    In this scenario we need to create an Engine
    and associate a connection with the context.

    """
    connectable = create_engine(get_url())

    with connectable.connect() as connection:
        context.configure(
            connection=connection, target_metadata=target_metadata
        )

        with context.begin_transaction():
            context.run_migrations()


if context.is_offline_mode():
    run_migrations_offline()
else:
    run_migrations_online()

Enter fullscreen mode Exit fullscreen mode
  • Add path to system
import os
import sys

sys.path.append(os.path.split(os.getcwd())[0])
Enter fullscreen mode Exit fullscreen mode
  • Load .env file
from dotenv import load_dotenv

load_dotenv()
Enter fullscreen mode Exit fullscreen mode
  • Add your metadata object for --autogenerate
from app.model import Base, Users
target_metadata = Base.metadata
Enter fullscreen mode Exit fullscreen mode
  • Read url from .env
def get_url():

    return os.getenv('MYSQL_URL')
Enter fullscreen mode Exit fullscreen mode
  • Modify default function

Function1

Function2

backend/
First commit

alembic revision --autogenerate -m "first commit"
In /migration/versions folder, you should see a python file like this

"""first commit

Revision ID: 9453de59bdc9
Revises: 
Create Date: 2022-02-15 10:21:24.459889

"""
from alembic import op
import sqlalchemy as sa
from sqlalchemy.dialects import mysql

# revision identifiers, used by Alembic.
revision = '9453de59bdc9'
down_revision = None
branch_labels = None
depends_on = None


def upgrade():
    # ### commands auto generated by Alembic - please adjust! ###
    op.create_table('users',
    sa.Column('id', mysql.INTEGER(unsigned=True), autoincrement=True, nullable=False),
    sa.Column('created_at', sa.DateTime(timezone=True), server_default=sa.text('now()'), nullable=True),
    sa.Column('updated_at', sa.DateTime(timezone=True), server_default=sa.text('now()'), nullable=True),
    sa.Column('username', sa.String(length=50), nullable=True),
    sa.Column('age', sa.Integer(), nullable=True),
    sa.PrimaryKeyConstraint('id')
    )
    # ### end Alembic commands ###


def downgrade():
    # ### commands auto generated by Alembic - please adjust! ###
    op.drop_table('users')
    # ### end Alembic commands ###
Enter fullscreen mode Exit fullscreen mode
First migration

alembic upgrade head
This command will run the upgrade operation, and generate the table in your database

Voila! You've learned alembic!

Top comments (0)