reizist's blog

ウェブ

migration comment gemを使っていたらtableのROW_FORMATがCompactになって困った

何があった

  • my.cnf にて innodb_large_prefix = ON, innodb_file_format = Barracuda であるのに varchar(255) カラムに対する unique index付与時 Specified key was too long; max key length is 767 bytes で怒られる
  • innodb_large_prefix = ON なのに large_prefix が有効にならないということは、 ROW_FORMAT=DYNAMIC でないとアタリをつけ検証したところ、確かに DYNAMIC でなく Compact になっていることが判明
  • 同migrationファイルを用いてstaging環境で実行すると、意図通りmigrationは実行されindexも付与される。

詳細

絵文字対応を行うためutf8でなく1文字4byteのutf8mb4エンコードでDB運用を行う場合、767byteまでしかindex column lengthを許容していないmysqlの仕様上varchar(255)のカラムに普通にindexを貼ることは出来ない。これに対応するためには、

  • mysqlの設定で innodb_large_prefix を有効に
  • tableの設定で ROW_FORMAT=DYNAMIC or COMPRESSED

する必要がある。 参考

このうち後者に関しては

ActiveSupport.on_load :active_record do
  module ActiveRecord::ConnectionAdapters

    class AbstractMysqlAdapter
      # utf8mb4 を使用するため index のサイズを拡大したい。
      # そのために必要な設定です。
      # @see config/database.yml
      def create_table_with_innodb_row_format(table_name, options = {})
        table_options = options.reverse_merge(options: 'ENGINE=InnoDB ROW_FORMAT=DYNAMIC')
        create_table_without_innodb_row_format(table_name, table_options) do |td|
          yield td if block_given?
        end
      end
      alias_method_chain :create_table, :innodb_row_format
    end
  end
end

のようにモンキーパッチを当てて対応を行っている。

だが、ローカル環境においてのみ上記のモンキーパッチによって付与されるはずの ROW_FORMAT=DYNAMIC が正しく有効にならず、作られるテーブルは Compact になっていることを発見した。 デバッグしたところ、何故かローカルではモンキーパッチ部分の引数にデフォルト値のようなものが渡されてしまい、意図せぬ挙動になってしまっていた。 development環境とstaging環境において挙動が変わる→development環境にのみ使用しているgemが怪しいとアタリをつけ調査を開始した。

調査

staging環境におけるデバッグ

From: ***/server/config/initializers/model_ext.rb @ line 193 ActiveRecord::ConnectionAdapters::AbstractMysqlAdapter#create_table_with_innodb_row_format:

    191: def create_table_with_innodb_row_format(table_name, options = {})
    192:   binding.pry
 => 193:   table_options = options.reverse_merge(options: 'ENGINE=InnoDB ROW_FORMAT=DYNAMIC')
    194:   create_table_without_innodb_row_format(table_name, table_options) do |td|
    195:     yield td if block_given?
    196:   end
    197: end

2.1.5 (#<ActiveRecord::ConnectionAdapters::Mysql2Adapter:0x007fe2a9ae94d8>):0 > show-stack
when_started hook failed: NoMethodError: private method `eval' called for nil:NilClass
***/server/vendor/bundle/ruby/2.1.0/gems/pry-stack_explorer-0.4.9.1/lib/pry-stack_explorer.rb:109:in `bindings_equal?'
(see _pry_.hooks.errors to debug)

Showing all accessible frames in stack (34 in total):
--
=> #0  create_table_with_innodb_row_format <ActiveRecord::ConnectionAdapters::AbstractMysqlAdapter#create_table_with_innodb_row_format(table_name, options=?)>
   #1 [block]   block in run <Byebug::PryProcessor#run(&_block)>
   #2 [method]  run <Byebug::PryProcessor#run(&_block)>
   #3 [method]  resume_pry <Byebug::PryProcessor#resume_pry()>
   #4 [method]  at_line <Byebug::PryProcessor#at_line()>
   #5 [method]  at_line <Byebug::Context#at_line()>
   #6 [method]  create_table_with_innodb_row_format <ActiveRecord::ConnectionAdapters::AbstractMysqlAdapter#create_table_with_innodb_row_format(table_name, options=?)>
   #7 [method]  create_table <ActiveRecord::SchemaMigration.create_table(limit=?)>
   #8 [method]  initialize_schema_migrations_table <ActiveRecord::ConnectionAdapters::Mysql2Adapter#initialize_schema_migrations_table()>
   #9 [method]  initialize <ActiveRecord::Migrator#initialize(direction, migrations, target_version=?)>
   #10 [method]  up <ActiveRecord::Migrator.up(migrations_paths, target_version=?)>
   #11 [method]  migrate <ActiveRecord::Migrator.migrate(migrations_paths, target_version=?, &block)>
  ....

development環境におけるデバッグ

2.1.5 (#<ActiveRecord::ConnectionAdapters::Mysql2Adapter:0x007fee429f8cc8>):0 > show-stack
when_started hook failed: NoMethodError: private method `eval' called for nil:NilClass
***/server/vendor/bundle/ruby/2.1.0/gems/pry-stack_explorer-0.4.9.1/lib/pry-stack_explorer.rb:109:in `bindings_equal?'
(see _pry_.hooks.errors to debug)

Showing all accessible frames in stack (36 in total):
--
=> #0  create_table_with_innodb_row_format <ActiveRecord::ConnectionAdapters::AbstractMysqlAdapter#create_table_with_innodb_row_format(table_name, options=?)>
   #1 [block]   block in run <Byebug::PryProcessor#run(&_block)>
   #2 [method]  run <Byebug::PryProcessor#run(&_block)>
   #3 [method]  resume_pry <Byebug::PryProcessor#resume_pry()>
   #4 [method]  at_line <Byebug::PryProcessor#at_line()>
   #5 [method]  at_line <Byebug::Context#at_line()>
   #6 [method]  create_table_with_innodb_row_format <ActiveRecord::ConnectionAdapters::AbstractMysqlAdapter#create_table_with_innodb_row_format(table_name, options=?)>
   #7 [method]  create_table <ActiveRecord::ConnectionAdapters::Mysql2Adapter#create_table_without_migration_comments(table_name, options=?)>
   #8 [method]  create_table_with_migration_comments <MigrationComments::ActiveRecord::ConnectionAdapters::MysqlAdapter#create_table_with_migration_comments(table_name, options=?)>
   #9 [method]  create_table <ActiveRecord::SchemaMigration.create_table(limit=?)>

#7 においてstgには無かった処理が介入しているのを発見。

2.1.5 (#<ActiveRecord::ConnectionAdapters::Mysql2Adapter:0x007fc938689b70>):0 > frame 7

Frame number: 7/35
Frame type: method

From: ***/server/vendor/bundle/ruby/2.1.0/gems/activerecord-4.0.8/lib/active_record/connection_adapters/abstract_mysql_adapter.rb @ line 471 ActiveRecord::ConnectionAdapters::Mysql2Adapter#create_table_without_migration_comments:

    470: def create_table(table_name, options = {}) #:nodoc:
 => 471:   super(table_name, options.reverse_merge(:options => "ENGINE=InnoDB"))
    472: end

migration comment gem をロードした結果、ActiveRecord::ConnectionAdapters::Mysql2Adapter::AbstractMysqlAdapter#create_table により下記メソッドの引数 options に常時 { options: 'ENGINE=InnoDB' } という値がわたってしまったことが原因。

def create_table_with_innodb_row_format(table_name, options = {})
  table_options = options.reverse_merge(options: 'ENGINE=InnoDB ROW_FORMAT=DYNAMIC')
  create_table_without_innodb_row_format(table_name, table_options) do |td|
    yield td if block_given?
  end
end

hash_a.reverse_merge(hash_b)hash_a の値を優先するため ROW_FORMAT=DYNAMIC が反映されなかった。

とはいえ、幸いなことに varchar(192) 以上必要なカラムは現プロジェクトに存在しないので、 migration commentのgemを使いindex使用時は limit: 191 をつける対応で乗りきれるので、問題は無かった。