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
をつける対応で乗りきれるので、問題は無かった。