日本語圏の皆さん、こんにちは。
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を呼び出せば、大丈夫なはずです。