これは、なにをしたくて書いたもの?
JavaでULIDを1度使っておきたいな、と思いまして。
ULID
ULIDは、Universally Unique Lexicographically Sortable Identifierの略です。辞書的にソート可能でユニークなID、ですね。
ULIDについては、こちらのGitHub Organizationにまとまっているようです。
仕様はこちらのリポジトリにあり、
GitHub - ulid/spec: The canonical spec for ulid
README.md
に書かれています。
https://github.com/ulid/spec/blob/master/README.md
README.md
の内容ですが、ULIDはUUIDの以下を課題として提案されたもののようです。
- 128ビットのランダム性をエンコードする最も文字効率の良い方法ではない
- UUID v1およびv2は一意性のためにMACアドレスにアクセスする必要があり、多くの環境では実用性がない
- UUID v3およびv5は一意のシードを必要とし、ランダムに分散されたIDを生成するため多くのデータ構造で断片化を引き起こす可能性がある
- UUID v4は多くのデータ構造で断片化を引き起こす可能性のあるランダム性以外の情報を提供しない
ULIDの特徴は、以下になります。
- UUIDと128ビットの互換性
- 1ミリ秒で1.21e+24のユニークなULIDを生成可能
- 辞書順ソートが可能
- 36文字のUUIDではなく、26文字の文字列としてエンコードされる
- Crockfordのbase32を使用して効率と可読性を向上(1文字あたり5ビット)
- 大文字小文字を区別しない
- 特殊文字を使用しないため、URLに対して安全
- 同じミリ秒を正しく検出して扱うため、単調なソート順となる
ULIDのJavaScript実装は、同Organization内にあります。
GitHub - ulid/javascript: Universally Unique Lexicographically Sortable Identifier
他の言語の実装については、コミュニティベースで行われており、以下に一覧が記載されています。
Universally Unique Lexicographically Sortable Identifier / Implementations in other languages
JavaでのULID実装
ULID仕様に書かれた「Implementations in other languages」のセクションで、現時点でのJavaの実装は以下の3つが挙げられています。
sulky/sulky-ulid at master · huxi/sulky · GitHub
「Binary Implementation」にチェックが入っているのは、最初のひとつだけです。
一覧に載っていないものとしては、こちらもありました。
GitHub - 0xShamil/ulid4j: A Java library to generate stupidly fast ULIDs
今回は、こちらのSulky ULIDを使うことにします。
sulky/sulky-ulid at v8.3.0 · huxi/sulky · GitHub
実はこのモジュール、非常にシンプルで1クラスしかありません。
https://github.com/huxi/sulky/blob/v8.3.0/sulky-ulid/src/main/java/de/huxhorn/sulky/ulid/ULID.java
Sulkyというのは、Lilithというツールで使われるライブラリ群らしいです。
環境
今回の環境は、こちら。
$ java --version openjdk 17.0.4 2022-07-19 OpenJDK Runtime Environment (build 17.0.4+8-Ubuntu-120.04) OpenJDK 64-Bit Server VM (build 17.0.4+8-Ubuntu-120.04, mixed mode, sharing) $ mvn --version Apache Maven 3.8.6 (84538c9988a25aec085021c365c560670ad80f63) Maven home: $HOME/.sdkman/candidates/maven/current Java version: 17.0.4, vendor: Private Build, runtime: /usr/lib/jvm/java-17-openjdk-amd64 Default locale: ja_JP, platform encoding: UTF-8 OS name: "linux", version: "5.4.0-128-generic", arch: "amd64", family: "unix"
準備
Maven依存関係などは、以下のように設定。
<properties> <maven.compiler.source>17</maven.compiler.source> <maven.compiler.target>17</maven.compiler.target> <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding> <project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding> </properties> <dependencies> <dependency> <groupId>de.huxhorn.sulky</groupId> <artifactId>de.huxhorn.sulky.ulid</artifactId> <version>8.3.0</version> </dependency> <dependency> <groupId>org.junit.jupiter</groupId> <artifactId>junit-jupiter</artifactId> <version>5.9.1</version> <scope>test</scope> </dependency> <dependency> <groupId>org.assertj</groupId> <artifactId>assertj-core</artifactId> <version>3.23.1</version> <scope>test</scope> </dependency> </dependencies> <build> <plugins> <plugin> <artifactId>maven-surefire-plugin</artifactId> <version>2.22.2</version> </plugin> </plugins> </build>
今回の主題は、こちらのSulky ULIDですね。
<dependency> <groupId>de.huxhorn.sulky</groupId> <artifactId>de.huxhorn.sulky.ulid</artifactId> <version>8.3.0</version> </dependency>
確認は、テストコードで行うことにします。
src/test/java/org/littlewings/ulid/SulkyUlidTest.java
package org.littlewings.ulid; import java.security.SecureRandom; import java.time.Duration; import java.util.Comparator; import java.util.UUID; import java.util.concurrent.TimeUnit; import de.huxhorn.sulky.ulid.ULID; import org.junit.jupiter.api.Test; import static org.assertj.core.api.Assertions.assertThat; public class SulkyUlidTest { // ここに、テストを書く! void waitFor(Duration waitTime) { try { TimeUnit.MILLISECONDS.sleep(waitTime.toMillis()); } catch (InterruptedException e) { // no-op } } }
Sulky ULIDを試す
このあたりを見ながら、Sulky ULIDを試していってみます。
Universally Unique Lexicographically Sortable Identifier / Usage
まずは、UUID v4と比較しながら。
@Test public void gettingStarted() { ULID ulid = new ULID(); String ulidString = ulid.nextULID(); System.out.printf("ulid = %s%n", ulidString); assertThat(ulidString.length()).isEqualTo(26); String uuidv4 = UUID.randomUUID().toString(); System.out.printf("uuid v4 = %s%n", uuidv4); assertThat(uuidv4.length()).isEqualTo(36); }
ULID
のインスタンスを作成し、ULID#nextULID
でULIDを文字列として取得します。
実行結果は、こんな感じですね。
ulid = 01GFDY4P6JT4RAKNGF50Q6A4M4 uuid v4 = 47fb9512-18c2-4be1-82d5-054d02f7b0fe
ULIDは24文字、UUID v4は36文字ですね。
ULIDを複数回生成する場合は、作成したULID
のインスタンスを使い回すようです。
@Test public void gettingStartedMultiple() { ULID ulid = new ULID(); String ulidString1 = ulid.nextULID(); waitFor(Duration.ofMillis(100L)); String ulidString2 = ulid.nextULID(); waitFor(Duration.ofMillis(100L)); String ulidString3 = ulid.nextULID(); System.out.printf("ulid1 = %s%n", ulidString1); System.out.printf("ulid2 = %s%n", ulidString2); System.out.printf("ulid3 = %s%n", ulidString3); assertThat(ulidString1).isNotEqualTo(ulidString2); assertThat(ulidString2).isNotEqualTo(ulidString3); assertThat(ulidString1).isNotEqualTo(ulidString3); assertThat(ulidString1.compareTo(ulidString2)).isLessThan(0); assertThat(ulidString2.compareTo(ulidString3)).isLessThan(0); assertThat(ulidString1.compareTo(ulidString3)).isLessThan(0); }
それぞれ別々のULIDが生成され、書かれていた通り順序も作成順になっています。
実行結果。
ulid1 = 01GFDY5TV27C3V5TG84VHP31T6 ulid2 = 01GFDY5TY7NNQF19Z2Q8FW7VAY ulid3 = 01GFDY5V1BMF6CPFN85JC7H1KZ
先頭の部分は同じ文字になっていますね。
が、待ち時間がしれっと入っているように、以下のようにするとULIDそのものは異なるものが作成されますが、たまに順序が裏返ります…。
以下のコードは、大小比較のところで時々失敗します。
@Test public void gettingStartedNoWaiting() { ULID ulid = new ULID(); String ulidString1 = ulid.nextULID(); String ulidString2 = ulid.nextULID(); String ulidString3 = ulid.nextULID(); System.out.printf("ulid1 = %s%n", ulidString1); System.out.printf("ulid2 = %s%n", ulidString2); System.out.printf("ulid3 = %s%n", ulidString3); assertThat(ulidString1).isNotEqualTo(ulidString2); assertThat(ulidString2).isNotEqualTo(ulidString3); assertThat(ulidString1).isNotEqualTo(ulidString3); assertThat(ulidString1.compareTo(ulidString2)).isLessThan(0); assertThat(ulidString2.compareTo(ulidString3)).isLessThan(0); assertThat(ulidString1.compareTo(ulidString3)).isLessThan(0); }
もう使い方の話に戻しましょう。
ULID
のインスタンスを作成する時に、特に引数は指定しませんでしたが、内部的にはSecureRandom
が指定されています。
Random
を明示的にコンストラクタに与えることもできます。
@Test public void constructorArg() { ULID ulid = new ULID(new SecureRandom()); System.out.printf("ulid1 = %s%n", ulid.nextULID()); System.out.printf("ulid2 = %s%n", ulid.nextULID()); }
また、String
にせずにULID.Value
という形で扱うこともできます。
@Test public void useValue() { ULID ulid = new ULID(); ULID.Value value1 = ulid.nextValue(); System.out.printf("ulid1 = %s%n", value1.toString()); ULID.Value value1Incremented = value1.increment(); System.out.printf("ulid1 incremented = %s%n", value1Incremented.toString()); ULID.Value value2 = ulid.nextValue(); System.out.printf("ulid2 = %s%n", value2.toString()); }
文字列にするにはULID.Value#toString
で良さそうです。
次のULID.Value#increment
とULID#nextValue
の違いは?とメソッドを見ていて思いましたが、ULID.Value#increment
はひとつ値を
進めるもののようですね。
ulid1 = 01GFDY5TSHJ9A4C28557YR344T ulid1 incremented = 01GFDY5TSHJ9A4C28557YR344V ulid2 = 01GFDY5TSJ3WYG32B3G37ZZ4G0
あとはbyte
配列で扱ったり
@Test public void useValueAsBinary() { ULID ulid = new ULID(); ULID.Value value1 = ulid.nextValue(); assertThat(value1.toBytes()).isInstanceOf(byte[].class); ULID.Value value2 = ulid.nextValue(); assertThat(value2.toBytes()).isInstanceOf(byte[].class); }
ULID文字列をパースしてULID.Value
にしたりできます。
@Test public void parse() { ULID ulid = new ULID(); String ulidString = ulid.nextULID(); ULID.Value ulidValue = ULID.parseULID(ulidString); assertThat(ulidValue.toString()).isEqualTo(ulidString); }
ですが、通常はULID#nextULID
を使って直接String
を取得すればよいかなと思います。
next〜
メソッドを呼び出す際に、long
でタイムスタンプ値を渡して現在時刻とは異なる値を指定することもできますが(指定しない場合は
System#currentTimeMillis
が使われています)、今回はパス。
とりあえず、こんなところでしょう。
まとめ
Sulky ULIDを使って、JavaでULIDを扱ってみました。
若干不安になるポイントもありましたが、小さい実装なのでこちらで十分かなと思います。小さいライブラリですしね。
ULIDというものがどういうものか、ちゃんと見る良い機会にもなりました。