こんにちは。EC事業部カラーミーショップグループの小野です。今回はカラーミーショップにおける「データベーススキーマの変更方法」と「開発環境データベースの管理方法」について紹介します。
アプリケーションの構成
カラーミーショップは複数のアプリケーションが1つのデータベースを共有して動作しています。使用言語はPHPとRubyです。PHPで書かれたアプリケーションはサービス開始当初から動き続けており特にWebアプリケーションフレームワークは使用していません。Rubyで書かれたアプリケーションはそれに比べると最近作られたものでRuby on Railsを使用しています。
この記事の問題領域
データベースのスキーマ情報はアプリケーションの進化とともに変わっていきます。アプリケーションのコードがgitなどのバージョン管理システムで管理されているように、データベースのスキーマ変更や最新のスキーマ情報もバージョン管理すべきでしょう。そのメリットは以下のとおりです。
- githubのプルリクエストベースで開発ができる
- 最新のスキーマ状態が常にコミットされている安心感
またアプリケーションを正常に動かすためにはデータベースのスキーマ情報の他に初期データ、マスターデータなどが必要になることも多いかと思います。これらのデータは主に開発環境を構築する際に頻繁に使用することになりますが、データがおかしくなっていると以下のような事が起きて無駄な時間が浪費されてしまいます。
- 開発環境を作ったけど必要なデータが足りなくてちゃんと動かない
- マスターデータを更新したけど他の開発メンバーの開発環境には反映されていなかった
Webアプリケーションフレームワークを利用している場合は、この辺の仕組みも機能として備わっているかもしれません。しかしながら何らかの事情でそのような仕組みを利用できていない場合もあるかと思います。この記事ではカラーミーショップではどのようにこれらの問題に対処しているのか紹介していきます。
データベーススキーマの変更方法
さて、データベースのスキーマ管理機能を持たないWebアプリケーションフレームワークを使っていたり、そもそもフレームワークを使っていないアプリケーションを運用している皆さんは、データベースのスキーマをどのように管理しているでしょうか?
- 特に管理していない。スキーマの変更が必要な時に単にSQLを実行する
- スキーマの変更内容をソースコードと同じようにバージョン管理している
- 最新のスキーマをダンプしたファイルを生成してバージョン管理している
- スキーマを変更したり巻き戻すための統一的な仕組みがある
ActiveRecord::Tasks::DatabaseTasks を使う
カラーミーショップではそれぞれのアプリケーションとは別にリポジトリを作成し、そこでRuby on RailsのActive Recordマイグレーションの仕組みを使用しています。この仕組みはRailsを利用していなくても単独で使うことができます。 Active Recordマイグレーションの機能は ActiveRecord::Tasks::DatabaseTasksクラスに実装されているので、これを使っていきます。
サンプルコードがあったほうが理解しやすいと思うので、この記事では例としてmy_db_tasks
というgemを作成して実装していきながら見ていきましょう。
$ bundle gem my_db_tasks
$ cd my_db_tasks
まずは依存ライブラリにactiverecordとmysql2を追加してから、Rakefileにこんな感じのコードを書いておきます。
my_db_tasks.gemspec
:
spec.add_dependency "activerecord", "~> 5.0"
spec.add_dependency “mysql2"
Rakefile
:
env = ENV['ENV'] || 'development'
db_config = YAML.load(ERB.new(File.read('config/database.yml')).result)
task :environment do
ActiveRecord::Base.logger = Logger.new(STDOUT)
ActiveRecord::Base.schema_format = :sql
ActiveRecord::Base.establish_connection(db_config[env])
end
namespace :db do
task :load_config do
ActiveRecord::Tasks::DatabaseTasks.env = env
ActiveRecord::Tasks::DatabaseTasks.db_dir = 'db'
ActiveRecord::Tasks::DatabaseTasks.database_configuration = db_config
ActiveRecord::Tasks::DatabaseTasks.migrations_paths = ['db/migrate']
ActiveRecord::Tasks::DatabaseTasks.root = Pathname.new(Dir.pwd)
if env != 'development'
ActiveRecord::Base.dump_schema_after_migration = false
end
end
end
load "active_record/railties/databases.rake"
config/database.yml
:
defaults: &defaults
adapter: mysql2
port: 3306
development:
<<: *defaults
host: localhost
username: root
password:
database: my_db_tasks_development
これでデータベースタスクが使えるようになりました。rake db:create
を叩くとデータベースが作成されます。かんたんですね。
$ bundle exec rake db:create
Created database 'my_db_tasks_development'
マイグレーションファイルの生成
タスクの準備ができたので、次は実際にスキーマを変更するためにマイグレーションファイルを作成していきます。Ruby on Railsにはマイグレーションジェネレータが用意されているためbin/rails generate migration
というコメンドで作成できますが、今回はActiveRecordしか利用していないので使えません。しかし毎回手でファイルを作るのも面倒です。カラーミーショップでは固定のテンプレートを出力するだけの簡単なジェネレータをbin/generate-migration
という名前で用意して運用しています。
$ bin/generate-migration CreateUsers
create db/migrate/20170407170908_create_users.rb
これでマイグレーションファイルの雛形ができました。試しに以下のようなテーブルを作ることにします。
class CreateUsers < ActiveRecord::Migration
def up
execute <<-SQL
CREATE TABLE users(
id int not null auto_increment,
name varchar(255) not null,
primary key (id)
) ENGINE=InnoDB
SQL
end
def down
execute <<-SQL
DROP TABLE users
SQL
end
end
rake db:migrate
を実行して変更を適用しましょう。
$ bundle exec rake db:migrate
これで変更が適用されました。ステータスを見てみましょう。
$ bundle exec rake db:migrate:status
D, [2017-04-07T17:16:56.533945 #76786] DEBUG -- : (0.2ms) SELECT `schema_migrations`.`version` FROM `schema_migrations`
database: my_db_tasks_development
Status Migration ID Migration Name
--------------------------------------------------
up 20170407170908 Create users
できています。development環境ではActiveRecord::Base.dump_schema_after_migration
をtrueにしているので、migrateを実行するとdb/structure.sql
にスキーマダンプファイルが生成されます。あとはこれらのファイルをバージョン管理システムにコミットすれば終わりです。
ここまででスキーマの変更と最新のスキーマ情報をバージョン管理できるようになりました。
AUTO_INCREMENTオプションの削除
ActiveRecord::Base.schema_format
を:sql
にしていると、Rubyのコードではなくそれぞれのデータベース独自の方式でスキーマ情報がダンプされます。MySQLの場合はmysqldumpが使われるわけですが、一点困ったことがあります。テーブルにレコードを幾つか追加したあとに再度ダンプするとテーブルオプションにAUTO_INCREMENT値が含まれてしまうのです。
いくつかレコードを追加して、、
mysql> select * from users ;
+----+--------+
| id | name |
+----+--------+
| 1 | ichiro |
| 2 | jiro |
+----+--------+
2 rows in set (0.00 sec)
再度ダンプすると、
$ bundle exec rake db:structure:dump
D, [2017-04-07T17:28:14.434991 #77672] DEBUG -- : (0.3ms) SELECT `schema_migrations`.`version` FROM `schema_migrations` ORDER BY version
こういう差分が発生します。
diff --git a/db/structure.sql b/db/structure.sql
index d306da4..d721e6b 100644
--- a/db/structure.sql
+++ b/db/structure.sql
@@ -55,7 +55,7 @@ CREATE TABLE `users` (
`id` int(11) NOT NULL AUTO_INCREMENT,
`name` varchar(255) NOT NULL,
PRIMARY KEY (`id`)
-) ENGINE=InnoDB DEFAULT CHARSET=utf8;
+) ENGINE=InnoDB AUTO_INCREMENT=3 DEFAULT CHARSET=utf8;
/*!40101 SET character_set_client = @saved_cs_client */;
--
@@ -71,7 +71,7 @@ CREATE TABLE `users` (
/*!40101 SET COLLATION_CONNECTION=@OLD_COLLATION_CONNECTION */;
/*!40111 SET SQL_NOTES=@OLD_SQL_NOTES */;
--- Dump completed on 2017-04-07 17:15:25
+-- Dump completed on 2017-04-07 17:28:14
INSERT INTO `schema_migrations` (version) VALUES
('20170407170908’);
これでは複数人で開発している場合に、それぞれのローカルにあるデータベースの値が反映されてしまうため、意図しない差分が発生してしまいます。この問題への対処としてAUTO_INCREMENT値を削除するためのタスクを追加することにします。
Rakefile
:
namespace :db do
namespace :structure do
desc "Remove AUTO_INCREMENT table option from the db/structure.sql file"
task :remove_auto_increment_table_option do
ActiveRecord::Base.logger.debug "Remove AUTO_INCREMENT table option from the db/structure.sql file"
structure = File.read('./db/structure.sql').gsub(/\sAUTO_INCREMENT=\d+/, '')
File.open('./db/structure.sql', 'w') do |f|
f.write(structure)
end
end
end
end
そしてそのタスクをdb:structure:dump
の後に呼んでおきます。
Rake::Task['db:structure:dump'].enhance do
Rake::Task['db:structure:remove_auto_increment_table_option'].invoke
end
これで余計な差分が入り込むのを防ぐことができました。
手順
最終的に以下のような手順でスキーマの変更を行えるようになりました。
- ジェネレータを使ってマイグレーションファイルの雛形を生成する
- そこにスキーマの変更内容を書く
- 開発環境に適用する
- 生成したマイグレーションファイル、更新されたスキーマダンプファイルをコミットしてプルリクエストを作成する
- レビューに通してLGTMをもらう
- インテグレーション環境に適用する
- 本番環境に適用する
開発環境データベースの管理方法
カラーミーショップではエンジニアとデザイナーがそれぞれ自分のローカル作業マシン上に開発環境を構築しています。データベースも同様です。ここではその管理方法について紹介します。
データの分類
まず、スキーマ情報以外に管理すべきデータを以下のように定義しました。
- 初期データ
- データベース構築時に一度だけ設定するデータ。構築後は人手による更新は行わない。
- マスターデータ
- データベース構築時に設定するデータだが開発・運用していく中で更新されるデータ。
初期データの管理方法
これもActiveRecordのマイグレーション機能にシードデータを管理するためのタスクrake db:seed
が定義されているのでこれを使っていきます。方法は簡単でload_seed
という名前のメソッドを持ったクラスを定義してActiveRecord::Taks::DatabaseTasks.seed_loader
にインスタンスを渡すだけです。
lib/my_db_tasks/seed_loader.rb
:
module MyDbTasks
class SeedLoader
def initialize(db_config)
@db_config = db_config
@logger = Logger.new(STDOUT)
@logger.info "database: #{@db_config}"
end
def load_seed
@logger.info 'Load seed data start.'
# ここでシードデータをロードする
@logger.info 'Load seed data end.'
end
end
end
Rakefile
:
ActiveRecord::Tasks::DatabaseTasks.seed_loader = MyDbTasks::SeedLoader.new(db_config[env])
これでrake db:seedを実行できるようになりました。
$ bundle exec rake db:seed
I, [2017-04-07T17:44:45.782157 #78647] INFO -- : database: {"adapter"=>"mysql2", "host"=>"localhost", "username"=>"root", "password"=>nil, "database"=>"my_db_tasks_development", "port"=>3306}
D, [2017-04-07T17:44:45.848587 #78647] DEBUG -- : ActiveRecord::SchemaMigration Load (0.3ms) SELECT `schema_migrations`.* FROM `schema_migrations`
I, [2017-04-07T17:44:45.858685 #78647] INFO -- : Load seed data start.
I, [2017-04-07T17:44:45.858727 #78647] INFO -- : Load seed data end.
初期データとしてロードするデータはdb/seeds/sql
ディレクトリにテーブルごとのファイルを作ってそれを読み込んでいます。また、一部のデータは開発者間でidが重複すると不都合が生じるためテーブルのAUTO_INCREMENT値をランダムに設定するなどの工夫をしています。
マスターデータの管理方法
マスターデータは運用中に更新が発生するデータなので、繰り返し作り直しが発生します。それを支える仕組みとして独自のタスク群を用意することにしました。
$ bundle exec rake -T -A | grep master_data
rake colorme:master_data:create # Creates colorme master data
rake colorme:master_data:drop # Drops colorme master data
rake colorme:master_data:dump # Create master data sql files in db/master_data/sql directory
rake colorme:master_data:load_config #
rake colorme:master_data:reset #
これらのタスクを使って以下のように運用しています。
- マスターデータをロードしたい
rake colorme:master_data:create
を実行する
- マスターデータを更新したい
- 自分の開発環境でデータを変更する
rake colorme:master_data:dump
を実行する- プルリクエストを作ってレビューする
- masterブランチにマージする
- マスターデータの更新を自分の環境に取り込みたい
rake colorme:master_data:reset
を実行する
この運用によってリポジトリにコミットされているデータを「正」として開発者間の同期が取れるようになっています。実装としてはマスターデータとするテーブル群をmysqldumpしてからテーブルごとにファイルを分割してコミットしておき、ロードするときはそれを読み込むだけというシンプルなものにしています。ファイルを分割するのはサイズを小さくして更新時のレビューをしやすくするためです。
(ちなみに本番環境ではこの方法は使用していません。)
セットアップ方法
さて、これらの初期データ、マスターデータの管理方法が出来上がると開発環境のデータベースをセットアップするのはとても簡単になります。
以下の2つのコマンドを実行するだけで完了します。
$ bundle exec rake db:setup
$ bundle exec rake colorme:master_data:create
これはActiveRecordのタスクに則ることができた恩恵です。ちなみにrake db:setup
は実際には以下のことを行うコマンドです。
- データベースの作成(rake db:create)
- スキーマの読み込み(rake db:structure:load)
- 初期データの作成(rake db:seed)
今後の展望
データベースのスキーマ変更について現状の課題は、実行に長い時間かかるような変更です。MySQLのバージョンは5.6なのでスキーマ変更中に書き込みをブロックせずに実行できるケースが多いのですが、マスター、スレーブ構成を取っているためスレーブ側でレプリケーション遅延が発生するという問題があります。今後はこのあたりの仕組みも整えていく必要があると考えています。また開発環境データベースの管理方法ももっといい方法があれば改善を続けていくつもりです。
まとめ
この記事ではカラーミーショップの「データベーススキーマの変更方法」と「開発環境データベースの管理方法」について紹介しました。
複数のPHPアプリケーションとRubyアプリケーションが1つのデータベースを共有する構成において、データベース周りの管理を別リポジトリとして切り出してRuby on RailsのActive Recordに実装されているデータベースタスクだけ流用することで、いい感じにタスクのレールに乗った運用ができているのではないかと思います。同じような境遇の方々にとって少しでも参考になれば幸いです。
最後にこの記事の中で作成したサンプルコードを以下に公開しています。実装の詳細が気になる方は参考にしてみてください。
https://github.com/takatoshiono/my_db_tasks