Database Migration with Python
Tags: python, database, beginners
Published: 2022-02-15 09:34:45

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](https://alembic.sqlalchemy.org/en/latest/tutorial.html)

```text
# python version
python==3.10

# requirements.txt
alembic==1.7.6
sqlalchemy==1.4.25
python-dotenv==0.19.0
```


#### Folder structure

![Folder Structure](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/lttelel4r27j5u96cae7.png)

##### backend/
`alembic init ./app/migration`
This command will generate a *alembic.ini* file and a *migration* folder

![Folder Structure after alembic init](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/ay8wh3g2bqnscfl1u7ud.png)

##### config *alembic.ini*
Make sure the path is correct
```Python
[alembic]
# path to migration scripts
script_location = ./app/migration
```
You can set up your sqlalchemy url here, but I prefer doing it in *env.py*
```
sqlalchemy.url = mysql+pymysql://user:password@host/database
```
##### model/
Define your model class in the *model* folder
```python
# 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())
```
```python
# 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())
```
```python
# __init__.py
from .base import Base, BaseMixin
from .model import Users
```

##### config *env.py*
I make some changes upon the default env.py

```python
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()

```

* Add path to system
```python
import os
import sys

sys.path.append(os.path.split(os.getcwd())[0])
```
* Load *.env* file
```python
from dotenv import load_dotenv

load_dotenv()
```
* Add your metadata object for *--autogenerate*
```python
from app.model import Base, Users
target_metadata = Base.metadata
```
* Read url from *.env*
```python
def get_url():
    
    return os.getenv('MYSQL_URL')
```
* Modify default function

![Function1](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/ht68jauk7hupgllwn2h8.png)

![Function2](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/qowwbmht1fy2qb4ql4k0.png)

##### backend/
###### First commit
`alembic revision --autogenerate -m "first commit"`
In */migration/versions* folder, you should see a python file like this
```python
"""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 ###
```
###### First migration
`alembic upgrade head`
This command will run the upgrade operation, and generate the table in your database

### Voila! You've learned alembic!