マイグレーションで既存データの複雑な変更を行うには
マイグレーションで既存データも修正したい場合があります。例えば、カラムを追加したときに、その初期値を投入したいということがあります。
マイグレーション内でモデルクラス(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







