create_tableでカラムを定義するのと同時にユニークインデックスを貼るやつ
なぜかレビューを通っていた下記のようなmigrationがあったのですが、 unique indexが貼られていなくて(当たり前ですが)先日ひどい目にあいました。
# db/migrate/20181231235959_create_products.rb class CreateProducts < ActiveRecord::Migration def change create_table :products do |t| # (略) t.integer :shop_id, unique: true # (略) end end end
はい。 ActiveRecord::ConnectionAdapters::TableDefinition
の column
メソッドの opthions
に unique: true
を渡してもunique indexは貼られませんね。
create_table でカラムを定義するのと同時に unique index を貼るやつの結論
上記のオプションの渡し方の正解は下記ですね。
t.integer :shop_id, index: { unique: true }
下記でいけるので。
t.type :column_name, index: { unique: true }
下記はだめ。
t.type :column_name, unique: true
そんなオプションの渡し方はない。
ActiveRecord::ConnectionAdapters::SchemaStatements の create_table メソッドのコードを読む
まあ、以上なのですが、せっかくなのでこの部分が実際にどういう実装になっているかを知るために ActiveRecord::ConnectionAdapters::SchemaStatements
の create_table
メソッドについて Active Recordのコードを読んでみますか。
読んだ環境はRails 5.2.2 (GitHub - rails/rails at v5.2.2)です。
create_table
def create_table(table_name, comment: nil, **options) td = create_table_definition table_name, options[:temporary], options[:options], options[:as], comment: comment # (中略) yield td if block_given? # (中略) end
yield
に渡しているのは td
なので、普段 t.integer
みたいに書いている t
は create_table_definition
で定義されているようだ。
create_table_definition table_name
def create_table_definition(*args) TableDefinition.new(*args) end
td
は TableDefinition
のインスタンスだった。
TableDefinition
class TableDefinition include ColumnMethods # (中略) end
ColumnMethods
をincludeしている。
ColumnMethods
module ColumnMethods # (中略) # Appends a column or columns of a specified type. # # t.string(:goat) # t.string(:goat, :sheep) # # See TableDefinition#column [ :bigint, :binary, :boolean, :date, :datetime, :decimal, :float, :integer, :json, :string, :text, :time, :timestamp, :virtual, ].each do |column_type| module_eval <<-CODE, __FILE__, __LINE__ + 1 def #{column_type}(*args, **options) args.each { |name| column(name, :#{column_type}, options) } end CODE end alias_method :numeric, :decimal end
普段使っているシンタックスシュガーがメタプロで定義されている。
t.integer
だと column(name, :integer, options) }
なので、結局 column
メソッドに options
引数が渡されて実行されている。
column
def column(name, type, options = {}) name = name.to_s type = type.to_sym if type options = options.dup # (中略) index_options = options.delete(:index) index(name, index_options.is_a?(Hash) ? index_options : {}) if index_options @columns_hash[name] = new_column_definition(name, type, options) self end
options
の index
キーの中身は index
メソッドに引数として渡されている。
index
# Adds index options to the indexes hash, keyed by column name # This is primarily used to track indexes that need to be created after the tab # # index(:account_id, name: 'index_projects_on_account_id') def index(column_name, options = {}) indexes << [column_name, options] end
コメントの通りですが、 indexes
に options
を追加している。
create_tableに戻る
def create_table(table_name, comment: nil, **options) td = create_table_definition table_name, options[:temporary], options[:options], options[:as], comment: comment # (中略) unless supports_indexes_in_create? td.indexes.each do |column_name, index_options| add_index(table_name, column_name, index_options) end end # (中略) end
td.indexes
のそれぞれの要素の2つ目の index_options
は add_index
メソッドに index_options
として渡している。
結局 add_index
が呼ばれていますね。
--
ActiveRecordのコードはこれ以上追いませんが、結局
t.type :column_name, index: { key: value }
の index
部分のオプションは
add_index(table_name, column_name, {key: value})
のように、 add_index
にオプションとして渡されるのでした。
そして add_index
は unique indexを作成するときは オプションとして unique: true
を渡す必要ので、 create_table
の中の t.type
でunique indexを貼りたいときはオプションとして index
をキーにして、
t.type :column_name, index: { unique: true }
としないといけないのでした。
コードを読むのは疲れますが、実際の処理の中身がよく分かるのでこれからも適宜読んでいきたいですね。
こちらからは以上です。
補足
上記で見た、ActiveRecord::ConnectionAdapters::TableDefinition
の column
メソッドのオプションでunique indexを貼る方法が以外の方法は下記がある。
ActiveRecord::ConnectionAdapters::SchemaStatements
の add_index
メソッド
テーブル作成後にindexを追加するやつ。
create_table :products do |t| t.integer :shop_id end add_index :products, :shop_id, unique: true, name: 'products_shop_id_index'
ActiveRecord::ConnectionAdapters::TableDefinition
の index
メソッド
create_table
の中でindexを追加するやつ。
create_table :products do |t| t.integer :shop_id t.index :category_id, unique: true, name: 'products_shop_id_index' end
ActiveRecord::ConnectionAdapters::Table
の index
メソッド
change_table
でindexを追加するやつ。
create_table
内で利用する1つ上と形は一緒ですね。
change_table :products do |t| t.index :shop_id, unique: true, name: 'products_shop_id_index' end