読者です 読者をやめる 読者になる 読者になる

Usual Software Engineer

よくあるソフトウェアエンジニアのブログ

シンプルなPythonのDBマイグレーションツールsimple-db-migrateとは

Webサービスを継続的に開発運用していくうえで
データベースのデータ構造の更新は避けられませんね。
複数人で開発する時に必須と言っても良いソースコードのバージョン管理ですが
ソースコードのみならず、データベースのバージョン管理をすることで
すでに存在するデータを壊さずに必要になった構造変更などを正確かつ便利に実行していくため
データベースのマイグレーションツールを利用することが多くなってきました。

Ruby on RailsやCodeIgniter、Djangoなどフレームワークとセットで
マイグレーションの機能を利用できるものもありますが
今回はそれ単体で動くPythonのDBマイグレーションツールであるsimple-db-migrateを使ってみました。 理由としては

  • 生のSQLで書きたい
  • シンプルな設定で、単純なUPとDOWNを実行したい
  • 導入するサービスの下回りが主にPythonを使っていたので、Python製のものが良い

というところです。
元々flywayを使っていたせいか、Webフレームワークのモデルに密結合しているような
マイグレーションを行いたくない、という好みもありました。

simple-db-migrateを利用する

GitHubリポジトリのREADMEと公式のドキュメントがあるのでこれで事足ります。

と言いつつネット上に日本語の情報があまり無さそうだったので
補足がてら書いておきます。

インストール

$ pip install simple-db-migrate

これだけです。

準備

用意するのは

  • simple-db-migrate.confという設定ファイル
  • 各.migrationファイル

だけです。※コマンドラインオプションでも対応できるので設定ファイルは必須ではないですが。

公式にも例があるので、参考にすると良いと思います。
https://github.com/guilhermechapiewski/simple-db-migrate/tree/master/example/mysql

使い方

下記のような設定ファイルを用意します。

simple-db-migrate.conf

DATABASE_HOST = os.getenv("DATABASE_HOST") or "localhost"
DATABASE_USER = os.getenv("DATABASE_USER") or "root"
DATABASE_PASSWORD = os.getenv("DATABASE_PASSWORD") or ""
DATABASE_NAME = os.getenv("DATABASE_NAME") or "migration_example"
DATABASE_MIGRATIONS_DIR = os.getenv("MIG_DIR") or "."

下記のコマンドで.migrationファイルを生成します。

$ db-migrate --create create_table_users

Starting DB migration on host/database "localhost/migration_example" with user "root"...
- Created file '/Users/innossh/tmp/simple-db-migrate/20141025203258_create_table_users.migration'

Done.

create_table_usersの部分は、.migrationの命名に利用されます。
生成された.migrationファイルを編集します。
下記のようにしました。

20141025203258_create_table_users.migration

#-*- coding:utf-8 -*-
SQL_UP = u"""
CREATE TABLE users (
  id int(11) NOT NULL auto_increment,
  oid varchar(500) default NULL,
  first_name varchar(255) default NULL,
  last_name varchar(255) default NULL,
  email varchar(255) default NULL,
  PRIMARY KEY  (id)
);
"""

SQL_DOWN = u"""
DROP TABLE users;
"""

では、マイグレーションを実行してみましょう。

$ db-migrate

Starting DB migration on host/database "localhost/migration_example" with user "root"...
- Current version is: 0
- Destination version is: 20141025203258

Starting migration up!
*** versions: ['20141025203258']

===== executing 20141025203258_create_table_users.migration (up) =====

Done.

これでUPのSQLが実行できました。確認してみると

mysql> use migration_example
Database changed
mysql> show tables;
+-----------------------------+
| Tables_in_migration_example |
+-----------------------------+
| __db_version__              |
| users                       |
+-----------------------------+
2 rows in set (0.00 sec)

mysql> desc users;
+------------+--------------+------+-----+---------+----------------+
| Field      | Type         | Null | Key | Default | Extra          |
+------------+--------------+------+-----+---------+----------------+
| id         | int(11)      | NO   | PRI | NULL    | auto_increment |
| oid        | varchar(500) | YES  |     | NULL    |                |
| first_name | varchar(255) | YES  |     | NULL    |                |
| last_name  | varchar(255) | YES  |     | NULL    |                |
| email      | varchar(255) | YES  |     | NULL    |                |
+------------+--------------+------+-----+---------+----------------+
5 rows in set (0.00 sec)

ちゃんとusersテーブルが作成されています。
ここで気になるのが__db_version__テーブルですね。

mysql> select * from __db_version__ \G
*************************** 1. row ***************************
      id: 1
 version: 0
   label: NULL
    name: NULL
  sql_up: NULL
sql_down: NULL
*************************** 2. row ***************************
      id: 2
 version: 20141025203258
   label: NULL
    name: 20141025203258_create_table_users.migration
  sql_up:
CREATE TABLE users (
  id int(11) NOT NULL auto_increment,
  oid varchar(500) default NULL,
  first_name varchar(255) default NULL,
  last_name varchar(255) default NULL,
  email varchar(255) default NULL,
  PRIMARY KEY  (id)
);

sql_down:
DROP TABLE users;

2 rows in set (0.00 sec)

上記のようなレコードが追加されていました。
このテーブルでどのバージョンまでのマイグレーションが実行済みなのか判別しているようです。
ついでにDOWNも実行してみましょう。

$ db-migrate --migration=0

Starting DB migration on host/database "localhost/migration_example" with user "root"...
- Current version is: 20141025203258
- Destination version is: 0

Starting migration down!
*** versions: ['20141025203258']

===== executing 20141025203258_create_table_users.migration (down) =====

Done.

mysqlを確認してみると

mysql> show tables;
+-----------------------------+
| Tables_in_migration_example |
+-----------------------------+
| __db_version__              |
+-----------------------------+
1 row in set (0.00 sec)

mysql> select * from __db_version__ \G
*************************** 1. row ***************************
      id: 1
 version: 0
   label: NULL
    name: NULL
  sql_up: NULL
sql_down: NULL
1 row in set (0.00 sec)

usersテーブルはDOWNにより削除されて、__db_version__テーブルが残っています。
このように、バージョン指定でマイグレーションを実行し
現在のバージョンとの差異でUPやDOWNのSQLが実行されるようになっています。便利!

ハマッたこと

  • 日本語コメントが付いたSQLを実行しようとするとエラーになってしまった
    • import sys; sys.setdefaultencoding('utf-8')と記述されたsitecustomize.pyファイルを
      Pythonインストールディレクトリのsite-packages配下に置いたらエラー解消した
  • 複数DBスキーマをいっぺんに扱いたかったが、ワークアラウンドな感じになってしまった

複数環境(develop,staging,productionなど)にも対応していたので
わりと問題なく使えそうでした。
とはいえ複数人で利用するとやはり競合(ファイルじゃなくてバージョンが)が起きてしまうので難しいですね。
良い案を思いついたら自分で作ってみたい。