なにこれ
私は Ruby on Rails でコードを書くことが好きなのだ。で、Rails で「ユニークでかつソート可能な ID」を発行してくれる関数を書いていたとき、悟ったのだ。これは「データベースにさせるべきで、Rails がやるべきコード」ではないのだと。
やりたかったこと、やらねばいけないこと
- ソートできる UUID v4 をデータベースに作らせる
- UUID v1 から時間だけをもらう
- UUID v4 から匿名性とバリアント情報をもらう
- Rails で save したら自動的に id が書き込まれている状態になっていること
- PostgreSQL で関数を書くのは「はじめて♡」なので、動きそうなコードを探してコピペして無理やり動かす
- もちろん、こんなことはプログラマとして「あってはならない」ことなのは百も承知しとるわ!
- コードの品質
- リファクタリングしないといけない
- 自分で書いたコードなのに、反吐がでる、がやる気がしない..
- テストコードを書かないといけない
- 時間とランダムを含んだ返り値に対してどうテストコードを書けば良いのか?
- リファクタリングしないといけない
- 負荷テスト
- 生成コストがどんなものか、計算していない
- 本当に目的が達成できているのか、分析していない
- 目をつぶったこと
- DBマスタが増えたときに、時間に対して ID が適切にソートされているかについて
- 将来 UUID v6 が出てきた時に、関数の置き換えだけで対応できるかについて
💩コード注意
Rails から SQL 文を発行して 関数をつくる。
class GenFakeUuids < ActiveRecord::Migration[6.0]
def change
reversible do |dir|
dir.up do
execute <<~"SQL"
CREATE OR REPLACE FUNCTION gen_fake_uuid()
RETURNS UUID AS $$
DECLARE
tlow varchar;
tmid varchar;
thig varchar;
last varchar;
abcd varchar;
BEGIN
tmid := uuid_generate_v1()::varchar;
tlow := substring(tmid from '^.{8}');
thig := substring(tmid from '.{4}-1.{3}-[89ab]');
tmid := substring(thig from '^.{4}');
thig := substring(thig from '-1.{3}');
thig := substring(thig from '.{3}$');
last := substring((gen_random_uuid()::varchar) from '.{4}-.{12}$');
abcd := (thig || tmid || tlow);
abcd := left(abcd, 12) || '4' || right(tlow, 3) || last;
RETURN uuid('{' || abcd || '}');
END;
$$ LANGUAGE plpgsql;
SQL
end
dir.down do
execute <<-SQL
DROP FUNCTION IF EXISTS gen_fake_uuid();
SQL
end
end
end
end
お試しで使うモデルを作成するコード
class CreateStaffs < ActiveRecord::Migration[6.0]
def change
reversible do |dir|
dir.up do
execute <<~"SQL"
CREATE TABLE staffs (id uuid DEFAULT gen_fake_uuid(), email varchar NOT NULL, cname varchar NOT NULL, created_at timestamp(6) NOT NULL, updated_at timestamp(6) NOT NULL, PRIMARY KEY (id)) PARTITION BY HASH (id);
#{(0..99).map{|num| "CREATE TABLE staffs_partition_#{num} PARTITION OF staffs FOR VALUES WITH(MODULUS 100,REMAINDER #{num});"}.join}
SQL
end
dir.down do
execute <<-SQL
DROP TABLE staffs;
SQL
end
end
end
end
irb で動作チェック
irb(main):013:0> (1..10000).each{|num| Staff.create(email: "abc-#{num}", cname: "efg-#{num}") }
irb(main):014:0> Staff.first.id
Staff Load (1.1ms) SELECT "staffs".* FROM "staffs" ORDER BY "staffs"."id" ASC LIMIT $1 [["LIMIT", 1]]
=> "1ea8460b-2b38-1318-b24c-a33a16b7c474"irb(main):010:0> Staff.first.id
Staff Load (6.7ms) SELECT "staffs".* FROM "staffs" ORDER BY "staffs"."id" ASC LIMIT $1 [["LIMIT", 1]]
=> "1ea84621-3a0d-459e-90ae-5b7104152e56"
irb(main):011:0> Staff.second.id
Staff Load (2.0ms) SELECT "staffs".* FROM "staffs" ORDER BY "staffs"."id" ASC LIMIT $1 OFFSET $2 [["LIMIT", 1], ["OFFSET", 1]]
=> "1ea84621-3a12-44cc-b036-3902b9413296"
irb(main):012:0> Staff.last.id
Staff Load (2.1ms) SELECT "staffs".* FROM "staffs" ORDER BY "staffs"."id" DESC LIMIT $1 [["LIMIT", 1]]
=> "1ea84621-ab15-400c-8044-815ebd9d12fa"
irb(main):014:0> Staff.first.id < Staff.last.id
Staff Load (2.1ms) SELECT "staffs".* FROM "staffs" ORDER BY "staffs"."id" ASC LIMIT $1 [["LIMIT", 1]]
Staff Load (1.6ms) SELECT "staffs".* FROM "staffs" ORDER BY "staffs"."id" DESC LIMIT $1 [["LIMIT", 1]]
=> true
irb(main):015:0> Staff.second.id < Staff.last.id
Staff Load (2.3ms) SELECT "staffs".* FROM "staffs" ORDER BY "staffs"."id" ASC LIMIT $1 OFFSET $2 [["LIMIT", 1], ["OFFSET", 1]]
Staff Load (1.6ms) SELECT "staffs".* FROM "staffs" ORDER BY "staffs"."id" DESC LIMIT $1 [["LIMIT", 1]]
=> true
まぁ、これを見る感じ良いっぽいですね。
おわりに
不眠症がひどく、寝れていないイライラを打破するために書いた。PostgreSQL でいつか関数を書こうと思っていたので、良い経験となったと思う。