Usual Software Engineer

よくあるソフトウェアエンジニアのブログ

jOOQ で audit フィールドを自動的に更新する方法

jOOQ 便利ですよね。 Java の ORM としては定番と言えるライブラリではありますが、 created_atupdated_at といった audit カラムを特に手動でセットせずとも、 自動的に更新したいというありがちなニーズに対応する方法が簡単には見つけられない気がします。

というわけで今回はその方法を紹介します。 StackOverflow に書いてあるんですけどね。

stackoverflow.com

方法は 4 つあるようです。

  1. SPI (Service Provider Interface) の RecordListener を使って audit フィールドをセットする
  2. SPI の VisitListener を使って SQL を改変することで audit フィールドを更新する
  3. jOOQ 側ではなくデータベース側の Trigger を使って audit フィールドを更新する
  4. jOOQ 3.10 を待つ

先に 2. 3. 4. の方法を確認していきます。

まず 2. の VisitListener を使う場合、公式のサンプルコードを参考に実装するわけですが、 これがまたなかなか厄介な実装になります。 手元で実装コードを書いてみましたが、力技になったりするのと Clause@Deprecated になっていたりするのを見て これはつらいなと思って途中でやめましたw GitHub に issue があるので要望がある人はそこで貢献していきましょうって感じですね。

github.com

続いて 3. ですが、こちらは jOOQ のリポジトリにサンプルコードを載せてくれている方がいたので参考にしましょう。

Recording row-level auditing information from an application's user session context in a Postgres DB · GitHub

そして 4. ですが、現在最新の jOOQ はすでに 3.11 です!!!残念ながらまだ実装されていません。 メインの issue はこちらですが難航しているようですね...

github.com

というわけで、一番手軽な 1 つめの方法で実装したサンプルコードを GitHub にあげました。

github.com

方法 1. のメリットは手軽さで、デメリットは jOOQ の呼び出し方法によっては RecordListener が invoke されないという点です。 StackOverflow にあるように所定のメソッドが呼ばれた時のみ、という制限があるんですね。

さてサンプルコードを簡単に説明しますと

jooq-spring-audit-example/AuditRecordListener.java at v0.0.1 · innossh/jooq-spring-audit-example · GitHub

public class AuditRecordListener extends DefaultRecordListener {

    @Override
    public void insertStart(RecordContext ctx) {
...
    }

    @Override
    public void updateStart(RecordContext ctx) {
...
    }
...
}

DefaultRecordListener を継承して insertStartupdateStart をオーバーライドし、 created_atupdated_at のフィールドに現在時刻をセットしてあげます。

jooq-spring-audit-example/Auditable.java at v0.0.1 · innossh/jooq-spring-audit-example · GitHub

...
public interface Auditable<T> {

    public LocalDateTime getCreatedAt();

    public T setCreatedAt(LocalDateTime value);

    public LocalDateTime getUpdatedAt();

    public T setUpdatedAt(LocalDateTime value);

}

jooq-spring-audit-example/AuditGeneratorStrategy.java at v0.0.1 · innossh/jooq-spring-audit-example · GitHub

...
public class AuditGeneratorStrategy extends DefaultGeneratorStrategy {

    @Override
    public List<String> getJavaClassImplements(Definition definition, Mode mode) {
        if (RECORD.equals(mode)) {
            return Arrays.asList(Auditable.class.getName());
        }
        return new ArrayList<>();
    }

}

jooq-spring-audit-example/build.gradle at v0.0.1 · innossh/jooq-spring-audit-example · GitHub

...
        generator {
            name = 'org.jooq.codegen.DefaultGenerator'
            strategy {
                name = 'innossh.jooq.spring.audit.example.db.codegen.AuditGeneratorStrategy'
            }
...
        }
...

サンプルでは上記のように Auditable インターフェースを作り、 jOOQ で Record クラスを generate する時の implements に指定していますが、 そのようなインターフェースを介さずともオーバーライドしたメソッドの中で、 単純にレコードの全フィールド名の String を created_atupdated_at に比較してセットする、でも問題ないです。

jooq-spring-audit-example/DefaultUserService.java at v0.0.1 · innossh/jooq-spring-audit-example · GitHub

@Service
public class DefaultUserService implements UserService {
...
    @Override
    public void createUser(User user) {
        userDao.insert(user);
    }
...
}

最後は DAO で insert とか update すれば勝手に現在時刻で更新してくれます。 この時に DSLContextinsertInto とか呼ぶと RecordListener は invoke されないので、 DSLContext を直接使用してクエリを構築する場合は自前で現在時刻をセットしましょう。悲しみ。

しかし jOOQ の generate はとても便利ですね。また公式でアップデートがあったら触ってみようと思います。