From 2823166c915bb063f9372fa8d17d4d6f09d45d68 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ilkka=20Sepp=C3=A4l=C3=A4?= Date: Sat, 1 Jan 2022 21:44:58 +0200 Subject: [PATCH] refactor unit of work --- unit-of-work/README.md | 216 +++++++++--------- unit-of-work/etc/unit-of-work.ucls | 6 +- .../java/com/iluwatar/unitofwork/App.java | 24 +- ...StudentRepository.java => ArmsDealer.java} | 62 ++--- .../unitofwork/{Student.java => Weapon.java} | 6 +- ...udentDatabase.java => WeaponDatabase.java} | 10 +- ...epositoryTest.java => ArmsDealerTest.java} | 82 +++---- 7 files changed, 205 insertions(+), 201 deletions(-) rename unit-of-work/src/main/java/com/iluwatar/unitofwork/{StudentRepository.java => ArmsDealer.java} (55%) rename unit-of-work/src/main/java/com/iluwatar/unitofwork/{Student.java => Weapon.java} (93%) rename unit-of-work/src/main/java/com/iluwatar/unitofwork/{StudentDatabase.java => WeaponDatabase.java} (87%) rename unit-of-work/src/test/java/com/iluwatar/unitofwork/{StudentRepositoryTest.java => ArmsDealerTest.java} (51%) diff --git a/unit-of-work/README.md b/unit-of-work/README.md index df2d03df2..f60667810 100644 --- a/unit-of-work/README.md +++ b/unit-of-work/README.md @@ -12,20 +12,20 @@ tags: ## Intent -When a business transaction is completed, all the the updates are sent as one big unit of work to be +When a business transaction is completed, all the updates are sent as one big unit of work to be persisted in one go to minimize database round-trips. ## Explanation -Real world example +Real-world example -> We have a database containing student information. Administrators all over the country are -> constantly updating this information and it causes high load on the database server. To make the +> Arms dealer has a database containing weapon information. Merchants all over the town are +> constantly updating this information and it causes a high load on the database server. To make the > load more manageable we apply to Unit of Work pattern to send many small updates in batches. In plain words -> Unit of Work merges many small database updates in single batch to optimize the number of +> Unit of Work merges many small database updates in a single batch to optimize the number of > round-trips. [MartinFowler.com](https://martinfowler.com/eaaCatalog/unitOfWork.html) says @@ -35,37 +35,20 @@ In plain words **Programmatic Example** -Here's the `Student` entity that is being persisted to the database. +Here's the `Weapon` entity that is being persisted in the database. ```java -public class Student { - private final Integer id; - private final String name; - private final String address; - - 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; - } +@Getter +@RequiredArgsConstructor +public class Weapon { + private final Integer id; + private final String name; } ``` -The essence of the implementation is the `StudentRepository` implementing the Unit of Work pattern. +The essence of the implementation is the `ArmsDealer` implementing the Unit of Work pattern. It maintains a map of database operations (`context`) that need to be done and when `commit` is -called it applies them in single batch. +called it applies them in a single batch. ```java public interface IUnitOfWork { @@ -84,96 +67,117 @@ public interface IUnitOfWork { } @Slf4j -public class StudentRepository implements IUnitOfWork { +@RequiredArgsConstructor +public class ArmsDealer implements IUnitOfWork { - private final Map> context; - private final StudentDatabase studentDatabase; + private final Map> context; + private final WeaponDatabase weaponDatabase; - public StudentRepository(Map> 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) { - var studentsToOperate = context.get(operation); - if (studentsToOperate == null) { - studentsToOperate = new ArrayList<>(); - } - studentsToOperate.add(student); - context.put(operation, studentsToOperate); - } - - @Override - public void commit() { - if (context == null || context.size() == 0) { - return; - } - LOGGER.info("Commit started"); - if (context.containsKey(IUnitOfWork.INSERT)) { - commitInsert(); + @Override + public void registerNew(Weapon weapon) { + LOGGER.info("Registering {} for insert in context.", weapon.getName()); + register(weapon, UnitActions.INSERT.getActionValue()); } - if (context.containsKey(IUnitOfWork.MODIFY)) { - commitModify(); - } - if (context.containsKey(IUnitOfWork.DELETE)) { - commitDelete(); - } - LOGGER.info("Commit finished."); - } + @Override + public void registerModified(Weapon weapon) { + LOGGER.info("Registering {} for modify in context.", weapon.getName()); + register(weapon, UnitActions.MODIFY.getActionValue()); - private void commitInsert() { - var studentsToBeInserted = context.get(IUnitOfWork.INSERT); - for (var student : studentsToBeInserted) { - LOGGER.info("Saving {} to database.", student.getName()); - studentDatabase.insert(student); } - } - private void commitModify() { - var modifiedStudents = context.get(IUnitOfWork.MODIFY); - for (var student : modifiedStudents) { - LOGGER.info("Modifying {} to database.", student.getName()); - studentDatabase.modify(student); + @Override + public void registerDeleted(Weapon weapon) { + LOGGER.info("Registering {} for delete in context.", weapon.getName()); + register(weapon, UnitActions.DELETE.getActionValue()); } - } - private void commitDelete() { - var deletedStudents = context.get(IUnitOfWork.DELETE); - for (var student : deletedStudents) { - LOGGER.info("Deleting {} to database.", student.getName()); - studentDatabase.delete(student); + private void register(Weapon weapon, String operation) { + var weaponsToOperate = context.get(operation); + if (weaponsToOperate == null) { + weaponsToOperate = new ArrayList<>(); + } + weaponsToOperate.add(weapon); + context.put(operation, weaponsToOperate); + } + + /** + * 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(UnitActions.INSERT.getActionValue())) { + commitInsert(); + } + + if (context.containsKey(UnitActions.MODIFY.getActionValue())) { + commitModify(); + } + if (context.containsKey(UnitActions.DELETE.getActionValue())) { + commitDelete(); + } + LOGGER.info("Commit finished."); + } + + private void commitInsert() { + var weaponsToBeInserted = context.get(UnitActions.INSERT.getActionValue()); + for (var weapon : weaponsToBeInserted) { + LOGGER.info("Inserting a new weapon {} to sales rack.", weapon.getName()); + weaponDatabase.insert(weapon); + } + } + + private void commitModify() { + var modifiedWeapons = context.get(UnitActions.MODIFY.getActionValue()); + for (var weapon : modifiedWeapons) { + LOGGER.info("Scheduling {} for modification work.", weapon.getName()); + weaponDatabase.modify(weapon); + } + } + + private void commitDelete() { + var deletedWeapons = context.get(UnitActions.DELETE.getActionValue()); + for (var weapon : deletedWeapons) { + LOGGER.info("Scrapping {}.", weapon.getName()); + weaponDatabase.delete(weapon); + } } - } } ``` -Finally, here's how we use the `StudentRepository` and `commit` the transaction. +Here is how the whole app is put together. ```java - studentRepository.registerNew(ram); - studentRepository.registerModified(shyam); - studentRepository.registerDeleted(gopi); - studentRepository.commit(); +// create some weapons +var enchantedHammer = new Weapon(1, "enchanted hammer"); +var brokenGreatSword = new Weapon(2, "broken great sword"); +var silverTrident = new Weapon(3, "silver trident"); + +// create repository +var weaponRepository = new ArmsDealer(new HashMap>(), new WeaponDatabase()); + +// perform operations on the weapons +weaponRepository.registerNew(enchantedHammer); +weaponRepository.registerModified(silverTrident); +weaponRepository.registerDeleted(brokenGreatSword); +weaponRepository.commit(); +``` + +Here is the console output. + +``` +21:39:21.984 [main] INFO com.iluwatar.unitofwork.ArmsDealer - Registering enchanted hammer for insert in context. +21:39:21.989 [main] INFO com.iluwatar.unitofwork.ArmsDealer - Registering silver trident for modify in context. +21:39:21.989 [main] INFO com.iluwatar.unitofwork.ArmsDealer - Registering broken great sword for delete in context. +21:39:21.989 [main] INFO com.iluwatar.unitofwork.ArmsDealer - Commit started +21:39:21.989 [main] INFO com.iluwatar.unitofwork.ArmsDealer - Inserting a new weapon enchanted hammer to sales rack. +21:39:21.989 [main] INFO com.iluwatar.unitofwork.ArmsDealer - Scheduling silver trident for modification work. +21:39:21.989 [main] INFO com.iluwatar.unitofwork.ArmsDealer - Scrapping broken great sword. +21:39:21.989 [main] INFO com.iluwatar.unitofwork.ArmsDealer - Commit finished. ``` ## Class diagram @@ -186,7 +190,7 @@ Use the Unit Of Work pattern when * To optimize the time taken for database transactions. * To send changes to database as a unit of work which ensures atomicity of the transaction. -* To reduce number of database calls. +* To reduce the number of database calls. ## Tutorials diff --git a/unit-of-work/etc/unit-of-work.ucls b/unit-of-work/etc/unit-of-work.ucls index 98181f805..0a80d680d 100644 --- a/unit-of-work/etc/unit-of-work.ucls +++ b/unit-of-work/etc/unit-of-work.ucls @@ -1,7 +1,7 @@ - - @@ -38,7 +38,7 @@ - >(); - var studentDatabase = new StudentDatabase(); - var studentRepository = new StudentRepository(context, studentDatabase); + // create repository + var weaponRepository = new ArmsDealer(new HashMap>(), + new WeaponDatabase()); - studentRepository.registerNew(ram); - studentRepository.registerModified(shyam); - studentRepository.registerDeleted(gopi); - studentRepository.commit(); + // perform operations on the weapons + weaponRepository.registerNew(enchantedHammer); + weaponRepository.registerModified(silverTrident); + weaponRepository.registerDeleted(brokenGreatSword); + weaponRepository.commit(); } } diff --git a/unit-of-work/src/main/java/com/iluwatar/unitofwork/StudentRepository.java b/unit-of-work/src/main/java/com/iluwatar/unitofwork/ArmsDealer.java similarity index 55% rename from unit-of-work/src/main/java/com/iluwatar/unitofwork/StudentRepository.java rename to unit-of-work/src/main/java/com/iluwatar/unitofwork/ArmsDealer.java index 991aef12a..c222e47d4 100644 --- a/unit-of-work/src/main/java/com/iluwatar/unitofwork/StudentRepository.java +++ b/unit-of-work/src/main/java/com/iluwatar/unitofwork/ArmsDealer.java @@ -30,41 +30,41 @@ import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; /** - * {@link StudentRepository} Student database repository. supports unit of work for student data. + * {@link ArmsDealer} Weapon repository that supports unit of work for weapons. */ @Slf4j @RequiredArgsConstructor -public class StudentRepository implements IUnitOfWork { +public class ArmsDealer implements IUnitOfWork { - private final Map> context; - private final StudentDatabase studentDatabase; + private final Map> context; + private final WeaponDatabase weaponDatabase; @Override - public void registerNew(Student student) { - LOGGER.info("Registering {} for insert in context.", student.getName()); - register(student, UnitActions.INSERT.getActionValue()); + public void registerNew(Weapon weapon) { + LOGGER.info("Registering {} for insert in context.", weapon.getName()); + register(weapon, UnitActions.INSERT.getActionValue()); } @Override - public void registerModified(Student student) { - LOGGER.info("Registering {} for modify in context.", student.getName()); - register(student, UnitActions.MODIFY.getActionValue()); + public void registerModified(Weapon weapon) { + LOGGER.info("Registering {} for modify in context.", weapon.getName()); + register(weapon, UnitActions.MODIFY.getActionValue()); } @Override - public void registerDeleted(Student student) { - LOGGER.info("Registering {} for delete in context.", student.getName()); - register(student, UnitActions.DELETE.getActionValue()); + public void registerDeleted(Weapon weapon) { + LOGGER.info("Registering {} for delete in context.", weapon.getName()); + register(weapon, UnitActions.DELETE.getActionValue()); } - private void register(Student student, String operation) { - var studentsToOperate = context.get(operation); - if (studentsToOperate == null) { - studentsToOperate = new ArrayList<>(); + private void register(Weapon weapon, String operation) { + var weaponsToOperate = context.get(operation); + if (weaponsToOperate == null) { + weaponsToOperate = new ArrayList<>(); } - studentsToOperate.add(student); - context.put(operation, studentsToOperate); + weaponsToOperate.add(weapon); + context.put(operation, weaponsToOperate); } /** @@ -90,26 +90,26 @@ public class StudentRepository implements IUnitOfWork { } private void commitInsert() { - var studentsToBeInserted = context.get(UnitActions.INSERT.getActionValue()); - for (var student : studentsToBeInserted) { - LOGGER.info("Saving {} to database.", student.getName()); - studentDatabase.insert(student); + var weaponsToBeInserted = context.get(UnitActions.INSERT.getActionValue()); + for (var weapon : weaponsToBeInserted) { + LOGGER.info("Inserting a new weapon {} to sales rack.", weapon.getName()); + weaponDatabase.insert(weapon); } } private void commitModify() { - var modifiedStudents = context.get(UnitActions.MODIFY.getActionValue()); - for (var student : modifiedStudents) { - LOGGER.info("Modifying {} to database.", student.getName()); - studentDatabase.modify(student); + var modifiedWeapons = context.get(UnitActions.MODIFY.getActionValue()); + for (var weapon : modifiedWeapons) { + LOGGER.info("Scheduling {} for modification work.", weapon.getName()); + weaponDatabase.modify(weapon); } } private void commitDelete() { - var deletedStudents = context.get(UnitActions.DELETE.getActionValue()); - for (var student : deletedStudents) { - LOGGER.info("Deleting {} to database.", student.getName()); - studentDatabase.delete(student); + var deletedWeapons = context.get(UnitActions.DELETE.getActionValue()); + for (var weapon : deletedWeapons) { + LOGGER.info("Scrapping {}.", weapon.getName()); + weaponDatabase.delete(weapon); } } } diff --git a/unit-of-work/src/main/java/com/iluwatar/unitofwork/Student.java b/unit-of-work/src/main/java/com/iluwatar/unitofwork/Weapon.java similarity index 93% rename from unit-of-work/src/main/java/com/iluwatar/unitofwork/Student.java rename to unit-of-work/src/main/java/com/iluwatar/unitofwork/Weapon.java index b3de369b4..bf4a3e071 100644 --- a/unit-of-work/src/main/java/com/iluwatar/unitofwork/Student.java +++ b/unit-of-work/src/main/java/com/iluwatar/unitofwork/Weapon.java @@ -27,14 +27,12 @@ import lombok.Getter; import lombok.RequiredArgsConstructor; /** - * {@link Student} is an entity. + * {@link Weapon} is an entity. */ @Getter @RequiredArgsConstructor -public class Student { +public class Weapon { private final Integer id; private final String name; - private final String address; - } diff --git a/unit-of-work/src/main/java/com/iluwatar/unitofwork/StudentDatabase.java b/unit-of-work/src/main/java/com/iluwatar/unitofwork/WeaponDatabase.java similarity index 87% rename from unit-of-work/src/main/java/com/iluwatar/unitofwork/StudentDatabase.java rename to unit-of-work/src/main/java/com/iluwatar/unitofwork/WeaponDatabase.java index c64c47e30..c9a8cee4b 100644 --- a/unit-of-work/src/main/java/com/iluwatar/unitofwork/StudentDatabase.java +++ b/unit-of-work/src/main/java/com/iluwatar/unitofwork/WeaponDatabase.java @@ -24,19 +24,19 @@ package com.iluwatar.unitofwork; /** - * Act as Database for student records. + * Act as database for weapon records. */ -public class StudentDatabase { +public class WeaponDatabase { - public void insert(Student student) { + public void insert(Weapon weapon) { //Some insert logic to DB } - public void modify(Student student) { + public void modify(Weapon weapon) { //Some modify logic to DB } - public void delete(Student student) { + public void delete(Weapon weapon) { //Some delete logic to DB } } diff --git a/unit-of-work/src/test/java/com/iluwatar/unitofwork/StudentRepositoryTest.java b/unit-of-work/src/test/java/com/iluwatar/unitofwork/ArmsDealerTest.java similarity index 51% rename from unit-of-work/src/test/java/com/iluwatar/unitofwork/StudentRepositoryTest.java rename to unit-of-work/src/test/java/com/iluwatar/unitofwork/ArmsDealerTest.java index 760d4a21b..65b65a623 100644 --- a/unit-of-work/src/test/java/com/iluwatar/unitofwork/StudentRepositoryTest.java +++ b/unit-of-work/src/test/java/com/iluwatar/unitofwork/ArmsDealerTest.java @@ -36,102 +36,102 @@ import java.util.Map; import org.junit.jupiter.api.Test; /** - * tests {@link StudentRepository} + * tests {@link ArmsDealer} */ -class StudentRepositoryTest { - private final Student student1 = new Student(1, "Ram", "street 9, cupertino"); - private final Student student2 = new Student(1, "Sham", "Z bridge, pune"); +class ArmsDealerTest { + private final Weapon weapon1 = new Weapon(1, "battle ram"); + private final Weapon weapon2 = new Weapon(1, "wooden lance"); - private final Map> context = new HashMap<>(); - private final StudentDatabase studentDatabase = mock(StudentDatabase.class); - private final StudentRepository studentRepository = new StudentRepository(context, studentDatabase);; + private final Map> context = new HashMap<>(); + private final WeaponDatabase weaponDatabase = mock(WeaponDatabase.class); + private final ArmsDealer armsDealer = new ArmsDealer(context, weaponDatabase);; @Test void shouldSaveNewStudentWithoutWritingToDb() { - studentRepository.registerNew(student1); - studentRepository.registerNew(student2); + armsDealer.registerNew(weapon1); + armsDealer.registerNew(weapon2); assertEquals(2, context.get(UnitActions.INSERT.getActionValue()).size()); - verifyNoMoreInteractions(studentDatabase); + verifyNoMoreInteractions(weaponDatabase); } @Test void shouldSaveDeletedStudentWithoutWritingToDb() { - studentRepository.registerDeleted(student1); - studentRepository.registerDeleted(student2); + armsDealer.registerDeleted(weapon1); + armsDealer.registerDeleted(weapon2); assertEquals(2, context.get(UnitActions.DELETE.getActionValue()).size()); - verifyNoMoreInteractions(studentDatabase); + verifyNoMoreInteractions(weaponDatabase); } @Test void shouldSaveModifiedStudentWithoutWritingToDb() { - studentRepository.registerModified(student1); - studentRepository.registerModified(student2); + armsDealer.registerModified(weapon1); + armsDealer.registerModified(weapon2); assertEquals(2, context.get(UnitActions.MODIFY.getActionValue()).size()); - verifyNoMoreInteractions(studentDatabase); + verifyNoMoreInteractions(weaponDatabase); } @Test void shouldSaveAllLocalChangesToDb() { - context.put(UnitActions.INSERT.getActionValue(), List.of(student1)); - context.put(UnitActions.MODIFY.getActionValue(), List.of(student1)); - context.put(UnitActions.DELETE.getActionValue(), List.of(student1)); + context.put(UnitActions.INSERT.getActionValue(), List.of(weapon1)); + context.put(UnitActions.MODIFY.getActionValue(), List.of(weapon1)); + context.put(UnitActions.DELETE.getActionValue(), List.of(weapon1)); - studentRepository.commit(); + armsDealer.commit(); - verify(studentDatabase, times(1)).insert(student1); - verify(studentDatabase, times(1)).modify(student1); - verify(studentDatabase, times(1)).delete(student1); + verify(weaponDatabase, times(1)).insert(weapon1); + verify(weaponDatabase, times(1)).modify(weapon1); + verify(weaponDatabase, times(1)).delete(weapon1); } @Test void shouldNotWriteToDbIfContextIsNull() { - var studentRepository = new StudentRepository(null, studentDatabase); + var weaponRepository = new ArmsDealer(null, weaponDatabase); - studentRepository.commit(); + weaponRepository.commit(); - verifyNoMoreInteractions(studentDatabase); + verifyNoMoreInteractions(weaponDatabase); } @Test void shouldNotWriteToDbIfNothingToCommit() { - var studentRepository = new StudentRepository(new HashMap<>(), studentDatabase); + var weaponRepository = new ArmsDealer(new HashMap<>(), weaponDatabase); - studentRepository.commit(); + weaponRepository.commit(); - verifyNoMoreInteractions(studentDatabase); + verifyNoMoreInteractions(weaponDatabase); } @Test void shouldNotInsertToDbIfNoRegisteredStudentsToBeCommitted() { - context.put(UnitActions.MODIFY.getActionValue(), List.of(student1)); - context.put(UnitActions.DELETE.getActionValue(), List.of(student1)); + context.put(UnitActions.MODIFY.getActionValue(), List.of(weapon1)); + context.put(UnitActions.DELETE.getActionValue(), List.of(weapon1)); - studentRepository.commit(); + armsDealer.commit(); - verify(studentDatabase, never()).insert(student1); + verify(weaponDatabase, never()).insert(weapon1); } @Test void shouldNotModifyToDbIfNotRegisteredStudentsToBeCommitted() { - context.put(UnitActions.INSERT.getActionValue(), List.of(student1)); - context.put(UnitActions.DELETE.getActionValue(), List.of(student1)); + context.put(UnitActions.INSERT.getActionValue(), List.of(weapon1)); + context.put(UnitActions.DELETE.getActionValue(), List.of(weapon1)); - studentRepository.commit(); + armsDealer.commit(); - verify(studentDatabase, never()).modify(student1); + verify(weaponDatabase, never()).modify(weapon1); } @Test void shouldNotDeleteFromDbIfNotRegisteredStudentsToBeCommitted() { - context.put(UnitActions.INSERT.getActionValue(), List.of(student1)); - context.put(UnitActions.MODIFY.getActionValue(), List.of(student1)); + context.put(UnitActions.INSERT.getActionValue(), List.of(weapon1)); + context.put(UnitActions.MODIFY.getActionValue(), List.of(weapon1)); - studentRepository.commit(); + armsDealer.commit(); - verify(studentDatabase, never()).delete(student1); + verify(weaponDatabase, never()).delete(weapon1); } }