Pythonのmigrationツールのalembicを動かしてみた

# 目標

python の migration パッケージ[alembic · PyPI](https://pypi.org/project/alembic/)を動かしてみる

## alembic とは？？

- [Welcome to Alembic’s documentation! — Alembic 1.4.2 documentation](https://alembic.sqlalchemy.org/en/latest/)

Python で SQLAlchemy を使用しているときに DB の管理をしてくれる migration ツール

## 環境準備

### Docker で環境を構築

手軽に構築したいので Docker で行う

```bash:フォルダ構成
.
├── README.md
├── docker-compose.yml
└── src
    └── model.py
```

```yaml:docker-compose.yml
version: "3"

services:
  db:
    image: postgres:11.7
    container_name: alembic-db
    ports:
      - 5432:5432
    environment:
      - POSTGRES_USER=postgres
      - POSTGRES_PASSWORD=postgres
      - POSTGRES_DB=almebic
  app:
    image: python:3.8.2-slim
    container_name: alembic-app
    volumes:
      - ./src:/usr/src
    environment:
      - PYTHONPATH=${PYTHONPATH}:/usr/src
    tty: true
```

### alembic のインストール

pip で必要なパッケージをインストールする

```bash
pip install alembic psycopg2-binary
```

alembic をインストールすれば **SQLAlchemy** も同時にインストールされる
**[psycopg2-binary · PyPI](https://pypi.org/project/psycopg2-binary/)** は postgres に接続するために使用する

```bash:alembicのインストール
root@9a7582105665:/usr/src# pip install alembic psycopg2-binary
Collecting alembic
  Downloading alembic-1.4.2.tar.gz (1.1 MB)
     |████████████████████████████████| 1.1 MB 7.8 MB/s
  Installing build dependencies ... done
  Getting requirements to build wheel ... done
    Preparing wheel metadata ... done
Collecting psycopg2-binary
  Downloading psycopg2_binary-2.8.5-cp38-cp38-manylinux1_x86_64.whl (3.0 MB)
     |████████████████████████████████| 3.0 MB 32.3 MB/s
Collecting python-dateutil
  Downloading python_dateutil-2.8.1-py2.py3-none-any.whl (227 kB)
     |████████████████████████████████| 227 kB 23.8 MB/s
Collecting Mako
  Downloading Mako-1.1.2-py2.py3-none-any.whl (75 kB)
     |████████████████████████████████| 75 kB 11.2 MB/s
Collecting SQLAlchemy>=1.1.0
  Downloading SQLAlchemy-1.3.16-cp38-cp38-manylinux2010_x86_64.whl (1.2 MB)
     |████████████████████████████████| 1.2 MB 54.3 MB/s
Collecting python-editor>=0.3
  Downloading python_editor-1.0.4-py3-none-any.whl (4.9 kB)
Collecting six>=1.5
  Downloading six-1.14.0-py2.py3-none-any.whl (10 kB)
Collecting MarkupSafe>=0.9.2
  Downloading MarkupSafe-1.1.1-cp38-cp38-manylinux1_x86_64.whl (32 kB)
Building wheels for collected packages: alembic
  Building wheel for alembic (PEP 517) ... done
  Created wheel for alembic: filename=alembic-1.4.2-py2.py3-none-any.whl size=159543 sha256=dc29f47f6c24908d9413da7e3c969c64c252d0cbf9f90fca7cfbb5782b2452d0
  Stored in directory: /root/.cache/pip/wheels/70/08/70/cea787a7e95817b831469fa42af076046e55a05f7c94657463
Successfully built alembic
Installing collected packages: six, python-dateutil, MarkupSafe, Mako, SQLAlchemy, python-editor, alembic, psycopg2-binary
Successfully installed Mako-1.1.2 MarkupSafe-1.1.1 SQLAlchemy-1.3.16 alembic-1.4.2 psycopg2-binary-2.8.5 python-dateutil-2.8.1 python-editor-1.0.4 six-1.14.0
```

```bash:パッケージのversionを確認
root@ecce2b20848e:/usr/src# pip list
Package         Version
--------------- -------
alembic         1.4.2
Mako            1.1.2
MarkupSafe      1.1.1
pip             20.1
psycopg2-binary 2.8.5
python-dateutil 2.8.1
python-editor   1.0.4
setuptools      46.1.3
six             1.14.0
SQLAlchemy      1.3.16
wheel           0.34.2
```

## alembic の環境を作成

### alembic init で migration の環境の作成

```bash
alembic init {migrationの環境名}
```

で migration の環境を作成する

```bash:alembicの初期設定
root@ecce2b20848e:/usr/src# alembic init migration
  Creating directory /usr/src/migration ...  done
  Creating directory /usr/src/migration/versions ...  done
  Generating /usr/src/migration/README ...  done
  Generating /usr/src/alembic.ini ...  done
  Generating /usr/src/migration/env.py ...  done
  Generating /usr/src/migration/script.py.mako ...  done
  Please edit configuration/connection/logging settings in '/usr/src/alembic.ini' before proceeding.
```

作成が完了すると以下のような構造になる
**migration ディレクトリ** と **alembic.ini ファイル** ができたことが確認できる

```bash:tree
.
├── README.md
├── docker-compose.yml
└── src
    ├── alembic.ini
    ├── migration
    │   ├── README
    │   ├── env.py
    │   ├── script.py.mako
    │   └── versions
    └── model.py
```

### alembic init で生成されるファイルについて

- **env.py**
  alembic のツールが起動する度に実行される Python のスクリプトが書かれている
  SQLAlchemy の Engine を設定・生成し migration が実行できるように書かれている
  カスタマイズ可能なスクリプト

- **README.md**
  どのような環境で migration 環境を作成したか記述してある

- **script.py.mako**
  新しい migration のスクリプトを生成するために使用される Mako テンプレートファイル
  ここにあるものは何でも version/内の新しいファイルを生成するために使用される

- **versions ディレクトリ**
  migration スクリプトが保存されるディレクトリ

- **alembic.ini**
  alembic のスクリプトが実行された時に探すファイル
  実行時の設定を記述する
  ex. env.py の場所・log の出力・migration ファイルの命名規則など

## migration の実行

### alembic.ini の編集

DB に接続できるように alembic.ini ファイルを編集する
ini ファイルの以下の箇所を DB の接続情報に書き換える

```ini:alembic.ini（編集前）
sqlalchemy.url = driver://user:pass@localhost/dbname
```

```ini:alembic.ini（編集後）
sqlalchemy.url = postgresql://postgres:postgres@alembic-db:5432/almebic
```

接続情報は **docker-compose.yml** に記述してあるものを使用する

### migration ファイルの作成

**revision** コマンドで migration ファイルを作成する

```bash
alembic revision -m {ファイル名}
```

```bash:migrationファイルの作成
root@ecce2b20848e:/usr/src# alembic revision -m "create account table"
  Generating /usr/src/migration/versions/b5f586d58141_create_account_table.py ...  done
```

実行後に versions ディレクトリ配下に migration ファイルが作成される

```python:b5f586d58141_create_account_table.py
"""create account table

Revision ID: b5f586d58141
Revises:
Create Date: 2020-05-02 17:49:20.493493

"""
from alembic import op
import sqlalchemy as sa


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


def upgrade():
    pass


def downgrade():
    pass

```

### migration ファイルの編集

生成された migration ファイルを編集する
ここでは公式のをコピーして account テーブルを作成する

```python:b5f586d58141_create_account_table.py
"""create account table

Revision ID: b5f586d58141
Revises:
Create Date: 2020-05-02 17:49:20.493493

"""
from alembic import op
import sqlalchemy as sa


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


def upgrade():
    op.create_table(
        'account',
        sa.Column('id', sa.Integer, primary_key=True),
        sa.Column('name', sa.String(50), nullable=False),
        sa.Column('description', sa.Unicode(200)),
    )

def downgrade():
    op.drop_table('account')

```

### migration の実行

**upgrade** コマンドで migration を実行する

```bash
alembic upgrade head
```

**head** は最新の version まで migration を実行する
1 つだけ version を上げたい場合は、head ではなく **+1** を使用する
version を下げたいときは **downgrade** コマンドを使用する
初期状態まで戻したい場合は

```bash
alembic downgrade base
```

を実行する
一つ前の version に戻すときは、base ではなく **-1** を使用する

```bash:migrationの実行
root@ecce2b20848e:/usr/src# alembic upgrade head
INFO  [alembic.runtime.migration] Context impl PostgresqlImpl.
INFO  [alembic.runtime.migration] Will assume transactional DDL.
INFO  [alembic.runtime.migration] Running upgrade  -> b5f586d58141, create account table
```

## migration のファイルを自動で生成させる

[Auto Generating Migrations — Alembic 1.4.2 documentation](https://alembic.sqlalchemy.org/en/latest/autogenerate.html)にあるように env.py を編集することで
Python で定義している **SQLAlchemy** のモデルの情報から自動的に migration ファイルを作成できるようになる
まずは SQLAlchemy でモデルの定義を行う

### SQLAlchemy のモデルの定義

モデルの定義では先ほど追加した account テーブルを SQLAlchemy のモデルで定義して
**created_at** と **updated_at** の列を追加する
env.py に SQLAlchemy の Engine を渡したいので定義する

```python:model.py
from datetime import datetime

from sqlalchemy import create_engine, Column, String, Integer, Unicode, DateTime
from sqlalchemy.ext.declarative import declarative_base

# Engine の作成
Engine = create_engine(
    "postgresql://postgres:postgres@alembic-db:5432/almebic",
    encoding="utf-8",
    echo=False
)

'''
モデルの Base を作成
この Base を基にモデルを定義するとmetadataにモデルの情報が格納されていく
'''
ModelBase = declarative_base()


class AcountModel(ModelBase):
    """
    AcountModel
    """
    __tablename__ = 'account'

    id = Column(Integer, primary_key=True)
    name = Column(String(50), nullable=False)
    description = Column(Unicode(200))
    created_at = Column(DateTime, default=datetime.now, nullable=False)
    updated_at = Column(DateTime, default=datetime.now, nullable=False)

```

### env.py の編集

env.py を編集して model.py で定義したモデルの情報を取得できるようにする
先頭で先ほど定義した **ModelBase** と **Engine** を import する
**target_metadata** に **ModelBase.metadata** を代入する
また、migration の実行時には **run_migrations_online()** を編集して migration が実行されるようにする

```python:env.py
from logging.config import fileConfig

from sqlalchemy import engine_from_config
from sqlalchemy import pool

from alembic import context
from model import ModelBase, Engine

# 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 myapp import mymodel
# target_metadata = mymodel.Base.metadata
target_metadata = ModelBase.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 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 = config.get_main_option("sqlalchemy.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.

    """
    url = config.get_main_option("sqlalchemy.url")
    connectable = Engine

    with connectable.connect() as connection:
        context.configure(
            url=url,
            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()

```

### autogenerate を使用した migration の実行

revision コマンドに **--autogenerate** というオプションを付けると SQLAlchemy で定義したモデルの情報から migration ファイルを作成する

```bash:revisionコマンド（--autogenerate有り）
root@9a7582105665:/usr/src# alembic revision --autogenerate -m "Added columns."
INFO  [alembic.runtime.migration] Context impl PostgresqlImpl.
INFO  [alembic.runtime.migration] Will assume transactional DDL.
INFO  [alembic.ddl.postgresql] Detected sequence named 'account_id_seq' as owned by integer column 'account(id)', assuming SERIAL and omitting
INFO  [alembic.autogenerate.compare] Detected added column 'account.created_at'
INFO  [alembic.autogenerate.compare] Detected added column 'account.updated_at'
  Generating /usr/src/migration/versions/dcd0d354f648_added_columns.py ...  done
```

実行後に以下のようなファイルが作成される

```python:dcd0d354f648_added_columns.py
"""Added columns.

Revision ID: dcd0d354f648
Revises: b5f586d58141
Create Date: 2020-05-02 18:58:03.864154

"""
from alembic import op
import sqlalchemy as sa


# revision identifiers, used by Alembic.
revision = 'dcd0d354f648'
down_revision = 'b5f586d58141'
branch_labels = None
depends_on = None


def upgrade():
    # ### commands auto generated by Alembic - please adjust! ###
    op.add_column('account', sa.Column('created_at', sa.DateTime(), nullable=False))
    op.add_column('account', sa.Column('updated_at', sa.DateTime(), nullable=False))
    # ### end Alembic commands ###


def downgrade():
    # ### commands auto generated by Alembic - please adjust! ###
    op.drop_column('account', 'updated_at')
    op.drop_column('account', 'created_at')
    # ### end Alembic commands ###

```

あとは upgrade コマンドで migration が完了する

## おまけ

### migration で生成されるファイルの名前を変えたい時は？

`dcd0d354f648_added_columns.py`ではなく命名規則を変更したい時（日付の情報を含ませたいなど）は alembic.ini を編集する
alembic.ini の **file_template** を編集する

```ini:例
file_template = %%(year)d%%(month).2d%%(day).2d-%%(hour).2d%%(minute).2d_%%(slug)s
```

### autogenerate はどんなな変更でも検知してくれるの？

一部検知してくれない変更もあるので注意が必要
詳しくは下記を参照
[Auto Generating Migrations — Alembic 1.4.2 documentation](https://alembic.sqlalchemy.org/en/latest/autogenerate.html#what-does-autogenerate-detect-and-what-does-it-not-detect)