トランザクションはいつの時代も難しい。

Unit of Workとは

“Unit of Work”(作業単位)は、ソフトウェア設計のパターンであり、主にオブジェクト永続性の管理に使用されます。このパターンの目的は、一連のオペレーションを「作業単位」にまとめ、その作業単位全体が完了するか失敗するかを一貫性を持って扱うことです。オペレーションは通常、データストア(多くの場合はデータベース)に対するものです。

一連の操作は、個々の操作が一つ一つ実行されるのではなく、全体としてまとめて実行されます。もし操作の途中で何か問題が発生した場合(例えば、データベースの更新中に例外が発生するなど)、UnitOfWorkは「ロールバック」を行い、作業単位全体を元の状態に戻します。これにより、作業単位全体がアトミック(不可分)であることを保証します。一方、作業単位が無事完了した場合(すべての操作が成功した場合)、UnitOfWorkは「コミット」を行い、すべての変更をデータストアに永続化します。

UnitOfWorkパターンは、特にオブジェクト関係マッピング(ORM)フレームワークでよく見られます。これらのフレームワークでは、データベースに対する操作を「トランザクション」としてまとめることで、データの整合性を保つためにUnitOfWorkパターンが使用されます。また、このパターンは、リソースの効率的な使用、エラーハンドリングの改善、コードの整理など、多くの利点を提供します。

ということで、ChatGPTに質問

UnitOfWorkについてJavaのサンプルコードを実装しながら教えてください。

## ユースケース
- ユーザーの登録
- ユーザーの更新
- ユーザーの削除

## UnitOfWorkのインタフェース
  void MarkDirty(object entity);
  void MarkNew(object entity);
  void MarkDeleted(object entity);
  void Commit();
  void Rollback();

Unit of WorkについてのJavaサンプルコード

このパターンを使用すると、複数の操作を1つのトランザクションとしてグループ化でき、トランザクションが成功するとすべての変更がデータストアに適用され、トランザクションが失敗するとすべての変更がロールバックされます。

以下は、その要件に基づいてJavaでのUnitOfWorkパターンの簡単な実装です。なお、今回は簡単化のために疑似データベースとしてHashMapを使い、UserクラスはIDと名前を属性として持つと仮定します。

まずは、Userクラスから見てみましょう。

public class User {
    private String id;
    private String name;

    public User(String id, String name) {
        this.id = id;
        this.name = name;
    }

    // Getter and Setter methods
}

次に、UnitOfWorkインタフェースを作成します。

public interface UnitOfWork {
    void markDirty(User user);
    void markNew(User user);
    void markDeleted(User user);
    void commit();
    void rollback();
}

ここから、具体的なUnitOfWorkの実装を見てみましょう。

import java.util.*;

public class UnitOfWorkImpl implements UnitOfWork {
    private HashMap<User, Boolean> dirtyUsers = new HashMap<>();
    private HashMap<User, Boolean> newUsers = new HashMap<>();
    private HashMap<User, Boolean> deletedUsers = new HashMap<>();
    private HashMap<String, User> database = new HashMap<>();

    @Override
    public void markDirty(User user) {
        if (!newUsers.containsKey(user)) {
            dirtyUsers.put(user, true);
        }
    }

    @Override
    public void markNew(User user) {
        newUsers.put(user, true);
    }

    @Override
    public void markDeleted(User user) {
        if (newUsers.containsKey(user)) {
            newUsers.remove(user);
        } else {
            dirtyUsers.remove(user);
            deletedUsers.put(user, true);
        }
    }

    @Override
    public void commit() {
        // Save new and dirty Users to the database
        for (User user : newUsers.keySet()) {
            database.put(user.getId(), user);
        }
        for (User user : dirtyUsers.keySet()) {
            database.put(user.getId(), user);
        }

        // Remove deleted Users from the database
        for (User user : deletedUsers.keySet()) {
            database.remove(user.getId());
        }

        // Clear all lists
        newUsers.clear();
        dirtyUsers.clear();
        deletedUsers.clear();
    }

    @Override
    public void rollback() {
        // Clear all lists
        newUsers.clear();
        dirtyUsers.clear();
        deletedUsers.clear();
    }
}

ここでUnitOfWorkImplクラスは、ユーザーが新規作成、更新、削除のマーキングを行うと、それぞれのHashMapにユーザーを保存します。commitメソッドを呼び出すと、これらの変更が「データベース」に保存されます。rollbackメソッドを呼び出すと、すべての変更がクリアされます。

main 関数を使ってこれらを使ってみましょう。

public class Main {
    public static void main(String[] args) {
        UnitOfWork unitOfWork = new UnitOfWorkImpl();

        // Create new users
        User user1 = new User("1", "Alice");
        User user2 = new User("2", "Bob");

        unitOfWork.markNew(user1);
        unitOfWork.markNew(user2);
        unitOfWork.commit(); // Changes are saved to the database

        // Update a user
        user1.setName("Updated Alice");
        unitOfWork.markDirty(user1);
        unitOfWork.commit(); // Changes are saved to the database

        // Delete a user
        unitOfWork.markDeleted(user1);
        unitOfWork.commit(); // Changes are saved to the database

        // Trying to do some changes but then decide to rollback
        User user3 = new User("3", "Charlie");
        unitOfWork.markNew(user3);
        unitOfWork.markDeleted(user2);
        unitOfWork.rollback(); // Changes are discarded
    }
}

このmain関数では、新たなユーザーの作成、ユーザー情報の更新、ユーザーの削除と、それぞれの後でcommitメソッドを呼び出してデータベースに変更を保存しています。

最後に、新たなユーザーの作成と既存ユーザーの削除を試みていますが、最終的にrollbackメソッドを呼び出し、これらの変更を破棄しています。

この例を通じて、UnitOfWorkパターンがどのように機能するかを理解できるはずです。

まとめ

ここでは、データの一貫性を維持しながら複数のデータ操作を行うためのパターンである”Unit of Work”について学びました。このパターンは一連の操作を「作業単位」にまとめ、その全体が成功するか失敗するかを一貫性を持って扱うことを目的としています。このパターンは特にデータベースのトランザクション処理において有用で、一連の操作がアトミックに(すなわち一つの不可分の作業として)行われることを保証します。

Javaにおける具体的な実装例を見ると、UnitOfWorkインターフェースは新規作成(markNew)、更新(markDirty)、削除(markDeleted)のマーキングを提供します。そして、commitメソッドで変更を確定(データベースに保存)し、rollbackメソッドで変更を破棄します。このように、UnitOfWorkは一連の操作をまとめて管理し、それらを一貫性のある形で制御します。

また、これらの操作は内部でHashMapに保存されることで、メモリ上で一時的に保持されます。これにより、操作が完全に終了するまで実際のデータベースには影響を与えないという特徴があります。これは、途中で何か問題が発生した場合に元の状態に戻すことが可能であり、データの整合性を保つのに非常に重要です。

したがって、このパターンはデータの整合性を保つだけでなく、リソースの効率的な使用、エラーハンドリングの改善、コードの整理といった複数の利点を提供します。特に、大規模なデータ操作を行う必要がある複雑なアプリケーションにおいては、その価値が最大化されます。

しかし、これらの利点を享受する一方で、実装はやや複雑になり、また必要なリソース(メモリ等)も増えるため、アプリケーションの要件とリソースを考慮して適切に使用する必要があります。