0
2

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 3 years have passed since last update.

「知らぬ間によくわからないDBスキーマが追加されてるんだけど・・・」みたいなレガシー環境でDBテーブル定義をバージョン管理したい

Posted at

やりたかったこと

  • DBテーブル定義をバージョン管理したい
  • 修正用をレビューしたい

(今時の新規サービスですと少ないかと思いますが)
テーブル定義のマスターがDBにあるのみで定義の変更を各々の開発者が行う(他の開発者は認識しづらい)という運用になってしまっているサービスだと便利かと思います

※MySQL系でのみ動作確認済みです、最低限になるので必要に応じてカスタマイズしてください

エクスポートコード

こちらは初回のみ実行することを想定してます
以後のテーブル定義変更は、出力されたファイルを変更していく形です

"""export_database_schema.py
    データベースからCREATE TABLE文をエクスポートするのに利用します
    optional arguments:
        -h, --help            show this help message and exit
        --user USER           データベースのユーザ名
        --password PASSWORD   データベースのパスワード
        --host HOST           データベースのホスト名
        --port PORT           データベースのポート番号
        --charset CHARSET     データベースの文字コード
        --target TARGET [TARGET ...]
                              エクスポート対象のDBリスト(スペース区切り)
"""

import os
import sys
import shutil
import pymysql
import argparse

ROOT_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))

# 引数の設定
parser = argparse.ArgumentParser()
parser.add_argument("--user", help="データベースのユーザ名", default="root")
parser.add_argument("--password", help="データベースのパスワード", default="password")
parser.add_argument("--host", help="データベースのホスト名", default="127.0.0.1")
parser.add_argument("--port", help="データベースのポート番号", default=3306)
parser.add_argument("--charset", help="データベースの文字コード", default="utf8mb4")
parser.add_argument("--target", help="エクスポート対象のDBリスト(スペース区切り)",
                    default=["users", "shops"],
                    nargs="+")
args = parser.parse_args()

print("Connection: mysql://%s:%s@%s:%s?charset=%s" % (
    args.user,
    args.password,
    args.host,
    args.port,
    args.charset))
con = pymysql.connect(
    user=args.user,
    password=args.password,
    host=args.host,
    port=args.port,
    charset=args.charset)

with con.cursor() as cursor:
    sql = "SHOW DATABASES"
    cursor.execute(sql)
    databases = cursor.fetchall()
    # 指定されたDBが存在する場合のみエクスポート処理を行う
    for d in databases:
        if d[0] not in args.target:
            continue
        # DBごとにディレクトリ作成(存在していたら一度削除)
        currentDirName = "%s/%s" % (ROOT_DIR, d[0])
        if os.path.isdir(currentDirName):
            shutil.rmtree(currentDirName)
        os.mkdir(currentDirName)
        dirname = "%s/schemas" % (currentDirName)
        os.mkdir(dirname)

        print("Export database: ", d[0])
        sql = "USE %s" % d[0]
        cursor.execute(sql)
        sql = "SHOW TABLES"
        cursor.execute(sql)
        tables = cursor.fetchall()
        for t in tables:
            print("\tExporting ", t[0])
            sql = "SHOW CREATE TABLE %s" % t
            cursor.execute(sql)
            schema = cursor.fetchone()
            filename = "%s/%s.sql" % (dirname, t[0])
            with open(filename, mode="w") as f:
                f.write(schema[1] + ";\n\n")

con.close()

例として、python export_database_schema.py --target users shops
を実行することで下記のようなディレクトリ構成でCREATE文ファイルが設置されます

├── users
│   └── schemas # users DB内のテーブルのCREATE文が格納
└── shops
    └── schemas # shops DB内のテーブルのCREATE文が格納

インポートコード

#!/bin/bash

usage() {
  cat << EOS
Usage: $0 [option]
	-t TARGET	対象のテーブル定義ディレクトリ
	-d DATABASE	インポート先データベース名
	-H HOST		データベースのホスト名
	-u USER		データベースのユーザ名
	-p PASSWORD	データベースのパスワード
	-P PORT		データベースのポート番号
	-y 		実行確認をプロンプト省略
EOS
  exit 1
}

TARGET=""
DATABASE_NAME=""
HOST="127.0.0.1"
USER="root"
PASSWORD=""
PORT="3306"
AUTO_YES=false


while getopts d:t:H:u:p:P:yh OPT
do
  case $OPT in
    d) DATABASE_NAME=$OPTARG ;;
    t) TARGET=$OPTARG ;;
    H) HOST=$OPTARG ;;
    u) USER=$OPTARG ;;
    p) PASSWORD=$OPTARG ;;
    P) PORT=$OPTARG ;;
    y) AUTO_YES=true ;;
    h) usage ;;
  esac
done

ROOT_PATH=$(dirname "$(cd "$(dirname "${BASH_SOURCE:-$0}")" && pwd)")

if [ "${DATABASE_NAME}" == "" ]; then
  echo "インポート対象のデータベースを指定してください。"
  echo "  ./$0 -d DATABASE_NAME"
  exit 1
fi

if [ ! -d $ROOT_PATH/$TARGET/schemas ]; then
  echo "指定されたテーブル定義ディレクトリが存在しません"
  echo "${ROOT_PATH}下にディレクトリが存在するか確認してください"
  echo "  ./$0 -t TARGET"
  exit 1
fi

echo "パラメータ -------------------------"
echo "TARGET:		$TARGET"
echo "DATABASE_NAME:	$DATABASE_NAME"
echo "HOST:		$HOST"
echo "USER:		$USER"
echo "PASSWORD:		$PASSWORD"
echo "PORT:		$PORT"
echo "------------------------------------"

# 確認
if ! "${AUTO_YES}"; then
  read -p "\"$DATABASE_NAME\" データベースを初期化していいですか?(y/N)" yn
  case "$yn" in
    [yY]*) ;;
    *) exit 1;;
  esac
fi

echo "データベース初期化中..."
CMD="mysql -h$HOST"
if [ "$USER" != "" ]; then
  CMD="$CMD -u$USER"
fi
if [ "$PASSWORD" != "" ]; then
  CMD="$CMD -p$PASSWORD"
fi
if [ "$PORT" != "" ]; then
  CMD="$CMD -P$PORT"
fi

echo 'SET FOREIGN_KEY_CHECKS = 0;' > "${ROOT_PATH}/tmp.sql"
cat $ROOT_PATH/$TARGET/schemas/*.sql >> "${ROOT_PATH}/tmp.sql"

`$CMD -e "set FOREIGN_KEY_CHECKS=0; DROP DATABASE IF EXISTS $DATABASE_NAME;"`
`$CMD -e "CREATE DATABASE $DATABASE_NAME;"`
`$CMD -t $DATABASE_NAME < "${ROOT_PATH}/tmp.sql"`

rm "${ROOT_PATH}/tmp.sql"
echo "完了"

# テスト用に初期データを入れたい場合は下記のような形でファイルを配置し、一緒にインポートする
# echo "初期データ作成中..."
# `$CMD -t $DATABASE_NAME < "${ROOT_PATH}/${TARGET}/testdata/dump.sql"`
# echo "完了"

exit 0

インポートは
例として、./scripts/import_database_schema.sh -t users -d users -y
という形で行います。

実運用の際は、下記のようにテストデータを設置し、インポート時にテストデータも一緒にインポートするような形にしてgithub actionsでテストを回すような運用をしています

├── users
│   ├── testdata # users DBのテストデータを格納
│   └── schemas  # users DB内のテーブルのCREATE文が格納
└── shops
    ├── testdata # shops DBのテストデータを格納
    └── schemas  # shops DB内のテーブルのCREATE文が格納
0
2
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
2

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?