HerokuのpostgresqlのためにカラムのCOLLATEをCにする

日本語圏の皆さん、こんにちは。

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を呼び出せば、大丈夫なはずです。


カテゴリー Ruby, Ruby on Rails | タグ   | パーマリンク

コメントを残す

メールアドレスが公開されることはありません。 * が付いている欄は必須項目です