Unit Of Work

This pattern belongs to Object-Relational Behavioral Patterns Catalog and this Catalog belongs to Patterns of Enterprise Application Architecture.

Intent

When a business transaction is completed, all these updates are sent as one big unit of work to be persisted in a database in one go so as to minimize database trips.

Explanation

Unit of Work design pattern does two important things: first, it maintains in-memory updates and second it sends these in-memory updates as one transaction to the database.
So to achieve the above goals it goes through two steps:
  • It maintains lists of business objects in-memory which have been changed (inserted, updated, or deleted) during a transaction.
  • Once the transaction is completed, all these updates are sent as one big unit of work to be persisted physically in a database in one go.
What is “Work” and “Unit” in a software application?
A simple definition of Work means performing some task.  From a software application perspective Work is nothing but inserting, updating, and deleting data. For instance, let’s say you have an application that maintains customer data into a database.

So when you add, update, or delete a customer record on the database it’s one unit. In simple words the equation is.
1 customer CRUD = 1 unit of work
Now consider the below scenario where every customer can have multiple addresses. Then many rows will become 1 unit of work.
3 customer CRUD = 1 unit of work
So in simple words, transactions for all of the three records should succeed or all of them should fail. It should be ATOMIC. In other words, it’s very much possible that many CRUD operations will be equal to 1 unit of work.

How It Works

1. The obvious things that cause you to deal with the database are changes: new object created and existing ones updated or deleted. Unit of Work is an object that keeps track of these things. As soon as you start doing something that may affect a database, you create a Unit of Work to keep track of the changes. Every time you create, change or delete an object you tell the Unit of Work. You can also let it know about objects you’ve read so that it can check for inconsistent reads by verifying that none of the objects changed on the database during the business transaction.
2. The key thing about Unit of Work is that, when it comes time to commit, the Unit of Work decides what to do. It opens a transaction, does any concurrency checking, and writes changes out to the database. Application programmers never explicitly call methods for database updates. This way they don’t have to keep track of what’s changed or worry about how referential integrity affects the order in which they need to do things.
The great strength of Unit of Work is that it keeps all this information in one place so it will be easy to change or track in future.

Sample Code

This example is to manage student data, every time we create, change, or delete a customer object we tell the Unit of Work when we commit the transaction to the database in one go so as to minimize database trips using this pattern.

Let's create a Class Diagram for this example.

Step 1: Create Student entity class.
public class Student {
  private final Integer id;
  private final String name;
  private final String address;

  /**
   * @param id      student unique id
   * @param name    name of student
   * @param address address of student
   */
  public Student(Integer id, String name, String address) {
    this.id = id;
    this.name = name;
    this.address = address;
  }

  public String getName() {
    return name;
  }

  public Integer getId() {
    return id;
  }

  public String getAddress() {
    return address;
  }
}
Step 2:  Create a generic interface for UnitOfWork implementation.
public interface IUnitOfWork<T> {
  String INSERT = "INSERT";
  String DELETE = "DELETE";
  String MODIFY = "MODIFY";

  /**
   * Any register new operation occurring on UnitOfWork is only going to be performed on commit.
   */
  void registerNew(T entity);

  /**
   * Any register modify operation occurring on UnitOfWork is only going to be performed on commit.
   */
  void registerModified(T entity);

  /**
   * Any register delete operation occurring on UnitOfWork is only going to be performed on commit.
   */
  void registerDeleted(T entity);

  /***
   * All UnitOfWork operations batched together executed in commit only.
   */
  void commit();

}
Step 3: Create UnitOfWork implementation which supports unit of work for student data.
public class UnitOfWork implements IUnitOfWork<Student> {
  private static final Logger LOGGER = LoggerFactory.getLogger(StudentRepository.class);

  private Map<String, List<Student>> context;
  private StudentDatabase studentDatabase;

  /**
   * @param context         set of operations to be perform during commit.
   * @param studentDatabase Database for student records.
   */
  public UnitOfWork (Map<String, List<Student>> context, StudentDatabase studentDatabase) {
    this.context = context;
    this.studentDatabase = studentDatabase;
  }

  @Override
  public void registerNew(Student student) {
    LOGGER.info("Registering {} for insert in context.", student.getName());
    register(student, IUnitOfWork.INSERT);
  }

  @Override
  public void registerModified(Student student) {
    LOGGER.info("Registering {} for modify in context.", student.getName());
    register(student, IUnitOfWork.MODIFY);

  }

  @Override
  public void registerDeleted(Student student) {
    LOGGER.info("Registering {} for delete in context.", student.getName());
    register(student, IUnitOfWork.DELETE);
  }

  private void register(Student student, String operation) {
    List<Student> studentsToOperate = context.get(operation);
    if (studentsToOperate == null) {
      studentsToOperate = new ArrayList<>();
    }
    studentsToOperate.add(student);
    context.put(operation, studentsToOperate);
  }

  /**
   * All UnitOfWork operations are batched and executed together on commit only.
   */
  @Override
  public void commit() {
    if (context == null || context.size() == 0) {
      return;
    }
    LOGGER.info("Commit started");
    if (context.containsKey(IUnitOfWork.INSERT)) {
      commitInsert();
    }

    if (context.containsKey(IUnitOfWork.MODIFY)) {
      commitModify();
    }
    if (context.containsKey(IUnitOfWork.DELETE)) {
      commitDelete();
    }
    LOGGER.info("Commit finished.");
  }

  private void commitInsert() {
    List<Student> studentsToBeInserted = context.get(IUnitOfWork.INSERT);
    for (Student student : studentsToBeInserted) {
      LOGGER.info("Saving {} to database.", student.getName());
      studentDatabase.insert(student);
    }
  }

  private void commitModify() {
    List<Student> modifiedStudents = context.get(IUnitOfWork.MODIFY);
    for (Student student : modifiedStudents) {
      LOGGER.info("Modifying {} to database.", student.getName());
      studentDatabase.modify(student);
    }
  }

  private void commitDelete() {
    List<Student> deletedStudents = context.get(IUnitOfWork.DELETE);
    for (Student student : deletedStudents) {
      LOGGER.info("Deleting {} to database.", student.getName());
      studentDatabase.delete(student);
    }
  }
}
Step 3:  This class acts as a Database for student records.
public class StudentDatabase {

  public void insert(Student student) {
    //Some insert logic to DB
  }

  public void modify(Student student) {
    //Some modify logic to DB
  }

  public void delete(Student student) {
    //Some delete logic to DB
  }
}
Step 4: Let's test this pattern to manage student data.
public class Client{
  /**
   *
   * @param args no argument sent
   */
  public static void main(String[] args) {
    Student ram = new Student(1, "Ram", "Street 9, Cupertino");
    Student shyam = new Student(2, "Shyam", "Z bridge, Pune");
    Student gopi = new Student(3, "Gopi", "Street 10, Mumbai");

    HashMap<String, List<Student>> context = new HashMap<>();
    StudentDatabase studentDatabase = new StudentDatabase();
    UnitOfwork unitOfwork = new UnitOfwork (context, studentDatabase);

    unitOfwork .registerNew(ram);
    unitOfwork .registerModified(shyam);
    unitOfwork .registerDeleted(gopi);
    unitOfwork .commit();
  }
}

Applicability

Use the Unit Of Work pattern when
  • To optimize the time taken for database transactions.
  • To send changes to the database as a unit of work which ensures the atomicity of the transaction.
  • To reduce the number of database calls.

References



Comments