その他
    ホーム 技術発信 DoRuby Rails時短の巻 -seed編-
     

    Rails時短の巻 -seed編-

    この記事はアピリッツの技術ブログ「DoRuby」から移行した記事です。情報が古い可能性がありますのでご注意ください。

    いちいち本番で1からデータを作る必要なんてありません。既存seedに時間がかかるならまず使うことをやめましょう。DBのダンプと差分管理と並列処理で本番へのデータ投入を爆速にするのです。

    はじめに

    みなさま、お久しぶりです。新卒エンジニアのくろすです。
    素手人間になって久しいですが、たまーにオレンジ折りたくなる衝動に襲われます。

    今年こそは一番綺麗な空を飛べると信じて初日参加します。
    推しが出るので2日目も参加します。
    3日目はチケットがありません。チケットが、ありません。

    データ投入用高速seed

    localでダンプしたデータを差分管理しつつ直接本番に流し込める高速seedをyaml_dbparallelを用いて作成したのでその紹介です。

    差分を管理してデータ更新を高速化する、seed_fu:expressのご紹介
    を参考に差分管理を実装し、
    RailsのDBの初期データ(rake db:seed用)をyamlで美しく管理する方法
    をヒントにデータをyaml_dbで管理することを思いつきました。
    実際にはテーブル毎のダンプが出来さえすればいいのでmysqldump等でも問題ないと思いますが、sql管理に比べデータの視認性が良い、手修正がしやすいという2点からyaml_dbを使っています。
    まだブロックスタイルでしかダンプできていないのであまり使い勝手はよくありませんが、フロースタイルでダンプできるようにできれば手修正はより容易になると思います。

    背景

    弊社ではExcelで作成していることが多いゲームのマスターデータですが、これはそのまま使うものもあればdbに投入する際にプログラム側で使いやすくするため加工するものもあります。
    このデータを加工する作業というものは、ゲーム運営が長くなりデータが増えていくに従い時間を食う作業になっていきます。
    また、このデータの作成に他のモデルを参照しながらデータを作るような書き方をしていると各masterに依存関係が生まれ並列処理できないため、既存seedプログラムにがっつり手を入れなければ高速化のしようがない…なんてこともあります。

    新卒としてアピリッツに入社しrubyを書き慣れるにつれ、DRYに精神を乗っ取られ始めました……
    スゥパァドゥルァァァァイ
    今回の場合Don’t Repeat Ourselfでいきましょう。
    なにもデータ作成までDBサーバーで行う必要なんてどこにもありません。
    データ作成はlocalのみで行い本番はそのデータだけを流し込んではいけない決まりなんてありません。
    すっぱりきっぱりと既存のseedを捨て去る覚悟をしましょう。

    投入用データ作成

    とりあえずdb:seedの後にダンプを行うrake taskを作ります。

    namespace :gundum do
      desc "gundum:seed"
      task :seed => :environment do
        Rake::Task["db:seed"].invoke
        Rake::Task["db:dump_masters"].invoke
      end
    end
    

    できました!!!ガンドゥムSEEDです!!!!
    この高速seedを作ってる時にseedにinvokeを生やそうと[ seed invoke ]でググってしまったあの日からこれをやろうと心に決めていました。

    さて、ここで呼んでいるdb:dump_mastersですが中身はyaml_dbの拡張である以下のプログラムをロードするようになっています。

    module YamlDb
      module SerializationHelper
        class Base
          # @Override
          def dump_to_dir(dirname, *dump_table_names)
            Dir.mkdir(dirname) if Dir[dirname].empty?
            tables = if dump_table_names.present?
                       dump_table_names
                     else
                       @dumper.tables
                     end
            tables.each do |table|
              File.open("#{dirname}/#{table}.#{@extension}", "w") do |io|
                @dumper.before_table(io, table)
                @dumper.dump_table io, table
                @dumper.after_table(io, table)
              end
            end
          end
        end
      end
      module RakeTasks
        MASTER_TABLES = ["fizz_master", "buzz_master"].freeze
        def self.dump_masters
          SerializationHelper::Base.new(helper).dump_to_dir("#{Rails.root}/db/masters/yml", *MASTER_TABLES)
        end
      end
    end
    
    #--------------------------
    YamlDb::RakeTasks.dump_masters
    

    テーブルを指定したダンプが行えるよう拡張して、マスターのみダンプする新しいRakeTaskを追加しています。

    データ投入と差分管理

    一番最初に紹介した
    差分を管理してデータ更新を高速化する、seed_fu:expressのご紹介
    を参考に差分管理しつつデータ投入を行えるようyaml_dbを拡張していきます。

    require 'parallel'
    module YamlDb
      module SerializationHelper
        class Base
          def load_masters_from_dir(dirname, truncate = true)
            updated_masters = []
            Dir.entries(dirname).each do |filename|
              next if filename =~ /^[.]/
              master = MasterVersion.find_or_create_by(master: File.basename(filename, ".yml"))
              checksum = Digest::MD5.file("#{dirname}/#{filename}").to_s
              if master.chesksum == checksum
                p "✔ "
              else
                p "✖"
                updated_masters << master
              end
            end
    
            if updated_masters.present?
              Parallel.each(updated_masters) do |master|
                filename = "#{master.name}.yml"
                begin
                  ActiveRecord::Base.transaction do
                    @loader.load(File.new("#{dirname}"/#{filename}", "r"), truncate)
                    master.save!
                  end
                rescue => e
                  p "✖"
                  raise e
                end
                p "✔ "
              end
            end
          end 
        end
      end
    
      module RakeTasks
        def self.load_masters
          SerializationHelper::Base.new(helper).load_masters_from_dir(dump_dir("/masters/yml"))
        end
      end
    end
    
    #--------------------------
    YamlDb::RakeTasks.load_masters
    

    テーブル情報の管理にMD5を使っています。
    MD5の衝突耐性は容易に突破され、ハッシュ値から入力値を求めることができるようになっていますが、テーブル管理に使うくらいなら問題ないでしょう。
    手元のファイルからハッシュ値を計算しているため、管理対象のテーブルがタイムスタンプを持っている場合は削除しておきましょう。
    seedで更新されるデータのの更新時間なんてそんな使うもんじゃありません。

    後はこれを標準のrakeから使えるようにrake taskを作ります。

    namespace :gundum do
      desc "gundum:seed_destiny"
      task :seed_destiny => :environment do
        load "gundum/seed_destiny.rb"
      end
    end
    

    U・N・M・E・I 感じちゃいますね

    まとめ

    今までローカル反映にすら15分ほどかかっていましたが、一度誰かが取り込み済みのデータなら全データ入れ替えても1分30秒で済むようになりました。
    既存seedが遅い原因は富豪プログラミングによるString.newが走りまくることだったので、それも解決。
    結果としてデータ入れ替えながらの作業が捗るようになりました。
    gundum:seed_destiny最高ですね。

    モバイルバージョンを終了