[Rails4]referencesカラムでインデックスを作成するならindexオプションを忘れずに
概要
こんにちは、M-Yamashitaです。
今回の記事は、Rails4でテーブルを作成する際にreferencesのカラムでインデックスを作成したいならindexオプションが必要の話です。
以下のようにマイグレーションファイルを作成し、マイグレーションさせたところデータベースにインデックスが作成されませんでした。
class CreateMicroposts < ActiveRecord::Migration
def change
create_table :microposts do |t|
t.text :content
t.references :user
t.timestamps null: false
end
end
end
そのため、インデックスを作成するためにはどう書くか、インデックスが作成されるまでのRailsのコードはどうなっているか、indexオプションを付ける経緯はどうなっていたのかについて調べました。
結論(インデックスを作成するためにはどう書くか)
Rails4でテーブル作成時に、referencesのカラムをデータベース上のインデックスとして作成したい場合、以下のようにindex: true
のindexオプションが必要です。
このオプションがなければインデックスは作成されません。
class CreateMicroposts < ActiveRecord::Migration
def change
create_table :microposts do |t|
t.text :content
t.references :user, index: true
t.timestamps null: false
end
end
end
コードリーディング
indexオプションをつけることで、どこでインデックスが作成されるのかコードリーディングして確認します。
create_table
メソッドからコードリーディングを始めます。
def create_table(table_name, options = {}) #:nodoc:
super(table_name, options.reverse_merge(:options => "ENGINE=InnoDB"))
end
ここは親メソッドを読んでいるだけなので、親クラスのAbstractAdapter
を見ます。
ただし、このAbstractAdapter
クラス自体にはcreate_table
メソッドはありません。SchemaStatements
をincludeしており、そこにcreate_table
メソッドがあります。
class AbstractAdapter
ADAPTER_NAME = 'Abstract'.freeze
include Quoting, DatabaseStatements, SchemaStatements
SchemaStatements
モジュールを見てみます。
def create_table(table_name, options = {})
td = create_table_definition table_name, options[:temporary], options[:options], options[:as]
if options[:id] != false && !options[:as]
pk = options.fetch(:primary_key) do
Base.get_primary_key table_name.to_s.singularize
end
td.primary_key pk, options.fetch(:id, :primary_key), options
end
yield td if block_given?
if options[:force] && table_exists?(table_name)
drop_table(table_name, options)
end
result = execute schema_creation.accept td
unless supports_indexes_in_create?
td.indexes.each_pair do |column_name, index_options|
add_index(table_name, column_name, index_options)
end
end
td.foreign_keys.each do |other_table_name, foreign_key_options|
add_foreign_key(table_name, other_table_name, foreign_key_options)
end
result
end
ここでテーブル作成とインデックス作成を行っています。
本件に関係がある部分のみ見ていきます。まずはcreate_table_definition
メソッドです。
def create_table_definition(name, temporary, options, as = nil)
TableDefinition.new native_database_types, name, temporary, options, as
end
TableDefinition
のインスタンスを作成するのみとなります。インスタンス作成時の初期化についてはスキップします。
次にyield td if block_given?
です。この処理でマイグレーションファイルのcreate_table
のブロックを実行します。ブロックにt.references
があった場合、t
はTableDefinition
クラスなので、そのクラスのreferences
メソッドを呼び出します。
def references(*args)
options = args.extract_options!
polymorphic = options.delete(:polymorphic)
index_options = options.delete(:index)
foreign_key_options = options.delete(:foreign_key)
type = options.delete(:type) || :integer
if polymorphic && foreign_key_options
raise ArgumentError, "Cannot add a foreign key on a polymorphic relation"
end
args.each do |col|
column("#{col}_id", type, options)
column("#{col}_type", :string, polymorphic.is_a?(Hash) ? polymorphic : options) if polymorphic
index(polymorphic ? %w(type id).map { |t| "#{col}_#{t}" } : "#{col}_id", index_options.is_a?(Hash) ? index_options : {}) if index_options
if foreign_key_options
to_table = Base.pluralize_table_names ? col.to_s.pluralize : col.to_s
foreign_key(to_table, foreign_key_options.is_a?(Hash) ? foreign_key_options : {})
end
end
end
options
、つまりreferences
の引数からindex
を取り出し、index
が存在すれば、references
に指定されたカラムを引数としてindex
メソッドを呼び出します。
def index(column_name, options = {})
indexes[column_name] = options
end
index
メソッドではindexes
に対象カラムとオプションをセットするのみとなります。
ブロックの処理を抜け、create_table
メソッドに戻ります。
result = execute schema_creation.accept td
を見ていきます。
まずはschema_creation
からです。これはSchemaStatements
モジュールのcreate_table
の呼び出し元がAbstractMysqlAdapter
クラスなので、そのクラスのメソッドを呼び出します。
def schema_creation
SchemaCreation.new self
end
ActiveRecord::ConnectionAdapters::AbstractMysqlAdapter::SchemaCreation
クラスのインスタンスを作ります。
次にそのクラスのインスタンスからaccept
メソッドを呼び出します。
def accept(o)
m = @cache[o.class] ||= "visit_#{o.class.name.split('::').last}"
send m, o
end
o
はTableDefinition
クラスのインスタンスなので、visit_TableDefinition
がm
にセットされ、そのメソッドを呼び出します。
def visit_TableDefinition(o)
name = o.name
create_sql = "CREATE#{' TEMPORARY' if o.temporary} TABLE #{quote_table_name(name)} "
statements = o.columns.map { |c| accept c }
statements.concat(o.indexes.map { |column_name, options| index_in_create(name, column_name, options) })
create_sql << "(#{statements.join(', ')}) " if statements.present?
create_sql << "#{o.options}"
create_sql << " AS #{@conn.to_sql(o.as)}" if o.as
create_sql
end
CREATE TABLE ・・・
のクエリを作り、その後必要なクエリを付け加えています。
statements.concat(o.indexes.map { |column_name, options| index_in_create(name, column_name, options) })
を見ると、前述のindexes
にセットされたインデックスをそれぞれ取り出し、index_in_create
メソッドを実行しています。
def index_in_create(table_name, column_name, options)
index_name, index_type, index_columns, index_options, index_algorithm, index_using = @conn.add_index_options(table_name, column_name, options)
"#{index_type} INDEX #{quote_column_name(index_name)} #{index_using} (#{index_columns})#{index_options} #{index_algorithm}"
end
index_in_create
メソッドで、インデックス作成のINDEX ・・・
のクエリを作成します。
visit_TableDefinition
メソッドに戻り、さきほどのインデックス作成クエリを結合することでCREATE TABLE・・・ INDEX ・・・
のクエリができあがります。
create_table
メソッドに戻り、result = execute schema_creation.accept td
のコードにてexecute
メソッドを実行します。
このexecute
メソッドで作成されたクエリを実行します。
以上で、indexオプションをつけることによりどこでインデックスが作成されるのか、がコード上で確認できました。
なぜデフォルトでインデックスが作成されないか
Rails4以前ではreferences
を設定しても、データベースにインデックスは自動作成されていませんでした。
その後、indexオプションを設定することでインデックスを自動作成できるPull Requestが挙がりました。
このPull Requestでは以下2つを行っています。
-
rake g model lock recording:references
のようなコマンドを実施することで、マイグレーションファイルにindex: true
のindexオプションを追加する - マイグレーションファイルにindexオプションがあればデータベースにインデックスを自動的に作成する
このPull Requestに対しインデックスに関する議論が挙がりました。
I think it's healthy to explicitly add indexes, but I think this should be the default. Nice patch; needs a changelog bump and some guide/docs updates!
このインデックス自動作成はデフォルトにしたほうが良いのではないかとの意見です。これに対し自動作成は避けたいとの意見もありました。
Nice patch! There are a couple things we need to have in mind though:
If we are going to make this approach the default, it needs to be turned on via a flag, otherwise it will break backwards compatibility;
When you use a generator to generate a migration, we already include indexes for each belongs_to/references. So if we are merging this patch, we need to reevaluate our current migration generator.
My $0.02: this patch adds a simpler syntax to add indexes which is welcome but I wouldn't make this the default. I would simply improve the current generators.
後方互換性の面を考えるとデフォルトにすべきではないとのことです。フラグを付けなければならず、古いマイグレーションファイルに対する懸念です。
また、indexオプションがなかったら警告を出したらどうかという意見も挙がりました。
その意見を受けて、もし警告を出す場合はあえて警告を出さないフラグも必要になること、古いマイグレーションファイルすべてに警告を表示しなければならなくなるといった意見もありました。
この結果、インデックス自動作成はデフォルトにはせず、indexオプションをつける結論に至りました。
その後、Rails4リリース後にDHHから、indexオプションなしでデフォルトでデータベースにインデックスを自動作成しようというIssueが挙がりました。
#references should automatically add a index -- it's what you want in 9/10 cases. So instead people can do index: false if they DON'T want the index.
ほとんどのケースでreferencesにはインデックスが必要とのことです。
このときは後方互換性に関する議論もなく、以下Pull Requestが作成され、Rails5の機能として取り込まれました。
Using references or belongs_to in migrations will always add index for the referenced column by default, without adding index: true option to generated migration file. Users can opt out of this by passing index: false.
Fixes #18146.
この結果、Rails5以降においては、テーブル作成時にindexオプションなしでデフォルトでインデックスが作成されるようになりました。
おわりに
今回の記事では、Rails4でテーブル作成時、referencesのカラムでインデックスを作成したいならindexオプションが必要の話をしました。
デフォルトで自動作成されるものと思い込んでいたので、認識を改めることができました。
この記事が誰かのお役に立てれば幸いです。
Discussion