CLOVER🍀

That was when it all began.

JavaでULIDを使いたい(Sulky ULIDを使う)

これは、なにをしたくて書いたもの?

JavaでULIDを1度使っておきたいな、と思いまして。

ULID

ULIDは、Universally Unique Lexicographically Sortable Identifierの略です。辞書的にソート可能でユニークなID、ですね。

ULIDについては、こちらのGitHub Organizationにまとまっているようです。

ULID · GitHub

仕様はこちらのリポジトリにあり、

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

GitHub - azam/ulidj: ULID (Universally Unique Lexicographically Sortable Identifier) generator and parser for Java.

GitHub - Lewiscowles1986/jULID: Universally Unique Lexicographically Sortable Identifier ported to Java

「Binary Implementation」にチェックが入っているのは、最初のひとつだけです。

一覧に載っていないものとしては、こちらもありました。

GitHub - 0xShamil/ulid4j: A Java library to generate stupidly fast ULIDs

GitHub - f4b6a3/ulid-creator: A Java library for generating Universally Unique Lexicographically Sortable Identifiers (ULID)

今回は、こちらの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というツールで使われるライブラリ群らしいです。

GitHub - huxi/lilith: Lilith is a Logging- and AccessEvent viewer for Logback, log4j, log4j2 and java.util.logging

環境

今回の環境は、こちら。

$ 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#incrementULID#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というものがどういうものか、ちゃんと見る良い機会にもなりました。