Entities
entity:
EntityName:
meta: entity [@annotations] -- label
fields:
fieldName: fieldType [@annotations] -- comment
fieldName: fieldType [@annotations] -- label | description
states: ... # a lifecycle (DDD bundles) - see Lifecycle
commands: ... # business commands (DDD) - see Lifecycle
-- text becomes the label (@label(text)); -- label | description gives both a label and a
description. The label becomes the column comment and the API description.
The meta line
| Annotation | Means |
|---|---|
@expose(n) | REST controller + service + search DTO; n is the page size. Alone it gives create, read, search, paging and REST |
@service(n) | the same without the controller |
@pageable | paging support |
@root | an aggregate root (DDD bundles) - needed only on a root another root lists |
@aggregate(Root) | a member of that root's aggregate (usually inferred) |
@editable | DDD bundles: the root is plain data - public setters, PUT; without it a root is sealed |
@sort(name+) | the default order when a search names none: property + + ascending / - descending, comma-separated (@sort(orderDate-, orderId-)) |
@terms(order, purchase, 구매) | what the business calls it - synonyms for the Ossie export; no effect on code |
@access(...), @owner(field), @signin | who may do what - see Access and sign-in |
@event(Placed(total), Shipped) | domain events of a root - see Derived values and events |
@expose(20, methods="read,search") | the controller serves only the methods listed (create, read, search, update, delete); the client and the use case follow, the service keeps all - see API and error answers |
@restExclude(update, deleteById) | deprecated for methods: leaves methods out of the service, controller, use case and TypeScript client |
@table(name) / @schema(name) | the table name / the database schema |
@Index(name="uk_member_email", columnList="email", unique=true) | a table index; a unique one makes a duplicate answer 409 |
@abstract | an abstract entity - inherited from, no table |
@extend(Parent) | inherits the parent's fields |
@rollup | columns rolled up into the parent table (single-table inheritance) |
@embeddable | a value object - see Relations and values |
@audited | Hibernate Envers auditing |
@sort is a 3.0 rule-engine feature; the 2.x generator does not read it. A property the entity does
not have is left out with a DSL warning (@sort(...): x is not a field of Product).
Example
entity:
AuditEntity:
meta: entity @abstract
fields:
createdBy: string(20) @updatable(false) @jpa:org.springframework.data.annotation.CreatedBy
createDate: datetime @updatable(false) @jpa:org.springframework.data.annotation.CreatedDate
Product:
meta: entity @root @editable @expose(20) @access(read=public) @sort(name+) -- product (Angelfish, Bulldog ...)
fields:
productId: long @id @sequence
name: string(30)! @search(like)
description: string(3000)!
category: Category @ManyToOne
Rules of a root (DDD bundles)
These need architecture = 'ddd' or 'hexagonal'.
@invariant(condition, 'message') - what the root always keeps true
Order:
meta: entity @root @expose(30) @invariant(items.size > 0, 'an order has at least one line')
An OrderInvariants listener on the root checks it whenever the row is written
(@PrePersist / @PreUpdate), on every path that saves. A violation answers 422. Write it more
than once for more rules. A rule about one field belongs on the field (@min, @minsize), which in
the DDD bundles is checked at save time too; @invariant is for rules that compare fields.
@specification(name, condition) - a named domain rule
Order:
meta: entity @root @specification(highValue, totalAmount > 100) @specification(hasLines, items notEmpty)
Generates OrderSpecifications.highValue() - a Predicate<Order>, so rules combine:
highValue().and(hasLines()). It chooses among rows already loaded; it is not a query. With the
ts-model bundle, the TypeScript client gets the same functions.
@factory(place) - where a valid aggregate is made
Generates OrderFactory.place(...), a generate-once stub whose parameters are the root's required
fields (generated ids and collections left out). The body throws until you write it.
The condition grammar
| Form | Means |
|---|---|
items.size > 0 | number of members of a to-many |
totalAmount > 100 | a number compared (< too) |
notes notNull | the field is set |
items notEmpty | the to-many holds something |
Fields, comparisons and literals only. >=, <= and == cannot be written (the annotation parser
reads = as a key-value list), and neither can : (the YAML scalar would turn into a mapping). A
condition that cannot be read is left out - check the generated file.
@behavior - a field only the root's methods change
On a field of a split root, @behavior keeps it out of requests (READ_ONLY) and out of
creation(); a protected setter is used by your methods. A root is split into XxxBase and your
Xxx without writing @behavior on the entity; on an entity that is not split, a field
@behavior is a DSL warning - use @readonly there.
See DDD for sealed roots, @editable and creation().
Composite keys
More than one @id field generates a KeyDTO class:
OrderItem:
meta: entity
fields:
orderId: string @id
itemSeq: int @id
quantity: int
Names to avoid
The entity name becomes the table name, so avoid SQL words (Order, User, Group - use
PurchaseOrder, Member, Team) and SQL words as field names (order, group, user, select,
from, where, table, index, key, value).