その他
    ホーム 技術発信 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を直接投げたものはログファイルに入らない模様。