Your logic in the owned service
The model says what the application is. What it cannot say goes into files the generator hands
over to you - first of all the extension service XxxService, which extends the regenerated
XxxCoreService. Its second line starts with // Initially generated by ElastiCORE;: it is yours
once you edit it.
| What | Where |
|---|---|
| a rule about writes - a value worked out from others, a check against other rows | beforeSave in XxxService |
| a sign-in rule (locked accounts) | override authenticate(login, password) |
| more work in a lifecycle step (a courier call) | override the command in XxxService (ship(orderId)) |
| a rule inside the aggregate (a state check, an event) | the behaviour class Xxx of a split root |
| an operation the CRUD does not have (return a loan, a report) | a method in XxxService + one small controller of yours |
| a custom query in a generated repository | one preserved block |
Do not write controllers, DTOs, repositories, security, sign-in or error handlers by hand: they are
generated. Add methods to the generated XxxService.java after the first generation - do not write
that file before it exists.
beforeSave - every write of a row
The core service calls it after the request's values are on the entity, just before saving - on
create, PUT /, PUT /batch and update by condition.
| Hook | When |
|---|---|
beforeSave(entity) | what the entity alone decides: recompute a total, stamp a state |
beforeSave(entity, request) | what depends on what the request carried |
The default beforeSave(entity, request) calls beforeSave(entity), so override one of them.
The pet store places a new order before it is first saved:
@Override
protected void beforeSave(PurchaseOrder order) {
if (order.getOrderDate() == null) {
order.place(LocalDate.now(clock), this::homeOf, vatRate, discountRate);
}
}
and the shop policy itself lives on the root:
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(); // the lines' @compute sum
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();
}
"Only the first time" is told apart by a state - an id that is still null with @sequence, an empty
date as above. For a time set when the row is made, you need no code: @readonly @default(now).
Refusing
Throw the runtime's exceptions; the generated error handler answers them with your message as
detail - see API and error answers:
throw new ConflictException("loan.no-copy-left", "No copy of {0} is left to lend", isbn); // 409
throw new BadRequestException("..."); // 400
entityManager is a field of the service: entityManager.find(Book.class, isbn) reads a row,
entityManager.createQuery("select count(l) from Loan l where ...", Long.class) counts.
Overriding update() - and PUT /batch
From 3.0.261022-rc1 batchUpdate (PUT /batch) calls update(dto) for each row in one transaction,
so a rule in an override of update() holds for batches too, with the same checks and 409
translation. Before, the batch copied update()'s body and skipped such overrides. A rule about
writes still belongs in beforeSave, which every path passes through.
An operation of your own
A method in the service plus one small controller. From the agent kit's shop recipe - an administrator's report:
/** 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 (the entity's @access), currentUser and entityManager are fields of the generated
service. Put the controller in the package of the generated controllers so the generated error
handler answers its 401 and 403. The next build reports the service SKIP(user-owned) and keeps the
method.
In the root: split roots (DDD bundles)
A root's lifecycle steps are generated; more work goes in your behaviour class with super:
public class PurchaseOrder extends PurchaseOrderBase {
@Override
public void ship() {
super.ship(); // state check + move + event
// then the rest
}
}
A command in commands: without a lifecycle is an abstract method on the base: the build does not
compile until you write it in the behaviour class.
Other files you own
- Controllers - generate-once. An edited controller does not receive new endpoints (a new
command,
@signin); the build warns which are missing. XxxApiErrors- the error answers' shape.- Domain services, process managers, factories - their bases are regenerated, your class is not.
- The application class, start-up data, event listeners - ordinary code of yours.