LoginSignup
0
0

More than 3 years have passed since last update.

私はデータベースに自作のUUIDを作る関数が欲しいと思った

Posted at

なにこれ

私は 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 でいつか関数を書こうと思っていたので、良い経験となったと思う。

0
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
0