この記事はアピリッツの技術ブログ「DoRuby」から移行した記事です。情報が古い可能性がありますのでご注意ください。
drecom/activerecord-turntableを使用した状態でマイグレーションファイルを纏めた過程。
こんにちは。甘いものを食べたくて食べたくて、けれど竹下通りというシャレオツな場所は怖くて避けていて、震えているHelloWorld??です。
問題
drecom/activerecord-turntable…DB水平分散を使用した状態では、既存の方法でマイグレーションを纏める事が出来なかった。けれど、色々な理由で纏めたかった。
過程1. gemを入れてみる
まず、マイグレーションを纏めると言ったらこれだよね、という事でsquasherを入れてみました。
そうしたら、お決まりのmysql2関連のエラーが出てしまって(対象となったRailsアプリではmysqlを使っています)。
Specified 'mysql2' for database adapter, but the gem is not loaded. Add `gem 'mysql2'` to your Gemfile (and ensure its version is at the minimum required by ActiveRecord). (Gem::LoadError)
この問題は、大抵の場合mysql2のバージョンに問題があるみたいですが(勿論mysql2は入れてます)、新しく入れたgemの為に現状のgemのバージョンを変えるとかは極力したくないですし。
という事で却下しました。
次に、他にマイグレーションファイルを纏めるgemは無いかと調べてみると、自分が調べた限りではありませんでした。以下のようなものは見つかりましたが、マイグレーションを纏めるというものではないですし、マイグレーションが使えなくなるのも困りますし……。
- Ridgepole 既存のマイグレーションを使用せずにDBを管理するgem。
- Convergence http://labs.timedia.co.jp/2014/10/railsdb.html Ridgepoleを参考にして一から作り上げた、Ridgepoleの改良版。
過程2. 詰まったから聞いてみた
そんな感じで、これ無理じゃないかなーと思いながら先輩に聞いてみたら、マイグレーション実行時に吐き出されるstructure.sql使えない? と言われて、これはもしかしたら、と光明が見えてきました。
ただ、問題が新しく出てきて。
題名の通り、このアプリでは、activerecord-turntableを使用しています。即ち、クラスタとかを指定して、DB水平分散をしてユーザーデータやらそれに紐づくデータやらを管理している訳です。
要するに、SQLを直接実行するにせよ、複数のDBを指定してSQLを実行しなければいけない。
これは、activerecord-turntableを調べなきゃいかんな……。という訳で、頑張りました。1~2時間位。
以下が、DB水平分散をしてマイグレーションをかけている実際のコードです。
/lib/activerecord-turntable-feature-rails4_1/lib/active_record/turntable/migration.rb
def migrate_with_turntable(direction)
config = ActiveRecord::Base.configurations
@@current_shard = nil
shards = (self.class.target_shards||=[]).flatten.uniq.compact
if self.class.target_shards.blank?
return migrate_without_turntable(direction)
end
shards_conf = shards.map do |shard|
config[Rails.env||"development"]["shards"][shard]
end
seqs = config[Rails.env||"development"]["seq"]
shards_conf += seqs.values
shards_conf << config[Rails.env||"development"]
shards_conf.each_with_index do |conf, idx|
@@current_shard = (shards[idx] || seqs.keys[idx - shards.size] || "master")
ActiveRecord::Base.establish_connection(conf)
if !ActiveRecord::Base.connection.table_exists?(ActiveRecord::Migrator.schema_migrations_table_name())
ActiveRecord::Base.connection.initialize_schema_migrations_table
end
migrate_without_turntable(direction)
end
end
このメソッドについて簡単に言えば、水平分散するように指定されているマイグレーションに対して、その水平分散するDBそれぞれにマイグレーションを掛ける処理をしています。
そして、水平分散をするDBを指定するコードが、
ActiveRecord::Base.establish_connection(conf)
confの中身は、以下のようなdatabase.ymlに書かれている設定ファイルを拾ってきているハッシュです(ローカル環境なら大体こんな感じかな)。
{adapter: "mysql2",
encoding: "utf8",
reconnect: false,
pool: 5,
username: "root",
password: "I_WANT_TO_EAT_SWEETS!!",
host: "127.0.0.1",
port: 3306,
database: ""}
ここまで分かれば、後は色々頑張っていくだけです。
実装過程
- SQLファイル作成
まず、纏めたい部分までのマイグレーションを1から実行します。ここでは2017年1月1日までのマイグレーション、バージョンは20170101000000としておきましょうか。
bundle exec rake db:migrate:reset version=20170101000000
そうすると、それまでのSQLの実行結果がdbディレクトリ以下に吐き出されます。
activerecord-turntableを使用していたならば、turntable.ymlで設定した分だけ複数吐き出されるはずですので、水平分散DBで種別に一つずつrenameして取っておきましょう。全部は必要ありません。例としては、
structure_AAA_seq.sql これと
structure_AAA1.sql これと
structure_AAA2.sql
structure_BBB_seq.sql これと
structure_BBB1.sql これと
structure_BBB2.sql
structure.yml これを
以下にrename。
old_structure_AAA_seq.sql
old_structure_AAA.sql
old_structure_BBB_seq.sql
old_structure_BBB.sql
old_structure.sql
といった感じです。
そして、ここからこのSQLファイルに対してひと手間必要になってきます。
これをこのままSQL文として実行しても、SQLは動きません。また、消しておかなければいけないSQL文も存在します。
まず、正規表現を使って、取っておいた全てのファイルからrubyのコメントアウト文を削除します。これがあるとSQLが走りません。
Visual Studio Codeなら、簡単に行けます。
1. 正規表現を使ったファイル内検索で引っ掛ける。
以下で行ける(macだとバックスラッシュはキーボードの設定弄らないと出てこないので要注意)。
/*.*\*/;
2.”全ての出現箇所を変更”で削除。
後、お好みでSQLのコメント文も削除しましょう。VisualStudioCodeの正規表現は以下で。
--.*
それから、全てのSQLを登録する、structure.sqlからのみ、SQLを削除する手間が必要になります。
それは、schema_migrations関連に対してです。このSQLを実行するマイグレーションを後々作成するのですが、schema_migrations関連のSQLを残したままだと、このマイグレーションの実行履歴が、schema_migrationsに入らず、マイグレーション実行してないよ、と怒られてしまいます。
まあ、他の水平分散するDBにもschema_migrationsのテーブルが入るのですが、rails側で参照しているのは、このstructure.sqlで作成されるところのDBだけなのでここだけ削除しておけば大丈夫でしょう。
それともう一つ、最後の方に羅列されている、INSERT INTO schema_migrations (version) VALUES (……)も削除します。古いバージョンのマイグレーション情報はもう、必要ありません。
2.マイグレーションファイル削除、作成。
まず、もう要らないマイグレーションファイルを一気に削除します。今回は20170101000000以前のマイグレーションファイルです。
そして、次にマイグレーションファイルを直接作成します。
20170101000000_init_database.rbと言った感じに、年月をその時に指定して作成します。
最後にその中で、detabase.ymlの内容からでもハッシュを作成し、DBを作成し、sqlを直接実行するcreateメソッドを作成してから、もう一仕事面倒な事を片付けてから、完了となります。
ローカル環境に限るなら、自分は以下になりました。
class InitDatabase < ActiveRecord::Migration
def change
environment = {}
databases = {AAA: [], AAA_seq: [], BBB: [], BBB_seq: [], :master => []}
if Rails.env.development?
#取り敢えずdatabase.ymlからじゃなくて手打ちで。
environment = {adapter: "mysql2",
encoding: "utf8",
reconnect: false,
pool: 5,
username: "root",
password: "I_WANT_TO_EAT_SWEETS!",
host: "127.0.0.1",
port: 3306,
database: ""}
#データベース名をそれぞれ入れていく。こっちも取り敢えず手打ちで。
databases[:AAA] = ["AAA1_development",
"AAA2_development"]
databases[:AAA_seq] = ["AAA_development_seq"]
databases[:BBB] = ["BBB1_development",
"BBB2_development"]
databases[:BBB_seq] = ["BBB_development_seq"]
databases[:master] = ["development"]
elsif Rails.env.test?
...
elsif Rails.env.XXX?
...
end
#ファイルを読みだしてから、SQL単位で分割する。SQLファイルの保存場所は任意で。この場合はdbディレクトリ以下。
databases[:AAA].each do |database|
file = File.read(Rails.root.to_s + "/db/old_structure_AAA.sql")
sqls = file2sqls(file)
environment[:database] = database
shard_sql_execute(environment, sqls)
end
databases[:AAA_seq].each do |database|
file = File.read(Rails.root.to_s + "/db/old_structure_AAA.sql")
sqls = file2sqls(file)
environment[:database] = database
shard_sql_execute(environment, sqls)
ActiveRecord::Base.connection("INSERT INTO *_id_seq(id) values(0);")
ActiveRecord::Base.connection("INSERT INTO *_id_seq(id) values(0);")
...
...
ActiveRecord::Base.connection("INSERT INTO *_id_seq(id) values(0);")
end
databases[:BBB].each do |database|
...
end
databases[:BBB_seq].each do |database|
...
end
databases[:master].each do |database|
...
end
end
end
#ファイルをSQL単位で分割するメソッド。コメントアウト文などを消しても、ファイルをそのままActiveRecord::Base.connection.executeに代入したら動かなかった。原因はまだ不明。
def file2sqls(file)
sqls = file.split(";")
sqls.delete_if{|sql| sql.blank?}
sqls.each do |sql|
sql.gsub!(/\n/,"")
sql << ";"
end
sqls
end
#接続を設定してSQL実行する。
def shard_sql_execute(env, sqls)
ActiveRecord::Base.establish_connection(env)
sqls.each do |sql|
ActiveRecord::Base.connection.execute(sql)
#sqlの中に"create table *_id_seq"の文字列が入っていたら...の時に初期値入れる事をやる?
end
end
面倒な事、というのは上のコードでの、
ActiveRecord::Base.connection("INSERT INTO *_id_seq(id) values(0)")
に関してです。
activerecord_turntableに依るDB水平分散によるID管理はまた別のseqDBなるものによって管理されています。普通にマイグレーションを実行すれば、そのseqDBのAAAやらBBB、それらに紐付いているデータのIDを管理する*_id_seqテーブルに初期値である0のレコードが入るのですが、こうしてSQL直接実行だとレコードが入りません。なので、手動で入れてあげる必要があります。……元々のマイグレーションファイルの中で”create_sequence_for”で紐付けているデータ分だけ。
多いと、何かしら簡略化のコードが必要でしょう。
しかし、それを頑張れば、完了です。
これで後はまたマイグレーションを問題なく実行出来たら、アプリを動かして、中身などを確認してみましょう。
それから、多過ぎるマイグレーションを纏める、という意味でこれを自分は行ったのですが、思わぬ良い副作用がありました。
マイグレーションをして吐き出されるstructure*.sqlには、最終的に実行されるSQLが入っています。即ち、過去のマイグレーションでadd_columnやらrename_columnやら、そんなテーブルに対する変更処理などが全くありません。
なので、纏めたこの特殊なマイグレーションは、纏めていない大量のマイグレーションファイルをそのまま実行するよりとても速くなります。
まとめ
acitiverecord-turntableを使用した状態でマイグレーションを纏めたい、でもsquasher使えないときには、
1. 纏めたいところまでマイグレーション実行。
2. 吐き出されたSQLファイルをrenameして保存、コメント文などを削除、schema_migrations関連のSQL文も削除。
3.纏めたいところまでのマイグレーションファイル削除。新たにその地点でマイグレーションを作成し、直接環境指定してからSQLを実行、seqDBにレコードを挿入するコードを書く。
で、出来る。
(これって邪道……?)
追記: executeとかでSQLを直接投げたものはログファイルに入らない模様。