日本語圏の皆さん、こんにちは。
Herokuを使っていると、ローカル環境ではソートがうまくいくのに、本番環境ではうまくいかない…。そんなことが起きると思います。はい、それはpostgresqlのCOLLATEがen_US.UTF-8だからです!Cにしましょう、Cに。
しかし、後で気付いて変更しようにも結構面倒なので、migrationでやってしまいましょう。
新しいテーブルを追加した後でも、簡単に呼び出せるように、rake taskにしてみました。
作ったファイルは、
- lib/collation.rb
- lib/tasks/db.rake
- マイグレーションファイル(rails g migration ChangeCollation)
の3つです。
実際に処理をするCollationクラスを作ります。
class Collation
def initialize
@connection = ActiveRecord::Base.connection
end
def change_all
migration_base do |table, column|
# 配列型に対応
column_type = column.array ? "#{column.sql_type}[]" : column.sql_type
@connection.execute "ALTER TABLE #{table} ALTER COLUMN #{column.name} TYPE #{column_type} COLLATE \"C\""
end
end
def rollback_all
migration_base do |table, column|
# 配列型に対応
column_type = column.array ? "#{column.sql_type}[]" : column.sql_type
@connection.execute "ALTER TABLE #{table} ALTER COLUMN #{column.name} TYPE #{column_type}"
end
end
private
def migration_base
@connection.tables.each do |table|
begin
model = Module.const_get(table.classify)
rescue
next
end
# 1からマイグレーションすると、schema cacheのせいで定義が古いまま。
# 削除したり、リネームしたカラムを扱おうとして落ちるので、リセットする。
model.connection.schema_cache.clear!
model.reset_column_information
model.columns.select {|column| column.type == :string || column.type == :text }.each do |column|
yield(table, column)
end
end
end
end
テーブル名一覧を取得し、そのテーブル名からモデルを取得します。gemによって作られたモデルによってはModule.const_getできないので、rescueで拾って次のモデルへ。テストの時に準備でmigrateしていると、モデルの定義が古いままで現在のカラム情報が取れなかったので困っていたのですが、schema_cache.clear!とreset_column_informationで現在の定義が取得できます。その後、テキストのカラムだけ取得して、yieldに投げます。
change_allとrollback_allでやってることは、COLLATEを設定しているかどうかの違いだけです。落とし穴だったのが配列型で、column.sql_typeだけでは型だけで配列になっていなかったのですが、column.arrayに配列型かどうかのbooleanを持っていたので、これで判定して配列型にしています。
rake taskで呼び出せるようにしておきます。
namespace :db do
namespace :collation do
task :change => :environment do
Collation.new.change_all
end
task :rollback => :environment do
Collation.new.rollback_all
end
end
end
最後に、migrationファイルを作ります。
class ChangeCollation < ActiveRecord::Migration
def up
Rake::Task['db:collation:change'].execute
end
def down
Rake::Task['db:collation:rollback'].execute
end
end
これで、もし後でテーブルを追加した場合でも、また次のマイグレーションの最後でこのrake taskを呼び出せば、大丈夫なはずです。
