ホーム DoRuby DB水平分散を使用した状態でマイグレーションファイルを纏めた過程。
DB水平分散を使用した状態でマイグレーションファイルを纏めた過程。
 

DB水平分散を使用した状態でマイグレーションファイルを纏めた過程。

この記事はアピリッツの技術ブログ「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は無いかと調べてみると、自分が調べた限りではありませんでした。以下のようなものは見つかりましたが、マイグレーションを纏めるというものではないですし、マイグレーションが使えなくなるのも困りますし……。

  1. Ridgepole 既存のマイグレーションを使用せずにDBを管理するgem。
  2. 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:   ""}

 ここまで分かれば、後は色々頑張っていくだけです。

実装過程

  1. 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を直接投げたものはログファイルに入らない模様。

記事を共有

最近人気な記事