개발자 소유 서비스에 쓰는 로직
모델은 애플리케이션이 무엇인지 말합니다. 모델이 말할 수 없는 것은 생성기가 넘겨준 파일에 씁니다 - 무엇보다
재생성되는 XxxCoreService 를 상속하는 확장 서비스 XxxService. 둘째 줄이 // Initially generated by ElastiCORE; 로
시작하는 파일이라, 한 번 고치면 내 것이 됩니다.
| 무엇을 | 어디에 |
|---|---|
| 쓰기에 대한 규칙 - 다른 값에서 정해지는 값, 다른 행을 보는 검사 | XxxService 의 beforeSave |
| 로그인 규칙 (계정 잠금) | authenticate(login, password) override |
| 생명주기 이동에 더 할 일 (택배사 호출) | XxxService 에서 그 명령 override (ship(orderId)) |
| 애그리거트 안의 규칙 (상태 확인, 이벤트) | 나뉜 루트의 행위 클래스 Xxx |
| CRUD 에 없는 연산 (대출 반납, 보고서) | XxxService 의 메서드 + 작은 컨트롤러 하나 |
| 생성된 리포지터리의 커스텀 쿼리 | 보존 블록 하나 |
컨트롤러, DTO, 리포지터리, 보안, 로그인, 오류 처리기는 손으로 쓰지 마세요 - 생성됩니다. 메서드는 첫 생성으로
XxxService.java 가 생긴 뒤에 거기 더합니다. 그 파일을 생성 전에 직접 쓰지 않습니다.
beforeSave - 행을 쓰는 모든 경로
코어 서비스는 요청 값이 엔티티에 들어간 뒤, 저장 직전에 부릅니다 - 생성, PUT /, PUT /batch, 조건 수정의 네 경로
모두.
| 훅 | 언제 |
|---|---|
beforeSave(entity) | 엔티티만 보고 끝낼 일: 합계 재계산, 상태 찍기 |
beforeSave(entity, request) | 요청이 무엇을 실어 왔는지 알아야 할 일 |
기본 beforeSave(entity, request) 가 beforeSave(entity) 를 부르므로 하나만 override 하면 됩니다.
Pet Store 는 새 주문이 처음 저장될 때 주문을 접수합니다:
@Override
protected void beforeSave(PurchaseOrder order) {
if (order.getOrderDate() == null) {
order.place(LocalDate.now(clock), this::homeOf, vatRate, discountRate);
}
}
상점의 정책 자체는 루트에 있습니다:
public void place(LocalDate today, Function<Long, Address> homeOf, BigDecimal vatRate, BigDecimal discountRate) {
if (getOrderDate() != null) throw new InvariantViolationException("order " + getOrderId() + " is already placed");
if (getDeliveryAddress() == null) setDeliveryAddress(homeOf.apply(getCustomerId()));
// ...
Money withoutVat = getTotalWithoutVat(); // 줄의 @compute 합
Money vat = percent(withoutVat, vatRate);
Money discount = percent(withoutVat.plus(vat), discountRate);
setOrderDate(today);
setVat(vat);
setDiscount(discount);
setTotal(withoutVat.plus(vat).minus(discount).rounded(2));
raisePlaced();
}
"처음 한 번만" 은 상태로 구분합니다 - @sequence 면 아직 null 인 ID, 위처럼 비어 있는 날짜. 행을 만들 때의 시각은 코드가
필요 없습니다: @readonly @default(now).
거절하기
런타임의 예외를 던지면 생성된 오류 처리기가 내 메시지를 detail 로 답합니다 -
API 와 오류 응답:
throw new ConflictException("loan.no-copy-left", "No copy of {0} is left to lend", isbn); // 409
throw new BadRequestException("..."); // 400
entityManager 는 서비스의 필드입니다: entityManager.find(Book.class, isbn) 로 행을 읽고,
entityManager.createQuery("select count(l) from Loan l where ...", Long.class) 로 셉니다.
update() override 와 PUT /batch
3.0.261022-rc1 부터 batchUpdate(PUT /batch)는 한 트랜잭션 안에서 행마다 update(dto) 를 부르므로, update()
override 의 규칙이 일괄 수정에도 같은 검사·409 변환과 함께 적용됩니다. 전에는 일괄 수정이 update() 의 본문을 복사해
그런 override 를 건너뛰었습니다. 쓰기에 대한 규칙은 여전히 모든 경로가 지나는 beforeSave 가 제자리입니다.
직접 만든 연산
서비스의 메서드와 작은 컨트롤러 하나입니다. 에이전트 키트의 상점 레시피에서 - 관리자 보고서:
/** Orders per status and the revenue of those not cancelled. "report" is not in the entity's
* @access, so it is the administrator's: 401 for a visitor, 403 for a member. */
@org.springframework.transaction.annotation.Transactional(readOnly = true)
public java.util.Map<String, Object> report() {
ACCESS.require(currentUser, "report");
java.util.Map<String, Long> counts = new java.util.LinkedHashMap<>();
java.math.BigDecimal revenue = java.math.BigDecimal.ZERO;
for (Object[] row : entityManager.createQuery(
"select o.status, count(o), coalesce(sum(o.total), 0) from PurchaseOrder o group by o.status",
Object[].class).getResultList()) {
com.acme.store.domain.model.OrderStatus status = (com.acme.store.domain.model.OrderStatus) row[0];
counts.put(status.name(), (Long) row[1]);
if (status != com.acme.store.domain.model.OrderStatus.CANCELLED) {
revenue = revenue.add(new java.math.BigDecimal(String.valueOf(row[2])));
}
}
java.util.Map<String, Object> report = new java.util.LinkedHashMap<>();
report.put("countByStatus", counts);
report.put("revenue", revenue.setScale(2, java.math.RoundingMode.HALF_UP));
return report;
}
@RestController
@RequestMapping("/api/store/purchaseOrder")
public class OrderReportController {
private final PurchaseOrderService service;
public OrderReportController(PurchaseOrderService service) {
this.service = service;
}
@GetMapping("/report")
public Map<String, Object> report() {
return service.report();
}
}
ACCESS(엔티티의 @access), currentUser, entityManager 는 생성된 서비스의 필드입니다. 컨트롤러는 생성된 컨트롤러와
같은 패키지에 두어야 생성된 오류 처리기가 그 401·403 을 답합니다. 다음 빌드는 서비스를 SKIP(user-owned) 로 보고하고
메서드를 남깁니다.
루트 안: 나뉜 루트 (DDD 번들)
루트의 생명주기 이동은 생성됩니다. 더 할 일은 행위 클래스에서 super 와 함께:
public class PurchaseOrder extends PurchaseOrderBase {
@Override
public void ship() {
super.ship(); // 상태 확인 + 이동 + 이벤트
// 그다음 할 일
}
}
생명주기가 없는 commands: 명령은 Base 의 추상 메서드라, 행위 클래스에 쓸 때까지 컴파일되지 않습니다.
그 밖의 내 파일
- 컨트롤러 - 한 번만 생성. 손댄 컨트롤러에는 새 엔드포인트(새 명령,
@signin)가 들어가지 않고, 빌드가 빠진 것을 경고합니다. XxxApiErrors- 오류 응답의 형식.- 도메인 서비스, 프로세스 매니저, 팩토리 - Base 는 다시 생성되고 내 클래스는 그대로.
- 애플리케이션 클래스, 시작 데이터, 이벤트 리스너 - 평범한 내 코드.