こんにちは、河野です。
RailsとPostgresqlの組み合わせでmigrationを通してテーブルを作成するとき、デフォルトでは、IDのデータ型はSERIAL(INT)になります。
特にデータ量が多くない場合には問題ないのですが、データ量多くなったときにINTの上限値(2147483647)を超えてしまうとデータがインサートできなくなり、大変な事態になります。
実は、先日あるプロジェクトで、テーブルのIDがINTの上限を超えてエラーになってしましました。IDをBIGINTに変更することで対応できたので良かったのですが、そもそもテーブル作成時にBIGINTにしておけば問題は発生しませんでした。
では、どうやったらテーブル作成時にBIGINT(PostgreSQLなのでBIGSERIAL)を使用することができるでしょうか。
create_tableのオプションでIDの型を指定する
create_tableのオプションに:idというのがあるので、それを:bigserialにします。
1 2 3 4 5 6 7 8 |
# db/migrate/20150220014057_create_histories.rb class CreateHistories < ActiveRecord::Migration def change create_table :histories, {id: :bigserial} do |t| t.integer :user_id end end end |
すごいシンプル!
確認
rakeを実行して、
1 2 3 4 5 6 |
% bundle exec rake db:migrate == 20150220014057 CreateHistories: migrating ================================== -- create_table(:histories, {:id=>:bigserial}) -> 0.0148s == 20150220014057 CreateHistories: migrated (0.0148s) ========================= |
psqlで確認します。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 |
% psql -c '\d histories*' テーブル "public.histories" カラム | 型 | 修飾語 ---------+---------+----------------------------------------------------------- id | bigint | not null デフォルト nextval('histories_id_seq'::regclass) user_id | integer | インデックス: "histories_pkey" PRIMARY KEY, btree (id) シーケンス "public.histories_id_seq" カラム | 型 | 値 ---------------+---------+--------------------- sequence_name | name | histories_id_seq last_value | bigint | 1 start_value | bigint | 1 increment_by | bigint | 1 max_value | bigint | 9223372036854775807 min_value | bigint | 1 cache_value | bigint | 1 log_cnt | bigint | 0 is_cycled | boolean | f is_called | boolean | f インデックス "public.histories_pkey" カラム | 型 --------+-------- id | bigint プライマリキー, btree, テーブル "public.histories" 用 |
ちゃんと、historiesのidの型がbigintになっていますね!
コードを見ていて気づいた
create_tableの挙動はどうなっているのか、確認してみました。
ActiveRecord::ConnectionAdapters::SchemaStatements
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 |
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_pair do |other_table_name, foreign_key_options| add_foreign_key(table_name, other_table_name, foreign_key_options) end result end |
options[:id]がfalseではない、かつ、options[:as]がfalse(nil)になっているときに、create_table_definitionで取得した、TableDefinitionのprimary_keyというメソッドを呼び出していることがわかります。
では、primary_keyはどういうメソッドなのでしょうか。(というか、primary_keyっていうメソッドあるんですね。)
ActiveRecord::ConnectionAdapters::PostgreSQL::ColumnMethods::TableDefinition
1 2 3 4 |
def primary_key(name, type = :primary_key, options = {}) options[:default] = options.fetch(:default, 'uuid_generate_v4()') if type == :uuid super end |
uuidに関しての記述があって、superを呼び出していますね。
ActiveRecord::ConnectionAdapters::TableDefinition
1 2 3 |
def primary_key(name, type = :primary_key, options = {}) column(name, type, options.merge(:primary_key => true)) end |
columnメソッドのラッパーになっています。
typeが :primary_keyになっていて、optionに :primary_key => true を追加しているだけですね。
つまりtypeのところに、:bigserialが渡せたらなんとかなりそうです。
改めてcreate_tableの挙動を確認すると、
1 2 3 4 5 6 7 |
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 |
options.fetch(:id, :primary_key) の戻り値が、columnメソッドのtypeに渡るようになっています。
では、:bigserialがホントに使えるのか。PostgreSQLAdapter の NATIVE_DATABASE_TYPES を見てみました。
ActiveRecord::ConnectionAdapters::PostgreSQLAdapter
1 2 3 4 5 |
NATIVE_DATABASE_TYPES = { primary_key: "serial primary key", bigserial: "bigserial", string: { name: "character varying" }, ...snip |
問題ないようですね。
まとめ
ということで、最初に記載した通りですが、以下のようにするとBIGSERIALが使えるようになります。
1 2 3 4 5 6 7 8 |
# db/migrate/20150220014057_create_histories.rb class CreateHistories < ActiveRecord::Migration def change create_table :histories, {id: :bigserial} do |t| t.integer :user_id end end end |
または、こういう書き方も大丈夫なようですね。
1 2 3 4 5 6 7 8 |
class CreateHistories < ActiveRecord::Migration def change create_table :histories, {id: false} do |t| t.primary_key :id, :bigserial t.integer :user_id end end end |
ちなみに。
create_table (ActiveRecord::ConnectionAdapters::SchemaStatements) - APIdock
↑こちらのcreate_tableのリファレンスを見ると、
Whether to automatically add a primary key column. Defaults to true. Join tables for has_and_belongs_to_many should set it to false.
と書いてあって、いかにもtrueかfalseしか設定できないような感じがします。
でも、実際には型の指定ができる、ということですね。なんとか出来ないかと思ってコードを見たのですが、大変勉強になりました。コード見るの大事ですね。