成果はこちら oieioi/ketsuban
使い方
class User < ApplicationRecord
include Ketsuban
ketsuban [4, 5]
end
5.times.map { User.create.id }
# => [1,2,3,6,7]
lambda も渡せる
class Book < ApplicationRecord
include Ketsuban
ketsuban -> next_id { next_id.odd? }
end
5.times.map { Book.create.id }
# => [2,4,6,8,10]
実装
-
before_create
フックで - 次に振られそうなIDを調べ
- それが設定値にマッチする場合はそのIDをスキップし
- 採番テーブルをインクリメントする
これだけだけど、 自動採番なカラムは PostgreSQL でも MySQL でも気軽にさわれない感じになっていて不安だった。
PostgreSQL
ドキュメント
次のシーケンスを取得する
以下の方法を検討したが、一番実装が簡単だった last_value
を使う方法で実装した。ID重複を起こす可能性があるけど、その場合はあきらめる。
max(id)
select max(id) from users;
-
max(id)
はレコードが削除されていたとき正しくなくなる。 - ちゃんとシーケンスを見たほうがいい。
currval
select currval('users_id_seq');
- 現在のセッション中に該当のシーケンスに対して操作を行っていないときエラーになる。
last_value
select last_value, is_called from users_id_seq;
- シーケンスを直接調べる方法。
-
last_value
は一度も使用されていなくても1を返すが、使われていない値の場合はis_called
が偽になるので判別可能。
シーケンスを進める
手動でシーケンスを使ったら、進める必要がある。 ActiveRecord がメソッドを用意しているのでそれを使った。
ActiveRecord::ConnectionAdapters::PostgreSQL::SchemaStatements#set_pk_sequence!
MySQL
ドキュメント
次の auto_incremant
な値を取得する
select auto_increment from information_schema.tables where table_name = users;
-
auto_increment
なカラムの次に振られる値を取得できる。 - 直前に
auto_increment
の値を指定してinsert
していた場合、この値は更新されていない。- 1を欠番にし、2を指定して
insert
した直後のとき、このSQLの返り値は1になる。
- 1を欠番にし、2を指定して
select max(id) from users;
- 上記のため、
max(id)
を併用して現在のDBの最高値と、auto_increment
な値を調べるようにした。- この状態の時に末尾レコードを削除すると、詰め番が発生してしまう。直したい。
- 1を欠番にし、2を指定して
User.create
し、 2をdestroy
すると、次にcreate
されたUser
の id は 2 になる。
- 1を欠番にし、2を指定して
- この状態の時に末尾レコードを削除すると、詰め番が発生してしまう。直したい。
auto_incremant な値を更新する
alter table users auto_increment = 42;
-
before_created
が属するトランザクションの中でalter table
は無理では? -
auto_increment
のカラムの値を指定せずにいつも通りのUser.create
を行うと自動で一番でかい採番をしてくれるので結果的に不要だった。
SQLite
次の auto_incremant
な値を取得する
select seq from sqlite_sequence where name = 'users';
で現在の最高値を取得できる。対象のテーブルにまだ insert
が行われていない時はレコード自体が存在しない。また、対象のテーブルのシーケンスに特定の値を指定して insert
した場合、こちらの値は自動でインクリメントされる。
auto_incremant な値を更新する
上記の通りなので、更新する必要がない。