Ruby on Rails Tips

マイグレーションで既存データの複雑な変更を行うには

マイグレーションで既存データも修正したい場合があります。例えば、カラムを追加したときに、その初期値を投入したいということがあります。

マイグレーション内でモデルクラス(ActiveRecord)を使うことも不可能ではありませんが、将来、スキーマが変わっても動作するように書くのは大変で、事実上、避けたほうが無難です。より手軽な方法として、executeをうまく使うと比較的簡単に、複雑な処理を記述することができます。

例えば、コミュニティとコミュニティメンバーにおいて、コミュニティに、メンバー数を数えておくカウンターキャッシュ用のカラム members_count を追加するとしましょう。すでに開発が進んでおり、データベース内には communities レコードが多く存在するとします。このような既存レコードの members_count 値を設定するためのコードを、カラムを追加するマイグレーションと一緒に記述しておくと、マイグレーションを実行するだけで各開発者の環境が正しい状態になり、便利です。このためのコード例は次のようになります。

class AddMembersCountToCommunities < ActiveRecord::Migration
  def self.up
    add_column :communities, :members_count, :integer, :default => 0
    # 既存データを修正する
    for community_id, members_count in execute(
      "select community_id, count(*) from community_members group by community_id")

      execute(
        "update communities set members_count = #{members_count} where id = #{community_id}")
    end
  end

  def self.down
    remove_column :communities, :members_count
  end
end

ポイントは、executeから検索結果を取り出して、次の処理のために使っているところです。この方式を使うと、ActiveRecordを使って行いたいような複雑な処理も、比較的小さいストレスで、ActiveRecordなしで記述できます。

  # 変更対象のコミュニティのidとmembers_countを得る
  for community_id, members_count in execute(
    "select community_id, count(*) from community_members group by community_id")
    # それを使ってレコードを変更
    execute("update communities set members_count = #{members_count} where id = #{community_id}")
  end

上記の例では必要ではありませんが、SQLの中に直接値を埋め込みたくない場合は、次のようにしてサニタイズが使えます。

ActiveRecord::Base.sanitize_sql_array(
  ["update communities set members_count = ? where id = ?", members_count, community_id])

なお、この例のカウンターキャッシュを実際に動作させるには次のようにします。

  class CommunityMember < ActiveRecord::Base
    belongs_to :community, :counter_cache => 'members_count'
  end