Business layer-create products/readOnly, CQRS/concurrency
✅ Business Logic
- (1) As a cafe manager, create products
- (2) product number should be created automatically
📌 Business for Product Service
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
@Transactional(readOnly = true)
@RequiredArgsConstructor
@Service
public class ProductService {
private final ProductRepository productRepository;
public List<ProductResponse> getSellingProducts(){
//reading from DB ➡️ readOnly = true
}
//only for CUD methods
//➡️ readOnly = false
// add Transactional
@Transactional
public ProductResponse createProduct(ProductCreateRequest request) {
//create product number (001, 002, 003...)
//get the last product number from DB, and add one
String nextProductNumber = createNextProductNumber();
Product product = request.toEntity(nextProductNumber);
Product savedProduct = productRepository.save(product);
return ProductResponse.of(savedProduct);
}
private String createNextProductNumber() {
String latestProductNumer = productRepository.findLatestProduct();
//if product is created for the first time
if(latestProductNumer == null){
return "001";
}
return String.format("%03d", Integer.valueOf(latestProductNumer) + 1);
}
}
✅ Transactional(readOnly=true), CQRS
readOnly=true- means check transaction for only Read
- so, in
CRUD, CUD will not work ❌ only Read ⭕️ in
JPA, CUD snapshot will not be created, will not check if updated(improve performance)- It is recommended method of CQRS
- CQRS: separate Command(CUD) and Query(Read)
- ❓ Why?
- As there are much more Read than Command, makes sense to separate command and read responsibilities
- ❓ How?
- 1️⃣ create a different sevice for CUD and another service for Read(Transactional readOnly = true)
- 2️⃣ create a different DB for writing and another for reading
📌 Test for Product Service
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
@ActiveProfiles("test") //not to use sql data
@SpringBootTest
class ProductServiceTest {
@Autowired
private ProductService productService;
@Autowired
private ProductRepository productRepository;
@AfterEach
void tearDown(){
productRepository.deleteAllInBatch(); //clean data after each test
}
//concurrency problem
@DisplayName("Create new product to DB")
@Test
void createProduct(){
Product product1 = createProduct("001", HANDMADE, SELLING, "americano", 4000);
Product product2 = createProduct("002", HANDMADE, HOLD, "latte", 4500);
Product product3 = createProduct("003", BAKERY, STOP_SELLING, "croissant", 4000);
productRepository.saveAll(List.of(product1, product2, product3));
ProductCreateRequest request = ProductCreateRequest.builder()
.type(HANDMADE)
.sellingStatus(SELLING)
.name("icecream")
.price(5000)
.build();
//when
ProductResponse response = productService.createProduct(request);
//then
assertThat(response).extracting("productNumber", "type", "sellingStatus", "name", "price")
.contains("004", HANDMADE, SELLING, "icecream", 5000);
List<Product> products = productRepository.findAll();
assertThat(products)
.hasSize(4)
.extracting("productNumber", "type", "sellingStatus", "name", "price")
.containsExactlyInAnyOrder(
tuple("001", HANDMADE, SELLING, "americano", 4000),
tuple("002", HANDMADE, HOLD, "latte", 4500),
tuple("003", BAKERY, STOP_SELLING, "croissant", 4000),
tuple("004", HANDMADE, SELLING, "icecream", 5000)
);
}
@DisplayName("When DB is empty, and the first product is created, the product numer should be 001")
@Test
void createProductFirstTime(){
//given
ProductCreateRequest request = ProductCreateRequest.builder()
.type(HANDMADE)
.sellingStatus(SELLING)
.name("icecream")
.price(5000)
.build();
//when
ProductResponse response = productService.createProduct(request);
//then
assertThat(response).extracting("productNumber", "type", "sellingStatus", "name", "price")
.contains("001", HANDMADE, SELLING, "icecream", 5000);
List<Product> products = productRepository.findAll();
assertThat(products)
.hasSize(1)
.extracting("productNumber", "type", "sellingStatus", "name", "price")
.contains(
tuple("001", HANDMADE, SELLING, "icecream", 5000)
);
}
private Product createProduct(String productNumber, ProductType productType, ProductSellingStatus sellingStatus, String name, int price) {
return Product.builder()
.productNumber(productNumber)
.type(productType)
.sellingStatus(sellingStatus)
.name(name)
.price(price)
.build();
}
}
✅ Concurrency
- if several managers create products at the same time,
product numbercan suffer from concunrrency issues- ❓ How to solve?
- 1️⃣ make productNumber
UNIQUEin DB, if fail, make the system retry - 2️⃣ make productNumber UUID, less conflicts
Optimistic lock 🆚 Pessimistic lock
- 🆚 Optimistic lock: no lock during read
- allow transactions to proceed without locking the data
- assume conflict is NOT likely
1
2
3
4
1. transaction reads a record
2. changes locally
3. before saving, check whether the record has been modified by another transaction
4. if changed, conflict detected, update fails/retries
- 👍🏻 higher concurrency
- 👎🏻 update failure
- 💊 handle conflicts by
detect & retry 🛠️ When
read >>>>> write, Hibernate- 🆚 Pessimistic lock: lock data immediately
- assume conflict is likely
1
2
3
1. transaction reads a record
2. database locks the record
3. other transactions must wait until the lock is released
- 👍🏻 prevent conflict entirely
- 👎🏻 lower concurrency
- 👎🏻 deadlock, waiting
- 💊 conflict is totally prevented
- 🛠️ MySQL, Banking systems, Financial
This post is licensed under CC BY 4.0 by the author.