VisasQ Dev Blog

ビザスク開発ブログ

alembicで複数DBを一括作成する

東京の夏は、人類が生きていくには暑すぎる。

夏だけ北海道に住みたいと最近本気で思い始めています。こんにちは、DPEチームの嶺岸です。

alembicを触ることになった経緯

入社して右も左もわからないまま、なんでもやりますよ〜、とだけ言っていた私にある日こんなタスクをやってみないかとお話がありました。

Github Actionsでユニットテストができるようにしたい」

はい!!よろこんで!!! なんでもやるけどなんでもできるとは言ってない、そんな私はもちろん当該のアプリケーションを触ったことがない。なんならこれまでPythonフレームワークをひとつも触ったことがない。もっと言うとここ五年くらいアプリケーションを触ってない。

まあ、技術選定はもう済んでいて実装するだけらしいし、Dockerの扱いなんてみんな一緒だし、Github Actionsは好きだから大丈夫だろう。これを日本語で安請け合いと言います。対戦よろしくお願いします。

ORMはもうSQLAlchemyで書いてあるとの話だったので、ほな環境作ってDB作れるようにしてテスト走るようにしたらええんやな。楽勝楽勝。と思ってましたね。

まぁ、そんな技術に一ミリも関係のない与太話はそこまでにしましょうか。

実際のところ、どのような実装にしたのか? 実装自体は至ってシンプルです。

alembicの導入

alembic init

今回のケースでは複数DBを扱い、かつ本番様の変更管理などを必要とせずテスト用に現状最新の構成を一発で作成するだけなので、multidbテンプレートを採用しています。

multidbテンプレートで構築される環境では複数DBへのマイグレーションファイルがひとつにまとめられてしまい、DBごとの管理ができないため、変更管理をしたい場合は慎重に検討する必要があります。

実行コマンド

alembic init --template multidb

作成されるファイル

├── alembic.ini
└── migrations
    ├── README
    ├── env.py
    ├── script.py.mako
    └── versions

各ファイルの編集

編集すべきファイルはalembic.iniとenv.pyのみです。

alembic.ini

このファイルにはalembicの設定を記述します。

multidbテンプレートを採用したためdatabasesを列挙できる形になっているので、各DB設定を記載します。

# DB名を列挙する
databases = database_a, database_b, database_c

# 各DBの設定を記述する
[database_a]
database_a.url = mysql+pymysql://user:pass@unittest-db/database_a

[database_b]
database_b.url = mysql+pymysql://user:pass@unittest-db/database_b

[database_c]
database_c.url = mysql+pymysql://user:pass@unittest-db/database_c

env.py

このファイルではマイグレーションファイルの作成ロジックを記述します。

今回はモデルファイルが増減した際も当ファイルに追記なく動的に読み込めるようにするため、ファイル上部にモデルファイル読み込みロジックを追加しています。

# モデルファイル読み込み
def list_target_models(db_name):
    directory = f"./src/models/{db_name}"
    m = MetaData()
    for root, _, files in os.walk(directory):
        if "__pycache__" not in root:
            for file in files:
                if "__init__.py" not in file:
                    module_name = (
                        os.path.join(root, file)
                        .replace("/", ".")
                        .replace(".py", "")
                        .replace("..", "")
                    )
                    for table in importlib.import_module(
                        module_name,
                    ).Base.metadata.tables.values():
                        table.to_metadata(m)
    return m

target_metadata = {}

for db_name in re.split(r",\s*", db_names):
    target_metadata[db_name] = list_target_models(db_name)

モデルファイルを動的読み込みとすることで、DBに変更があった場合もモデルファイルさえ修正すればよく、通常の開発においてalembicの存在を意識する必要がなくなります。

今回のケースではalembicはユニットテストにのみ利用するため、極力alembic自体のメンテナンスの手間をなくす方針で作成しています。

留意すべき点など

マイグレーションするとき外部制約キーを外す

この構造はマイグレーションの歴史を保持せず、ORMで表現されているDBをそのまま作成しようとします。しかし、これまでに様々な歴史を積み重ねてきたDBは一発で作ろうとするとだいたい外部制約の関係でマイグレーション失敗します。歴史の通りにマイグレーションを実行していかなければ作成できない形になってしまっているわけですね。

これを避けるために、ユニットテスト環境のDBを作成する際は下記のような実行をする必要があります。

$ SET GLOBAL FOREIGN_KEY_CHECKS = 0;
$ create initial table
$ alembic upgrade head
$ SET GLOBAL FOREIGN_KEY_CHECKS = 1;

この辺りの処理はGithub Actionsに記述してあるので、一度書いてしまえば普段はさほど意識するタイミングはありません。楽でいいですね。

SQLAlchemyのBaseオブジェクトをDBを跨いで共有しない

これはalembicではなくSQLAlchemyの話になりますが、BaseオブジェクトをDBを跨いで共有してしまうと、すべてのモデルファイルを読み込むenv.pyではTable情報をDBを跨いで持ち回ってしまうため、target_metadataの中身がグチャグチャになります。なりました。

各DBごとにalembicのマイグレーション環境を作る場合には起こらない現象のような気はしますが、なるべくBaseオブジェクトはDBごとに分けたほうがいいのかなと思います。

おわり

alembicの導入についてはこれで終わりです。

複数DBを扱うにあたりどのような構成にするべきか、散々悩んで色々試作したのちに行き着いた構成なのですが、まあまあすっきりして見通しよいものにできたかなと思います。

ユニットテスト用のDocker環境構築などは、特に目新しい事をしていないので今回は割愛とします。

夏が早く過ぎ去ることを祈って。