eva4j 1.0.11 β 1.0.13
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/AGENTS.md +441 -14
- package/DOMAIN_YAML_GUIDE.md +425 -21
- package/FUTURE_FEATURES.md +315 -115
- package/QUICK_REFERENCE.md +101 -153
- package/README.md +77 -70
- package/bin/eva4j.js +57 -1
- package/config/defaults.json +3 -0
- package/docs/commands/GENERATE_ENTITIES.md +662 -1968
- package/docs/commands/GENERATE_HTTP_EXCHANGE.md +274 -450
- package/docs/commands/GENERATE_KAFKA_EVENT.md +219 -498
- package/docs/commands/GENERATE_KAFKA_LISTENER.md +18 -18
- package/docs/commands/GENERATE_RECORD.md +335 -311
- package/docs/commands/GENERATE_TEMPORAL_ACTIVITY.md +174 -0
- package/docs/commands/GENERATE_TEMPORAL_FLOW.md +237 -0
- package/docs/commands/GENERATE_USECASE.md +216 -282
- package/docs/commands/INDEX.md +36 -7
- package/examples/doctor-evaluation.yaml +3 -3
- package/examples/domain-audit-complete.yaml +2 -2
- package/examples/domain-collections.yaml +2 -2
- package/examples/domain-ecommerce.yaml +2 -2
- package/examples/domain-events.yaml +201 -0
- package/examples/domain-field-visibility.yaml +11 -5
- package/examples/domain-multi-aggregate.yaml +12 -6
- package/examples/domain-one-to-many.yaml +1 -1
- package/examples/domain-one-to-one.yaml +1 -1
- package/examples/domain-secondary-onetomany.yaml +1 -1
- package/examples/domain-secondary-onetoone.yaml +1 -1
- package/examples/domain-simple.yaml +1 -1
- package/examples/domain-soft-delete.yaml +3 -3
- package/examples/domain-transitions.yaml +1 -1
- package/examples/domain-value-objects.yaml +1 -1
- package/package.json +2 -2
- package/src/commands/add-kafka-client.js +3 -1
- package/src/commands/add-temporal-client.js +286 -0
- package/src/commands/generate-entities.js +75 -4
- package/src/commands/generate-kafka-event.js +273 -89
- package/src/commands/generate-temporal-activity.js +228 -0
- package/src/commands/generate-temporal-flow.js +216 -0
- package/src/generators/module-generator.js +1 -0
- package/src/generators/shared-generator.js +26 -0
- package/src/utils/yaml-to-entity.js +93 -4
- package/templates/aggregate/AggregateRepository.java.ejs +3 -2
- package/templates/aggregate/AggregateRepositoryImpl.java.ejs +15 -7
- package/templates/aggregate/AggregateRoot.java.ejs +38 -2
- package/templates/aggregate/DomainEntity.java.ejs +6 -2
- package/templates/aggregate/DomainEventHandler.java.ejs +62 -0
- package/templates/aggregate/DomainEventRecord.java.ejs +50 -0
- package/templates/aggregate/JpaAggregateRoot.java.ejs +3 -1
- package/templates/aggregate/JpaEntity.java.ejs +3 -1
- package/templates/base/docker/kafka-services.yaml.ejs +2 -2
- package/templates/base/docker/temporal-services.yaml.ejs +29 -0
- package/templates/base/resources/parameters/develop/temporal.yaml.ejs +9 -0
- package/templates/base/resources/parameters/local/temporal.yaml.ejs +9 -0
- package/templates/base/resources/parameters/production/temporal.yaml.ejs +9 -0
- package/templates/base/resources/parameters/test/temporal.yaml.ejs +9 -0
- package/templates/base/root/AGENTS.md.ejs +916 -51
- package/templates/crud/Controller.java.ejs +36 -6
- package/templates/crud/ListQuery.java.ejs +6 -2
- package/templates/crud/ListQueryHandler.java.ejs +24 -10
- package/templates/crud/UpdateCommand.java.ejs +52 -0
- package/templates/crud/UpdateCommandHandler.java.ejs +105 -0
- package/templates/kafka-event/DomainEventHandlerMethod.ejs +1 -0
- package/templates/kafka-event/Event.java.ejs +23 -0
- package/templates/shared/application/dtos/PagedResponse.java.ejs +30 -0
- package/templates/shared/configurations/temporalConfig/TemporalConfig.java.ejs +104 -0
- package/templates/shared/domain/DomainEvent.java.ejs +40 -0
- package/templates/shared/interfaces/HeavyActivity.java.ejs +4 -0
- package/templates/shared/interfaces/LightActivity.java.ejs +4 -0
- package/templates/temporal-activity/ActivityImpl.java.ejs +14 -0
- package/templates/temporal-activity/ActivityInterface.java.ejs +11 -0
- package/templates/temporal-flow/WorkFlowImpl.java.ejs +64 -0
- package/templates/temporal-flow/WorkFlowInterface.java.ejs +19 -0
- package/templates/temporal-flow/WorkFlowService.java.ejs +49 -0
|
@@ -1,2215 +1,909 @@
|
|
|
1
1
|
# Command `generate entities` (alias: `g entities`)
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
---
|
|
4
|
+
|
|
5
|
+
## Table of Contents
|
|
6
|
+
|
|
7
|
+
1. [Description and purpose](#1-description-and-purpose)
|
|
8
|
+
2. [Syntax and YAML location](#2-syntax-and-yaml-location)
|
|
9
|
+
3. [Base domain.yaml structure](#3-base-domainyaml-structure)
|
|
10
|
+
4. [Supported data types](#4-supported-data-types)
|
|
11
|
+
5. [Field properties](#5-field-properties)
|
|
12
|
+
6. [JSR-303 Validations](#6-jsr-303-validations)
|
|
13
|
+
7. [Auditing](#7-auditing)
|
|
14
|
+
8. [Relationships](#8-relationships)
|
|
15
|
+
9. [Value Objects](#9-value-objects)
|
|
16
|
+
10. [Enums and state transitions](#10-enums-and-state-transitions)
|
|
17
|
+
11. [Domain events](#11-domain-events)
|
|
18
|
+
12. [Multiple aggregates](#12-multiple-aggregates)
|
|
19
|
+
13. [Generated files](#13-generated-files)
|
|
20
|
+
14. [Complete examples](#14-complete-examples)
|
|
21
|
+
15. [Prerequisites and common errors](#15-prerequisites-and-common-errors)
|
|
22
|
+
|
|
23
|
+
---
|
|
4
24
|
|
|
5
|
-
|
|
25
|
+
## 1. Description and purpose
|
|
6
26
|
|
|
7
|
-
|
|
27
|
+
`generate entities` is the core command of eva4j. From a `domain.yaml` file, it generates the complete hexagonal architecture for the module:
|
|
8
28
|
|
|
9
|
-
|
|
29
|
+
- **Domain layer** β Entities, Value Objects, Enums, repository interfaces
|
|
30
|
+
- **Application layer** β Commands, Queries, handlers, DTOs, mappers
|
|
31
|
+
- **Infrastructure layer** β JPA entities, Spring Data repositories, repository implementations, REST controllers
|
|
10
32
|
|
|
11
|
-
|
|
33
|
+
The generator understands relationships, auditing, field visibility, validations, state transitions, and domain events.
|
|
34
|
+
|
|
35
|
+
---
|
|
36
|
+
|
|
37
|
+
## 2. Syntax and YAML location
|
|
12
38
|
|
|
13
39
|
```bash
|
|
14
|
-
|
|
15
|
-
|
|
40
|
+
eva generate entities <module>
|
|
41
|
+
eva g entities <module> # short alias
|
|
16
42
|
```
|
|
17
43
|
|
|
18
44
|
### Parameters
|
|
19
45
|
|
|
20
46
|
| Parameter | Required | Description |
|
|
21
47
|
|-----------|----------|-------------|
|
|
22
|
-
|
|
|
23
|
-
|
|
24
|
-
## π YAML File Structure
|
|
25
|
-
|
|
26
|
-
The command expects a YAML file at `examples/<aggregate-name>.yaml` with the following structure:
|
|
27
|
-
|
|
28
|
-
```yaml
|
|
29
|
-
module: <module-name> # Target module for generation
|
|
30
|
-
|
|
31
|
-
aggregates:
|
|
32
|
-
- name: <AggregateName>
|
|
33
|
-
tableName: <table_name>
|
|
34
|
-
auditable: true|false
|
|
35
|
-
|
|
36
|
-
entities:
|
|
37
|
-
- name: <EntityName>
|
|
38
|
-
isRoot: true|false
|
|
39
|
-
tableName: <table_name>
|
|
40
|
-
fields:
|
|
41
|
-
- name: <fieldName>
|
|
42
|
-
type: <JavaType|ValueObject|Enum>
|
|
43
|
-
validations:
|
|
44
|
-
- <@Annotation>
|
|
45
|
-
relationships:
|
|
46
|
-
- type: OneToMany|ManyToOne|OneToOne|ManyToMany
|
|
47
|
-
target: <TargetEntity>
|
|
48
|
-
mappedBy: <fieldName> # For inverse side
|
|
49
|
-
cascade: ALL|PERSIST|MERGE
|
|
50
|
-
fetch: LAZY|EAGER
|
|
51
|
-
|
|
52
|
-
valueObjects:
|
|
53
|
-
- name: <ValueObjectName>
|
|
54
|
-
fields:
|
|
55
|
-
- name: <fieldName>
|
|
56
|
-
type: <JavaType>
|
|
57
|
-
|
|
58
|
-
enums:
|
|
59
|
-
- name: <EnumName>
|
|
60
|
-
values:
|
|
61
|
-
- VALUE1
|
|
62
|
-
- VALUE2
|
|
63
|
-
```
|
|
48
|
+
| `<module>` | Yes | Module name (must already exist in the project) |
|
|
64
49
|
|
|
65
|
-
|
|
50
|
+
### Options
|
|
66
51
|
|
|
67
|
-
|
|
52
|
+
| Option | Description |
|
|
53
|
+
|--------|-------------|
|
|
54
|
+
| `--force` | Overwrite files that have developer changes |
|
|
68
55
|
|
|
69
|
-
|
|
56
|
+
### YAML location
|
|
70
57
|
|
|
71
|
-
|
|
72
|
-
module: customer
|
|
58
|
+
The file is read from:
|
|
73
59
|
|
|
74
|
-
aggregates:
|
|
75
|
-
- name: Customer
|
|
76
|
-
tableName: customers
|
|
77
|
-
auditable: true
|
|
78
|
-
|
|
79
|
-
entities:
|
|
80
|
-
- name: customer
|
|
81
|
-
isRoot: true
|
|
82
|
-
fields:
|
|
83
|
-
- name: id
|
|
84
|
-
type: Long
|
|
85
|
-
- name: firstName
|
|
86
|
-
type: String
|
|
87
|
-
validations:
|
|
88
|
-
- "@NotBlank"
|
|
89
|
-
- "@Size(max = 100)"
|
|
90
|
-
- name: email
|
|
91
|
-
type: String
|
|
92
|
-
validations:
|
|
93
|
-
- "@Email"
|
|
94
|
-
- name: status
|
|
95
|
-
type: CustomerStatus
|
|
96
60
|
```
|
|
97
|
-
|
|
98
|
-
**Generate:**
|
|
99
|
-
```bash
|
|
100
|
-
eva4j g entities customer
|
|
61
|
+
src/main/java/<package>/<module>/domain.yaml
|
|
101
62
|
```
|
|
102
63
|
|
|
103
|
-
|
|
64
|
+
> The generator detects developer changes via checksums. If a file was manually modified, it is **not overwritten** unless you use `--force`.
|
|
104
65
|
|
|
105
|
-
|
|
66
|
+
---
|
|
106
67
|
|
|
107
|
-
|
|
108
|
-
module: order
|
|
68
|
+
## 3. Base domain.yaml structure
|
|
109
69
|
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
70
|
+
```yaml
|
|
71
|
+
aggregates: # List of aggregates in the module
|
|
72
|
+
- name: Order # Aggregate name (PascalCase)
|
|
73
|
+
entities: # Entities of the aggregate
|
|
74
|
+
- name: Order # Entity name (PascalCase)
|
|
75
|
+
isRoot: true # true = aggregate root
|
|
76
|
+
tableName: orders # SQL table name (optional)
|
|
77
|
+
audit: # Auditing (optional)
|
|
78
|
+
enabled: true
|
|
79
|
+
trackUser: false
|
|
80
|
+
fields: # Entity fields
|
|
119
81
|
- name: id
|
|
120
|
-
type: Long
|
|
121
|
-
- name: orderNumber
|
|
122
82
|
type: String
|
|
123
|
-
- name: totalAmount
|
|
124
|
-
type: BigDecimal
|
|
125
83
|
- name: status
|
|
126
|
-
type: OrderStatus
|
|
127
|
-
relationships:
|
|
84
|
+
type: OrderStatus # Reference to enum or VO
|
|
85
|
+
relationships: # JPA relationships (optional)
|
|
128
86
|
- type: OneToMany
|
|
129
87
|
target: OrderItem
|
|
130
88
|
mappedBy: order
|
|
131
|
-
cascade:
|
|
89
|
+
cascade: [PERSIST, MERGE, REMOVE]
|
|
132
90
|
fetch: LAZY
|
|
133
|
-
|
|
134
|
-
- name:
|
|
135
|
-
isRoot: false
|
|
91
|
+
|
|
92
|
+
- name: OrderItem # Secondary entity (no isRoot or isRoot: false)
|
|
136
93
|
tableName: order_items
|
|
137
94
|
fields:
|
|
138
95
|
- name: id
|
|
139
96
|
type: Long
|
|
140
97
|
- name: quantity
|
|
141
98
|
type: Integer
|
|
142
|
-
|
|
99
|
+
|
|
100
|
+
valueObjects: # Aggregate Value Objects
|
|
101
|
+
- name: Money
|
|
102
|
+
fields:
|
|
103
|
+
- name: amount
|
|
143
104
|
type: BigDecimal
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
enums:
|
|
105
|
+
- name: currency
|
|
106
|
+
type: String
|
|
107
|
+
|
|
108
|
+
enums: # Aggregate enums
|
|
150
109
|
- name: OrderStatus
|
|
151
|
-
values:
|
|
152
|
-
- PENDING
|
|
153
|
-
- CONFIRMED
|
|
154
|
-
- SHIPPED
|
|
155
|
-
- DELIVERED
|
|
156
|
-
- CANCELLED
|
|
157
|
-
```
|
|
110
|
+
values: [PENDING, CONFIRMED, SHIPPED, DELIVERED, CANCELLED]
|
|
158
111
|
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
112
|
+
events: # Domain events (optional)
|
|
113
|
+
- name: OrderPlaced
|
|
114
|
+
fields:
|
|
115
|
+
- name: customerId
|
|
116
|
+
type: String
|
|
162
117
|
```
|
|
163
118
|
|
|
164
|
-
|
|
119
|
+
> **Supported synonyms**: `fields` = `properties`; `target` = `targetEntity`
|
|
165
120
|
|
|
166
|
-
|
|
121
|
+
### The `id` field rule
|
|
167
122
|
|
|
168
|
-
|
|
169
|
-
module: evaluation
|
|
123
|
+
Every entity **must** have a field named exactly `id`:
|
|
170
124
|
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
entities:
|
|
176
|
-
- name: evaluation
|
|
177
|
-
isRoot: true
|
|
178
|
-
fields:
|
|
179
|
-
- name: id
|
|
180
|
-
type: String
|
|
181
|
-
- name: score
|
|
182
|
-
type: Integer
|
|
183
|
-
relationships:
|
|
184
|
-
- type: OneToMany
|
|
185
|
-
target: EvaluationDoctor
|
|
186
|
-
cascade: ALL
|
|
187
|
-
|
|
188
|
-
- name: evaluationDoctor
|
|
189
|
-
isRoot: false
|
|
190
|
-
fields:
|
|
191
|
-
- name: id
|
|
192
|
-
type: Long
|
|
193
|
-
- name: degrees
|
|
194
|
-
type: List<Degrees>
|
|
195
|
-
|
|
196
|
-
valueObjects:
|
|
197
|
-
- name: Degrees
|
|
198
|
-
fields:
|
|
199
|
-
- name: title
|
|
200
|
-
type: String
|
|
201
|
-
- name: institution
|
|
202
|
-
type: String
|
|
203
|
-
- name: year
|
|
204
|
-
type: Integer
|
|
205
|
-
- name: typeDegrees
|
|
206
|
-
type: TypeDegrees
|
|
207
|
-
|
|
208
|
-
enums:
|
|
209
|
-
- name: TypeDegrees
|
|
210
|
-
values:
|
|
211
|
-
- BACHELOR
|
|
212
|
-
- MASTER
|
|
213
|
-
- PHD
|
|
214
|
-
```
|
|
125
|
+
| `id` type | Generated strategy |
|
|
126
|
+
|-----------|--------------------|
|
|
127
|
+
| `String` | `@GeneratedValue(strategy = GenerationType.UUID)` |
|
|
128
|
+
| `Long` | `@GeneratedValue(strategy = GenerationType.IDENTITY)` |
|
|
215
129
|
|
|
216
|
-
|
|
217
|
-
```bash
|
|
218
|
-
eva4j g entities evaluation
|
|
219
|
-
```
|
|
130
|
+
---
|
|
220
131
|
|
|
221
|
-
##
|
|
132
|
+
## 4. Supported data types
|
|
133
|
+
|
|
134
|
+
| YAML type | Java type | Notes |
|
|
135
|
+
|-----------|-----------|-------|
|
|
136
|
+
| `String` | `String` | For `id` generates UUID |
|
|
137
|
+
| `Integer` | `Integer` | For `id` generates IDENTITY |
|
|
138
|
+
| `Long` | `Long` | For `id` generates IDENTITY |
|
|
139
|
+
| `Double` | `Double` | |
|
|
140
|
+
| `BigDecimal` | `BigDecimal` | |
|
|
141
|
+
| `Boolean` | `Boolean` | |
|
|
142
|
+
| `LocalDate` | `LocalDate` | Auto-imported |
|
|
143
|
+
| `LocalDateTime` | `LocalDateTime` | Auto-imported |
|
|
144
|
+
| `LocalTime` | `LocalTime` | Auto-imported |
|
|
145
|
+
| `UUID` | `UUID` | Auto-imported |
|
|
146
|
+
| `List<String>` | `List<String>` | `@ElementCollection` |
|
|
147
|
+
| `List<VO>` | `List<VoJpa>` | `@ElementCollection` |
|
|
148
|
+
| Enum name | Module enum | `@Enumerated(STRING)` |
|
|
149
|
+
| VO name | Value Object | `@Embedded` |
|
|
222
150
|
|
|
223
|
-
|
|
224
|
-
src/main/java/com/example/project/<module>/
|
|
225
|
-
βββ domain/
|
|
226
|
-
β βββ models/
|
|
227
|
-
β β βββ Customer.java # Domain entity (root)
|
|
228
|
-
β β βββ OrderItem.java # Domain entity (secondary)
|
|
229
|
-
β β βββ valueobjects/
|
|
230
|
-
β β β βββ Degrees.java # Value object
|
|
231
|
-
β β βββ enums/
|
|
232
|
-
β β βββ OrderStatus.java # Enum
|
|
233
|
-
β βββ repositories/
|
|
234
|
-
β βββ CustomerRepository.java # Repository port (interface)
|
|
235
|
-
β
|
|
236
|
-
βββ application/
|
|
237
|
-
β βββ commands/
|
|
238
|
-
β β βββ CreateCustomerCommand.java # Create command
|
|
239
|
-
β β βββ CreateCustomerCommandHandler.java # Command handler
|
|
240
|
-
β βββ queries/
|
|
241
|
-
β β βββ GetCustomerQuery.java # Get query
|
|
242
|
-
β β βββ GetCustomerQueryHandler.java # Get handler
|
|
243
|
-
β β βββ ListCustomersQuery.java # List query
|
|
244
|
-
β β βββ ListCustomersQueryHandler.java # List handler
|
|
245
|
-
β βββ dtos/
|
|
246
|
-
β β βββ CreateCustomerDto.java # Create DTO
|
|
247
|
-
β β βββ CreateOrderItemDto.java # Nested entity DTO
|
|
248
|
-
β β βββ CustomerResponseDto.java # Response DTO
|
|
249
|
-
β βββ mappers/
|
|
250
|
-
β βββ CustomerApplicationMapper.java # Application mapper (Command/DTO β Domain)
|
|
251
|
-
β
|
|
252
|
-
βββ infrastructure/
|
|
253
|
-
βββ database/
|
|
254
|
-
β βββ entities/
|
|
255
|
-
β β βββ CustomerJpa.java # JPA entity (root)
|
|
256
|
-
β β βββ OrderItemJpa.java # JPA entity (secondary)
|
|
257
|
-
β β βββ valueobjects/
|
|
258
|
-
β β βββ DegreesJpa.java # JPA value object
|
|
259
|
-
β βββ repositories/
|
|
260
|
-
β β βββ CustomerJpaRepository.java # Spring Data repository
|
|
261
|
-
β β βββ CustomerRepositoryImpl.java # Repository implementation
|
|
262
|
-
β βββ mappers/
|
|
263
|
-
β βββ CustomerMapper.java # Infrastructure mapper (Domain β JPA)
|
|
264
|
-
βββ rest/
|
|
265
|
-
βββ controllers/
|
|
266
|
-
βββ CustomerController.java # REST controller with CRUD endpoints
|
|
267
|
-
```
|
|
151
|
+
---
|
|
268
152
|
|
|
269
|
-
##
|
|
270
|
-
|
|
271
|
-
### 1. Domain Layer (Pure Business Logic)
|
|
272
|
-
- β
**Entities** - Aggregate root and secondary entities
|
|
273
|
-
- β
**Value Objects** - Immutable value types with `@Embedded` support
|
|
274
|
-
- β
**Enums** - Type-safe enumerations
|
|
275
|
-
- β
**Repository Interfaces** - Ports for persistence
|
|
276
|
-
|
|
277
|
-
### 2. Application Layer (Use Cases - CQRS)
|
|
278
|
-
- β
**Commands** - `CreateCustomerCommand` with validation
|
|
279
|
-
- β
**CommandHandlers** - Business logic orchestration
|
|
280
|
-
- β
**Queries** - `GetCustomerQuery`, `ListCustomersQuery`
|
|
281
|
-
- β
**QueryHandlers** - Read operations with pagination
|
|
282
|
-
- β
**DTOs** - Request/Response data transfer objects
|
|
283
|
-
- β
**Application Mappers** - Command/DTO β Domain transformations
|
|
284
|
-
|
|
285
|
-
### 3. Infrastructure Layer (Technical Details)
|
|
286
|
-
- β
**JPA Entities** - Persistence annotations (`@Entity`, `@Table`)
|
|
287
|
-
- β
**JPA Repositories** - Spring Data JPA implementation
|
|
288
|
-
- β
**Infrastructure Mappers** - Domain β JPA bidirectional mapping
|
|
289
|
-
- β
**REST Controllers** - CRUD endpoints (`POST`, `GET`, `GET list`)
|
|
290
|
-
|
|
291
|
-
### 4. Advanced Capabilities
|
|
292
|
-
- β
**Relationships** - OneToMany, ManyToOne, OneToOne, ManyToMany
|
|
293
|
-
- β
**Nested Entities** - Secondary entities with their own relationships
|
|
294
|
-
- β
**Value Object Collections** - `List<ValueObject>` with `@ElementCollection`
|
|
295
|
-
- β
**Auditing** - `@CreatedDate`, `@LastModifiedDate` when `auditable: true`
|
|
296
|
-
- β
**Cascade Operations** - Configurable cascade types
|
|
297
|
-
- β
**Fetch Strategies** - LAZY/EAGER configuration
|
|
298
|
-
- β
**Validations** - Bean Validation annotations
|
|
299
|
-
- β
**Pagination** - Built-in pagination support for list queries
|
|
300
|
-
|
|
301
|
-
## π Supported Relationships
|
|
302
|
-
|
|
303
|
-
### OneToMany / ManyToOne (Bidirectional)
|
|
153
|
+
## 5. Field properties
|
|
304
154
|
|
|
305
155
|
```yaml
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
156
|
+
fields:
|
|
157
|
+
- name: fieldName # camelCase, required
|
|
158
|
+
type: String # Java type, required
|
|
159
|
+
readOnly: false # default false
|
|
160
|
+
hidden: false # default false
|
|
161
|
+
validations: [] # JSR-303 annotations
|
|
162
|
+
annotations: [] # raw JPA annotations
|
|
163
|
+
reference: # semantic reference to another aggregate
|
|
164
|
+
aggregate: Customer
|
|
165
|
+
module: customers
|
|
166
|
+
enumValues: [] # inline enum (alternative to enums:)
|
|
167
|
+
```
|
|
315
168
|
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
169
|
+
### Visibility matrix
|
|
170
|
+
|
|
171
|
+
| Field | Creation constructor | CreateDto/Command | Full constructor | ResponseDto |
|
|
172
|
+
|-------|---------------------|-------------------|------------------|-------------|
|
|
173
|
+
| normal | β
| β
| β
| β
|
|
|
174
|
+
| `readOnly: true` | β | β | β
| β
|
|
|
175
|
+
| `hidden: true` | β
| β
| β
| β |
|
|
176
|
+
| `readOnly + hidden` | β | β | β
| β |
|
|
177
|
+
|
|
178
|
+
### readOnly
|
|
179
|
+
|
|
180
|
+
Marks a field as calculated/derived: excluded from the business constructor and `CreateDto`/`CreateCommand`, but present in the full constructor (reconstruction from persistence) and in `ResponseDto`.
|
|
181
|
+
|
|
182
|
+
```yaml
|
|
183
|
+
fields:
|
|
184
|
+
- name: totalAmount
|
|
185
|
+
type: BigDecimal
|
|
186
|
+
readOnly: true # calculated from the sum of items
|
|
322
187
|
```
|
|
323
188
|
|
|
324
|
-
|
|
189
|
+
> When an enum has `initialValue`, the corresponding field is automatically treated as `readOnly`.
|
|
190
|
+
|
|
191
|
+
### hidden
|
|
192
|
+
|
|
193
|
+
Marks a field as sensitive: included on creation but does NOT appear in `ResponseDto`.
|
|
325
194
|
|
|
326
195
|
```yaml
|
|
327
|
-
|
|
328
|
-
- name:
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
cascade: ALL
|
|
196
|
+
fields:
|
|
197
|
+
- name: passwordHash
|
|
198
|
+
type: String
|
|
199
|
+
hidden: true # do not expose in API
|
|
200
|
+
```
|
|
333
201
|
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
202
|
+
### annotations (raw JPA)
|
|
203
|
+
|
|
204
|
+
Allows adding custom JPA annotations to the generated JPA entity.
|
|
205
|
+
|
|
206
|
+
```yaml
|
|
207
|
+
fields:
|
|
208
|
+
- name: email
|
|
209
|
+
type: String
|
|
210
|
+
annotations:
|
|
211
|
+
- "@Column(unique = true, nullable = false)"
|
|
338
212
|
```
|
|
339
213
|
|
|
340
|
-
###
|
|
214
|
+
### reference
|
|
215
|
+
|
|
216
|
+
Declares a semantic reference to a field in another aggregate. Generates a Javadoc comment indicating the relationship, without creating a code dependency.
|
|
341
217
|
|
|
342
218
|
```yaml
|
|
343
|
-
|
|
344
|
-
- name:
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
219
|
+
fields:
|
|
220
|
+
- name: customerId
|
|
221
|
+
type: String
|
|
222
|
+
reference:
|
|
223
|
+
aggregate: Customer
|
|
224
|
+
module: customers
|
|
225
|
+
```
|
|
349
226
|
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
227
|
+
Generated in the domain entity:
|
|
228
|
+
|
|
229
|
+
```java
|
|
230
|
+
/** @see customers.Customer */
|
|
231
|
+
private String customerId;
|
|
355
232
|
```
|
|
356
233
|
|
|
357
|
-
|
|
234
|
+
---
|
|
358
235
|
|
|
359
|
-
|
|
360
|
-
entities:
|
|
361
|
-
- name: evaluationDoctor
|
|
362
|
-
relationships:
|
|
363
|
-
- type: OneToMany
|
|
364
|
-
target: EvaluationBranch # Another secondary entity
|
|
365
|
-
cascade: ALL
|
|
236
|
+
## 6. JSR-303 Validations
|
|
366
237
|
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
238
|
+
Validations are declared on the field and applied to `CreateCommand` and `CreateDto`. They are **not** added to domain entities.
|
|
239
|
+
|
|
240
|
+
```yaml
|
|
241
|
+
fields:
|
|
242
|
+
- name: name
|
|
243
|
+
type: String
|
|
244
|
+
validations:
|
|
245
|
+
- type: NotBlank
|
|
246
|
+
message: "Name is required"
|
|
247
|
+
- type: Size
|
|
248
|
+
min: 2
|
|
249
|
+
max: 100
|
|
371
250
|
```
|
|
372
251
|
|
|
373
|
-
|
|
252
|
+
Auto-generates import: `import jakarta.validation.constraints.*;`
|
|
374
253
|
|
|
375
|
-
###
|
|
376
|
-
- `String`, `Integer`, `Long`, `Double`, `Float`, `Boolean`
|
|
377
|
-
- `BigDecimal`, `BigInteger`
|
|
378
|
-
- `LocalDate`, `LocalDateTime`, `LocalTime`
|
|
379
|
-
- `ZonedDateTime`, `Instant`
|
|
254
|
+
### Supported parameters
|
|
380
255
|
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
256
|
+
| Parameter | Description |
|
|
257
|
+
|-----------|-------------|
|
|
258
|
+
| `type` | Annotation name without `@` (required) |
|
|
259
|
+
| `message` | Custom error message |
|
|
260
|
+
| `value` | Single value (for `@Min`, `@Max`) |
|
|
261
|
+
| `min` | Minimum value (for `@Size`, `@DecimalMin`) |
|
|
262
|
+
| `max` | Maximum value (for `@Size`, `@DecimalMax`) |
|
|
263
|
+
| `regexp` | Regular expression (for `@Pattern`) |
|
|
264
|
+
| `integer` | Integer digits (for `@Digits`) |
|
|
265
|
+
| `fraction` | Decimal digits (for `@Digits`) |
|
|
266
|
+
| `inclusive` | Inclusive boundary (for `@DecimalMin`, `@DecimalMax`) |
|
|
384
267
|
|
|
385
|
-
###
|
|
386
|
-
- Value Objects (defined in `valueObjects` section)
|
|
387
|
-
- Enums (defined in `enums` section)
|
|
268
|
+
### Examples by type
|
|
388
269
|
|
|
389
|
-
|
|
270
|
+
```yaml
|
|
271
|
+
# @NotBlank
|
|
272
|
+
- type: NotBlank
|
|
273
|
+
message: "Field is required"
|
|
390
274
|
|
|
391
|
-
|
|
275
|
+
# @NotNull
|
|
276
|
+
- type: NotNull
|
|
392
277
|
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
```
|
|
278
|
+
# @Size
|
|
279
|
+
- type: Size
|
|
280
|
+
min: 2
|
|
281
|
+
max: 255
|
|
398
282
|
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
- Implement domain validations
|
|
402
|
-
- Add domain events if needed
|
|
283
|
+
# @Email
|
|
284
|
+
- type: Email
|
|
403
285
|
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
# GET http://localhost:8080/api/<module>/<entity>
|
|
410
|
-
```
|
|
286
|
+
# @Min / @Max (for numeric fields)
|
|
287
|
+
- type: Min
|
|
288
|
+
value: 1
|
|
289
|
+
- type: Max
|
|
290
|
+
value: 999
|
|
411
291
|
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
```
|
|
292
|
+
# @Pattern
|
|
293
|
+
- type: Pattern
|
|
294
|
+
regexp: "^[A-Z]{2}[0-9]{6}$"
|
|
295
|
+
message: "Invalid format"
|
|
417
296
|
|
|
418
|
-
|
|
297
|
+
# @DecimalMin / @DecimalMax
|
|
298
|
+
- type: DecimalMin
|
|
299
|
+
min: "0.01"
|
|
300
|
+
inclusive: true
|
|
301
|
+
- type: DecimalMax
|
|
302
|
+
max: "9999.99"
|
|
419
303
|
|
|
420
|
-
|
|
421
|
-
-
|
|
422
|
-
|
|
304
|
+
# @Digits
|
|
305
|
+
- type: Digits
|
|
306
|
+
integer: 6
|
|
307
|
+
fraction: 2
|
|
308
|
+
```
|
|
423
309
|
|
|
424
|
-
|
|
310
|
+
---
|
|
425
311
|
|
|
426
|
-
|
|
427
|
-
- β
Valid eva4j project
|
|
428
|
-
- β
Target module exists
|
|
429
|
-
- β
YAML file exists and is valid
|
|
430
|
-
- β
No syntax errors in YAML
|
|
431
|
-
- β
Entity names are unique
|
|
432
|
-
- β
Relationship targets exist
|
|
433
|
-
- β
Field types are valid
|
|
312
|
+
## 7. Auditing
|
|
434
313
|
|
|
435
|
-
|
|
314
|
+
### Syntax
|
|
436
315
|
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
316
|
+
```yaml
|
|
317
|
+
# New (recommended)
|
|
318
|
+
audit:
|
|
319
|
+
enabled: true # adds createdAt, updatedAt
|
|
320
|
+
trackUser: true # also adds createdBy, updatedBy
|
|
440
321
|
|
|
441
|
-
|
|
322
|
+
# Legacy (equivalent to audit.enabled: true, trackUser: false)
|
|
323
|
+
auditable: true
|
|
324
|
+
```
|
|
442
325
|
|
|
443
|
-
|
|
444
|
-
- Solution: Create `examples/<aggregate-name>.yaml` file first
|
|
326
|
+
### Generated JPA inheritance
|
|
445
327
|
|
|
446
|
-
|
|
447
|
-
|
|
328
|
+
| Configuration | JPA base class |
|
|
329
|
+
|---------------|----------------|
|
|
330
|
+
| No auditing | no inheritance |
|
|
331
|
+
| `audit.enabled: true` | `extends AuditableEntity` |
|
|
332
|
+
| `audit.trackUser: true` | `extends FullAuditableEntity` |
|
|
448
333
|
|
|
449
|
-
|
|
450
|
-
- Solution: Ensure the target entity is defined in the same aggregate
|
|
334
|
+
### Generated fields
|
|
451
335
|
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
336
|
+
| Field | `audit.enabled` | `audit.trackUser` | In ResponseDto |
|
|
337
|
+
|-------|-----------------|-------------------|----------------|
|
|
338
|
+
| `createdAt` | β
| β
| β
|
|
|
339
|
+
| `updatedAt` | β
| β
| β
|
|
|
340
|
+
| `createdBy` | β | β
| β |
|
|
341
|
+
| `updatedBy` | β | β
| β |
|
|
455
342
|
|
|
456
|
-
|
|
457
|
-
- Solution: Updated in latest version to use `List<ValueObjectJpa>` in JPA entities
|
|
458
|
-
- Mapper name: `OrderMapper.java`
|
|
459
|
-
- File organization
|
|
460
|
-
- Generated code references
|
|
343
|
+
> `createdBy` and `updatedBy` are administrative metadata: they are never exposed in response DTOs.
|
|
461
344
|
|
|
462
|
-
|
|
345
|
+
### Infrastructure generated with `trackUser: true`
|
|
463
346
|
|
|
464
|
-
|
|
347
|
+
When `trackUser` is enabled, eva4j automatically generates:
|
|
465
348
|
|
|
466
|
-
|
|
349
|
+
| File | Purpose |
|
|
350
|
+
|------|---------|
|
|
351
|
+
| `UserContextHolder.java` | ThreadLocal for the current user |
|
|
352
|
+
| `UserContextFilter.java` | Captures the `X-User` header from each request |
|
|
353
|
+
| `AuditorAwareImpl.java` | Provides the current user to JPA Auditing |
|
|
467
354
|
|
|
468
|
-
|
|
355
|
+
`Application.java` is configured with `@EnableJpaAuditing(auditorAwareRef = "auditorProvider")`.
|
|
469
356
|
|
|
470
|
-
|
|
357
|
+
### Example
|
|
471
358
|
|
|
472
359
|
```yaml
|
|
473
|
-
|
|
360
|
+
entities:
|
|
474
361
|
- name: Order
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
type: String
|
|
486
|
-
|
|
487
|
-
- name: status
|
|
488
|
-
type: OrderStatus # Reference to an enum
|
|
489
|
-
|
|
490
|
-
- name: totalAmount
|
|
491
|
-
type: Money # Reference to a value object
|
|
492
|
-
|
|
493
|
-
- name: createdAt
|
|
494
|
-
type: LocalDateTime
|
|
495
|
-
|
|
496
|
-
relationships:
|
|
497
|
-
- type: OneToMany
|
|
498
|
-
target: OrderItem
|
|
499
|
-
mappedBy: order
|
|
500
|
-
cascade: [PERSIST, MERGE, REMOVE]
|
|
501
|
-
fetch: LAZY
|
|
362
|
+
isRoot: true
|
|
363
|
+
tableName: orders
|
|
364
|
+
audit:
|
|
365
|
+
enabled: true
|
|
366
|
+
trackUser: true
|
|
367
|
+
fields:
|
|
368
|
+
- name: id
|
|
369
|
+
type: String
|
|
370
|
+
- name: amount
|
|
371
|
+
type: BigDecimal
|
|
502
372
|
```
|
|
503
373
|
|
|
504
|
-
|
|
374
|
+
> Audit fields **must not be defined manually** in `fields:`; they are inherited from the JPA base class.
|
|
375
|
+
|
|
376
|
+
---
|
|
377
|
+
|
|
378
|
+
## 8. Relationships
|
|
379
|
+
|
|
380
|
+
### Properties
|
|
381
|
+
|
|
382
|
+
| Property | Values | Description |
|
|
383
|
+
|----------|--------|-------------|
|
|
384
|
+
| `type` | `OneToMany`, `ManyToOne`, `OneToOne`, `ManyToMany` | Relationship type |
|
|
385
|
+
| `target` / `targetEntity` | Entity name | Related entity |
|
|
386
|
+
| `mappedBy` | field name | Inverse side of the relationship |
|
|
387
|
+
| `joinColumn` | column name | FK column name |
|
|
388
|
+
| `cascade` | array of `PERSIST`, `MERGE`, `REMOVE`, `REFRESH`, `DETACH`, `ALL` | Cascade operations |
|
|
389
|
+
| `fetch` | `LAZY` (default), `EAGER` | Loading strategy |
|
|
390
|
+
|
|
391
|
+
### Automatic inverse side generation
|
|
505
392
|
|
|
506
|
-
|
|
393
|
+
When you define `OneToMany` with `mappedBy`, eva4j automatically generates `@ManyToOne` in the target JPA entity. **Defining both sides is not required.**
|
|
507
394
|
|
|
508
395
|
```yaml
|
|
509
|
-
|
|
396
|
+
# β
Only this is needed
|
|
397
|
+
entities:
|
|
510
398
|
- name: Order
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
type: String
|
|
524
|
-
|
|
525
|
-
- name: quantity
|
|
526
|
-
type: Integer
|
|
527
|
-
|
|
528
|
-
- name: unitPrice
|
|
529
|
-
type: Money
|
|
530
|
-
|
|
531
|
-
relationships:
|
|
532
|
-
- type: ManyToOne
|
|
533
|
-
target: Order
|
|
534
|
-
joinColumn: order_id
|
|
535
|
-
fetch: LAZY
|
|
399
|
+
isRoot: true
|
|
400
|
+
relationships:
|
|
401
|
+
- type: OneToMany
|
|
402
|
+
target: OrderItem
|
|
403
|
+
mappedBy: order
|
|
404
|
+
cascade: [PERSIST, MERGE, REMOVE]
|
|
405
|
+
fetch: LAZY
|
|
406
|
+
|
|
407
|
+
# eva4j generates in OrderItemJpa:
|
|
408
|
+
# @ManyToOne(fetch = FetchType.LAZY)
|
|
409
|
+
# @JoinColumn(name = "order_id")
|
|
410
|
+
# private OrderJpa order;
|
|
536
411
|
```
|
|
537
412
|
|
|
538
|
-
|
|
413
|
+
> If you define `ManyToOne` manually, that definition takes priority over auto-generation.
|
|
539
414
|
|
|
540
|
-
|
|
415
|
+
### OneToMany
|
|
541
416
|
|
|
542
417
|
```yaml
|
|
543
|
-
|
|
544
|
-
-
|
|
545
|
-
|
|
418
|
+
relationships:
|
|
419
|
+
- type: OneToMany
|
|
420
|
+
target: OrderItem
|
|
421
|
+
mappedBy: order
|
|
422
|
+
cascade: [PERSIST, MERGE, REMOVE]
|
|
423
|
+
fetch: LAZY
|
|
546
424
|
```
|
|
547
425
|
|
|
548
|
-
|
|
549
|
-
- `name`: Field name (required)
|
|
550
|
-
- `type`: Java data type (required)
|
|
426
|
+
Generated in domain:
|
|
551
427
|
|
|
552
|
-
|
|
428
|
+
```java
|
|
429
|
+
private List<OrderItem> orderItems = new ArrayList<>();
|
|
430
|
+
public void addOrderItem(OrderItem item) { orderItems.add(item); }
|
|
431
|
+
public void removeOrderItem(OrderItem item) { orderItems.remove(item); }
|
|
432
|
+
```
|
|
553
433
|
|
|
554
|
-
|
|
434
|
+
### ManyToOne (manual, when you need a specific FK)
|
|
555
435
|
|
|
556
|
-
**β
Value Objects** - Automatically detected
|
|
557
436
|
```yaml
|
|
558
|
-
|
|
559
|
-
-
|
|
560
|
-
|
|
437
|
+
relationships:
|
|
438
|
+
- type: ManyToOne
|
|
439
|
+
target: Order
|
|
440
|
+
joinColumn: fk_order_uuid
|
|
441
|
+
fetch: LAZY
|
|
561
442
|
```
|
|
562
443
|
|
|
563
|
-
|
|
564
|
-
```yaml
|
|
565
|
-
fields:
|
|
566
|
-
- name: status
|
|
567
|
-
type: OrderStatus # If OrderStatus is in enums β @Enumerated(STRING)
|
|
568
|
-
```
|
|
444
|
+
### OneToOne
|
|
569
445
|
|
|
570
|
-
**β
Primitive types**
|
|
571
446
|
```yaml
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
```
|
|
447
|
+
# Inverse side (with mappedBy)
|
|
448
|
+
relationships:
|
|
449
|
+
- type: OneToOne
|
|
450
|
+
target: OrderSummary
|
|
451
|
+
mappedBy: order
|
|
452
|
+
cascade: [PERSIST, MERGE]
|
|
453
|
+
fetch: LAZY
|
|
580
454
|
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
|
|
455
|
+
# Owner side (with FK)
|
|
456
|
+
relationships:
|
|
457
|
+
- type: OneToOne
|
|
458
|
+
target: Order
|
|
459
|
+
joinColumn: order_id
|
|
460
|
+
fetch: LAZY
|
|
586
461
|
```
|
|
587
462
|
|
|
588
|
-
|
|
463
|
+
### When to define ManyToOne manually
|
|
464
|
+
|
|
465
|
+
| Scenario | Define ManyToOne? |
|
|
466
|
+
|----------|------------------|
|
|
467
|
+
| Standard relationship with `mappedBy` | β eva4j generates it |
|
|
468
|
+
| FK with custom name | β
Yes, to control `joinColumn` |
|
|
469
|
+
| Multiple FKs to the same entity | β
Yes, for distinct names |
|
|
470
|
+
| Unidirectional relationship (no inverse) | β
Yes |
|
|
471
|
+
|
|
472
|
+
### Recommended cascade
|
|
473
|
+
|
|
589
474
|
```yaml
|
|
590
|
-
|
|
591
|
-
|
|
592
|
-
type: List<String> # β @ElementCollection with secondary table
|
|
593
|
-
```
|
|
475
|
+
# Child has no meaning without parent β include REMOVE
|
|
476
|
+
cascade: [PERSIST, MERGE, REMOVE]
|
|
594
477
|
|
|
595
|
-
|
|
478
|
+
# Child has an independent lifecycle
|
|
479
|
+
cascade: [PERSIST, MERGE]
|
|
480
|
+
```
|
|
596
481
|
|
|
597
|
-
|
|
598
|
-
- `@Embedded` for Value Objects
|
|
599
|
-
- `@Enumerated(EnumType.STRING)` for Enums
|
|
600
|
-
- `@ElementCollection` for lists
|
|
601
|
-
- Required imports
|
|
482
|
+
---
|
|
602
483
|
|
|
603
|
-
|
|
484
|
+
## 9. Value Objects
|
|
604
485
|
|
|
605
|
-
|
|
486
|
+
Immutable objects that represent domain concepts without their own identity.
|
|
606
487
|
|
|
607
488
|
```yaml
|
|
608
|
-
|
|
609
|
-
|
|
610
|
-
- name: order
|
|
611
|
-
isRoot: true
|
|
612
|
-
fields:
|
|
613
|
-
- name: id # β REQUIRED
|
|
614
|
-
type: String # String = UUID, Long = IDENTITY
|
|
615
|
-
- name: orderNumber
|
|
616
|
-
type: String
|
|
617
|
-
|
|
618
|
-
- name: orderItem
|
|
489
|
+
valueObjects:
|
|
490
|
+
- name: Money
|
|
619
491
|
fields:
|
|
620
|
-
- name:
|
|
621
|
-
type:
|
|
622
|
-
- name:
|
|
492
|
+
- name: amount
|
|
493
|
+
type: BigDecimal
|
|
494
|
+
- name: currency
|
|
623
495
|
type: String
|
|
624
496
|
```
|
|
625
497
|
|
|
626
|
-
|
|
627
|
-
- β
JPA requires `@Id` in all entities
|
|
628
|
-
- β
Eva4j automatically generates `@Id` and `@GeneratedValue` for the `id` field
|
|
629
|
-
- β
Clear and consistent convention across the domain
|
|
498
|
+
Generates:
|
|
630
499
|
|
|
631
|
-
|
|
632
|
-
- `
|
|
633
|
-
- `Long` β Generates `@GeneratedValue(strategy = GenerationType.IDENTITY)`
|
|
500
|
+
- `Money.java` β immutable domain class with constructor, getters, `equals()`, `hashCode()`
|
|
501
|
+
- `MoneyJpa.java` β `@Embeddable` with Lombok
|
|
634
502
|
|
|
635
|
-
|
|
636
|
-
```yaml
|
|
637
|
-
# β Without 'id' field - Application will fail
|
|
638
|
-
fields:
|
|
639
|
-
- name: orderNumber
|
|
640
|
-
type: String
|
|
641
|
-
# β Missing 'id' field
|
|
503
|
+
Usage in a field:
|
|
642
504
|
|
|
643
|
-
|
|
644
|
-
|
|
645
|
-
|
|
646
|
-
type: String
|
|
505
|
+
```yaml
|
|
506
|
+
- name: totalAmount
|
|
507
|
+
type: Money # automatically detected as @Embedded
|
|
647
508
|
```
|
|
648
509
|
|
|
649
|
-
|
|
650
|
-
|
|
651
|
-
If you need a business identifier in addition to the technical ID:
|
|
510
|
+
### List of Value Objects
|
|
652
511
|
|
|
653
512
|
```yaml
|
|
654
|
-
|
|
655
|
-
|
|
656
|
-
type: String
|
|
657
|
-
- name: orderNumber # β Business ID (optional)
|
|
658
|
-
type: String
|
|
659
|
-
- name: invoiceNumber # β Another business identifier
|
|
660
|
-
type: String
|
|
661
|
-
```
|
|
662
|
-
|
|
663
|
-
---
|
|
664
|
-
|
|
665
|
-
#### Correct Examples
|
|
666
|
-
|
|
667
|
-
```yaml
|
|
668
|
-
# Value Object
|
|
669
|
-
fields:
|
|
670
|
-
- name: totalAmount
|
|
671
|
-
type: Money # β
Sufficient - eva4j automatically detects
|
|
672
|
-
|
|
673
|
-
# Enum
|
|
674
|
-
fields:
|
|
675
|
-
- name: status
|
|
676
|
-
type: OrderStatus # β
Sufficient - eva4j automatically detects
|
|
677
|
-
|
|
678
|
-
# Primitive type
|
|
679
|
-
fields:
|
|
680
|
-
- name: description
|
|
681
|
-
type: String # β
Basic type
|
|
682
|
-
|
|
683
|
-
# Collection
|
|
684
|
-
fields:
|
|
685
|
-
- name: tags
|
|
686
|
-
type: List<String> # β
Automatic @ElementCollection
|
|
687
|
-
```
|
|
688
|
-
|
|
689
|
-
---
|
|
690
|
-
|
|
691
|
-
### Automatic Auditing
|
|
692
|
-
|
|
693
|
-
eva4j supports automatic entity auditing using the `auditable` property. When set to `true`, the entity will automatically include creation and modification date fields.
|
|
694
|
-
|
|
695
|
-
#### Syntax
|
|
696
|
-
|
|
697
|
-
```yaml
|
|
698
|
-
entities:
|
|
699
|
-
- name: order
|
|
700
|
-
isRoot: true
|
|
701
|
-
auditable: true # β Activates automatic auditing
|
|
702
|
-
fields:
|
|
703
|
-
- name: orderNumber
|
|
704
|
-
type: String
|
|
705
|
-
```
|
|
706
|
-
|
|
707
|
-
#### What `auditable: true` Generates
|
|
708
|
-
|
|
709
|
-
**In the domain entity (`Order.java`):**
|
|
710
|
-
```java
|
|
711
|
-
public class Order {
|
|
712
|
-
private String orderNumber;
|
|
713
|
-
private LocalDateTime createdAt; // β Automatically added
|
|
714
|
-
private LocalDateTime updatedAt; // β Automatically added
|
|
715
|
-
|
|
716
|
-
// getters/setters automatically generated
|
|
717
|
-
}
|
|
718
|
-
```
|
|
719
|
-
|
|
720
|
-
**In the JPA entity (`OrderJpa.java`):**
|
|
721
|
-
```java
|
|
722
|
-
@Entity
|
|
723
|
-
@Table(name = "orders")
|
|
724
|
-
public class OrderJpa extends AuditableEntity { // β Extends base class
|
|
725
|
-
@Id
|
|
726
|
-
@GeneratedValue(strategy = GenerationType.UUID)
|
|
727
|
-
private String orderNumber;
|
|
728
|
-
|
|
729
|
-
// createdAt/updatedAt fields inherited from AuditableEntity
|
|
730
|
-
}
|
|
731
|
-
```
|
|
732
|
-
|
|
733
|
-
**Generated base class (`AuditableEntity.java`):**
|
|
734
|
-
```java
|
|
735
|
-
@MappedSuperclass
|
|
736
|
-
@EntityListeners(AuditingEntityListener.class)
|
|
737
|
-
public abstract class AuditableEntity {
|
|
738
|
-
|
|
739
|
-
@CreatedDate
|
|
740
|
-
@Column(name = "created_at", nullable = false, updatable = false)
|
|
741
|
-
private LocalDateTime createdAt;
|
|
742
|
-
|
|
743
|
-
@LastModifiedDate
|
|
744
|
-
@Column(name = "updated_at", nullable = false)
|
|
745
|
-
private LocalDateTime updatedAt;
|
|
746
|
-
|
|
747
|
-
// getters/setters
|
|
748
|
-
}
|
|
749
|
-
```
|
|
750
|
-
|
|
751
|
-
#### Features
|
|
752
|
-
|
|
753
|
-
β
**Fully automatic**: Timestamps update without additional code
|
|
754
|
-
β
**Entity level**: Can be enabled for specific entities
|
|
755
|
-
β
**Spring Data JPA**: Uses `@CreatedDate` and `@LastModifiedDate`
|
|
756
|
-
β
**Mapper included**: Audit fields are automatically mapped between domain and JPA
|
|
757
|
-
|
|
758
|
-
#### Required Configuration
|
|
759
|
-
|
|
760
|
-
The Spring Boot application already has JPA auditing enabled in the main class:
|
|
761
|
-
|
|
762
|
-
```java
|
|
763
|
-
@SpringBootApplication
|
|
764
|
-
@EnableJpaAuditing // β Already configured by eva4j
|
|
765
|
-
public class Application {
|
|
766
|
-
public static void main(String[] args) {
|
|
767
|
-
SpringApplication.run(Application.class, args);
|
|
768
|
-
}
|
|
769
|
-
}
|
|
770
|
-
```
|
|
771
|
-
|
|
772
|
-
#### Complete Example
|
|
773
|
-
|
|
774
|
-
```yaml
|
|
775
|
-
aggregates:
|
|
776
|
-
- name: Product
|
|
777
|
-
entities:
|
|
778
|
-
- name: product
|
|
779
|
-
isRoot: true
|
|
780
|
-
auditable: true # β Enables auditing
|
|
781
|
-
fields:
|
|
782
|
-
- name: productId
|
|
783
|
-
type: String
|
|
784
|
-
- name: name
|
|
785
|
-
type: String
|
|
786
|
-
- name: price
|
|
787
|
-
type: BigDecimal
|
|
788
|
-
# createdAt and updatedAt are automatically added
|
|
789
|
-
|
|
790
|
-
- name: review
|
|
791
|
-
auditable: true # β Secondary entities can also have auditing
|
|
792
|
-
fields:
|
|
793
|
-
- name: reviewId
|
|
794
|
-
type: Long
|
|
795
|
-
- name: comment
|
|
796
|
-
type: String
|
|
797
|
-
relationships:
|
|
798
|
-
- type: ManyToOne
|
|
799
|
-
target: product
|
|
800
|
-
fetch: LAZY
|
|
801
|
-
joinColumn: product_id
|
|
802
|
-
```
|
|
803
|
-
|
|
804
|
-
**Resultado en la tabla:**
|
|
805
|
-
```sql
|
|
806
|
-
CREATE TABLE products (
|
|
807
|
-
product_id VARCHAR(36) PRIMARY KEY,
|
|
808
|
-
name VARCHAR(255),
|
|
809
|
-
price DECIMAL(19,2),
|
|
810
|
-
created_at TIMESTAMP NOT NULL, -- β AutomΓ‘tico
|
|
811
|
-
updated_at TIMESTAMP NOT NULL -- β AutomΓ‘tico
|
|
812
|
-
);
|
|
813
|
-
|
|
814
|
-
CREATE TABLE reviews (
|
|
815
|
-
review_id BIGINT PRIMARY KEY AUTO_INCREMENT,
|
|
816
|
-
comment TEXT,
|
|
817
|
-
product_id VARCHAR(36),
|
|
818
|
-
created_at TIMESTAMP NOT NULL, -- β AutomΓ‘tico
|
|
819
|
-
updated_at TIMESTAMP NOT NULL, -- β AutomΓ‘tico
|
|
820
|
-
FOREIGN KEY (product_id) REFERENCES products(product_id)
|
|
821
|
-
);
|
|
822
|
-
```
|
|
823
|
-
|
|
824
|
-
#### Notas importantes
|
|
825
|
-
|
|
826
|
-
- β
`auditable` es **opcional** - por defecto es `false`
|
|
827
|
-
- β
Puede usarse en **entidad raΓz** o **entidades secundarias**
|
|
828
|
-
- β
Los campos `createdAt` y `updatedAt` **no deben** definirse manualmente en `fields`
|
|
829
|
-
- β
El tipo es siempre `LocalDateTime`
|
|
830
|
-
- β **No incluye** auditorΓa de usuario (createdBy/updatedBy) - ver [FUTURE_FEATURES.md](FUTURE_FEATURES.md) para esa funcionalidad
|
|
831
|
-
|
|
832
|
-
---
|
|
833
|
-
|
|
834
|
-
## Value Objects
|
|
835
|
-
|
|
836
|
-
Los Value Objects son objetos inmutables que representan conceptos del dominio sin identidad propia.
|
|
837
|
-
|
|
838
|
-
### DefiniciΓ³n bΓ‘sica
|
|
839
|
-
|
|
840
|
-
```yaml
|
|
841
|
-
valueObjects:
|
|
842
|
-
- name: Money
|
|
843
|
-
fields:
|
|
844
|
-
- name: amount
|
|
845
|
-
type: BigDecimal
|
|
846
|
-
|
|
847
|
-
- name: currency
|
|
848
|
-
type: String
|
|
849
|
-
```
|
|
850
|
-
|
|
851
|
-
### Generated Value Object (Domain)
|
|
852
|
-
|
|
853
|
-
```java
|
|
854
|
-
public class Money {
|
|
855
|
-
private final BigDecimal amount;
|
|
856
|
-
private final String currency;
|
|
857
|
-
|
|
858
|
-
public Money(BigDecimal amount, String currency) {
|
|
859
|
-
this.amount = amount;
|
|
860
|
-
this.currency = currency;
|
|
861
|
-
}
|
|
862
|
-
|
|
863
|
-
// Getters
|
|
864
|
-
public BigDecimal getAmount() { return amount; }
|
|
865
|
-
public String getCurrency() { return currency; }
|
|
866
|
-
|
|
867
|
-
// equals() and hashCode() based on all fields
|
|
868
|
-
}
|
|
869
|
-
```
|
|
870
|
-
|
|
871
|
-
### Value Object JPA (@Embeddable)
|
|
872
|
-
|
|
873
|
-
```java
|
|
874
|
-
@Embeddable
|
|
875
|
-
public class MoneyJpa {
|
|
876
|
-
private BigDecimal amount;
|
|
877
|
-
private String currency;
|
|
878
|
-
|
|
879
|
-
// Constructor, getters, setters (Lombok)
|
|
880
|
-
}
|
|
881
|
-
```
|
|
882
|
-
|
|
883
|
-
### Usage in Entities
|
|
884
|
-
|
|
885
|
-
```yaml
|
|
886
|
-
fields:
|
|
887
|
-
- name: totalAmount
|
|
888
|
-
type: Money # Automatically detected as VO
|
|
889
|
-
```
|
|
890
|
-
|
|
891
|
-
Generates in JPA:
|
|
892
|
-
```java
|
|
893
|
-
@Embedded
|
|
894
|
-
private MoneyJpa totalAmount;
|
|
895
|
-
```
|
|
896
|
-
|
|
897
|
-
### Example: Complex Value Object
|
|
898
|
-
|
|
899
|
-
```yaml
|
|
900
|
-
valueObjects:
|
|
901
|
-
- name: Address
|
|
902
|
-
fields:
|
|
903
|
-
- name: street
|
|
904
|
-
type: String
|
|
905
|
-
|
|
906
|
-
- name: city
|
|
907
|
-
type: String
|
|
908
|
-
|
|
909
|
-
- name: state
|
|
910
|
-
type: String
|
|
911
|
-
|
|
912
|
-
- name: zipCode
|
|
913
|
-
type: String
|
|
914
|
-
|
|
915
|
-
- name: country
|
|
916
|
-
type: String
|
|
917
|
-
```
|
|
918
|
-
|
|
919
|
-
---
|
|
920
|
-
|
|
921
|
-
## Enums
|
|
922
|
-
|
|
923
|
-
### Definition
|
|
924
|
-
|
|
925
|
-
```yaml
|
|
926
|
-
enums:
|
|
927
|
-
- name: OrderStatus
|
|
928
|
-
values:
|
|
929
|
-
- PENDING
|
|
930
|
-
- CONFIRMED
|
|
931
|
-
- SHIPPED
|
|
932
|
-
- DELIVERED
|
|
933
|
-
- CANCELLED
|
|
934
|
-
```
|
|
935
|
-
|
|
936
|
-
### Generated Enum
|
|
937
|
-
|
|
938
|
-
```java
|
|
939
|
-
package com.example.myapp.order.domain.models.enums;
|
|
940
|
-
|
|
941
|
-
public enum OrderStatus {
|
|
942
|
-
PENDING,
|
|
943
|
-
CONFIRMED,
|
|
944
|
-
SHIPPED,
|
|
945
|
-
DELIVERED,
|
|
946
|
-
CANCELLED
|
|
947
|
-
}
|
|
948
|
-
```
|
|
949
|
-
|
|
950
|
-
### Uso en entidades
|
|
951
|
-
|
|
952
|
-
```yaml
|
|
953
|
-
fields:
|
|
954
|
-
- name: status
|
|
955
|
-
type: OrderStatus # Se detecta y se importa automΓ‘ticamente
|
|
956
|
-
```
|
|
957
|
-
|
|
958
|
-
Genera en JPA:
|
|
959
|
-
```java
|
|
960
|
-
@Enumerated(EnumType.STRING)
|
|
961
|
-
private OrderStatus status;
|
|
962
|
-
```
|
|
963
|
-
|
|
964
|
-
### MΓΊltiples enums
|
|
965
|
-
|
|
966
|
-
```yaml
|
|
967
|
-
enums:
|
|
968
|
-
- name: OrderStatus
|
|
969
|
-
values: [PENDING, CONFIRMED, SHIPPED, DELIVERED, CANCELLED]
|
|
970
|
-
|
|
971
|
-
- name: PaymentMethod
|
|
972
|
-
values: [CREDIT_CARD, DEBIT_CARD, CASH, BANK_TRANSFER]
|
|
973
|
-
|
|
974
|
-
- name: ShippingMethod
|
|
975
|
-
values: [STANDARD, EXPRESS, OVERNIGHT]
|
|
976
|
-
```
|
|
977
|
-
|
|
978
|
-
---
|
|
979
|
-
|
|
980
|
-
## Relaciones
|
|
981
|
-
|
|
982
|
-
eva4j soporta relaciones JPA bidireccionales completas con generaciΓ³n automΓ‘tica del lado inverso.
|
|
983
|
-
|
|
984
|
-
### π― Relaciones Bidireccionales AutomΓ‘ticas
|
|
985
|
-
|
|
986
|
-
**CaracterΓstica clave**: Cuando defines una relaciΓ³n OneToMany con `mappedBy`, eva4j genera AUTOMΓTICAMENTE la relaciΓ³n inversa ManyToOne en la entidad target.
|
|
987
|
-
|
|
988
|
-
**Solo necesitas definir UN lado:**
|
|
989
|
-
|
|
990
|
-
```yaml
|
|
991
|
-
entities:
|
|
992
|
-
- name: order
|
|
993
|
-
isRoot: true
|
|
994
|
-
relationships:
|
|
995
|
-
- type: OneToMany
|
|
996
|
-
target: OrderItem
|
|
997
|
-
mappedBy: order # β eva4j crea automΓ‘ticamente ManyToOne en OrderItem
|
|
998
|
-
cascade: [PERSIST, MERGE]
|
|
999
|
-
fetch: LAZY
|
|
1000
|
-
```
|
|
1001
|
-
|
|
1002
|
-
**eva4j genera automΓ‘ticamente en OrderItem:**
|
|
1003
|
-
|
|
1004
|
-
```java
|
|
1005
|
-
// OrderItemJpa.java (automatically generated)
|
|
1006
|
-
@ManyToOne(fetch = FetchType.LAZY)
|
|
1007
|
-
@JoinColumn(name = "order_id")
|
|
1008
|
-
private OrderJpa order;
|
|
1009
|
-
```
|
|
1010
|
-
|
|
1011
|
-
**Ventajas:**
|
|
1012
|
-
- β
No necesitas definir ambos lados manualmente
|
|
1013
|
-
- β
Evita inconsistencias entre relaciones
|
|
1014
|
-
- β
JPA persiste correctamente la relaciΓ³n bidireccional
|
|
1015
|
-
- β
Menos cΓ³digo YAML, misma funcionalidad
|
|
1016
|
-
|
|
1017
|
-
**Nota**: Si defines manualmente ambos lados en el YAML, la definiciΓ³n manual tiene prioridad sobre la autogeneraciΓ³n.
|
|
1018
|
-
|
|
1019
|
-
---
|
|
1020
|
-
|
|
1021
|
-
### OneToMany (Uno a Muchos)
|
|
1022
|
-
|
|
1023
|
-
**DefiniciΓ³n en la entidad que tiene la colecciΓ³n:**
|
|
1024
|
-
|
|
1025
|
-
```yaml
|
|
1026
|
-
entities:
|
|
1027
|
-
- name: order
|
|
1028
|
-
isRoot: true
|
|
1029
|
-
relationships:
|
|
1030
|
-
- type: OneToMany
|
|
1031
|
-
target: OrderItem # Entidad relacionada
|
|
1032
|
-
mappedBy: order # Campo en OrderItem que apunta a Order
|
|
1033
|
-
cascade: [PERSIST, MERGE, REMOVE]
|
|
1034
|
-
fetch: LAZY
|
|
1035
|
-
```
|
|
1036
|
-
|
|
1037
|
-
**Genera en dominio:**
|
|
1038
|
-
```java
|
|
1039
|
-
private List<OrderItem> orderItems = new ArrayList<>();
|
|
1040
|
-
|
|
1041
|
-
public void addOrderItem(OrderItem orderItem) {
|
|
1042
|
-
this.orderItems.add(orderItem);
|
|
1043
|
-
}
|
|
1044
|
-
|
|
1045
|
-
public void removeOrderItem(OrderItem orderItem) {
|
|
1046
|
-
this.orderItems.remove(orderItem);
|
|
1047
|
-
}
|
|
1048
|
-
```
|
|
1049
|
-
|
|
1050
|
-
**Genera en JPA:**
|
|
1051
|
-
```java
|
|
1052
|
-
@OneToMany(mappedBy = "order", cascade = {CascadeType.PERSIST, CascadeType.MERGE, CascadeType.REMOVE}, fetch = FetchType.LAZY)
|
|
1053
|
-
@Builder.Default
|
|
1054
|
-
private List<OrderItemJpa> orderItems = new ArrayList<>();
|
|
1055
|
-
```
|
|
1056
|
-
|
|
1057
|
-
**Genera automΓ‘ticamente en OrderItem (lado inverso):**
|
|
1058
|
-
```java
|
|
1059
|
-
@ManyToOne(fetch = FetchType.LAZY)
|
|
1060
|
-
@JoinColumn(name = "order_id") // Inferido desde mappedBy
|
|
1061
|
-
private OrderJpa order;
|
|
1062
|
-
```
|
|
1063
|
-
|
|
1064
|
-
### ManyToOne (Muchos a Uno)
|
|
1065
|
-
|
|
1066
|
-
**DefiniciΓ³n manual (opcional si ya usaste mappedBy en OneToMany):**
|
|
1067
|
-
|
|
1068
|
-
```yaml
|
|
1069
|
-
entities:
|
|
1070
|
-
- name: orderItem
|
|
1071
|
-
# Sin isRoot = entidad secundaria
|
|
1072
|
-
relationships:
|
|
1073
|
-
- type: ManyToOne
|
|
1074
|
-
target: Order
|
|
1075
|
-
joinColumn: order_id # Columna FK en la tabla
|
|
1076
|
-
fetch: LAZY
|
|
1077
|
-
```
|
|
1078
|
-
|
|
1079
|
-
**Genera en JPA:**
|
|
1080
|
-
```java
|
|
1081
|
-
@ManyToOne(fetch = FetchType.LAZY)
|
|
1082
|
-
@JoinColumn(name = "order_id")
|
|
1083
|
-
private OrderJpa order;
|
|
1084
|
-
```
|
|
1085
|
-
|
|
1086
|
-
**π‘ Tip**: Si ya definiste `OneToMany` con `mappedBy` en Order, NO necesitas definir manualmente el `ManyToOne` en OrderItem. eva4j lo genera automΓ‘ticamente.
|
|
1087
|
-
|
|
1088
|
-
---
|
|
1089
|
-
|
|
1090
|
-
### β οΈ REGLA CRΓTICA: Relaciones Bidireccionales
|
|
1091
|
-
|
|
1092
|
-
**Para relaciones bidireccionales OneToMany/ManyToOne:**
|
|
1093
|
-
|
|
1094
|
-
#### β
CORRECTO - Solo definir en la entidad raΓz
|
|
1095
|
-
|
|
1096
|
-
```yaml
|
|
1097
|
-
entities:
|
|
1098
|
-
- name: invoice
|
|
1099
|
-
isRoot: true
|
|
1100
|
-
relationships:
|
|
1101
|
-
- type: OneToMany
|
|
1102
|
-
target: InvoiceItem
|
|
1103
|
-
mappedBy: invoice # β Solo esta definiciΓ³n
|
|
1104
|
-
cascade: [PERSIST, MERGE, REMOVE]
|
|
1105
|
-
fetch: LAZY
|
|
1106
|
-
|
|
1107
|
-
- name: invoiceItem
|
|
1108
|
-
fields:
|
|
1109
|
-
- name: id
|
|
1110
|
-
type: Long
|
|
1111
|
-
# β SIN relationships definidas
|
|
1112
|
-
# Eva4j genera automΓ‘ticamente el ManyToOne en InvoiceItemJpa
|
|
1113
|
-
```
|
|
1114
|
-
|
|
1115
|
-
**Resultado generado:**
|
|
1116
|
-
```java
|
|
1117
|
-
// InvoiceJpa.java
|
|
1118
|
-
@OneToMany(mappedBy = "invoice", cascade = {...})
|
|
1119
|
-
private List<InvoiceItemJpa> invoiceItems;
|
|
1120
|
-
|
|
1121
|
-
// InvoiceItemJpa.java (automatically generated)
|
|
1122
|
-
@ManyToOne(fetch = FetchType.LAZY)
|
|
1123
|
-
@JoinColumn(name = "invoice_id")
|
|
1124
|
-
private InvoiceJpa invoice;
|
|
1125
|
-
```
|
|
1126
|
-
|
|
1127
|
-
#### β INCORRECTO - Definir en ambos lados
|
|
1128
|
-
|
|
1129
|
-
```yaml
|
|
1130
|
-
entities:
|
|
1131
|
-
- name: invoice
|
|
1132
|
-
isRoot: true
|
|
1133
|
-
relationships:
|
|
1134
|
-
- type: OneToMany
|
|
1135
|
-
target: InvoiceItem
|
|
1136
|
-
mappedBy: invoice # β Primera definiciΓ³n
|
|
1137
|
-
|
|
1138
|
-
- name: invoiceItem
|
|
1139
|
-
relationships:
|
|
1140
|
-
- type: ManyToOne # β β DUPLICADO - CausarΓ‘ error
|
|
1141
|
-
target: Invoice
|
|
1142
|
-
joinColumn: invoice_id
|
|
1143
|
-
```
|
|
1144
|
-
|
|
1145
|
-
**Problema:** Genera DOS relaciones `@ManyToOne` en `InvoiceItemJpa`, ambas mapeando a `invoice_id`:
|
|
1146
|
-
|
|
1147
|
-
```java
|
|
1148
|
-
// InvoiceItemJpa.java (INCORRECTO - Duplicado)
|
|
1149
|
-
@ManyToOne
|
|
1150
|
-
@JoinColumn(name = "invoice_id")
|
|
1151
|
-
private InvoiceJpa invoice; // β Del mappedBy
|
|
1152
|
-
|
|
1153
|
-
@ManyToOne
|
|
1154
|
-
@JoinColumn(name = "invoice_id")
|
|
1155
|
-
private InvoiceJpa invoices; // β Del ManyToOne explΓcito
|
|
1156
|
-
|
|
1157
|
-
// Error de Hibernate:
|
|
1158
|
-
// "Column 'invoice_id' is duplicated in mapping"
|
|
1159
|
-
```
|
|
1160
|
-
|
|
1161
|
-
#### π Regla de Oro
|
|
1162
|
-
|
|
1163
|
-
| Escenario | Definir en RaΓz | Definir en Secundaria | Eva4j Genera |
|
|
1164
|
-
|-----------|-----------------|----------------------|-------------|
|
|
1165
|
-
| **Bidireccional** | `OneToMany` con `mappedBy` | β NADA | `@OneToMany` en raΓz + `@ManyToOne` en JPA de secundaria |
|
|
1166
|
-
| **Unidireccional** | Opcional | `ManyToOne` con `joinColumn` | Solo lo definido |
|
|
1167
|
-
|
|
1168
|
-
#### π‘ SeparaciΓ³n Dominio/Persistencia
|
|
1169
|
-
|
|
1170
|
-
**Importante:** Eva4j sigue correctamente DDD:
|
|
1171
|
-
|
|
1172
|
-
- **Capa de Dominio:** Las entidades secundarias NO tienen referencia a la raΓz
|
|
1173
|
-
```java
|
|
1174
|
-
// InvoiceItem.java (dominio puro)
|
|
1175
|
-
public class InvoiceItem {
|
|
1176
|
-
private Long id;
|
|
1177
|
-
private String description;
|
|
1178
|
-
// β SIN private Invoice invoice
|
|
1179
|
-
}
|
|
1180
|
-
```
|
|
1181
|
-
|
|
1182
|
-
- **Capa de Persistencia (JPA):** Solo aquΓ existe la relaciΓ³n
|
|
1183
|
-
```java
|
|
1184
|
-
// InvoiceItemJpa.java (persistencia)
|
|
1185
|
-
public class InvoiceItemJpa {
|
|
1186
|
-
private Long id;
|
|
1187
|
-
|
|
1188
|
-
@ManyToOne
|
|
1189
|
-
@JoinColumn(name = "invoice_id")
|
|
1190
|
-
private InvoiceJpa invoice; // β Solo en capa JPA
|
|
1191
|
-
}
|
|
1192
|
-
```
|
|
1193
|
-
|
|
1194
|
-
**Ventajas:**
|
|
1195
|
-
- β
Sin dependencias circulares en dominio
|
|
1196
|
-
- β
Modelo de dominio mΓ‘s simple
|
|
1197
|
-
- β
RelaciΓ³n bidireccional solo donde se necesita (persistencia)
|
|
1198
|
-
- β
Cumple principios de DDD y arquitectura hexagonal
|
|
1199
|
-
|
|
1200
|
-
---
|
|
1201
|
-
|
|
1202
|
-
### OneToOne (Uno a Uno)
|
|
1203
|
-
|
|
1204
|
-
**Bidireccional con mappedBy:**
|
|
1205
|
-
|
|
1206
|
-
```yaml
|
|
1207
|
-
entities:
|
|
1208
|
-
- name: order
|
|
1209
|
-
isRoot: true
|
|
1210
|
-
relationships:
|
|
1211
|
-
- type: OneToOne
|
|
1212
|
-
target: OrderSummary
|
|
1213
|
-
mappedBy: order
|
|
1214
|
-
cascade: [PERSIST, MERGE]
|
|
1215
|
-
fetch: LAZY
|
|
1216
|
-
```
|
|
1217
|
-
|
|
1218
|
-
**Sin mappedBy (owner):**
|
|
1219
|
-
|
|
1220
|
-
```yaml
|
|
1221
|
-
entities:
|
|
1222
|
-
- name: orderSummary
|
|
1223
|
-
relationships:
|
|
1224
|
-
- type: OneToOne
|
|
1225
|
-
target: Order
|
|
1226
|
-
joinColumn: order_id
|
|
1227
|
-
fetch: LAZY
|
|
1228
|
-
```
|
|
1229
|
-
|
|
1230
|
-
### Relationship Options
|
|
1231
|
-
|
|
1232
|
-
| Option | Values | Description |
|
|
1233
|
-
|--------|--------|-------------|
|
|
1234
|
-
| `type` | OneToMany, ManyToOne, OneToOne, ManyToMany | Relationship type |
|
|
1235
|
-
| `target` | EntityName | Related entity |
|
|
1236
|
-
| `mappedBy` | fieldName | For the inverse side of the relationship |
|
|
1237
|
-
| `joinColumn` | column_name | FK column name |
|
|
1238
|
-
| `cascade` | [PERSIST, MERGE, REMOVE, REFRESH, DETACH, ALL] | Cascade operations |
|
|
1239
|
-
| `fetch` | LAZY, EAGER | Loading strategy |
|
|
1240
|
-
|
|
1241
|
-
---
|
|
1242
|
-
|
|
1243
|
-
### π₯ Cascade Options (Cascade Operations)
|
|
1244
|
-
|
|
1245
|
-
The `cascade` options determine which operations on the parent are automatically propagated to related entities.
|
|
1246
|
-
|
|
1247
|
-
#### **β οΈ IMPORTANT: Cascade and Persistence**
|
|
1248
|
-
|
|
1249
|
-
If you DON'T define `cascade`, related entities will **NOT be persisted automatically**. This is the most common error:
|
|
1250
|
-
|
|
1251
|
-
```yaml
|
|
1252
|
-
# β BAD - OrderItems will NOT be saved in DB
|
|
1253
|
-
relationships:
|
|
1254
|
-
- type: OneToMany
|
|
1255
|
-
target: OrderItem
|
|
1256
|
-
mappedBy: order
|
|
1257
|
-
cascade: [] # β Empty array = no cascade
|
|
1258
|
-
fetch: LAZY
|
|
1259
|
-
|
|
1260
|
-
# β
GOOD - OrderItems are saved automatically with Order
|
|
1261
|
-
relationships:
|
|
1262
|
-
- type: OneToMany
|
|
1263
|
-
target: OrderItem
|
|
1264
|
-
mappedBy: order
|
|
1265
|
-
cascade: [PERSIST, MERGE, REMOVE] # β Required to persist
|
|
1266
|
-
fetch: LAZY
|
|
1267
|
-
```
|
|
1268
|
-
|
|
1269
|
-
#### **Cascade Options:**
|
|
1270
|
-
|
|
1271
|
-
| Option | Description | When to use? |
|
|
1272
|
-
|--------|-------------|--------------|
|
|
1273
|
-
| `PERSIST` | When saving the parent, saves new children | β
**Always in OneToMany** to create items |
|
|
1274
|
-
| `MERGE` | When updating the parent, updates children | β
**Always in OneToMany** to edit items |
|
|
1275
|
-
| `REMOVE` | When deleting the parent, deletes children | β
If children don't make sense without the parent |
|
|
1276
|
-
| `REFRESH` | When refreshing the parent, refreshes children | β οΈ Rarely needed |
|
|
1277
|
-
| `DETACH` | When detaching the parent, detaches children | β οΈ Rarely needed |
|
|
1278
|
-
| `ALL` | All of the above operations | β οΈ Only if you're sure |
|
|
1279
|
-
|
|
1280
|
-
#### **Recommended Configurations:**
|
|
1281
|
-
|
|
1282
|
-
```yaml
|
|
1283
|
-
# π― RECOMMENDED for OneToMany (Order β OrderItem)
|
|
1284
|
-
relationships:
|
|
1285
|
-
- type: OneToMany
|
|
1286
|
-
target: OrderItem
|
|
1287
|
-
mappedBy: order
|
|
1288
|
-
cascade: [PERSIST, MERGE, REMOVE] # β Creates, updates and deletes items
|
|
1289
|
-
fetch: LAZY
|
|
1290
|
-
|
|
1291
|
-
# π― RECOMMENDED for entities with independent lifecycle
|
|
1292
|
-
relationships:
|
|
1293
|
-
- type: OneToMany
|
|
1294
|
-
target: OrderItem
|
|
1295
|
-
mappedBy: order
|
|
1296
|
-
cascade: [PERSIST, MERGE] # β Without REMOVE, items persist
|
|
1297
|
-
fetch: LAZY
|
|
1298
|
-
|
|
1299
|
-
# β οΈ CAREFUL with ALL - includes REMOVE
|
|
1300
|
-
relationships:
|
|
1301
|
-
- type: OneToMany
|
|
1302
|
-
target: OrderItem
|
|
1303
|
-
mappedBy: order
|
|
1304
|
-
cascade: [ALL] # β Deleting Order removes all OrderItems
|
|
1305
|
-
fetch: LAZY
|
|
1306
|
-
|
|
1307
|
-
# β AVOID empty array if you want to persist children
|
|
1308
|
-
relationships:
|
|
1309
|
-
- type: OneToMany
|
|
1310
|
-
target: OrderItem
|
|
1311
|
-
mappedBy: order
|
|
1312
|
-
cascade: [] # β Requires manually saving OrderItem
|
|
1313
|
-
fetch: LAZY
|
|
1314
|
-
```
|
|
1315
|
-
|
|
1316
|
-
#### **What happens without Cascade?**
|
|
1317
|
-
|
|
1318
|
-
```yaml
|
|
1319
|
-
# Without cascade: [PERSIST]
|
|
1320
|
-
cascade: []
|
|
1321
|
-
|
|
1322
|
-
# Behavior:
|
|
1323
|
-
order.addOrderItem(item);
|
|
1324
|
-
repository.save(order); // β Order is saved, OrderItem is NOT
|
|
1325
|
-
```
|
|
1326
|
-
|
|
1327
|
-
```yaml
|
|
1328
|
-
# With cascade: [PERSIST, MERGE]
|
|
1329
|
-
cascade: [PERSIST, MERGE]
|
|
1330
|
-
|
|
1331
|
-
# Behavior:
|
|
1332
|
-
order.addOrderItem(item);
|
|
1333
|
-
repository.save(order); // β
Order and OrderItem are saved automatically
|
|
1334
|
-
```
|
|
1335
|
-
|
|
1336
|
-
---
|
|
1337
|
-
|
|
1338
|
-
### π Fetch Options (Loading Strategy)
|
|
1339
|
-
|
|
1340
|
-
The `fetch` options determine WHEN related entities are loaded from the database.
|
|
1341
|
-
|
|
1342
|
-
#### **Fetch Options:**
|
|
1343
|
-
|
|
1344
|
-
| Option | Description | Behavior | When to use? |
|
|
1345
|
-
|--------|-------------|----------|--------------|
|
|
1346
|
-
| `LAZY` | Load on demand (when accessed) | Only fetches parent initially | β
**Recommended by default** |
|
|
1347
|
-
| `EAGER` | Immediate load (always) | Fetches parent + children in same query | β οΈ Only if you ALWAYS need children |
|
|
1348
|
-
|
|
1349
|
-
#### **LAZY Example (Recommended):**
|
|
1350
|
-
|
|
1351
|
-
```yaml
|
|
1352
|
-
relationships:
|
|
1353
|
-
- type: OneToMany
|
|
1354
|
-
target: OrderItem
|
|
1355
|
-
mappedBy: order
|
|
1356
|
-
cascade: [PERSIST, MERGE]
|
|
1357
|
-
fetch: LAZY # β Loads items only when accessed
|
|
1358
|
-
```
|
|
1359
|
-
|
|
1360
|
-
**Generated SQL:**
|
|
1361
|
-
```sql
|
|
1362
|
-
-- First query: Only fetches Order
|
|
1363
|
-
SELECT * FROM orders WHERE id = ?
|
|
1364
|
-
|
|
1365
|
-
-- Second query: Only if you access order.getOrderItems()
|
|
1366
|
-
SELECT * FROM order_items WHERE order_id = ?
|
|
1367
|
-
```
|
|
1368
|
-
|
|
1369
|
-
**β
Advantages:**
|
|
1370
|
-
- Better initial performance
|
|
1371
|
-
- Only loads what you need
|
|
1372
|
-
- Avoids loading unnecessary data
|
|
1373
|
-
|
|
1374
|
-
**β οΈ Disadvantage:**
|
|
1375
|
-
- Can cause N+1 queries if you don't use `JOIN FETCH`
|
|
1376
|
-
|
|
1377
|
-
#### **Ejemplo EAGER (Usar con cuidado):**
|
|
1378
|
-
|
|
1379
|
-
```yaml
|
|
1380
|
-
relationships:
|
|
1381
|
-
- type: OneToMany
|
|
1382
|
-
target: OrderItem
|
|
1383
|
-
mappedBy: order
|
|
1384
|
-
cascade: [PERSIST, MERGE]
|
|
1385
|
-
fetch: EAGER # β Always loads items with Order
|
|
1386
|
-
```
|
|
1387
|
-
|
|
1388
|
-
**Generated SQL:**
|
|
1389
|
-
```sql
|
|
1390
|
-
-- Single query: Fetches Order + OrderItems
|
|
1391
|
-
SELECT o.*, i.*
|
|
1392
|
-
FROM orders o
|
|
1393
|
-
LEFT JOIN order_items i ON i.order_id = o.id
|
|
1394
|
-
WHERE o.id = ?
|
|
1395
|
-
```
|
|
1396
|
-
|
|
1397
|
-
**β
Advantage:**
|
|
1398
|
-
- Single SQL query
|
|
1399
|
-
- Data available immediately
|
|
1400
|
-
|
|
1401
|
-
**β Disadvantages:**
|
|
1402
|
-
- Loads data even if unused
|
|
1403
|
-
- Heavier queries
|
|
1404
|
-
- Can cause performance issues
|
|
1405
|
-
|
|
1406
|
-
#### **Recommended Configurations by Type:**
|
|
1407
|
-
|
|
1408
|
-
```yaml
|
|
1409
|
-
# OneToMany: ALWAYS LAZY
|
|
1410
|
-
relationships:
|
|
1411
|
-
- type: OneToMany
|
|
1412
|
-
target: OrderItem
|
|
1413
|
-
mappedBy: order
|
|
1414
|
-
cascade: [PERSIST, MERGE]
|
|
1415
|
-
fetch: LAZY # β Avoids loading all items always
|
|
1416
|
-
|
|
1417
|
-
# ManyToOne: LAZY by default, EAGER only if always needed
|
|
1418
|
-
relationships:
|
|
1419
|
-
- type: ManyToOne
|
|
1420
|
-
target: Customer
|
|
1421
|
-
joinColumn: customer_id
|
|
1422
|
-
fetch: LAZY # β LAZY by default
|
|
1423
|
-
|
|
1424
|
-
# OneToOne: LAZY if optional, EAGER if always exists
|
|
1425
|
-
relationships:
|
|
1426
|
-
- type: OneToOne
|
|
1427
|
-
target: OrderSummary
|
|
1428
|
-
mappedBy: order
|
|
1429
|
-
cascade: [PERSIST, MERGE]
|
|
1430
|
-
fetch: LAZY # β LAZY if not always used
|
|
1431
|
-
```
|
|
1432
|
-
|
|
1433
|
-
#### **N+1 Problem and how to solve it:**
|
|
1434
|
-
|
|
1435
|
-
**Problem:**
|
|
1436
|
-
```java
|
|
1437
|
-
// With LAZY fetch
|
|
1438
|
-
List<Order> orders = orderRepository.findAll(); // 1 query
|
|
1439
|
-
orders.forEach(order -> {
|
|
1440
|
-
order.getOrderItems().forEach(item -> { // N queries (one per Order)
|
|
1441
|
-
System.out.println(item.getProductName());
|
|
1442
|
-
});
|
|
1443
|
-
});
|
|
1444
|
-
// Total: 1 + N queries = N+1 problem
|
|
1445
|
-
```
|
|
1446
|
-
|
|
1447
|
-
**Solution - Use JOIN FETCH in queries:**
|
|
1448
|
-
```java
|
|
1449
|
-
@Query("SELECT o FROM OrderJpa o LEFT JOIN FETCH o.orderItems WHERE o.id = :id")
|
|
1450
|
-
OrderJpa findByIdWithItems(@Param("id") String id);
|
|
1451
|
-
```
|
|
1452
|
-
|
|
1453
|
-
---
|
|
1454
|
-
|
|
1455
|
-
### When to manually define inverse relationships?
|
|
1456
|
-
|
|
1457
|
-
#### β You DON'T need to define ManyToOne if:
|
|
1458
|
-
|
|
1459
|
-
You already defined `OneToMany` with `mappedBy` on the "parent" side. eva4j automatically generates the inverse relationship.
|
|
1460
|
-
|
|
1461
|
-
**Example - Only define OneToMany:**
|
|
1462
|
-
|
|
1463
|
-
```yaml
|
|
1464
|
-
# β
SUFFICIENT: Only define this in Order
|
|
1465
|
-
entities:
|
|
1466
|
-
- name: order
|
|
1467
|
-
isRoot: true
|
|
1468
|
-
relationships:
|
|
1469
|
-
- type: OneToMany
|
|
1470
|
-
target: OrderItem
|
|
1471
|
-
mappedBy: order # β eva4j generates ManyToOne automatically
|
|
1472
|
-
cascade: [PERSIST, MERGE, REMOVE]
|
|
1473
|
-
fetch: LAZY
|
|
1474
|
-
|
|
1475
|
-
# β DON'T NEED this in OrderItem (generated automatically)
|
|
1476
|
-
# - name: orderItem
|
|
1477
|
-
# relationships:
|
|
1478
|
-
# - type: ManyToOne
|
|
1479
|
-
# target: Order
|
|
1480
|
-
# joinColumn: order_id
|
|
1481
|
-
# fetch: LAZY
|
|
1482
|
-
```
|
|
1483
|
-
|
|
1484
|
-
**Result:** Complete bidirectional relationship with FK `order_id` generated automatically.
|
|
1485
|
-
|
|
1486
|
-
**β
Advantages:**
|
|
1487
|
-
- Less YAML code (only define one side)
|
|
1488
|
-
- No duplication or inconsistencies
|
|
1489
|
-
- Works the same as defining both sides
|
|
1490
|
-
- FK inferred automatically: `{mappedBy}_id`
|
|
1491
|
-
|
|
1492
|
-
---
|
|
1493
|
-
|
|
1494
|
-
#### β
You SHOULD define ManyToOne manually if:
|
|
1495
|
-
|
|
1496
|
-
##### 1. **You need a specific FK column name**
|
|
1497
|
-
|
|
1498
|
-
```yaml
|
|
1499
|
-
# Define both sides to control FK name
|
|
1500
|
-
entities:
|
|
1501
|
-
- name: order
|
|
1502
|
-
isRoot: true
|
|
1503
|
-
relationships:
|
|
1504
|
-
- type: OneToMany
|
|
1505
|
-
target: OrderItem
|
|
1506
|
-
mappedBy: order
|
|
1507
|
-
cascade: [PERSIST, MERGE]
|
|
1508
|
-
fetch: LAZY
|
|
1509
|
-
|
|
1510
|
-
- name: orderItem
|
|
1511
|
-
relationships:
|
|
1512
|
-
- type: ManyToOne
|
|
1513
|
-
target: Order
|
|
1514
|
-
joinColumn: fk_pedido_uuid # β Custom name
|
|
1515
|
-
fetch: LAZY
|
|
1516
|
-
```
|
|
1517
|
-
|
|
1518
|
-
**When to use:**
|
|
1519
|
-
- Your DB has specific conventions (`fk_*`, prefixes, etc.)
|
|
1520
|
-
- Need to maintain compatibility with existing schema
|
|
1521
|
-
- Migration from another tool/framework
|
|
1522
|
-
|
|
1523
|
-
---
|
|
1524
|
-
|
|
1525
|
-
##### 2. **Multiple FKs to the same entity**
|
|
1526
|
-
|
|
1527
|
-
```yaml
|
|
1528
|
-
# Transaction has 'from' and 'to' Account
|
|
1529
|
-
entities:
|
|
1530
|
-
- name: transaction
|
|
1531
|
-
tableName: transactions
|
|
1532
|
-
|
|
1533
|
-
fields:
|
|
1534
|
-
- name: id
|
|
1535
|
-
type: String
|
|
1536
|
-
- name: amount
|
|
1537
|
-
type: BigDecimal
|
|
1538
|
-
|
|
1539
|
-
relationships:
|
|
1540
|
-
# First relationship
|
|
1541
|
-
- type: ManyToOne
|
|
1542
|
-
target: Account
|
|
1543
|
-
joinColumn: from_account_id # β Explicit name required
|
|
1544
|
-
fetch: LAZY
|
|
1545
|
-
|
|
1546
|
-
# Second relationship to same entity
|
|
1547
|
-
- type: ManyToOne
|
|
1548
|
-
target: Account
|
|
1549
|
-
joinColumn: to_account_id # β Different FK name
|
|
1550
|
-
fetch: LAZY
|
|
1551
|
-
```
|
|
1552
|
-
|
|
1553
|
-
**When to use:**
|
|
1554
|
-
- Self-relationships (category tree, org chart)
|
|
1555
|
-
- Multiple relationships to same type (from/to, parent/child)
|
|
1556
|
-
- Can't use `mappedBy` (which one would it be?)
|
|
1557
|
-
|
|
1558
|
-
---
|
|
1559
|
-
|
|
1560
|
-
##### 3. **Unidirectional relationship (no inverse side)**
|
|
1561
|
-
|
|
1562
|
-
```yaml
|
|
1563
|
-
# OrderItem needs Product, but Product DOESN'T need OrderItems
|
|
1564
|
-
entities:
|
|
1565
|
-
- name: orderItem
|
|
1566
|
-
relationships:
|
|
1567
|
-
- type: ManyToOne
|
|
1568
|
-
target: Product # Product has NO List<OrderItem>
|
|
1569
|
-
joinColumn: product_id
|
|
1570
|
-
fetch: LAZY
|
|
1571
|
-
|
|
1572
|
-
# In Product DON'T define OneToMany
|
|
1573
|
-
- name: product
|
|
1574
|
-
isRoot: true
|
|
1575
|
-
fields:
|
|
1576
|
-
- name: id
|
|
1577
|
-
type: String
|
|
1578
|
-
- name: name
|
|
1579
|
-
type: String
|
|
1580
|
-
# No relationships to OrderItem
|
|
1581
|
-
```
|
|
1582
|
-
|
|
1583
|
-
**When to use:**
|
|
1584
|
-
- Performance: avoid loading unnecessary collections
|
|
1585
|
-
- Product is not part of Order aggregate
|
|
1586
|
-
- Only need navigation in one direction
|
|
1587
|
-
|
|
1588
|
-
---
|
|
1589
|
-
|
|
1590
|
-
#### π Quick Comparison
|
|
1591
|
-
|
|
1592
|
-
| Scenario | Define ManyToOne? | Why? |
|
|
1593
|
-
|----------|-------------------|------|
|
|
1594
|
-
| Standard relationship with `mappedBy` | β No | eva4j generates it automatically |
|
|
1595
|
-
| FK with custom name | β
Yes | To control `joinColumn` |
|
|
1596
|
-
| Multiple FKs to same entity | β
Yes | Need explicit names |
|
|
1597
|
-
| Unidirectional relationship | β
Yes | No inverse side (`mappedBy`) |
|
|
1598
|
-
| Specific DB conventions | β
Yes | To comply with standards |
|
|
1599
|
-
| Simple standard case | β No | Let eva4j generate it |
|
|
1600
|
-
|
|
1601
|
-
---
|
|
1602
|
-
|
|
1603
|
-
#### β οΈ Error ComΓΊn
|
|
1604
|
-
|
|
1605
|
-
**NO hagas esto:**
|
|
1606
|
-
|
|
1607
|
-
```yaml
|
|
1608
|
-
# β INCORRECTO: Inconsistencia entre ambos lados
|
|
1609
|
-
entities:
|
|
1610
|
-
- name: order
|
|
1611
|
-
isRoot: true
|
|
1612
|
-
relationships:
|
|
1613
|
-
- type: OneToMany
|
|
1614
|
-
target: OrderItem
|
|
1615
|
-
mappedBy: order # β Espera campo "order" en OrderItem
|
|
1616
|
-
fetch: LAZY
|
|
1617
|
-
|
|
1618
|
-
- name: orderItem
|
|
1619
|
-
relationships:
|
|
1620
|
-
- type: ManyToOne
|
|
1621
|
-
target: Order
|
|
1622
|
-
joinColumn: pedido_id # β Pero la FK se llama diferente
|
|
1623
|
-
fetch: LAZY
|
|
513
|
+
- name: addresses
|
|
514
|
+
type: List<Address>
|
|
1624
515
|
```
|
|
1625
516
|
|
|
1626
|
-
|
|
517
|
+
Generates:
|
|
1627
518
|
|
|
1628
|
-
|
|
1629
|
-
|
|
1630
|
-
|
|
1631
|
-
|
|
1632
|
-
|
|
1633
|
-
entities:
|
|
1634
|
-
- name: order
|
|
1635
|
-
isRoot: true
|
|
1636
|
-
relationships:
|
|
1637
|
-
- type: OneToMany
|
|
1638
|
-
target: OrderItem
|
|
1639
|
-
mappedBy: order
|
|
1640
|
-
fetch: LAZY
|
|
519
|
+
```java
|
|
520
|
+
@ElementCollection
|
|
521
|
+
@CollectionTable(name = "entity_addresses", joinColumns = @JoinColumn(name = "entity_id"))
|
|
522
|
+
@Builder.Default
|
|
523
|
+
private List<AddressJpa> addresses = new ArrayList<>();
|
|
1641
524
|
```
|
|
1642
525
|
|
|
1643
|
-
|
|
526
|
+
---
|
|
527
|
+
|
|
528
|
+
## 10. Enums and state transitions
|
|
529
|
+
|
|
530
|
+
### Simple enum
|
|
531
|
+
|
|
1644
532
|
```yaml
|
|
1645
|
-
|
|
1646
|
-
- name:
|
|
1647
|
-
|
|
1648
|
-
relationships:
|
|
1649
|
-
- type: OneToMany
|
|
1650
|
-
target: OrderItem
|
|
1651
|
-
mappedBy: pedido # β Coincide con el nombre del campo
|
|
1652
|
-
fetch: LAZY
|
|
1653
|
-
|
|
1654
|
-
- name: orderItem
|
|
1655
|
-
relationships:
|
|
1656
|
-
- type: ManyToOne
|
|
1657
|
-
target: Order
|
|
1658
|
-
joinColumn: pedido_id
|
|
1659
|
-
fetch: LAZY
|
|
533
|
+
enums:
|
|
534
|
+
- name: OrderStatus
|
|
535
|
+
values: [PENDING, CONFIRMED, SHIPPED, DELIVERED, CANCELLED]
|
|
1660
536
|
```
|
|
1661
537
|
|
|
1662
|
-
|
|
538
|
+
Generates `OrderStatus.java` with the enumerated values. In JPA: `@Enumerated(EnumType.STRING)`.
|
|
1663
539
|
|
|
1664
|
-
|
|
540
|
+
### Enum with state transitions
|
|
1665
541
|
|
|
1666
|
-
|
|
542
|
+
Transitions generate business methods in the entity, validation logic in the enum, and prevent invalid states.
|
|
1667
543
|
|
|
1668
544
|
```yaml
|
|
1669
|
-
|
|
1670
|
-
|
|
1671
|
-
|
|
1672
|
-
|
|
1673
|
-
|
|
1674
|
-
-
|
|
1675
|
-
|
|
1676
|
-
|
|
1677
|
-
|
|
1678
|
-
|
|
1679
|
-
|
|
1680
|
-
#
|
|
1681
|
-
|
|
1682
|
-
|
|
1683
|
-
|
|
545
|
+
enums:
|
|
546
|
+
- name: OrderStatus
|
|
547
|
+
initialValue: PENDING # assigns an initial value; field becomes readOnly
|
|
548
|
+
values: [PENDING, CONFIRMED, SHIPPED, DELIVERED, CANCELLED]
|
|
549
|
+
transitions:
|
|
550
|
+
- from: PENDING # can be a string or [array]
|
|
551
|
+
to: CONFIRMED
|
|
552
|
+
method: confirm # name of the method generated in the entity
|
|
553
|
+
- from: [PENDING, CONFIRMED]
|
|
554
|
+
to: CANCELLED
|
|
555
|
+
method: cancel
|
|
556
|
+
guard: "this.status == OrderStatus.DELIVERED" # throws BusinessException if true
|
|
557
|
+
- from: CONFIRMED
|
|
558
|
+
to: SHIPPED
|
|
559
|
+
method: ship
|
|
1684
560
|
```
|
|
1685
561
|
|
|
1686
|
-
|
|
562
|
+
#### What is generated in the Enum
|
|
1687
563
|
|
|
1688
|
-
|
|
564
|
+
```java
|
|
565
|
+
private static final Map<OrderStatus, List<OrderStatus>> VALID_TRANSITIONS = Map.of(
|
|
566
|
+
PENDING, List.of(CONFIRMED, CANCELLED),
|
|
567
|
+
CONFIRMED, List.of(SHIPPED, CANCELLED),
|
|
568
|
+
SHIPPED, List.of(DELIVERED));
|
|
1689
569
|
|
|
1690
|
-
|
|
570
|
+
public boolean canTransitionTo(OrderStatus next) {
|
|
571
|
+
return VALID_TRANSITIONS.getOrDefault(this, List.of()).contains(next);
|
|
572
|
+
}
|
|
1691
573
|
|
|
1692
|
-
|
|
574
|
+
public OrderStatus transitionTo(OrderStatus next) {
|
|
575
|
+
if (!canTransitionTo(next)) {
|
|
576
|
+
throw new InvalidStateTransitionException(this, next);
|
|
577
|
+
}
|
|
578
|
+
return next;
|
|
579
|
+
}
|
|
580
|
+
```
|
|
1693
581
|
|
|
1694
|
-
|
|
1695
|
-
|------|------|-----|---------------|
|
|
1696
|
-
| `String` | String | VARCHAR | En ID genera UUID |
|
|
1697
|
-
| `Integer` | Integer | INTEGER | En ID genera IDENTITY |
|
|
1698
|
-
| `Long` | Long | BIGINT | En ID genera IDENTITY |
|
|
1699
|
-
| `Double` | Double | DOUBLE | - |
|
|
1700
|
-
| `Float` | Float | FLOAT | - |
|
|
1701
|
-
| `Boolean` | Boolean | BOOLEAN | - |
|
|
1702
|
-
| `BigDecimal` | BigDecimal | DECIMAL | Importa automΓ‘ticamente |
|
|
582
|
+
#### What is generated in the aggregate root
|
|
1703
583
|
|
|
1704
|
-
|
|
584
|
+
One method per transition, plus `is*()` and `can*()` helpers:
|
|
1705
585
|
|
|
1706
|
-
|
|
1707
|
-
|
|
1708
|
-
|
|
1709
|
-
|
|
1710
|
-
| `LocalTime` | LocalTime | java.time.LocalTime |
|
|
586
|
+
```java
|
|
587
|
+
public void confirm() {
|
|
588
|
+
this.status = this.status.transitionTo(OrderStatus.CONFIRMED);
|
|
589
|
+
}
|
|
1711
590
|
|
|
1712
|
-
|
|
591
|
+
public void cancel() {
|
|
592
|
+
if (this.status == OrderStatus.DELIVERED) {
|
|
593
|
+
throw new BusinessException("Cannot cancel a delivered order");
|
|
594
|
+
}
|
|
595
|
+
this.status = this.status.transitionTo(OrderStatus.CANCELLED);
|
|
596
|
+
}
|
|
1713
597
|
|
|
1714
|
-
|
|
1715
|
-
|
|
1716
|
-
|
|
1717
|
-
| Cualquier Enum | Enum personalizado | Estados, tipos |
|
|
1718
|
-
| Cualquier VO | Value Object | Conceptos de dominio |
|
|
598
|
+
public boolean isPending() { return this.status == OrderStatus.PENDING; }
|
|
599
|
+
public boolean canConfirm() { return this.status.canTransitionTo(OrderStatus.CONFIRMED); }
|
|
600
|
+
```
|
|
1719
601
|
|
|
1720
|
-
###
|
|
602
|
+
### `initialValue`
|
|
1721
603
|
|
|
1722
|
-
|
|
604
|
+
Assigns a default value to the status field in the creation constructor. The field is automatically marked as `readOnly` (does not appear in `CreateDto`/`CreateCommand`).
|
|
1723
605
|
|
|
1724
606
|
```yaml
|
|
1725
|
-
|
|
1726
|
-
- name:
|
|
1727
|
-
|
|
607
|
+
enums:
|
|
608
|
+
- name: OrderStatus
|
|
609
|
+
initialValue: PENDING
|
|
1728
610
|
```
|
|
1729
611
|
|
|
1730
|
-
|
|
1731
|
-
```java
|
|
1732
|
-
@ElementCollection
|
|
1733
|
-
@CollectionTable(name = "order_tags", joinColumns = @JoinColumn(name = "order_id"))
|
|
1734
|
-
@Column(name = "tags")
|
|
1735
|
-
@Builder.Default
|
|
1736
|
-
private List<String> tags = new ArrayList<>();
|
|
1737
|
-
```
|
|
612
|
+
### `guard`
|
|
1738
613
|
|
|
1739
|
-
|
|
614
|
+
Java condition evaluated in the transition method. If the expression is `true`, a `BusinessException` is thrown.
|
|
1740
615
|
|
|
1741
616
|
```yaml
|
|
1742
|
-
|
|
1743
|
-
|
|
1744
|
-
|
|
1745
|
-
|
|
1746
|
-
|
|
1747
|
-
Genera:
|
|
1748
|
-
```java
|
|
1749
|
-
@ElementCollection
|
|
1750
|
-
@CollectionTable(name = "customer_addresses", joinColumns = @JoinColumn(name = "customer_id"))
|
|
1751
|
-
@Builder.Default
|
|
1752
|
-
private List<AddressJpa> addresses = new ArrayList<>();
|
|
617
|
+
- from: [PENDING, CONFIRMED]
|
|
618
|
+
to: CANCELLED
|
|
619
|
+
method: cancel
|
|
620
|
+
guard: "this.totalAmount.compareTo(BigDecimal.ZERO) == 0"
|
|
1753
621
|
```
|
|
1754
622
|
|
|
1755
623
|
---
|
|
1756
624
|
|
|
1757
|
-
##
|
|
625
|
+
## 11. Domain events
|
|
1758
626
|
|
|
1759
|
-
|
|
627
|
+
Events are declared under the aggregate (at the same level as `entities:`, `enums:`, `valueObjects:`).
|
|
1760
628
|
|
|
1761
629
|
```yaml
|
|
1762
630
|
aggregates:
|
|
1763
631
|
- name: Order
|
|
1764
|
-
|
|
1765
|
-
- name:
|
|
1766
|
-
isRoot: true
|
|
1767
|
-
tableName: orders
|
|
1768
|
-
|
|
632
|
+
events:
|
|
633
|
+
- name: OrderPlaced
|
|
1769
634
|
fields:
|
|
1770
|
-
- name: id
|
|
1771
|
-
type: String
|
|
1772
|
-
|
|
1773
|
-
- name: orderNumber
|
|
1774
|
-
type: String
|
|
1775
|
-
|
|
1776
635
|
- name: customerId
|
|
1777
636
|
type: String
|
|
1778
|
-
|
|
1779
|
-
- name: status
|
|
1780
|
-
type: OrderStatus
|
|
1781
|
-
|
|
1782
637
|
- name: totalAmount
|
|
1783
|
-
type: Money
|
|
1784
|
-
|
|
1785
|
-
- name: shippingAddress
|
|
1786
|
-
type: Address
|
|
1787
|
-
|
|
1788
|
-
- name: createdAt
|
|
1789
|
-
type: LocalDateTime
|
|
1790
|
-
|
|
1791
|
-
- name: updatedAt
|
|
1792
|
-
type: LocalDateTime
|
|
1793
|
-
|
|
1794
|
-
relationships:
|
|
1795
|
-
- type: OneToMany
|
|
1796
|
-
target: OrderItem
|
|
1797
|
-
mappedBy: order
|
|
1798
|
-
cascade: [PERSIST, MERGE, REMOVE]
|
|
1799
|
-
fetch: LAZY
|
|
1800
|
-
|
|
1801
|
-
- name: orderItem
|
|
1802
|
-
tableName: order_items
|
|
1803
|
-
|
|
1804
|
-
fields:
|
|
1805
|
-
- name: id
|
|
1806
|
-
type: Long
|
|
1807
|
-
|
|
1808
|
-
- name: productId
|
|
1809
|
-
type: String
|
|
1810
|
-
|
|
1811
|
-
- name: productName
|
|
1812
|
-
type: String
|
|
1813
|
-
|
|
1814
|
-
- name: quantity
|
|
1815
|
-
type: Integer
|
|
1816
|
-
|
|
1817
|
-
- name: unitPrice
|
|
1818
|
-
type: Money
|
|
1819
|
-
|
|
1820
|
-
- name: subtotal
|
|
1821
|
-
type: Money
|
|
1822
|
-
|
|
1823
|
-
relationships:
|
|
1824
|
-
- type: ManyToOne
|
|
1825
|
-
target: Order
|
|
1826
|
-
joinColumn: order_id
|
|
1827
|
-
fetch: LAZY
|
|
1828
|
-
|
|
1829
|
-
valueObjects:
|
|
1830
|
-
- name: Money
|
|
1831
|
-
fields:
|
|
1832
|
-
- name: amount
|
|
1833
638
|
type: BigDecimal
|
|
1834
|
-
|
|
1835
|
-
type: String
|
|
1836
|
-
|
|
1837
|
-
- name: Address
|
|
639
|
+
- name: OrderCancelled
|
|
1838
640
|
fields:
|
|
1839
|
-
- name:
|
|
1840
|
-
type: String
|
|
1841
|
-
- name: city
|
|
1842
|
-
type: String
|
|
1843
|
-
- name: state
|
|
1844
|
-
type: String
|
|
1845
|
-
- name: zipCode
|
|
641
|
+
- name: reason
|
|
1846
642
|
type: String
|
|
1847
|
-
|
|
1848
|
-
|
|
1849
|
-
|
|
1850
|
-
|
|
1851
|
-
|
|
1852
|
-
|
|
1853
|
-
|
|
1854
|
-
|
|
1855
|
-
|
|
1856
|
-
|
|
1857
|
-
|
|
1858
|
-
|
|
1859
|
-
|
|
643
|
+
entities:
|
|
644
|
+
- name: Order
|
|
645
|
+
# ...
|
|
646
|
+
```
|
|
647
|
+
|
|
648
|
+
### Generated files
|
|
649
|
+
|
|
650
|
+
| File | Description |
|
|
651
|
+
|------|-------------|
|
|
652
|
+
| `shared/domain/DomainEvent.java` | Abstract base class (generated once per project) |
|
|
653
|
+
| `domain/models/events/OrderPlaced.java` | Concrete event extending `DomainEvent` |
|
|
654
|
+
| `domain/models/events/OrderCancelled.java` | Concrete event |
|
|
655
|
+
| `raise()` / `pullDomainEvents()` in the aggregate root | Event infrastructure in the entity |
|
|
656
|
+
| `OrderRepositoryImpl.java` | Calls `eventPublisher.publishEvent()` when saving |
|
|
657
|
+
| `OrderDomainEventHandler.java` | Class with `@TransactionalEventListener` per event |
|
|
658
|
+
|
|
659
|
+
### Generated event
|
|
660
|
+
|
|
661
|
+
```java
|
|
662
|
+
public final class OrderPlaced extends DomainEvent {
|
|
663
|
+
private final String customerId;
|
|
664
|
+
private final BigDecimal totalAmount;
|
|
665
|
+
|
|
666
|
+
public OrderPlaced(String customerId, BigDecimal totalAmount) {
|
|
667
|
+
this.customerId = customerId;
|
|
668
|
+
this.totalAmount = totalAmount;
|
|
669
|
+
}
|
|
670
|
+
|
|
671
|
+
// getters
|
|
672
|
+
}
|
|
673
|
+
```
|
|
674
|
+
|
|
675
|
+
### How to raise an event in the entity
|
|
676
|
+
|
|
677
|
+
```java
|
|
678
|
+
public class Order {
|
|
679
|
+
private final List<DomainEvent> domainEvents = new ArrayList<>();
|
|
680
|
+
|
|
681
|
+
public void place(String customerId, BigDecimal totalAmount) {
|
|
682
|
+
// business logic...
|
|
683
|
+
raise(new OrderPlaced(customerId, totalAmount));
|
|
684
|
+
}
|
|
685
|
+
|
|
686
|
+
protected void raise(DomainEvent event) {
|
|
687
|
+
domainEvents.add(event);
|
|
688
|
+
}
|
|
689
|
+
|
|
690
|
+
public List<DomainEvent> pullDomainEvents() {
|
|
691
|
+
List<DomainEvent> events = new ArrayList<>(domainEvents);
|
|
692
|
+
domainEvents.clear();
|
|
693
|
+
return events;
|
|
694
|
+
}
|
|
695
|
+
}
|
|
1860
696
|
```
|
|
1861
697
|
|
|
1862
|
-
|
|
698
|
+
---
|
|
699
|
+
|
|
700
|
+
## 12. Multiple aggregates
|
|
701
|
+
|
|
702
|
+
A `domain.yaml` can contain multiple aggregates. Each one generates its own set of files.
|
|
1863
703
|
|
|
1864
704
|
```yaml
|
|
1865
705
|
aggregates:
|
|
1866
|
-
- name:
|
|
706
|
+
- name: Customer
|
|
1867
707
|
entities:
|
|
1868
|
-
- name:
|
|
708
|
+
- name: Customer
|
|
1869
709
|
isRoot: true
|
|
1870
|
-
tableName: posts
|
|
1871
|
-
|
|
1872
710
|
fields:
|
|
1873
711
|
- name: id
|
|
1874
|
-
type: Long
|
|
1875
|
-
|
|
1876
|
-
- name: title
|
|
1877
|
-
type: String
|
|
1878
|
-
|
|
1879
|
-
- name: slug
|
|
1880
712
|
type: String
|
|
1881
|
-
|
|
1882
|
-
- name: content
|
|
1883
|
-
type: String
|
|
1884
|
-
|
|
1885
|
-
- name: authorId
|
|
713
|
+
- name: email
|
|
1886
714
|
type: String
|
|
1887
|
-
|
|
1888
|
-
|
|
1889
|
-
|
|
1890
|
-
|
|
1891
|
-
|
|
1892
|
-
type: LocalDateTime
|
|
1893
|
-
|
|
1894
|
-
- name: tags
|
|
1895
|
-
type: List<String>
|
|
1896
|
-
|
|
1897
|
-
- name: metadata
|
|
1898
|
-
type: PostMetadata
|
|
1899
|
-
|
|
1900
|
-
relationships:
|
|
1901
|
-
- type: OneToMany
|
|
1902
|
-
target: Comment
|
|
1903
|
-
mappedBy: post
|
|
1904
|
-
cascade: [PERSIST, MERGE, REMOVE]
|
|
1905
|
-
fetch: LAZY
|
|
1906
|
-
|
|
1907
|
-
- name: comment
|
|
1908
|
-
tableName: comments
|
|
1909
|
-
|
|
715
|
+
|
|
716
|
+
- name: Product
|
|
717
|
+
entities:
|
|
718
|
+
- name: Product
|
|
719
|
+
isRoot: true
|
|
1910
720
|
fields:
|
|
1911
721
|
- name: id
|
|
1912
|
-
type: Long
|
|
1913
|
-
|
|
1914
|
-
- name: authorId
|
|
1915
722
|
type: String
|
|
1916
|
-
|
|
1917
|
-
- name: authorName
|
|
1918
|
-
type: String
|
|
1919
|
-
|
|
1920
|
-
- name: content
|
|
723
|
+
- name: name
|
|
1921
724
|
type: String
|
|
1922
|
-
|
|
1923
|
-
- name: createdAt
|
|
1924
|
-
type: LocalDateTime
|
|
1925
|
-
|
|
1926
|
-
- name: approved
|
|
1927
|
-
type: Boolean
|
|
1928
|
-
|
|
1929
|
-
relationships:
|
|
1930
|
-
- type: ManyToOne
|
|
1931
|
-
target: Post
|
|
1932
|
-
joinColumn: post_id
|
|
1933
|
-
fetch: LAZY
|
|
1934
|
-
|
|
1935
|
-
valueObjects:
|
|
1936
|
-
- name: PostMetadata
|
|
1937
|
-
fields:
|
|
1938
|
-
- name: viewCount
|
|
1939
|
-
type: Integer
|
|
1940
|
-
- name: likeCount
|
|
1941
|
-
type: Integer
|
|
1942
|
-
- name: shareCount
|
|
1943
|
-
type: Integer
|
|
1944
|
-
|
|
1945
725
|
enums:
|
|
1946
|
-
- name:
|
|
1947
|
-
values: [
|
|
726
|
+
- name: ProductCategory
|
|
727
|
+
values: [ELECTRONICS, CLOTHING, FOOD]
|
|
1948
728
|
```
|
|
1949
729
|
|
|
1950
|
-
|
|
730
|
+
> Enums and Value Objects are local to the aggregate where they are defined. If two aggregates need the same VO, it must be declared in each one.
|
|
731
|
+
|
|
732
|
+
---
|
|
733
|
+
|
|
734
|
+
## 13. Generated files
|
|
735
|
+
|
|
736
|
+
For each aggregate, approximately the following files are generated:
|
|
737
|
+
|
|
738
|
+
| File | Layer | Description |
|
|
739
|
+
|------|-------|-------------|
|
|
740
|
+
| `{Root}.java` | Domain | Aggregate root entity |
|
|
741
|
+
| `{Entity}.java` | Domain | Secondary entities |
|
|
742
|
+
| `{Vo}.java` | Domain | Value Objects |
|
|
743
|
+
| `{Enum}.java` | Domain | Enums (with VALID_TRANSITIONS if transitions exist) |
|
|
744
|
+
| `{Root}Repository.java` | Domain | Repository interface (port) |
|
|
745
|
+
| `Create{Root}Command.java` | Application | Create command |
|
|
746
|
+
| `Create{Root}CommandHandler.java` | Application | Command handler |
|
|
747
|
+
| `Get{Root}Query.java` | Application | Get by ID query |
|
|
748
|
+
| `Get{Root}QueryHandler.java` | Application | Query handler |
|
|
749
|
+
| `List{Root}Query.java` | Application | Paginated list query |
|
|
750
|
+
| `List{Root}QueryHandler.java` | Application | List handler |
|
|
751
|
+
| `{Root}ResponseDto.java` | Application | Response DTO |
|
|
752
|
+
| `Create{Root}Dto.java` | Application | Create DTO |
|
|
753
|
+
| `{Root}ApplicationMapper.java` | Application | Mapper Command/DTO β Domain |
|
|
754
|
+
| `{Root}Jpa.java` | Infrastructure | JPA entity |
|
|
755
|
+
| `{Entity}Jpa.java` | Infrastructure | Secondary JPA entities |
|
|
756
|
+
| `{Vo}Jpa.java` | Infrastructure | JPA Value Objects (@Embeddable) |
|
|
757
|
+
| `{Root}Mapper.java` | Infrastructure | Mapper Domain β JPA |
|
|
758
|
+
| `{Root}JpaRepository.java` | Infrastructure | Spring Data repository |
|
|
759
|
+
| `{Root}RepositoryImpl.java` | Infrastructure | Repository implementation |
|
|
760
|
+
| `{Root}Controller.java` | Infrastructure | REST controller |
|
|
761
|
+
|
|
762
|
+
### Generated REST endpoints
|
|
763
|
+
|
|
764
|
+
| Method | Path | Description |
|
|
765
|
+
|--------|------|-------------|
|
|
766
|
+
| `POST` | `/api/{module}/{entity}` | Create |
|
|
767
|
+
| `GET` | `/api/{module}/{entity}/{id}` | Get by ID |
|
|
768
|
+
| `GET` | `/api/{module}/{entity}?page=0&size=20` | Paginated list |
|
|
769
|
+
| `PUT` | `/api/{module}/{entity}/{id}` | Update |
|
|
770
|
+
| `DELETE` | `/api/{module}/{entity}/{id}` | Delete |
|
|
771
|
+
|
|
772
|
+
---
|
|
773
|
+
|
|
774
|
+
## 14. Complete examples
|
|
775
|
+
|
|
776
|
+
### Example 1: Order with transitions and events
|
|
1951
777
|
|
|
1952
778
|
```yaml
|
|
1953
779
|
aggregates:
|
|
1954
|
-
- name:
|
|
780
|
+
- name: Order
|
|
1955
781
|
entities:
|
|
1956
|
-
- name:
|
|
782
|
+
- name: Order
|
|
1957
783
|
isRoot: true
|
|
1958
|
-
tableName:
|
|
1959
|
-
|
|
784
|
+
tableName: orders
|
|
785
|
+
audit:
|
|
786
|
+
enabled: true
|
|
1960
787
|
fields:
|
|
1961
788
|
- name: id
|
|
1962
789
|
type: String
|
|
1963
|
-
|
|
1964
|
-
- name: accountNumber
|
|
1965
|
-
type: String
|
|
1966
|
-
|
|
1967
790
|
- name: customerId
|
|
1968
791
|
type: String
|
|
1969
|
-
|
|
1970
|
-
|
|
1971
|
-
|
|
1972
|
-
|
|
1973
|
-
- name: balance
|
|
1974
|
-
type: Money
|
|
1975
|
-
|
|
792
|
+
reference:
|
|
793
|
+
aggregate: Customer
|
|
794
|
+
module: customers
|
|
1976
795
|
- name: status
|
|
1977
|
-
type:
|
|
1978
|
-
|
|
1979
|
-
|
|
1980
|
-
|
|
1981
|
-
|
|
796
|
+
type: OrderStatus
|
|
797
|
+
- name: totalAmount
|
|
798
|
+
type: BigDecimal
|
|
799
|
+
readOnly: true
|
|
1982
800
|
relationships:
|
|
1983
801
|
- type: OneToMany
|
|
1984
|
-
target:
|
|
1985
|
-
mappedBy:
|
|
1986
|
-
cascade: [PERSIST, MERGE]
|
|
802
|
+
target: OrderItem
|
|
803
|
+
mappedBy: order
|
|
804
|
+
cascade: [PERSIST, MERGE, REMOVE]
|
|
1987
805
|
fetch: LAZY
|
|
1988
|
-
|
|
1989
|
-
- name:
|
|
1990
|
-
tableName:
|
|
1991
|
-
|
|
806
|
+
|
|
807
|
+
- name: OrderItem
|
|
808
|
+
tableName: order_items
|
|
1992
809
|
fields:
|
|
1993
810
|
- name: id
|
|
811
|
+
type: Long
|
|
812
|
+
- name: productId
|
|
1994
813
|
type: String
|
|
1995
|
-
|
|
1996
|
-
|
|
1997
|
-
|
|
1998
|
-
|
|
1999
|
-
|
|
2000
|
-
|
|
2001
|
-
|
|
2002
|
-
|
|
2003
|
-
|
|
2004
|
-
|
|
2005
|
-
|
|
814
|
+
- name: quantity
|
|
815
|
+
type: Integer
|
|
816
|
+
validations:
|
|
817
|
+
- type: Min
|
|
818
|
+
value: 1
|
|
819
|
+
- name: unitPrice
|
|
820
|
+
type: BigDecimal
|
|
821
|
+
|
|
822
|
+
enums:
|
|
823
|
+
- name: OrderStatus
|
|
824
|
+
initialValue: PENDING
|
|
825
|
+
values: [PENDING, CONFIRMED, SHIPPED, DELIVERED, CANCELLED]
|
|
826
|
+
transitions:
|
|
827
|
+
- from: PENDING
|
|
828
|
+
to: CONFIRMED
|
|
829
|
+
method: confirm
|
|
830
|
+
- from: CONFIRMED
|
|
831
|
+
to: SHIPPED
|
|
832
|
+
method: ship
|
|
833
|
+
- from: [PENDING, CONFIRMED]
|
|
834
|
+
to: CANCELLED
|
|
835
|
+
method: cancel
|
|
836
|
+
guard: "this.status == OrderStatus.DELIVERED"
|
|
837
|
+
|
|
838
|
+
events:
|
|
839
|
+
- name: OrderPlaced
|
|
840
|
+
fields:
|
|
841
|
+
- name: customerId
|
|
2006
842
|
type: String
|
|
2007
|
-
|
|
2008
|
-
- name: timestamp
|
|
2009
|
-
type: LocalDateTime
|
|
2010
|
-
|
|
2011
|
-
- name: balanceAfter
|
|
2012
|
-
type: Money
|
|
2013
|
-
|
|
2014
|
-
relationships:
|
|
2015
|
-
- type: ManyToOne
|
|
2016
|
-
target: Account
|
|
2017
|
-
joinColumn: account_id
|
|
2018
|
-
fetch: LAZY
|
|
2019
|
-
|
|
2020
|
-
valueObjects:
|
|
2021
|
-
- name: Money
|
|
843
|
+
- name: OrderCancelled
|
|
2022
844
|
fields:
|
|
2023
|
-
- name:
|
|
2024
|
-
type: BigDecimal
|
|
2025
|
-
- name: currency
|
|
845
|
+
- name: reason
|
|
2026
846
|
type: String
|
|
2027
|
-
|
|
2028
|
-
enums:
|
|
2029
|
-
- name: AccountType
|
|
2030
|
-
values: [CHECKING, SAVINGS, INVESTMENT, CREDIT]
|
|
2031
|
-
|
|
2032
|
-
- name: AccountStatus
|
|
2033
|
-
values: [ACTIVE, INACTIVE, SUSPENDED, CLOSED]
|
|
2034
|
-
|
|
2035
|
-
- name: TransactionType
|
|
2036
|
-
values: [DEPOSIT, WITHDRAWAL, TRANSFER, FEE, INTEREST]
|
|
2037
847
|
```
|
|
2038
848
|
|
|
2039
|
-
###
|
|
849
|
+
### Example 2: User with auditing and a sensitive field
|
|
2040
850
|
|
|
2041
851
|
```yaml
|
|
2042
852
|
aggregates:
|
|
2043
|
-
- name:
|
|
853
|
+
- name: User
|
|
2044
854
|
entities:
|
|
2045
|
-
- name:
|
|
855
|
+
- name: User
|
|
2046
856
|
isRoot: true
|
|
857
|
+
tableName: users
|
|
858
|
+
audit:
|
|
859
|
+
enabled: true
|
|
860
|
+
trackUser: true
|
|
2047
861
|
fields:
|
|
2048
862
|
- name: id
|
|
2049
863
|
type: String
|
|
2050
|
-
- name:
|
|
2051
|
-
type: String
|
|
2052
|
-
- name: email
|
|
2053
|
-
type: String
|
|
2054
|
-
- name: phone
|
|
864
|
+
- name: username
|
|
2055
865
|
type: String
|
|
2056
|
-
|
|
2057
|
-
|
|
2058
|
-
|
|
2059
|
-
|
|
2060
|
-
|
|
2061
|
-
fields:
|
|
866
|
+
validations:
|
|
867
|
+
- type: NotBlank
|
|
868
|
+
- type: Size
|
|
869
|
+
min: 3
|
|
870
|
+
max: 50
|
|
2062
871
|
- name: email
|
|
2063
872
|
type: String
|
|
2064
|
-
|
|
2065
|
-
|
|
2066
|
-
|
|
2067
|
-
|
|
2068
|
-
|
|
2069
|
-
|
|
2070
|
-
|
|
2071
|
-
|
|
2072
|
-
|
|
2073
|
-
|
|
2074
|
-
|
|
2075
|
-
type: String
|
|
2076
|
-
- name: description
|
|
2077
|
-
type: String
|
|
2078
|
-
- name: price
|
|
2079
|
-
type: Money
|
|
2080
|
-
- name: stock
|
|
2081
|
-
type: Integer
|
|
2082
|
-
- name: category
|
|
2083
|
-
type: ProductCategory
|
|
2084
|
-
|
|
2085
|
-
valueObjects:
|
|
2086
|
-
- name: Money
|
|
2087
|
-
fields:
|
|
2088
|
-
- name: amount
|
|
2089
|
-
type: BigDecimal
|
|
2090
|
-
- name: currency
|
|
2091
|
-
type: String
|
|
2092
|
-
|
|
2093
|
-
enums:
|
|
2094
|
-
- name: ProductCategory
|
|
2095
|
-
values: [ELECTRONICS, CLOTHING, FOOD, BOOKS, TOYS]
|
|
2096
|
-
```
|
|
2097
|
-
|
|
2098
|
-
---
|
|
2099
|
-
|
|
2100
|
-
## Comando de GeneraciΓ³n
|
|
2101
|
-
|
|
2102
|
-
```bash
|
|
2103
|
-
# Generar todas las entidades del mΓ³dulo
|
|
2104
|
-
eva4j generate entities <module-name>
|
|
2105
|
-
```
|
|
2106
|
-
|
|
2107
|
-
### Salida generada
|
|
873
|
+
validations:
|
|
874
|
+
- type: Email
|
|
875
|
+
annotations:
|
|
876
|
+
- "@Column(unique = true)"
|
|
877
|
+
- name: passwordHash
|
|
878
|
+
type: String
|
|
879
|
+
hidden: true
|
|
880
|
+
- name: role
|
|
881
|
+
type: UserRole
|
|
882
|
+
- name: active
|
|
883
|
+
type: Boolean
|
|
2108
884
|
|
|
885
|
+
enums:
|
|
886
|
+
- name: UserRole
|
|
887
|
+
values: [ADMIN, USER, MODERATOR]
|
|
2109
888
|
```
|
|
2110
|
-
β Found 1 aggregate(s) and 1 enum(s)
|
|
2111
|
-
|
|
2112
|
-
π¦ Aggregates to generate:
|
|
2113
|
-
βββ Order (Root: Order)
|
|
2114
|
-
β βββ OrderItem
|
|
2115
|
-
β βββ Money (VO)
|
|
2116
|
-
|
|
2117
|
-
β Generating files...
|
|
2118
|
-
|
|
2119
|
-
β
Successfully generated 13 files for module 'order'
|
|
2120
|
-
|
|
2121
|
-
π Generated Files:
|
|
2122
|
-
β Enum: OrderStatus
|
|
2123
|
-
β Domain Entity: Order
|
|
2124
|
-
β JPA Entity: OrderJpa
|
|
2125
|
-
β Domain Entity: OrderItem
|
|
2126
|
-
β JPA Entity: OrderItemJpa
|
|
2127
|
-
β Domain VO: Money
|
|
2128
|
-
β JPA VO: MoneyJpa
|
|
2129
|
-
β Mapper: OrderMapper
|
|
2130
|
-
β Repository: OrderRepository
|
|
2131
|
-
β JPA Repository: OrderJpaRepository
|
|
2132
|
-
β Repository Impl: OrderRepositoryImpl
|
|
2133
|
-
```
|
|
2134
|
-
|
|
2135
|
-
---
|
|
2136
|
-
|
|
2137
|
-
## Tips y Mejores PrΓ‘cticas
|
|
2138
|
-
|
|
2139
|
-
### β
Hacer
|
|
2140
|
-
|
|
2141
|
-
1. **Usa nombres descriptivos**: `orderNumber` en lugar de `number`
|
|
2142
|
-
2. **PascalCase para tipos**: `OrderStatus`, `Money`, `Address`
|
|
2143
|
-
3. **camelCase para campos**: `totalAmount`, `createdAt`
|
|
2144
|
-
4. **snake_case para tablas**: `order_items`, `customer_addresses`
|
|
2145
|
-
5. **Define IDs apropiados**: String para UUIDs, Long para secuencias
|
|
2146
|
-
6. **Usa Value Objects**: Para conceptos cohesivos (Money, Address)
|
|
2147
|
-
7. **Cascade apropiado**: PERSIST, MERGE para agregados; evita ALL
|
|
2148
|
-
|
|
2149
|
-
### β Evitar
|
|
2150
|
-
|
|
2151
|
-
1. **Don't use Long for UUIDs**: Use String
|
|
2152
|
-
2. **Don't create bidirectional relationships without mappedBy**: Define the owner
|
|
2153
|
-
3. **Don't use EAGER without reason**: LAZY is better for performance
|
|
2154
|
-
4. **Don't mix concepts**: One aggregate = one transaction
|
|
2155
|
-
5. **Don't use @Column in domain.yaml**: It's for JPA, generated automatically
|
|
2156
|
-
|
|
2157
|
-
---
|
|
2158
|
-
|
|
2159
|
-
## Current Support and Limitations
|
|
2160
|
-
|
|
2161
|
-
### β
Supported
|
|
2162
|
-
|
|
2163
|
-
- Aggregates with root and secondary entities
|
|
2164
|
-
- Embedded Value Objects
|
|
2165
|
-
- Enums with values
|
|
2166
|
-
- OneToMany, ManyToOne, OneToOne relationships
|
|
2167
|
-
- Java primitive and date types
|
|
2168
|
-
- Collections of primitives and VOs
|
|
2169
|
-
- IDs: String (UUID), Long/Integer (IDENTITY)
|
|
2170
|
-
- Custom Cascade and Fetch
|
|
2171
|
-
|
|
2172
|
-
### π§ Coming Soon
|
|
2173
|
-
|
|
2174
|
-
- JSR-303 validations
|
|
2175
|
-
- Automatic auditing
|
|
2176
|
-
- Soft delete
|
|
2177
|
-
- Custom query methods
|
|
2178
|
-
- Indexes and constraints
|
|
2179
|
-
- Entity inheritance
|
|
2180
|
-
|
|
2181
|
-
---
|
|
2182
|
-
|
|
2183
|
-
## Frequently Asked Questions
|
|
2184
|
-
|
|
2185
|
-
**Q: Can I have multiple aggregates in one domain.yaml?**
|
|
2186
|
-
A: Yes, define multiple entries in the `aggregates` array.
|
|
2187
|
-
|
|
2188
|
-
**Q: How do I reference an enum from another aggregate?**
|
|
2189
|
-
A: Enums are global to the module, just use the name: `type: OrderStatus`
|
|
2190
|
-
|
|
2191
|
-
**Q: Can I use a VO in multiple aggregates?**
|
|
2192
|
-
A: Yes, but you must define it in each aggregate (for now).
|
|
2193
|
-
|
|
2194
|
-
**Q: What happens if I regenerate the code?**
|
|
2195
|
-
A: Files are overwritten. Modify only in templates, not in generated code.
|
|
2196
|
-
|
|
2197
|
-
**Q: Can I customize generated entities?**
|
|
2198
|
-
A: Yes, modify the templates in `templates/aggregate/`.
|
|
2199
889
|
|
|
2200
890
|
---
|
|
2201
891
|
|
|
2202
|
-
##
|
|
892
|
+
## 15. Prerequisites and common errors
|
|
2203
893
|
|
|
2204
|
-
|
|
2205
|
-
- [Testing Guide](TESTING_GUIDE.md)
|
|
2206
|
-
- [Quick Reference](QUICK_REFERENCE.md)
|
|
2207
|
-
- [DDD Documentation](https://martinfowler.com/bliki/DomainDrivenDesign.html)
|
|
894
|
+
### Prerequisites
|
|
2208
895
|
|
|
2209
|
-
|
|
896
|
+
- Project created with `eva create`
|
|
897
|
+
- Existing module (`eva add module <module>`)
|
|
898
|
+
- `domain.yaml` file at `src/main/java/<package>/<module>/`
|
|
2210
899
|
|
|
2211
|
-
|
|
900
|
+
### Common errors
|
|
2212
901
|
|
|
2213
|
-
|
|
2214
|
-
|
|
2215
|
-
|
|
902
|
+
| Error | Cause | Solution |
|
|
903
|
+
|-------|-------|----------|
|
|
904
|
+
| `Module does not exist` | Module was not created | Run `eva add module <module>` |
|
|
905
|
+
| `YAML file not found` | No `domain.yaml` at the expected path | Check `src/main/java/<pkg>/<module>/domain.yaml` |
|
|
906
|
+
| `Invalid relationship target` | Target entity not defined in the same YAML | Define the target entity in the same `domain.yaml` |
|
|
907
|
+
| `Column 'x_id' is duplicated` | ManyToOne defined manually + auto-generated | Remove the manual ManyToOne; let eva4j generate it |
|
|
908
|
+
| File not regenerated | File was manually modified (checksum) | Use `--force` to overwrite |
|
|
909
|
+
| Import errors | Field `type` doesn't match name in `enums:` or `valueObjects:` | Verify names match exactly |
|