refactoring: unit of work (#1940)

Co-authored-by: Subhrodip Mohanta <hello@subho.xyz>
This commit is contained in:
Ilkka SeppäläandSubhrodip Mohanta authored and GitHub committed 2022-01-08 17:57:11 +05:30
1 parent 11f20593b2
commit 3cc9bc2dea
7 files changed
+205 -201

No files matched your search

+110 -106
View File
@@ -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<T> {
@@ -84,96 +67,117 @@ public interface IUnitOfWork<T> {
}
@Slf4j
public class StudentRepository implements IUnitOfWork<Student> {
@RequiredArgsConstructor
public class ArmsDealer implements IUnitOfWork<Weapon> {
private final Map<String, List<Student>> context;
private final StudentDatabase studentDatabase;
private final Map<String, List<Weapon>> context;
private final WeaponDatabase weaponDatabase;
public StudentRepository(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) {
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<String, List<Weapon>>(), 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