eva4j 1.0.12 → 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.
@@ -2,94 +2,94 @@
2
2
 
3
3
  ---
4
4
 
5
- ## Índice
6
-
7
- 1. [Descripción y propósito](#1-descripción-y-propósito)
8
- 2. [Sintaxis y ubicación del YAML](#2-sintaxis-y-ubicación-del-yaml)
9
- 3. [Estructura base del domain.yaml](#3-estructura-base-del-domainyaml)
10
- 4. [Tipos de datos soportados](#4-tipos-de-datos-soportados)
11
- 5. [Propiedades de campo](#5-propiedades-de-campo)
12
- 6. [Validaciones JSR-303](#6-validaciones-jsr-303)
13
- 7. [Auditoría](#7-auditoría)
14
- 8. [Relaciones](#8-relaciones)
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
15
  9. [Value Objects](#9-value-objects)
16
- 10. [Enums y transiciones de estado](#10-enums-y-transiciones-de-estado)
17
- 11. [Eventos de dominio](#11-eventos-de-dominio)
18
- 12. [Múltiples agregados](#12-múltiples-agregados)
19
- 13. [Archivos generados](#13-archivos-generados)
20
- 14. [Ejemplos completos](#14-ejemplos-completos)
21
- 15. [Prerequisitos y errores comunes](#15-prerequisitos-y-errores-comunes)
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
22
 
23
23
  ---
24
24
 
25
- ## 1. Descripción y propósito
25
+ ## 1. Description and purpose
26
26
 
27
- `generate entities` es el comando central de eva4j. A partir de un archivo `domain.yaml`, genera la arquitectura hexagonal completa del módulo:
27
+ `generate entities` is the core command of eva4j. From a `domain.yaml` file, it generates the complete hexagonal architecture for the module:
28
28
 
29
- - **Capa de dominio** – Entidades, Value Objects, Enums, interfaces de repositorio
30
- - **Capa de aplicación** – Commands, Queries, handlers, DTOs, mappers
31
- - **Capa de infraestructura** – Entidades JPA, repositorios Spring Data, implementaciones de repositorio, controladores REST
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
32
32
 
33
- El generador entiende relaciones, auditoría, visibilidad de campos, validaciones, transiciones de estado y eventos de dominio.
33
+ The generator understands relationships, auditing, field visibility, validations, state transitions, and domain events.
34
34
 
35
35
  ---
36
36
 
37
- ## 2. Sintaxis y ubicación del YAML
37
+ ## 2. Syntax and YAML location
38
38
 
39
39
  ```bash
40
40
  eva generate entities <module>
41
- eva g entities <module> # alias corto
41
+ eva g entities <module> # short alias
42
42
  ```
43
43
 
44
- ### Parámetros
44
+ ### Parameters
45
45
 
46
- | Parámetro | Requerido | Descripción |
47
- |-----------|-----------|-------------|
48
- | `<module>` | | Nombre del módulo (debe existir en el proyecto) |
46
+ | Parameter | Required | Description |
47
+ |-----------|----------|-------------|
48
+ | `<module>` | Yes | Module name (must already exist in the project) |
49
49
 
50
- ### Opciones
50
+ ### Options
51
51
 
52
- | Opción | Descripción |
52
+ | Option | Description |
53
53
  |--------|-------------|
54
- | `--force` | Sobrescribe archivos con cambios del desarrollador |
54
+ | `--force` | Overwrite files that have developer changes |
55
55
 
56
- ### Ubicación del YAML
56
+ ### YAML location
57
57
 
58
- El archivo se lee desde:
58
+ The file is read from:
59
59
 
60
60
  ```
61
61
  src/main/java/<package>/<module>/domain.yaml
62
62
  ```
63
63
 
64
- > El generador detecta cambios de desarrollador mediante checksums. Si un archivo fue modificado manualmente, **no se sobreescribe** a menos que uses `--force`.
64
+ > The generator detects developer changes via checksums. If a file was manually modified, it is **not overwritten** unless you use `--force`.
65
65
 
66
66
  ---
67
67
 
68
- ## 3. Estructura base del domain.yaml
68
+ ## 3. Base domain.yaml structure
69
69
 
70
70
  ```yaml
71
- aggregates: # Lista de agregados en el módulo
72
- - name: Order # Nombre del agregado (PascalCase)
73
- entities: # Entidades del agregado
74
- - name: order # Nombre de entidad (camelCase)
75
- isRoot: true # true = raíz del agregado
76
- tableName: orders # Nombre de tabla SQL (opcional)
77
- audit: # Auditoría (opcional)
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
78
  enabled: true
79
79
  trackUser: false
80
- fields: # Campos de la entidad
80
+ fields: # Entity fields
81
81
  - name: id
82
82
  type: String
83
83
  - name: status
84
- type: OrderStatus # Referencia a enum o VO
85
- relationships: # Relaciones JPA (opcional)
84
+ type: OrderStatus # Reference to enum or VO
85
+ relationships: # JPA relationships (optional)
86
86
  - type: OneToMany
87
87
  target: OrderItem
88
88
  mappedBy: order
89
89
  cascade: [PERSIST, MERGE, REMOVE]
90
90
  fetch: LAZY
91
91
 
92
- - name: orderItem # Entidad secundaria (sin isRoot o isRoot: false)
92
+ - name: OrderItem # Secondary entity (no isRoot or isRoot: false)
93
93
  tableName: order_items
94
94
  fields:
95
95
  - name: id
@@ -97,7 +97,7 @@ aggregates: # Lista de agregados en el módulo
97
97
  - name: quantity
98
98
  type: Integer
99
99
 
100
- valueObjects: # Value Objects del agregado
100
+ valueObjects: # Aggregate Value Objects
101
101
  - name: Money
102
102
  fields:
103
103
  - name: amount
@@ -105,71 +105,71 @@ aggregates: # Lista de agregados en el módulo
105
105
  - name: currency
106
106
  type: String
107
107
 
108
- enums: # Enums del agregado
108
+ enums: # Aggregate enums
109
109
  - name: OrderStatus
110
110
  values: [PENDING, CONFIRMED, SHIPPED, DELIVERED, CANCELLED]
111
111
 
112
- events: # Eventos de dominio (opcional)
112
+ events: # Domain events (optional)
113
113
  - name: OrderPlaced
114
114
  fields:
115
115
  - name: customerId
116
116
  type: String
117
117
  ```
118
118
 
119
- > **Sinónimos soportados**: `fields` = `properties`; `target` = `targetEntity`
119
+ > **Supported synonyms**: `fields` = `properties`; `target` = `targetEntity`
120
120
 
121
- ### Regla del campo `id`
121
+ ### The `id` field rule
122
122
 
123
- Toda entidad **debe** tener un campo llamado exactamente `id`:
123
+ Every entity **must** have a field named exactly `id`:
124
124
 
125
- | Tipo del `id` | Estrategia generada |
126
- |---------------|---------------------|
125
+ | `id` type | Generated strategy |
126
+ |-----------|--------------------|
127
127
  | `String` | `@GeneratedValue(strategy = GenerationType.UUID)` |
128
128
  | `Long` | `@GeneratedValue(strategy = GenerationType.IDENTITY)` |
129
129
 
130
130
  ---
131
131
 
132
- ## 4. Tipos de datos soportados
132
+ ## 4. Supported data types
133
133
 
134
- | Tipo YAML | Tipo Java | Observaciones |
135
- |-----------|-----------|---------------|
136
- | `String` | `String` | Para `id` genera UUID |
137
- | `Integer` | `Integer` | Para `id` genera IDENTITY |
138
- | `Long` | `Long` | Para `id` genera IDENTITY |
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
139
  | `Double` | `Double` | |
140
140
  | `BigDecimal` | `BigDecimal` | |
141
141
  | `Boolean` | `Boolean` | |
142
- | `LocalDate` | `LocalDate` | Import automático |
143
- | `LocalDateTime` | `LocalDateTime` | Import automático |
144
- | `LocalTime` | `LocalTime` | Import automático |
145
- | `UUID` | `UUID` | Import automático |
142
+ | `LocalDate` | `LocalDate` | Auto-imported |
143
+ | `LocalDateTime` | `LocalDateTime` | Auto-imported |
144
+ | `LocalTime` | `LocalTime` | Auto-imported |
145
+ | `UUID` | `UUID` | Auto-imported |
146
146
  | `List<String>` | `List<String>` | `@ElementCollection` |
147
147
  | `List<VO>` | `List<VoJpa>` | `@ElementCollection` |
148
- | Nombre de Enum | Enum del módulo | `@Enumerated(STRING)` |
149
- | Nombre de VO | Value Object | `@Embedded` |
148
+ | Enum name | Module enum | `@Enumerated(STRING)` |
149
+ | VO name | Value Object | `@Embedded` |
150
150
 
151
151
  ---
152
152
 
153
- ## 5. Propiedades de campo
153
+ ## 5. Field properties
154
154
 
155
155
  ```yaml
156
156
  fields:
157
- - name: fieldName # camelCase, requerido
158
- type: String # tipo Java, requerido
157
+ - name: fieldName # camelCase, required
158
+ type: String # Java type, required
159
159
  readOnly: false # default false
160
160
  hidden: false # default false
161
- validations: [] # anotaciones JSR-303
162
- annotations: [] # anotaciones JPA crudas
163
- reference: # referencia semántica a otro agregado
161
+ validations: [] # JSR-303 annotations
162
+ annotations: [] # raw JPA annotations
163
+ reference: # semantic reference to another aggregate
164
164
  aggregate: Customer
165
165
  module: customers
166
- enumValues: [] # enum inline (alternativa a enums:)
166
+ enumValues: [] # inline enum (alternative to enums:)
167
167
  ```
168
168
 
169
- ### Matriz de visibilidad
169
+ ### Visibility matrix
170
170
 
171
- | Campo | Constructor creación | CreateDto/Command | Constructor completo | ResponseDto |
172
- |-------|---------------------|-------------------|----------------------|-------------|
171
+ | Field | Creation constructor | CreateDto/Command | Full constructor | ResponseDto |
172
+ |-------|---------------------|-------------------|------------------|-------------|
173
173
  | normal | ✅ | ✅ | ✅ | ✅ |
174
174
  | `readOnly: true` | ❌ | ❌ | ✅ | ✅ |
175
175
  | `hidden: true` | ✅ | ✅ | ✅ | ❌ |
@@ -177,31 +177,31 @@ fields:
177
177
 
178
178
  ### readOnly
179
179
 
180
- Marca un campo como calculado/derivado: se excluye del constructor de negocio y del `CreateDto`/`CreateCommand`, pero aparece en el constructor completo (reconstrucción desde persistencia) y en `ResponseDto`.
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
181
 
182
182
  ```yaml
183
183
  fields:
184
184
  - name: totalAmount
185
185
  type: BigDecimal
186
- readOnly: true # calculado de la suma de items
186
+ readOnly: true # calculated from the sum of items
187
187
  ```
188
188
 
189
- > Cuando un enum tiene `initialValue`, el campo correspondiente se trata como `readOnly` automáticamente.
189
+ > When an enum has `initialValue`, the corresponding field is automatically treated as `readOnly`.
190
190
 
191
191
  ### hidden
192
192
 
193
- Marca un campo como sensible: se incluye en creación pero NO aparece en `ResponseDto`.
193
+ Marks a field as sensitive: included on creation but does NOT appear in `ResponseDto`.
194
194
 
195
195
  ```yaml
196
196
  fields:
197
197
  - name: passwordHash
198
198
  type: String
199
- hidden: true # no exponer en API
199
+ hidden: true # do not expose in API
200
200
  ```
201
201
 
202
- ### annotations (JPA crudas)
202
+ ### annotations (raw JPA)
203
203
 
204
- Permite agregar anotaciones JPA personalizadas a la entidad JPA generada.
204
+ Allows adding custom JPA annotations to the generated JPA entity.
205
205
 
206
206
  ```yaml
207
207
  fields:
@@ -213,7 +213,7 @@ fields:
213
213
 
214
214
  ### reference
215
215
 
216
- Declara una referencia semántica a un campo de otro agregado. Genera un comentario Javadoc indicando la relación, sin crear dependencia de código.
216
+ Declares a semantic reference to a field in another aggregate. Generates a Javadoc comment indicating the relationship, without creating a code dependency.
217
217
 
218
218
  ```yaml
219
219
  fields:
@@ -224,7 +224,7 @@ fields:
224
224
  module: customers
225
225
  ```
226
226
 
227
- Genera en la entidad de dominio:
227
+ Generated in the domain entity:
228
228
 
229
229
  ```java
230
230
  /** @see customers.Customer */
@@ -233,9 +233,9 @@ private String customerId;
233
233
 
234
234
  ---
235
235
 
236
- ## 6. Validaciones JSR-303
236
+ ## 6. JSR-303 Validations
237
237
 
238
- Las validaciones se declaran en el campo y se aplican al `CreateCommand` y `CreateDto`. **No** se añaden a las entidades de dominio.
238
+ Validations are declared on the field and applied to `CreateCommand` and `CreateDto`. They are **not** added to domain entities.
239
239
 
240
240
  ```yaml
241
241
  fields:
@@ -243,34 +243,34 @@ fields:
243
243
  type: String
244
244
  validations:
245
245
  - type: NotBlank
246
- message: "El nombre es obligatorio"
246
+ message: "Name is required"
247
247
  - type: Size
248
248
  min: 2
249
249
  max: 100
250
250
  ```
251
251
 
252
- Genera import automático: `import jakarta.validation.constraints.*;`
252
+ Auto-generates import: `import jakarta.validation.constraints.*;`
253
253
 
254
- ### Parámetros soportados
254
+ ### Supported parameters
255
255
 
256
- | Parámetro | Descripción |
256
+ | Parameter | Description |
257
257
  |-----------|-------------|
258
- | `type` | Nombre de la anotación sin `@` (requerido) |
259
- | `message` | Mensaje de error personalizado |
260
- | `value` | Valor único (para `@Min`, `@Max`) |
261
- | `min` | Valor mínimo (para `@Size`, `@DecimalMin`) |
262
- | `max` | Valor máximo (para `@Size`, `@DecimalMax`) |
263
- | `regexp` | Expresión regular (para `@Pattern`) |
264
- | `integer` | Dígitos enteros (para `@Digits`) |
265
- | `fraction` | Dígitos decimales (para `@Digits`) |
266
- | `inclusive` | Inclusivo (para `@DecimalMin`, `@DecimalMax`) |
267
-
268
- ### Ejemplos por tipo
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`) |
267
+
268
+ ### Examples by type
269
269
 
270
270
  ```yaml
271
271
  # @NotBlank
272
272
  - type: NotBlank
273
- message: "Campo obligatorio"
273
+ message: "Field is required"
274
274
 
275
275
  # @NotNull
276
276
  - type: NotNull
@@ -283,7 +283,7 @@ Genera import automático: `import jakarta.validation.constraints.*;`
283
283
  # @Email
284
284
  - type: Email
285
285
 
286
- # @Min / @Max (para numéricos)
286
+ # @Min / @Max (for numeric fields)
287
287
  - type: Min
288
288
  value: 1
289
289
  - type: Max
@@ -292,7 +292,7 @@ Genera import automático: `import jakarta.validation.constraints.*;`
292
292
  # @Pattern
293
293
  - type: Pattern
294
294
  regexp: "^[A-Z]{2}[0-9]{6}$"
295
- message: "Formato inválido"
295
+ message: "Invalid format"
296
296
 
297
297
  # @DecimalMin / @DecimalMax
298
298
  - type: DecimalMin
@@ -309,56 +309,56 @@ Genera import automático: `import jakarta.validation.constraints.*;`
309
309
 
310
310
  ---
311
311
 
312
- ## 7. Auditoría
312
+ ## 7. Auditing
313
313
 
314
- ### Sintaxis
314
+ ### Syntax
315
315
 
316
316
  ```yaml
317
- # Nuevo (recomendado)
317
+ # New (recommended)
318
318
  audit:
319
- enabled: true # agrega createdAt, updatedAt
320
- trackUser: true # también agrega createdBy, updatedBy
319
+ enabled: true # adds createdAt, updatedAt
320
+ trackUser: true # also adds createdBy, updatedBy
321
321
 
322
- # Legacy (equivalente a audit.enabled: true, trackUser: false)
322
+ # Legacy (equivalent to audit.enabled: true, trackUser: false)
323
323
  auditable: true
324
324
  ```
325
325
 
326
- ### Herencia JPA generada
326
+ ### Generated JPA inheritance
327
327
 
328
- | Configuración | Clase base JPA |
328
+ | Configuration | JPA base class |
329
329
  |---------------|----------------|
330
- | Sin auditoría | sin herencia |
330
+ | No auditing | no inheritance |
331
331
  | `audit.enabled: true` | `extends AuditableEntity` |
332
332
  | `audit.trackUser: true` | `extends FullAuditableEntity` |
333
333
 
334
- ### Campos generados
334
+ ### Generated fields
335
335
 
336
- | Campo | `audit.enabled` | `audit.trackUser` | En ResponseDto |
336
+ | Field | `audit.enabled` | `audit.trackUser` | In ResponseDto |
337
337
  |-------|-----------------|-------------------|----------------|
338
338
  | `createdAt` | ✅ | ✅ | ✅ |
339
339
  | `updatedAt` | ✅ | ✅ | ✅ |
340
340
  | `createdBy` | ❌ | ✅ | ❌ |
341
341
  | `updatedBy` | ❌ | ✅ | ❌ |
342
342
 
343
- > `createdBy` y `updatedBy` son metadatos administrativos: nunca se exponen en DTOs de respuesta.
343
+ > `createdBy` and `updatedBy` are administrative metadata: they are never exposed in response DTOs.
344
344
 
345
- ### Infraestructura generada con `trackUser: true`
345
+ ### Infrastructure generated with `trackUser: true`
346
346
 
347
- Cuando se activa `trackUser`, eva4j genera automáticamente:
347
+ When `trackUser` is enabled, eva4j automatically generates:
348
348
 
349
- | Archivo | Propósito |
350
- |---------|-----------|
351
- | `UserContextHolder.java` | ThreadLocal para el usuario actual |
352
- | `UserContextFilter.java` | Captura el header `X-User` de cada request |
353
- | `AuditorAwareImpl.java` | Provee el usuario actual para JPA Auditing |
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 |
354
354
 
355
- La clase `Application.java` se configura con `@EnableJpaAuditing(auditorAwareRef = "auditorProvider")`.
355
+ `Application.java` is configured with `@EnableJpaAuditing(auditorAwareRef = "auditorProvider")`.
356
356
 
357
- ### Ejemplo
357
+ ### Example
358
358
 
359
359
  ```yaml
360
360
  entities:
361
- - name: order
361
+ - name: Order
362
362
  isRoot: true
363
363
  tableName: orders
364
364
  audit:
@@ -371,31 +371,31 @@ entities:
371
371
  type: BigDecimal
372
372
  ```
373
373
 
374
- > Los campos de auditoría **no se definen manualmente** en `fields:`; se heredan de la clase base JPA.
374
+ > Audit fields **must not be defined manually** in `fields:`; they are inherited from the JPA base class.
375
375
 
376
376
  ---
377
377
 
378
- ## 8. Relaciones
378
+ ## 8. Relationships
379
379
 
380
- ### Propiedades
380
+ ### Properties
381
381
 
382
- | Propiedad | Valores | Descripción |
383
- |-----------|---------|-------------|
384
- | `type` | `OneToMany`, `ManyToOne`, `OneToOne`, `ManyToMany` | Tipo de relación |
385
- | `target` / `targetEntity` | Nombre de entidad | Entidad relacionada |
386
- | `mappedBy` | nombre de campo | Lado inverso de la relación |
387
- | `joinColumn` | nombre de columna | Nombre de la FK |
388
- | `cascade` | array de `PERSIST`, `MERGE`, `REMOVE`, `REFRESH`, `DETACH`, `ALL` | Operaciones en cascada |
389
- | `fetch` | `LAZY` (default), `EAGER` | Estrategia de carga |
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
390
 
391
- ### Auto-generación del lado inverso
391
+ ### Automatic inverse side generation
392
392
 
393
- Cuando defines `OneToMany` con `mappedBy`, eva4j genera automáticamente el `@ManyToOne` en la entidad JPA del target. **No es necesario definir ambos lados.**
393
+ When you define `OneToMany` with `mappedBy`, eva4j automatically generates `@ManyToOne` in the target JPA entity. **Defining both sides is not required.**
394
394
 
395
395
  ```yaml
396
- # ✅ Solo esto es necesario
396
+ # ✅ Only this is needed
397
397
  entities:
398
- - name: order
398
+ - name: Order
399
399
  isRoot: true
400
400
  relationships:
401
401
  - type: OneToMany
@@ -404,13 +404,13 @@ entities:
404
404
  cascade: [PERSIST, MERGE, REMOVE]
405
405
  fetch: LAZY
406
406
 
407
- # Eva4j genera en OrderItemJpa:
407
+ # eva4j generates in OrderItemJpa:
408
408
  # @ManyToOne(fetch = FetchType.LAZY)
409
409
  # @JoinColumn(name = "order_id")
410
410
  # private OrderJpa order;
411
411
  ```
412
412
 
413
- > Si defines `ManyToOne` manualmente, esa definición tiene prioridad sobre la auto-generación.
413
+ > If you define `ManyToOne` manually, that definition takes priority over auto-generation.
414
414
 
415
415
  ### OneToMany
416
416
 
@@ -423,7 +423,7 @@ relationships:
423
423
  fetch: LAZY
424
424
  ```
425
425
 
426
- Genera en dominio:
426
+ Generated in domain:
427
427
 
428
428
  ```java
429
429
  private List<OrderItem> orderItems = new ArrayList<>();
@@ -431,7 +431,7 @@ public void addOrderItem(OrderItem item) { orderItems.add(item); }
431
431
  public void removeOrderItem(OrderItem item) { orderItems.remove(item); }
432
432
  ```
433
433
 
434
- ### ManyToOne (manual, cuando necesitas FK específica)
434
+ ### ManyToOne (manual, when you need a specific FK)
435
435
 
436
436
  ```yaml
437
437
  relationships:
@@ -444,7 +444,7 @@ relationships:
444
444
  ### OneToOne
445
445
 
446
446
  ```yaml
447
- # Lado con mappedBy (inverso)
447
+ # Inverse side (with mappedBy)
448
448
  relationships:
449
449
  - type: OneToOne
450
450
  target: OrderSummary
@@ -452,7 +452,7 @@ relationships:
452
452
  cascade: [PERSIST, MERGE]
453
453
  fetch: LAZY
454
454
 
455
- # Lado propietario (con FK)
455
+ # Owner side (with FK)
456
456
  relationships:
457
457
  - type: OneToOne
458
458
  target: Order
@@ -460,22 +460,22 @@ relationships:
460
460
  fetch: LAZY
461
461
  ```
462
462
 
463
- ### Cuándo definir ManyToOne manualmente
463
+ ### When to define ManyToOne manually
464
464
 
465
- | Escenario | ¿Definir ManyToOne? |
466
- |-----------|---------------------|
467
- | Relación estándar con `mappedBy` | ❌ Eva4j lo genera |
468
- | FK con nombre personalizado | ✅ Sí, para controlar `joinColumn` |
469
- | Múltiples FKs a la misma entidad | ✅ Sí, para nombres distintos |
470
- | Relación unidireccional (sin inverso) | ✅ |
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
471
 
472
- ### Cascade recomendado
472
+ ### Recommended cascade
473
473
 
474
474
  ```yaml
475
- # Hijo no tiene sentido sin padre incluir REMOVE
475
+ # Child has no meaning without parentinclude REMOVE
476
476
  cascade: [PERSIST, MERGE, REMOVE]
477
477
 
478
- # Hijo tiene ciclo de vida independiente
478
+ # Child has an independent lifecycle
479
479
  cascade: [PERSIST, MERGE]
480
480
  ```
481
481
 
@@ -483,7 +483,7 @@ cascade: [PERSIST, MERGE]
483
483
 
484
484
  ## 9. Value Objects
485
485
 
486
- Son objetos inmutables que representan conceptos de dominio sin identidad propia.
486
+ Immutable objects that represent domain concepts without their own identity.
487
487
 
488
488
  ```yaml
489
489
  valueObjects:
@@ -495,26 +495,26 @@ valueObjects:
495
495
  type: String
496
496
  ```
497
497
 
498
- Genera:
498
+ Generates:
499
499
 
500
- - `Money.java` – clase de dominio inmutable con constructor, getters, `equals()`, `hashCode()`
501
- - `MoneyJpa.java` – `@Embeddable` con Lombok
500
+ - `Money.java` – immutable domain class with constructor, getters, `equals()`, `hashCode()`
501
+ - `MoneyJpa.java` – `@Embeddable` with Lombok
502
502
 
503
- Uso en campo:
503
+ Usage in a field:
504
504
 
505
505
  ```yaml
506
506
  - name: totalAmount
507
- type: Money # detectado automáticamente como @Embedded
507
+ type: Money # automatically detected as @Embedded
508
508
  ```
509
509
 
510
- ### Lista de Value Objects
510
+ ### List of Value Objects
511
511
 
512
512
  ```yaml
513
513
  - name: addresses
514
514
  type: List<Address>
515
515
  ```
516
516
 
517
- Genera:
517
+ Generates:
518
518
 
519
519
  ```java
520
520
  @ElementCollection
@@ -525,9 +525,9 @@ private List<AddressJpa> addresses = new ArrayList<>();
525
525
 
526
526
  ---
527
527
 
528
- ## 10. Enums y transiciones de estado
528
+ ## 10. Enums and state transitions
529
529
 
530
- ### Enum simple
530
+ ### Simple enum
531
531
 
532
532
  ```yaml
533
533
  enums:
@@ -535,31 +535,31 @@ enums:
535
535
  values: [PENDING, CONFIRMED, SHIPPED, DELIVERED, CANCELLED]
536
536
  ```
537
537
 
538
- Genera `OrderStatus.java` con los valores enumerados. En JPA: `@Enumerated(EnumType.STRING)`.
538
+ Generates `OrderStatus.java` with the enumerated values. In JPA: `@Enumerated(EnumType.STRING)`.
539
539
 
540
- ### Enum con transiciones de estado
540
+ ### Enum with state transitions
541
541
 
542
- Las transiciones generan métodos de negocio en la entidad, lógica de validación en el enum y previenen estados inválidos.
542
+ Transitions generate business methods in the entity, validation logic in the enum, and prevent invalid states.
543
543
 
544
544
  ```yaml
545
545
  enums:
546
546
  - name: OrderStatus
547
- initialValue: PENDING # asigna valor inicial; campo queda readOnly
547
+ initialValue: PENDING # assigns an initial value; field becomes readOnly
548
548
  values: [PENDING, CONFIRMED, SHIPPED, DELIVERED, CANCELLED]
549
549
  transitions:
550
- - from: PENDING # puede ser string o [array]
550
+ - from: PENDING # can be a string or [array]
551
551
  to: CONFIRMED
552
- method: confirm # nombre del método generado en la entidad
552
+ method: confirm # name of the method generated in the entity
553
553
  - from: [PENDING, CONFIRMED]
554
554
  to: CANCELLED
555
555
  method: cancel
556
- guard: "this.status == OrderStatus.DELIVERED" # BusinessException si es true
556
+ guard: "this.status == OrderStatus.DELIVERED" # throws BusinessException if true
557
557
  - from: CONFIRMED
558
558
  to: SHIPPED
559
559
  method: ship
560
560
  ```
561
561
 
562
- #### Lo que genera en el Enum
562
+ #### What is generated in the Enum
563
563
 
564
564
  ```java
565
565
  private static final Map<OrderStatus, List<OrderStatus>> VALID_TRANSITIONS = Map.of(
@@ -579,9 +579,9 @@ public OrderStatus transitionTo(OrderStatus next) {
579
579
  }
580
580
  ```
581
581
 
582
- #### Lo que genera en la entidad raíz
582
+ #### What is generated in the aggregate root
583
583
 
584
- Un método por transición, más helpers `is*()` y `can*()`:
584
+ One method per transition, plus `is*()` and `can*()` helpers:
585
585
 
586
586
  ```java
587
587
  public void confirm() {
@@ -601,7 +601,7 @@ public boolean canConfirm() { return this.status.canTransitionTo(OrderStatus.CON
601
601
 
602
602
  ### `initialValue`
603
603
 
604
- Asigna un valor por defecto al campo de estado en el constructor de creación. El campo queda marcado como `readOnly` automáticamente (no aparece en `CreateDto`/`CreateCommand`).
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`).
605
605
 
606
606
  ```yaml
607
607
  enums:
@@ -611,7 +611,7 @@ enums:
611
611
 
612
612
  ### `guard`
613
613
 
614
- Condición Java evaluada en el método de transición. Si la expresión es `true`, se lanza `BusinessException`.
614
+ Java condition evaluated in the transition method. If the expression is `true`, a `BusinessException` is thrown.
615
615
 
616
616
  ```yaml
617
617
  - from: [PENDING, CONFIRMED]
@@ -622,15 +622,15 @@ Condición Java evaluada en el método de transición. Si la expresión es `true
622
622
 
623
623
  ---
624
624
 
625
- ## 11. Eventos de dominio
625
+ ## 11. Domain events
626
626
 
627
- Los eventos se declaran bajo el agregado (a mismo nivel que `entities:`, `enums:`, `valueObjects:`).
627
+ Events are declared under the aggregate (at the same level as `entities:`, `enums:`, `valueObjects:`).
628
628
 
629
629
  ```yaml
630
630
  aggregates:
631
631
  - name: Order
632
632
  events:
633
- - name: OrderPlaced # sufijo "Event" se agrega automáticamente
633
+ - name: OrderPlaced
634
634
  fields:
635
635
  - name: customerId
636
636
  type: String
@@ -641,29 +641,29 @@ aggregates:
641
641
  - name: reason
642
642
  type: String
643
643
  entities:
644
- - name: order
644
+ - name: Order
645
645
  # ...
646
646
  ```
647
647
 
648
- ### Archivos generados
648
+ ### Generated files
649
649
 
650
- | Archivo | Descripción |
651
- |---------|-------------|
652
- | `shared/domain/DomainEvent.java` | Clase base abstracta (generada una vez por proyecto) |
653
- | `domain/models/events/OrderPlacedEvent.java` | Evento concreto que extiende `DomainEvent` |
654
- | `domain/models/events/OrderCancelledEvent.java` | Evento concreto |
655
- | `raise()` / `pullDomainEvents()` en el agregado raíz | Infraestructura de eventos en la entidad |
656
- | `OrderRepositoryImpl.java` | Llama `eventPublisher.publishEvent()` al guardar |
657
- | `OrderDomainEventHandler.java` | Clase con `@TransactionalEventListener` por cada evento |
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
658
 
659
- ### Evento generado
659
+ ### Generated event
660
660
 
661
661
  ```java
662
- public final class OrderPlacedEvent extends DomainEvent {
662
+ public final class OrderPlaced extends DomainEvent {
663
663
  private final String customerId;
664
664
  private final BigDecimal totalAmount;
665
665
 
666
- public OrderPlacedEvent(String customerId, BigDecimal totalAmount) {
666
+ public OrderPlaced(String customerId, BigDecimal totalAmount) {
667
667
  this.customerId = customerId;
668
668
  this.totalAmount = totalAmount;
669
669
  }
@@ -672,15 +672,15 @@ public final class OrderPlacedEvent extends DomainEvent {
672
672
  }
673
673
  ```
674
674
 
675
- ### Cómo disparar el evento en la entidad
675
+ ### How to raise an event in the entity
676
676
 
677
677
  ```java
678
678
  public class Order {
679
679
  private final List<DomainEvent> domainEvents = new ArrayList<>();
680
680
 
681
681
  public void place(String customerId, BigDecimal totalAmount) {
682
- // lógica de negocio...
683
- raise(new OrderPlacedEvent(customerId, totalAmount));
682
+ // business logic...
683
+ raise(new OrderPlaced(customerId, totalAmount));
684
684
  }
685
685
 
686
686
  protected void raise(DomainEvent event) {
@@ -697,15 +697,15 @@ public class Order {
697
697
 
698
698
  ---
699
699
 
700
- ## 12. Múltiples agregados
700
+ ## 12. Multiple aggregates
701
701
 
702
- Un `domain.yaml` puede contener varios agregados. Cada uno genera su propio conjunto de archivos.
702
+ A `domain.yaml` can contain multiple aggregates. Each one generates its own set of files.
703
703
 
704
704
  ```yaml
705
705
  aggregates:
706
706
  - name: Customer
707
707
  entities:
708
- - name: customer
708
+ - name: Customer
709
709
  isRoot: true
710
710
  fields:
711
711
  - name: id
@@ -715,7 +715,7 @@ aggregates:
715
715
 
716
716
  - name: Product
717
717
  entities:
718
- - name: product
718
+ - name: Product
719
719
  isRoot: true
720
720
  fields:
721
721
  - name: id
@@ -727,59 +727,59 @@ aggregates:
727
727
  values: [ELECTRONICS, CLOTHING, FOOD]
728
728
  ```
729
729
 
730
- > Los enums y Value Objects son locales al agregado donde se definen. Si dos agregados necesitan el mismo VO, se debe declarar en cada uno.
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
731
 
732
732
  ---
733
733
 
734
- ## 13. Archivos generados
734
+ ## 13. Generated files
735
735
 
736
- Por cada agregado se generan aproximadamente los siguientes archivos:
736
+ For each aggregate, approximately the following files are generated:
737
737
 
738
- | Archivo | Capa | Descripción |
739
- |---------|------|-------------|
740
- | `{Root}.java` | Domain | Entidad raíz del agregado |
741
- | `{Entity}.java` | Domain | Entidades secundarias |
738
+ | File | Layer | Description |
739
+ |------|-------|-------------|
740
+ | `{Root}.java` | Domain | Aggregate root entity |
741
+ | `{Entity}.java` | Domain | Secondary entities |
742
742
  | `{Vo}.java` | Domain | Value Objects |
743
- | `{Enum}.java` | Domain | Enums (con VALID_TRANSITIONS si hay transiciones) |
744
- | `{Root}Repository.java` | Domain | Interfaz de repositorio (puerto) |
745
- | `Create{Root}Command.java` | Application | Comando de creación |
746
- | `Create{Root}CommandHandler.java` | Application | Handler del comando |
747
- | `Get{Root}Query.java` | Application | Query por ID |
748
- | `Get{Root}QueryHandler.java` | Application | Handler de query |
749
- | `List{Root}Query.java` | Application | Query paginada |
750
- | `List{Root}QueryHandler.java` | Application | Handler de lista |
751
- | `{Root}ResponseDto.java` | Application | DTO de respuesta |
752
- | `Create{Root}Dto.java` | Application | DTO de creación |
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
753
  | `{Root}ApplicationMapper.java` | Application | Mapper Command/DTO ↔ Domain |
754
- | `{Root}Jpa.java` | Infrastructure | Entidad JPA |
755
- | `{Entity}Jpa.java` | Infrastructure | Entidades secundarias JPA |
756
- | `{Vo}Jpa.java` | Infrastructure | Value Objects JPA (@Embeddable) |
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
757
  | `{Root}Mapper.java` | Infrastructure | Mapper Domain ↔ JPA |
758
- | `{Root}JpaRepository.java` | Infrastructure | Repositorio Spring Data |
759
- | `{Root}RepositoryImpl.java` | Infrastructure | Implementación del repositorio |
760
- | `{Root}Controller.java` | Infrastructure | Controlador REST |
758
+ | `{Root}JpaRepository.java` | Infrastructure | Spring Data repository |
759
+ | `{Root}RepositoryImpl.java` | Infrastructure | Repository implementation |
760
+ | `{Root}Controller.java` | Infrastructure | REST controller |
761
761
 
762
- ### Endpoints REST generados
762
+ ### Generated REST endpoints
763
763
 
764
- | Método | Ruta | Descripción |
764
+ | Method | Path | Description |
765
765
  |--------|------|-------------|
766
- | `POST` | `/api/{module}/{entity}` | Crear |
767
- | `GET` | `/api/{module}/{entity}/{id}` | Obtener por ID |
768
- | `GET` | `/api/{module}/{entity}?page=0&size=20` | Listar paginado |
769
- | `PUT` | `/api/{module}/{entity}/{id}` | Actualizar |
770
- | `DELETE` | `/api/{module}/{entity}/{id}` | Eliminar |
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
771
 
772
772
  ---
773
773
 
774
- ## 14. Ejemplos completos
774
+ ## 14. Complete examples
775
775
 
776
- ### Ejemplo 1: Pedido con transiciones y eventos
776
+ ### Example 1: Order with transitions and events
777
777
 
778
778
  ```yaml
779
779
  aggregates:
780
780
  - name: Order
781
781
  entities:
782
- - name: order
782
+ - name: Order
783
783
  isRoot: true
784
784
  tableName: orders
785
785
  audit:
@@ -804,7 +804,7 @@ aggregates:
804
804
  cascade: [PERSIST, MERGE, REMOVE]
805
805
  fetch: LAZY
806
806
 
807
- - name: orderItem
807
+ - name: OrderItem
808
808
  tableName: order_items
809
809
  fields:
810
810
  - name: id
@@ -846,13 +846,13 @@ aggregates:
846
846
  type: String
847
847
  ```
848
848
 
849
- ### Ejemplo 2: Usuario con auditoría y campo sensible
849
+ ### Example 2: User with auditing and a sensitive field
850
850
 
851
851
  ```yaml
852
852
  aggregates:
853
853
  - name: User
854
854
  entities:
855
- - name: user
855
+ - name: User
856
856
  isRoot: true
857
857
  tableName: users
858
858
  audit:
@@ -889,2173 +889,21 @@ aggregates:
889
889
 
890
890
  ---
891
891
 
892
- ## 15. Prerequisitos y errores comunes
892
+ ## 15. Prerequisites and common errors
893
893
 
894
- ### Prerequisitos
894
+ ### Prerequisites
895
895
 
896
- - Proyecto creado con `eva create`
897
- - Módulo existente (`eva add module <module>`)
898
- - Archivo `domain.yaml` en `src/main/java/<package>/<module>/`
896
+ - Project created with `eva create`
897
+ - Existing module (`eva add module <module>`)
898
+ - `domain.yaml` file at `src/main/java/<package>/<module>/`
899
899
 
900
- ### Errores comunes
900
+ ### Common errors
901
901
 
902
- | Error | Causa | Solución |
902
+ | Error | Cause | Solution |
903
903
  |-------|-------|----------|
904
- | `Module does not exist` | El módulo no fue creado | Ejecutar `eva add module <module>` |
905
- | `YAML file not found` | No existe `domain.yaml` en la ruta correcta | Verificar `src/main/java/<pkg>/<module>/domain.yaml` |
906
- | `Invalid relationship target` | El target no está definido en el mismo YAML | Definir la entidad target en el mismo `domain.yaml` |
907
- | `Column 'x_id' is duplicated` | ManyToOne definido manualmente + auto-generado | Eliminar el ManyToOne manual; dejar que eva4j lo genere |
908
- | Archivo no regenerado | El archivo fue modificado manualmente (checksum) | Usar `--force` para sobreescribir |
909
- | Import errors | Campo `type` no coincide con nombre en `enums:` o `valueObjects:` | Verificar que los nombres coincidan exactamente |
910
-
911
- ## 💡 Examples
912
-
913
- ### Example 1: Simple Customer Aggregate
914
-
915
- **File:** `examples/customer.yaml`
916
-
917
- ```yaml
918
- module: customer
919
-
920
- aggregates:
921
- - name: Customer
922
- tableName: customers
923
- auditable: true
924
-
925
- entities:
926
- - name: customer
927
- isRoot: true
928
- fields:
929
- - name: id
930
- type: Long
931
- - name: firstName
932
- type: String
933
- validations:
934
- - "@NotBlank"
935
- - "@Size(max = 100)"
936
- - name: email
937
- type: String
938
- validations:
939
- - "@Email"
940
- - name: status
941
- type: CustomerStatus
942
- ```
943
-
944
- **Generate:**
945
- ```bash
946
- eva4j g entities customer
947
- ```
948
-
949
- ### Example 2: Complex Order with Relations
950
-
951
- **File:** `examples/order.yaml`
952
-
953
- ```yaml
954
- module: order
955
-
956
- aggregates:
957
- - name: Order
958
- tableName: orders
959
- auditable: true
960
-
961
- entities:
962
- - name: order
963
- isRoot: true
964
- fields:
965
- - name: id
966
- type: Long
967
- - name: orderNumber
968
- type: String
969
- - name: totalAmount
970
- type: BigDecimal
971
- - name: status
972
- type: OrderStatus
973
- relationships:
974
- - type: OneToMany
975
- target: OrderItem
976
- mappedBy: order
977
- cascade: ALL
978
- fetch: LAZY
979
-
980
- - name: orderItem
981
- isRoot: false
982
- tableName: order_items
983
- fields:
984
- - name: id
985
- type: Long
986
- - name: quantity
987
- type: Integer
988
- - name: unitPrice
989
- type: BigDecimal
990
- relationships:
991
- - type: ManyToOne
992
- target: Order
993
- fetch: LAZY
994
-
995
- enums:
996
- - name: OrderStatus
997
- values:
998
- - PENDING
999
- - CONFIRMED
1000
- - SHIPPED
1001
- - DELIVERED
1002
- - CANCELLED
1003
- ```
1004
-
1005
- **Generate:**
1006
- ```bash
1007
- eva4j g entities order
1008
- ```
1009
-
1010
- ### Example 3: With Value Objects
1011
-
1012
- **File:** `examples/evaluation.yaml`
1013
-
1014
- ```yaml
1015
- module: evaluation
1016
-
1017
- aggregates:
1018
- - name: Evaluation
1019
- tableName: evaluations
1020
-
1021
- entities:
1022
- - name: evaluation
1023
- isRoot: true
1024
- fields:
1025
- - name: id
1026
- type: String
1027
- - name: score
1028
- type: Integer
1029
- relationships:
1030
- - type: OneToMany
1031
- target: EvaluationDoctor
1032
- cascade: ALL
1033
-
1034
- - name: evaluationDoctor
1035
- isRoot: false
1036
- fields:
1037
- - name: id
1038
- type: Long
1039
- - name: degrees
1040
- type: List<Degrees>
1041
-
1042
- valueObjects:
1043
- - name: Degrees
1044
- fields:
1045
- - name: title
1046
- type: String
1047
- - name: institution
1048
- type: String
1049
- - name: year
1050
- type: Integer
1051
- - name: typeDegrees
1052
- type: TypeDegrees
1053
-
1054
- enums:
1055
- - name: TypeDegrees
1056
- values:
1057
- - BACHELOR
1058
- - MASTER
1059
- - PHD
1060
- ```
1061
-
1062
- **Generate:**
1063
- ```bash
1064
- eva4j g entities evaluation
1065
- ```
1066
-
1067
- ## 📦 Generated Code Structure
1068
-
1069
- ```
1070
- src/main/java/com/example/project/<module>/
1071
- ├── domain/
1072
- │ ├── models/
1073
- │ │ ├── Customer.java # Domain entity (root)
1074
- │ │ ├── OrderItem.java # Domain entity (secondary)
1075
- │ │ ├── valueobjects/
1076
- │ │ │ └── Degrees.java # Value object
1077
- │ │ └── enums/
1078
- │ │ └── OrderStatus.java # Enum
1079
- │ └── repositories/
1080
- │ └── CustomerRepository.java # Repository port (interface)
1081
-
1082
- ├── application/
1083
- │ ├── commands/
1084
- │ │ ├── CreateCustomerCommand.java # Create command
1085
- │ │ └── CreateCustomerCommandHandler.java # Command handler
1086
- │ ├── queries/
1087
- │ │ ├── GetCustomerQuery.java # Get query
1088
- │ │ ├── GetCustomerQueryHandler.java # Get handler
1089
- │ │ ├── ListCustomersQuery.java # List query
1090
- │ │ └── ListCustomersQueryHandler.java # List handler
1091
- │ ├── dtos/
1092
- │ │ ├── CreateCustomerDto.java # Create DTO
1093
- │ │ ├── CreateOrderItemDto.java # Nested entity DTO
1094
- │ │ └── CustomerResponseDto.java # Response DTO
1095
- │ └── mappers/
1096
- │ └── CustomerApplicationMapper.java # Application mapper (Command/DTO → Domain)
1097
-
1098
- └── infrastructure/
1099
- ├── database/
1100
- │ ├── entities/
1101
- │ │ ├── CustomerJpa.java # JPA entity (root)
1102
- │ │ ├── OrderItemJpa.java # JPA entity (secondary)
1103
- │ │ └── valueobjects/
1104
- │ │ └── DegreesJpa.java # JPA value object
1105
- │ ├── repositories/
1106
- │ │ ├── CustomerJpaRepository.java # Spring Data repository
1107
- │ │ └── CustomerRepositoryImpl.java # Repository implementation
1108
- │ └── mappers/
1109
- │ └── CustomerMapper.java # Infrastructure mapper (Domain ↔ JPA)
1110
- └── rest/
1111
- └── controllers/
1112
- └── CustomerController.java # REST controller with CRUD endpoints
1113
- ```
1114
-
1115
- ## ✨ Features
1116
-
1117
- ### 1. Domain Layer (Pure Business Logic)
1118
- - ✅ **Entities** - Aggregate root and secondary entities
1119
- - ✅ **Value Objects** - Immutable value types with `@Embedded` support
1120
- - ✅ **Enums** - Type-safe enumerations
1121
- - ✅ **Repository Interfaces** - Ports for persistence
1122
-
1123
- ### 2. Application Layer (Use Cases - CQRS)
1124
- - ✅ **Commands** - `CreateCustomerCommand` with validation
1125
- - ✅ **CommandHandlers** - Business logic orchestration
1126
- - ✅ **Queries** - `GetCustomerQuery`, `ListCustomersQuery`
1127
- - ✅ **QueryHandlers** - Read operations with pagination
1128
- - ✅ **DTOs** - Request/Response data transfer objects
1129
- - ✅ **Application Mappers** - Command/DTO → Domain transformations
1130
-
1131
- ### 3. Infrastructure Layer (Technical Details)
1132
- - ✅ **JPA Entities** - Persistence annotations (`@Entity`, `@Table`)
1133
- - ✅ **JPA Repositories** - Spring Data JPA implementation
1134
- - ✅ **Infrastructure Mappers** - Domain ↔ JPA bidirectional mapping
1135
- - ✅ **REST Controllers** - CRUD endpoints (`POST`, `GET`, `GET list`)
1136
-
1137
- ### 4. Advanced Capabilities
1138
- - ✅ **Relationships** - OneToMany, ManyToOne, OneToOne, ManyToMany
1139
- - ✅ **Nested Entities** - Secondary entities with their own relationships
1140
- - ✅ **Value Object Collections** - `List<ValueObject>` with `@ElementCollection`
1141
- - ✅ **Auditing** - `@CreatedDate`, `@LastModifiedDate` when `auditable: true`
1142
- - ✅ **Cascade Operations** - Configurable cascade types
1143
- - ✅ **Fetch Strategies** - LAZY/EAGER configuration
1144
- - ✅ **Validations** - Bean Validation annotations
1145
- - ✅ **Pagination** - Built-in pagination support for list queries
1146
-
1147
- ## 🔄 Supported Relationships
1148
-
1149
- ### OneToMany / ManyToOne (Bidirectional)
1150
-
1151
- ```yaml
1152
- # Parent entity
1153
- entities:
1154
- - name: order
1155
- relationships:
1156
- - type: OneToMany
1157
- target: OrderItem
1158
- mappedBy: order # Field in OrderItem that owns the relationship
1159
- cascade: ALL
1160
- fetch: LAZY
1161
-
1162
- # Child entity
1163
- - name: orderItem
1164
- relationships:
1165
- - type: ManyToOne
1166
- target: Order
1167
- fetch: LAZY
1168
- ```
1169
-
1170
- ### OneToOne
1171
-
1172
- ```yaml
1173
- entities:
1174
- - name: user
1175
- relationships:
1176
- - type: OneToOne
1177
- target: UserProfile
1178
- cascade: ALL
1179
-
1180
- - name: userProfile
1181
- relationships:
1182
- - type: OneToOne
1183
- target: User
1184
- ```
1185
-
1186
- ### ManyToMany
1187
-
1188
- ```yaml
1189
- entities:
1190
- - name: student
1191
- relationships:
1192
- - type: ManyToMany
1193
- target: Course
1194
- cascade: PERSIST
1195
-
1196
- - name: course
1197
- relationships:
1198
- - type: ManyToMany
1199
- target: Student
1200
- mappedBy: courses
1201
- ```
1202
-
1203
- ### Relations Between Secondary Entities
1204
-
1205
- ```yaml
1206
- entities:
1207
- - name: evaluationDoctor
1208
- relationships:
1209
- - type: OneToMany
1210
- target: EvaluationBranch # Another secondary entity
1211
- cascade: ALL
1212
-
1213
- - name: evaluationBranch
1214
- relationships:
1215
- - type: ManyToOne
1216
- target: EvaluationDoctor
1217
- ```
1218
-
1219
- ## 🎯 Supported Data Types
1220
-
1221
- ### Primitive Types
1222
- - `String`, `Integer`, `Long`, `Double`, `Float`, `Boolean`
1223
- - `BigDecimal`, `BigInteger`
1224
- - `LocalDate`, `LocalDateTime`, `LocalTime`
1225
- - `ZonedDateTime`, `Instant`
1226
-
1227
- ### Collections
1228
- - `List<ValueObject>` - Generates `@ElementCollection`
1229
- - `List<Entity>` - Generates `@OneToMany`
1230
-
1231
- ### Custom Types
1232
- - Value Objects (defined in `valueObjects` section)
1233
- - Enums (defined in `enums` section)
1234
-
1235
- ## 🚀 Next Steps
1236
-
1237
- After generating entities:
1238
-
1239
- 1. **Review generated code:**
1240
- ```bash
1241
- # Check domain models
1242
- cat src/main/java/com/example/project/<module>/domain/models/*.java
1243
- ```
1244
-
1245
- 2. **Add business logic:**
1246
- - Edit domain entities to add business methods
1247
- - Implement domain validations
1248
- - Add domain events if needed
1249
-
1250
- 3. **Test the API:**
1251
- ```bash
1252
- ./gradlew bootRun
1253
- # POST http://localhost:8080/api/<module>/<entity>
1254
- # GET http://localhost:8080/api/<module>/<entity>/{id}
1255
- # GET http://localhost:8080/api/<module>/<entity>
1256
- ```
1257
-
1258
- 4. **Extend functionality:**
1259
- ```bash
1260
- eva4j g usecase UpdateCustomer --type command
1261
- eva4j g usecase DeleteCustomer --type command
1262
- ```
1263
-
1264
- ## ⚠️ Prerequisites
1265
-
1266
- - Be in a project created with `eva4j create`
1267
- - Module must exist (created with `eva4j add module`)
1268
- - YAML file must exist at `examples/<aggregate-name>.yaml`
1269
-
1270
- ## 🔍 Validations
1271
-
1272
- The command validates:
1273
- - ✅ Valid eva4j project
1274
- - ✅ Target module exists
1275
- - ✅ YAML file exists and is valid
1276
- - ✅ No syntax errors in YAML
1277
- - ✅ Entity names are unique
1278
- - ✅ Relationship targets exist
1279
- - ✅ Field types are valid
1280
-
1281
- ## 📚 See Also
1282
-
1283
- - [DOMAIN_YAML_GUIDE.md](../../DOMAIN_YAML_GUIDE.md) - Complete YAML syntax reference
1284
- - [add-module](./ADD_MODULE.md) - Create modules
1285
- - [generate-usecase](./GENERATE_USECASE.md) - Add more use cases
1286
-
1287
- ## 🐛 Troubleshooting
1288
-
1289
- **Error: "YAML file not found"**
1290
- - Solution: Create `examples/<aggregate-name>.yaml` file first
1291
-
1292
- **Error: "Module does not exist"**
1293
- - Solution: Run `eva4j add module <module-name>` first
1294
-
1295
- **Error: "Invalid relationship target"**
1296
- - Solution: Ensure the target entity is defined in the same aggregate
1297
-
1298
- **Import errors after generation**
1299
- - Solution: This has been fixed in recent versions. Make sure you're using eva4j 1.0.3+
1300
- - If still happening, check that field types match defined ValueObjects/Enums
1301
-
1302
- **Compilation errors with List<ValueObject>**
1303
- - Solution: Updated in latest version to use `List<ValueObjectJpa>` in JPA entities
1304
- - Mapper name: `OrderMapper.java`
1305
- - File organization
1306
- - Generated code references
1307
-
1308
- ---
1309
-
1310
- ## Entities
1311
-
1312
- ### Root Entity (Aggregate Root)
1313
-
1314
- The root entity is the entry point to the aggregate. All operations must go through it.
1315
-
1316
- **⚠️ Important**: The root entity is defined within the `entities` array with `isRoot: true`.
1317
-
1318
- ```yaml
1319
- aggregates:
1320
- - name: Order
1321
- entities:
1322
- - name: order # Entity name (camelCase or snake_case)
1323
- isRoot: true # ← REQUIRED to mark the root
1324
- tableName: orders # Table name in DB (optional)
1325
-
1326
- fields:
1327
- - name: id
1328
- type: String # String generates UUID, Long generates IDENTITY
1329
-
1330
- - name: orderNumber
1331
- type: String
1332
-
1333
- - name: status
1334
- type: OrderStatus # Reference to an enum
1335
-
1336
- - name: totalAmount
1337
- type: Money # Reference to a value object
1338
-
1339
- - name: createdAt
1340
- type: LocalDateTime
1341
-
1342
- relationships:
1343
- - type: OneToMany
1344
- target: OrderItem
1345
- mappedBy: order
1346
- cascade: [PERSIST, MERGE, REMOVE]
1347
- fetch: LAZY
1348
- ```
1349
-
1350
- ### Secondary Entities
1351
-
1352
- Entities that belong to the aggregate but are not the root. They are defined in the same `entities` array **without** `isRoot` (or with `isRoot: false`).
1353
-
1354
- ```yaml
1355
- aggregates:
1356
- - name: Order
1357
- entities:
1358
- # ... root entity order with isRoot: true ...
1359
-
1360
- - name: orderItem # ← Secondary entity
1361
- tableName: order_items
1362
- # Without isRoot or isRoot: false = secondary
1363
-
1364
- fields:
1365
- - name: id
1366
- type: Long
1367
-
1368
- - name: productId
1369
- type: String
1370
-
1371
- - name: quantity
1372
- type: Integer
1373
-
1374
- - name: unitPrice
1375
- type: Money
1376
-
1377
- relationships:
1378
- - type: ManyToOne
1379
- target: Order
1380
- joinColumn: order_id
1381
- fetch: LAZY
1382
- ```
1383
-
1384
- ### Fields
1385
-
1386
- #### Syntax
1387
-
1388
- ```yaml
1389
- fields:
1390
- - name: fieldName # Field name (camelCase) - REQUIRED
1391
- type: String # Java data type - REQUIRED
1392
- ```
1393
-
1394
- **Supported properties:**
1395
- - `name`: Field name (required)
1396
- - `type`: Java data type (required)
1397
-
1398
- #### Automatic Type Detection
1399
-
1400
- eva4j automatically detects field types based **only** on `type`:
1401
-
1402
- **✅ Value Objects** - Automatically detected
1403
- ```yaml
1404
- fields:
1405
- - name: totalAmount
1406
- type: Money # If Money is in valueObjects → automatic @Embedded
1407
- ```
1408
-
1409
- **✅ Enums** - Automatically detected
1410
- ```yaml
1411
- fields:
1412
- - name: status
1413
- type: OrderStatus # If OrderStatus is in enums → @Enumerated(STRING)
1414
- ```
1415
-
1416
- **✅ Primitive types**
1417
- ```yaml
1418
- fields:
1419
- - name: name
1420
- type: String # → VARCHAR
1421
- - name: age
1422
- type: Integer # → INTEGER
1423
- - name: price
1424
- type: BigDecimal # → DECIMAL
1425
- ```
1426
-
1427
- **✅ Date types** - Automatically imported
1428
- ```yaml
1429
- fields:
1430
- - name: createdAt
1431
- type: LocalDateTime # → timestamp + import java.time.LocalDateTime
1432
- ```
1433
-
1434
- **✅ Collections** - Automatic @ElementCollection
1435
- ```yaml
1436
- fields:
1437
- - name: tags
1438
- type: List<String> # → @ElementCollection with secondary table
1439
- ```
1440
-
1441
- #### ❌ NO need to specify
1442
-
1443
- eva4j automatically generates the correct JPA annotations:
1444
- - `@Embedded` for Value Objects
1445
- - `@Enumerated(EnumType.STRING)` for Enums
1446
- - `@ElementCollection` for lists
1447
- - Required imports
1448
-
1449
- #### ⚠️ MANDATORY RULE: `id` Field
1450
-
1451
- **All entities MUST have a field named exactly `id`.**
1452
-
1453
- ```yaml
1454
- # ✅ CORRECT - All entities have 'id'
1455
- entities:
1456
- - name: order
1457
- isRoot: true
1458
- fields:
1459
- - name: id # ← REQUIRED
1460
- type: String # String = UUID, Long = IDENTITY
1461
- - name: orderNumber
1462
- type: String
1463
-
1464
- - name: orderItem
1465
- fields:
1466
- - name: id # ← REQUIRED also in secondary entities
1467
- type: Long
1468
- - name: productId
1469
- type: String
1470
- ```
1471
-
1472
- **Reasons:**
1473
- - ✅ JPA requires `@Id` in all entities
1474
- - ✅ Eva4j automatically generates `@Id` and `@GeneratedValue` for the `id` field
1475
- - ✅ Clear and consistent convention across the domain
1476
-
1477
- **Supported types for `id`:**
1478
- - `String` → Generates `@GeneratedValue(strategy = GenerationType.UUID)`
1479
- - `Long` → Generates `@GeneratedValue(strategy = GenerationType.IDENTITY)`
1480
-
1481
- **❌ INCORRECT:**
1482
- ```yaml
1483
- # ❌ Without 'id' field - Application will fail
1484
- fields:
1485
- - name: orderNumber
1486
- type: String
1487
- # ← Missing 'id' field
1488
-
1489
- # ❌ Different name - Won't work
1490
- fields:
1491
- - name: orderId # ← Must be named exactly 'id'
1492
- type: String
1493
- ```
1494
-
1495
- **💡 Business Identifiers:**
1496
-
1497
- If you need a business identifier in addition to the technical ID:
1498
-
1499
- ```yaml
1500
- fields:
1501
- - name: id # ← Technical ID (required)
1502
- type: String
1503
- - name: orderNumber # ← Business ID (optional)
1504
- type: String
1505
- - name: invoiceNumber # ← Another business identifier
1506
- type: String
1507
- ```
1508
-
1509
- ---
1510
-
1511
- #### Correct Examples
1512
-
1513
- ```yaml
1514
- # Value Object
1515
- fields:
1516
- - name: totalAmount
1517
- type: Money # ✅ Sufficient - eva4j automatically detects
1518
-
1519
- # Enum
1520
- fields:
1521
- - name: status
1522
- type: OrderStatus # ✅ Sufficient - eva4j automatically detects
1523
-
1524
- # Primitive type
1525
- fields:
1526
- - name: description
1527
- type: String # ✅ Basic type
1528
-
1529
- # Collection
1530
- fields:
1531
- - name: tags
1532
- type: List<String> # ✅ Automatic @ElementCollection
1533
- ```
1534
-
1535
- ---
1536
-
1537
- ### Automatic Auditing
1538
-
1539
- eva4j supports automatic entity auditing using the `auditable` property. When set to `true`, the entity will automatically include creation and modification date fields.
1540
-
1541
- #### Syntax
1542
-
1543
- ```yaml
1544
- entities:
1545
- - name: order
1546
- isRoot: true
1547
- auditable: true # ← Activates automatic auditing
1548
- fields:
1549
- - name: orderNumber
1550
- type: String
1551
- ```
1552
-
1553
- #### What `auditable: true` Generates
1554
-
1555
- **In the domain entity (`Order.java`):**
1556
- ```java
1557
- public class Order {
1558
- private String orderNumber;
1559
- private LocalDateTime createdAt; // ← Automatically added
1560
- private LocalDateTime updatedAt; // ← Automatically added
1561
-
1562
- // getters/setters automatically generated
1563
- }
1564
- ```
1565
-
1566
- **In the JPA entity (`OrderJpa.java`):**
1567
- ```java
1568
- @Entity
1569
- @Table(name = "orders")
1570
- public class OrderJpa extends AuditableEntity { // ← Extends base class
1571
- @Id
1572
- @GeneratedValue(strategy = GenerationType.UUID)
1573
- private String orderNumber;
1574
-
1575
- // createdAt/updatedAt fields inherited from AuditableEntity
1576
- }
1577
- ```
1578
-
1579
- **Generated base class (`AuditableEntity.java`):**
1580
- ```java
1581
- @MappedSuperclass
1582
- @EntityListeners(AuditingEntityListener.class)
1583
- public abstract class AuditableEntity {
1584
-
1585
- @CreatedDate
1586
- @Column(name = "created_at", nullable = false, updatable = false)
1587
- private LocalDateTime createdAt;
1588
-
1589
- @LastModifiedDate
1590
- @Column(name = "updated_at", nullable = false)
1591
- private LocalDateTime updatedAt;
1592
-
1593
- // getters/setters
1594
- }
1595
- ```
1596
-
1597
- #### Features
1598
-
1599
- ✅ **Fully automatic**: Timestamps update without additional code
1600
- ✅ **Entity level**: Can be enabled for specific entities
1601
- ✅ **Spring Data JPA**: Uses `@CreatedDate` and `@LastModifiedDate`
1602
- ✅ **Mapper included**: Audit fields are automatically mapped between domain and JPA
1603
-
1604
- #### Required Configuration
1605
-
1606
- The Spring Boot application already has JPA auditing enabled in the main class:
1607
-
1608
- ```java
1609
- @SpringBootApplication
1610
- @EnableJpaAuditing // ← Already configured by eva4j
1611
- public class Application {
1612
- public static void main(String[] args) {
1613
- SpringApplication.run(Application.class, args);
1614
- }
1615
- }
1616
- ```
1617
-
1618
- #### Complete Example
1619
-
1620
- ```yaml
1621
- aggregates:
1622
- - name: Product
1623
- entities:
1624
- - name: product
1625
- isRoot: true
1626
- auditable: true # ← Enables auditing
1627
- fields:
1628
- - name: productId
1629
- type: String
1630
- - name: name
1631
- type: String
1632
- - name: price
1633
- type: BigDecimal
1634
- # createdAt and updatedAt are automatically added
1635
-
1636
- - name: review
1637
- auditable: true # ← Secondary entities can also have auditing
1638
- fields:
1639
- - name: reviewId
1640
- type: Long
1641
- - name: comment
1642
- type: String
1643
- relationships:
1644
- - type: ManyToOne
1645
- target: product
1646
- fetch: LAZY
1647
- joinColumn: product_id
1648
- ```
1649
-
1650
- **Resultado en la tabla:**
1651
- ```sql
1652
- CREATE TABLE products (
1653
- product_id VARCHAR(36) PRIMARY KEY,
1654
- name VARCHAR(255),
1655
- price DECIMAL(19,2),
1656
- created_at TIMESTAMP NOT NULL, -- ← Automático
1657
- updated_at TIMESTAMP NOT NULL -- ← Automático
1658
- );
1659
-
1660
- CREATE TABLE reviews (
1661
- review_id BIGINT PRIMARY KEY AUTO_INCREMENT,
1662
- comment TEXT,
1663
- product_id VARCHAR(36),
1664
- created_at TIMESTAMP NOT NULL, -- ← Automático
1665
- updated_at TIMESTAMP NOT NULL, -- ← Automático
1666
- FOREIGN KEY (product_id) REFERENCES products(product_id)
1667
- );
1668
- ```
1669
-
1670
- #### Notas importantes
1671
-
1672
- - ✅ `auditable` es **opcional** - por defecto es `false`
1673
- - ✅ Puede usarse en **entidad raíz** o **entidades secundarias**
1674
- - ✅ Los campos `createdAt` y `updatedAt` **no deben** definirse manualmente en `fields`
1675
- - ✅ El tipo es siempre `LocalDateTime`
1676
- - ❌ **No incluye** auditoría de usuario (createdBy/updatedBy) - ver [FUTURE_FEATURES.md](FUTURE_FEATURES.md) para esa funcionalidad
1677
-
1678
- ---
1679
-
1680
- ## Value Objects
1681
-
1682
- Los Value Objects son objetos inmutables que representan conceptos del dominio sin identidad propia.
1683
-
1684
- ### Definición básica
1685
-
1686
- ```yaml
1687
- valueObjects:
1688
- - name: Money
1689
- fields:
1690
- - name: amount
1691
- type: BigDecimal
1692
-
1693
- - name: currency
1694
- type: String
1695
- ```
1696
-
1697
- ### Generated Value Object (Domain)
1698
-
1699
- ```java
1700
- public class Money {
1701
- private final BigDecimal amount;
1702
- private final String currency;
1703
-
1704
- public Money(BigDecimal amount, String currency) {
1705
- this.amount = amount;
1706
- this.currency = currency;
1707
- }
1708
-
1709
- // Getters
1710
- public BigDecimal getAmount() { return amount; }
1711
- public String getCurrency() { return currency; }
1712
-
1713
- // equals() and hashCode() based on all fields
1714
- }
1715
- ```
1716
-
1717
- ### Value Object JPA (@Embeddable)
1718
-
1719
- ```java
1720
- @Embeddable
1721
- public class MoneyJpa {
1722
- private BigDecimal amount;
1723
- private String currency;
1724
-
1725
- // Constructor, getters, setters (Lombok)
1726
- }
1727
- ```
1728
-
1729
- ### Usage in Entities
1730
-
1731
- ```yaml
1732
- fields:
1733
- - name: totalAmount
1734
- type: Money # Automatically detected as VO
1735
- ```
1736
-
1737
- Generates in JPA:
1738
- ```java
1739
- @Embedded
1740
- private MoneyJpa totalAmount;
1741
- ```
1742
-
1743
- ### Example: Complex Value Object
1744
-
1745
- ```yaml
1746
- valueObjects:
1747
- - name: Address
1748
- fields:
1749
- - name: street
1750
- type: String
1751
-
1752
- - name: city
1753
- type: String
1754
-
1755
- - name: state
1756
- type: String
1757
-
1758
- - name: zipCode
1759
- type: String
1760
-
1761
- - name: country
1762
- type: String
1763
- ```
1764
-
1765
- ---
1766
-
1767
- ## Enums
1768
-
1769
- ### Definition
1770
-
1771
- ```yaml
1772
- enums:
1773
- - name: OrderStatus
1774
- values:
1775
- - PENDING
1776
- - CONFIRMED
1777
- - SHIPPED
1778
- - DELIVERED
1779
- - CANCELLED
1780
- ```
1781
-
1782
- ### Generated Enum
1783
-
1784
- ```java
1785
- package com.example.myapp.order.domain.models.enums;
1786
-
1787
- public enum OrderStatus {
1788
- PENDING,
1789
- CONFIRMED,
1790
- SHIPPED,
1791
- DELIVERED,
1792
- CANCELLED
1793
- }
1794
- ```
1795
-
1796
- ### Uso en entidades
1797
-
1798
- ```yaml
1799
- fields:
1800
- - name: status
1801
- type: OrderStatus # Se detecta y se importa automáticamente
1802
- ```
1803
-
1804
- Genera en JPA:
1805
- ```java
1806
- @Enumerated(EnumType.STRING)
1807
- private OrderStatus status;
1808
- ```
1809
-
1810
- ### Múltiples enums
1811
-
1812
- ```yaml
1813
- enums:
1814
- - name: OrderStatus
1815
- values: [PENDING, CONFIRMED, SHIPPED, DELIVERED, CANCELLED]
1816
-
1817
- - name: PaymentMethod
1818
- values: [CREDIT_CARD, DEBIT_CARD, CASH, BANK_TRANSFER]
1819
-
1820
- - name: ShippingMethod
1821
- values: [STANDARD, EXPRESS, OVERNIGHT]
1822
- ```
1823
-
1824
- ---
1825
-
1826
- ## Relaciones
1827
-
1828
- eva4j soporta relaciones JPA bidireccionales completas con generación automática del lado inverso.
1829
-
1830
- ### 🎯 Relaciones Bidireccionales Automáticas
1831
-
1832
- **Característica clave**: Cuando defines una relación OneToMany con `mappedBy`, eva4j genera AUTOMÁTICAMENTE la relación inversa ManyToOne en la entidad target.
1833
-
1834
- **Solo necesitas definir UN lado:**
1835
-
1836
- ```yaml
1837
- entities:
1838
- - name: order
1839
- isRoot: true
1840
- relationships:
1841
- - type: OneToMany
1842
- target: OrderItem
1843
- mappedBy: order # ← eva4j crea automáticamente ManyToOne en OrderItem
1844
- cascade: [PERSIST, MERGE]
1845
- fetch: LAZY
1846
- ```
1847
-
1848
- **eva4j genera automáticamente en OrderItem:**
1849
-
1850
- ```java
1851
- // OrderItemJpa.java (automatically generated)
1852
- @ManyToOne(fetch = FetchType.LAZY)
1853
- @JoinColumn(name = "order_id")
1854
- private OrderJpa order;
1855
- ```
1856
-
1857
- **Ventajas:**
1858
- - ✅ No necesitas definir ambos lados manualmente
1859
- - ✅ Evita inconsistencias entre relaciones
1860
- - ✅ JPA persiste correctamente la relación bidireccional
1861
- - ✅ Menos código YAML, misma funcionalidad
1862
-
1863
- **Nota**: Si defines manualmente ambos lados en el YAML, la definición manual tiene prioridad sobre la autogeneración.
1864
-
1865
- ---
1866
-
1867
- ### OneToMany (Uno a Muchos)
1868
-
1869
- **Definición en la entidad que tiene la colección:**
1870
-
1871
- ```yaml
1872
- entities:
1873
- - name: order
1874
- isRoot: true
1875
- relationships:
1876
- - type: OneToMany
1877
- target: OrderItem # Entidad relacionada
1878
- mappedBy: order # Campo en OrderItem que apunta a Order
1879
- cascade: [PERSIST, MERGE, REMOVE]
1880
- fetch: LAZY
1881
- ```
1882
-
1883
- **Genera en dominio:**
1884
- ```java
1885
- private List<OrderItem> orderItems = new ArrayList<>();
1886
-
1887
- public void addOrderItem(OrderItem orderItem) {
1888
- this.orderItems.add(orderItem);
1889
- }
1890
-
1891
- public void removeOrderItem(OrderItem orderItem) {
1892
- this.orderItems.remove(orderItem);
1893
- }
1894
- ```
1895
-
1896
- **Genera en JPA:**
1897
- ```java
1898
- @OneToMany(mappedBy = "order", cascade = {CascadeType.PERSIST, CascadeType.MERGE, CascadeType.REMOVE}, fetch = FetchType.LAZY)
1899
- @Builder.Default
1900
- private List<OrderItemJpa> orderItems = new ArrayList<>();
1901
- ```
1902
-
1903
- **Genera automáticamente en OrderItem (lado inverso):**
1904
- ```java
1905
- @ManyToOne(fetch = FetchType.LAZY)
1906
- @JoinColumn(name = "order_id") // Inferido desde mappedBy
1907
- private OrderJpa order;
1908
- ```
1909
-
1910
- ### ManyToOne (Muchos a Uno)
1911
-
1912
- **Definición manual (opcional si ya usaste mappedBy en OneToMany):**
1913
-
1914
- ```yaml
1915
- entities:
1916
- - name: orderItem
1917
- # Sin isRoot = entidad secundaria
1918
- relationships:
1919
- - type: ManyToOne
1920
- target: Order
1921
- joinColumn: order_id # Columna FK en la tabla
1922
- fetch: LAZY
1923
- ```
1924
-
1925
- **Genera en JPA:**
1926
- ```java
1927
- @ManyToOne(fetch = FetchType.LAZY)
1928
- @JoinColumn(name = "order_id")
1929
- private OrderJpa order;
1930
- ```
1931
-
1932
- **💡 Tip**: Si ya definiste `OneToMany` con `mappedBy` en Order, NO necesitas definir manualmente el `ManyToOne` en OrderItem. eva4j lo genera automáticamente.
1933
-
1934
- ---
1935
-
1936
- ### ⚠️ REGLA CRÍTICA: Relaciones Bidireccionales
1937
-
1938
- **Para relaciones bidireccionales OneToMany/ManyToOne:**
1939
-
1940
- #### ✅ CORRECTO - Solo definir en la entidad raíz
1941
-
1942
- ```yaml
1943
- entities:
1944
- - name: invoice
1945
- isRoot: true
1946
- relationships:
1947
- - type: OneToMany
1948
- target: InvoiceItem
1949
- mappedBy: invoice # ← Solo esta definición
1950
- cascade: [PERSIST, MERGE, REMOVE]
1951
- fetch: LAZY
1952
-
1953
- - name: invoiceItem
1954
- fields:
1955
- - name: id
1956
- type: Long
1957
- # ← SIN relationships definidas
1958
- # Eva4j genera automáticamente el ManyToOne en InvoiceItemJpa
1959
- ```
1960
-
1961
- **Resultado generado:**
1962
- ```java
1963
- // InvoiceJpa.java
1964
- @OneToMany(mappedBy = "invoice", cascade = {...})
1965
- private List<InvoiceItemJpa> invoiceItems;
1966
-
1967
- // InvoiceItemJpa.java (automatically generated)
1968
- @ManyToOne(fetch = FetchType.LAZY)
1969
- @JoinColumn(name = "invoice_id")
1970
- private InvoiceJpa invoice;
1971
- ```
1972
-
1973
- #### ❌ INCORRECTO - Definir en ambos lados
1974
-
1975
- ```yaml
1976
- entities:
1977
- - name: invoice
1978
- isRoot: true
1979
- relationships:
1980
- - type: OneToMany
1981
- target: InvoiceItem
1982
- mappedBy: invoice # ← Primera definición
1983
-
1984
- - name: invoiceItem
1985
- relationships:
1986
- - type: ManyToOne # ← ❌ DUPLICADO - Causará error
1987
- target: Invoice
1988
- joinColumn: invoice_id
1989
- ```
1990
-
1991
- **Problema:** Genera DOS relaciones `@ManyToOne` en `InvoiceItemJpa`, ambas mapeando a `invoice_id`:
1992
-
1993
- ```java
1994
- // InvoiceItemJpa.java (INCORRECTO - Duplicado)
1995
- @ManyToOne
1996
- @JoinColumn(name = "invoice_id")
1997
- private InvoiceJpa invoice; // ← Del mappedBy
1998
-
1999
- @ManyToOne
2000
- @JoinColumn(name = "invoice_id")
2001
- private InvoiceJpa invoices; // ← Del ManyToOne explícito
2002
-
2003
- // Error de Hibernate:
2004
- // "Column 'invoice_id' is duplicated in mapping"
2005
- ```
2006
-
2007
- #### 📋 Regla de Oro
2008
-
2009
- | Escenario | Definir en Raíz | Definir en Secundaria | Eva4j Genera |
2010
- |-----------|-----------------|----------------------|-------------|
2011
- | **Bidireccional** | `OneToMany` con `mappedBy` | ❌ NADA | `@OneToMany` en raíz + `@ManyToOne` en JPA de secundaria |
2012
- | **Unidireccional** | Opcional | `ManyToOne` con `joinColumn` | Solo lo definido |
2013
-
2014
- #### 💡 Separación Dominio/Persistencia
2015
-
2016
- **Importante:** Eva4j sigue correctamente DDD:
2017
-
2018
- - **Capa de Dominio:** Las entidades secundarias NO tienen referencia a la raíz
2019
- ```java
2020
- // InvoiceItem.java (dominio puro)
2021
- public class InvoiceItem {
2022
- private Long id;
2023
- private String description;
2024
- // ← SIN private Invoice invoice
2025
- }
2026
- ```
2027
-
2028
- - **Capa de Persistencia (JPA):** Solo aquí existe la relación
2029
- ```java
2030
- // InvoiceItemJpa.java (persistencia)
2031
- public class InvoiceItemJpa {
2032
- private Long id;
2033
-
2034
- @ManyToOne
2035
- @JoinColumn(name = "invoice_id")
2036
- private InvoiceJpa invoice; // ← Solo en capa JPA
2037
- }
2038
- ```
2039
-
2040
- **Ventajas:**
2041
- - ✅ Sin dependencias circulares en dominio
2042
- - ✅ Modelo de dominio más simple
2043
- - ✅ Relación bidireccional solo donde se necesita (persistencia)
2044
- - ✅ Cumple principios de DDD y arquitectura hexagonal
2045
-
2046
- ---
2047
-
2048
- ### OneToOne (Uno a Uno)
2049
-
2050
- **Bidireccional con mappedBy:**
2051
-
2052
- ```yaml
2053
- entities:
2054
- - name: order
2055
- isRoot: true
2056
- relationships:
2057
- - type: OneToOne
2058
- target: OrderSummary
2059
- mappedBy: order
2060
- cascade: [PERSIST, MERGE]
2061
- fetch: LAZY
2062
- ```
2063
-
2064
- **Sin mappedBy (owner):**
2065
-
2066
- ```yaml
2067
- entities:
2068
- - name: orderSummary
2069
- relationships:
2070
- - type: OneToOne
2071
- target: Order
2072
- joinColumn: order_id
2073
- fetch: LAZY
2074
- ```
2075
-
2076
- ### Relationship Options
2077
-
2078
- | Option | Values | Description |
2079
- |--------|--------|-------------|
2080
- | `type` | OneToMany, ManyToOne, OneToOne, ManyToMany | Relationship type |
2081
- | `target` | EntityName | Related entity |
2082
- | `mappedBy` | fieldName | For the inverse side of the relationship |
2083
- | `joinColumn` | column_name | FK column name |
2084
- | `cascade` | [PERSIST, MERGE, REMOVE, REFRESH, DETACH, ALL] | Cascade operations |
2085
- | `fetch` | LAZY, EAGER | Loading strategy |
2086
-
2087
- ---
2088
-
2089
- ### 🔥 Cascade Options (Cascade Operations)
2090
-
2091
- The `cascade` options determine which operations on the parent are automatically propagated to related entities.
2092
-
2093
- #### **⚠️ IMPORTANT: Cascade and Persistence**
2094
-
2095
- If you DON'T define `cascade`, related entities will **NOT be persisted automatically**. This is the most common error:
2096
-
2097
- ```yaml
2098
- # ❌ BAD - OrderItems will NOT be saved in DB
2099
- relationships:
2100
- - type: OneToMany
2101
- target: OrderItem
2102
- mappedBy: order
2103
- cascade: [] # ← Empty array = no cascade
2104
- fetch: LAZY
2105
-
2106
- # ✅ GOOD - OrderItems are saved automatically with Order
2107
- relationships:
2108
- - type: OneToMany
2109
- target: OrderItem
2110
- mappedBy: order
2111
- cascade: [PERSIST, MERGE, REMOVE] # ← Required to persist
2112
- fetch: LAZY
2113
- ```
2114
-
2115
- #### **Cascade Options:**
2116
-
2117
- | Option | Description | When to use? |
2118
- |--------|-------------|--------------|
2119
- | `PERSIST` | When saving the parent, saves new children | ✅ **Always in OneToMany** to create items |
2120
- | `MERGE` | When updating the parent, updates children | ✅ **Always in OneToMany** to edit items |
2121
- | `REMOVE` | When deleting the parent, deletes children | ✅ If children don't make sense without the parent |
2122
- | `REFRESH` | When refreshing the parent, refreshes children | ⚠️ Rarely needed |
2123
- | `DETACH` | When detaching the parent, detaches children | ⚠️ Rarely needed |
2124
- | `ALL` | All of the above operations | ⚠️ Only if you're sure |
2125
-
2126
- #### **Recommended Configurations:**
2127
-
2128
- ```yaml
2129
- # 🎯 RECOMMENDED for OneToMany (Order → OrderItem)
2130
- relationships:
2131
- - type: OneToMany
2132
- target: OrderItem
2133
- mappedBy: order
2134
- cascade: [PERSIST, MERGE, REMOVE] # ← Creates, updates and deletes items
2135
- fetch: LAZY
2136
-
2137
- # 🎯 RECOMMENDED for entities with independent lifecycle
2138
- relationships:
2139
- - type: OneToMany
2140
- target: OrderItem
2141
- mappedBy: order
2142
- cascade: [PERSIST, MERGE] # ← Without REMOVE, items persist
2143
- fetch: LAZY
2144
-
2145
- # ⚠️ CAREFUL with ALL - includes REMOVE
2146
- relationships:
2147
- - type: OneToMany
2148
- target: OrderItem
2149
- mappedBy: order
2150
- cascade: [ALL] # ← Deleting Order removes all OrderItems
2151
- fetch: LAZY
2152
-
2153
- # ❌ AVOID empty array if you want to persist children
2154
- relationships:
2155
- - type: OneToMany
2156
- target: OrderItem
2157
- mappedBy: order
2158
- cascade: [] # ← Requires manually saving OrderItem
2159
- fetch: LAZY
2160
- ```
2161
-
2162
- #### **What happens without Cascade?**
2163
-
2164
- ```yaml
2165
- # Without cascade: [PERSIST]
2166
- cascade: []
2167
-
2168
- # Behavior:
2169
- order.addOrderItem(item);
2170
- repository.save(order); // ❌ Order is saved, OrderItem is NOT
2171
- ```
2172
-
2173
- ```yaml
2174
- # With cascade: [PERSIST, MERGE]
2175
- cascade: [PERSIST, MERGE]
2176
-
2177
- # Behavior:
2178
- order.addOrderItem(item);
2179
- repository.save(order); // ✅ Order and OrderItem are saved automatically
2180
- ```
2181
-
2182
- ---
2183
-
2184
- ### 🚀 Fetch Options (Loading Strategy)
2185
-
2186
- The `fetch` options determine WHEN related entities are loaded from the database.
2187
-
2188
- #### **Fetch Options:**
2189
-
2190
- | Option | Description | Behavior | When to use? |
2191
- |--------|-------------|----------|--------------|
2192
- | `LAZY` | Load on demand (when accessed) | Only fetches parent initially | ✅ **Recommended by default** |
2193
- | `EAGER` | Immediate load (always) | Fetches parent + children in same query | ⚠️ Only if you ALWAYS need children |
2194
-
2195
- #### **LAZY Example (Recommended):**
2196
-
2197
- ```yaml
2198
- relationships:
2199
- - type: OneToMany
2200
- target: OrderItem
2201
- mappedBy: order
2202
- cascade: [PERSIST, MERGE]
2203
- fetch: LAZY # ← Loads items only when accessed
2204
- ```
2205
-
2206
- **Generated SQL:**
2207
- ```sql
2208
- -- First query: Only fetches Order
2209
- SELECT * FROM orders WHERE id = ?
2210
-
2211
- -- Second query: Only if you access order.getOrderItems()
2212
- SELECT * FROM order_items WHERE order_id = ?
2213
- ```
2214
-
2215
- **✅ Advantages:**
2216
- - Better initial performance
2217
- - Only loads what you need
2218
- - Avoids loading unnecessary data
2219
-
2220
- **⚠️ Disadvantage:**
2221
- - Can cause N+1 queries if you don't use `JOIN FETCH`
2222
-
2223
- #### **Ejemplo EAGER (Usar con cuidado):**
2224
-
2225
- ```yaml
2226
- relationships:
2227
- - type: OneToMany
2228
- target: OrderItem
2229
- mappedBy: order
2230
- cascade: [PERSIST, MERGE]
2231
- fetch: EAGER # ← Always loads items with Order
2232
- ```
2233
-
2234
- **Generated SQL:**
2235
- ```sql
2236
- -- Single query: Fetches Order + OrderItems
2237
- SELECT o.*, i.*
2238
- FROM orders o
2239
- LEFT JOIN order_items i ON i.order_id = o.id
2240
- WHERE o.id = ?
2241
- ```
2242
-
2243
- **✅ Advantage:**
2244
- - Single SQL query
2245
- - Data available immediately
2246
-
2247
- **❌ Disadvantages:**
2248
- - Loads data even if unused
2249
- - Heavier queries
2250
- - Can cause performance issues
2251
-
2252
- #### **Recommended Configurations by Type:**
2253
-
2254
- ```yaml
2255
- # OneToMany: ALWAYS LAZY
2256
- relationships:
2257
- - type: OneToMany
2258
- target: OrderItem
2259
- mappedBy: order
2260
- cascade: [PERSIST, MERGE]
2261
- fetch: LAZY # ← Avoids loading all items always
2262
-
2263
- # ManyToOne: LAZY by default, EAGER only if always needed
2264
- relationships:
2265
- - type: ManyToOne
2266
- target: Customer
2267
- joinColumn: customer_id
2268
- fetch: LAZY # ← LAZY by default
2269
-
2270
- # OneToOne: LAZY if optional, EAGER if always exists
2271
- relationships:
2272
- - type: OneToOne
2273
- target: OrderSummary
2274
- mappedBy: order
2275
- cascade: [PERSIST, MERGE]
2276
- fetch: LAZY # ← LAZY if not always used
2277
- ```
2278
-
2279
- #### **N+1 Problem and how to solve it:**
2280
-
2281
- **Problem:**
2282
- ```java
2283
- // With LAZY fetch
2284
- List<Order> orders = orderRepository.findAll(); // 1 query
2285
- orders.forEach(order -> {
2286
- order.getOrderItems().forEach(item -> { // N queries (one per Order)
2287
- System.out.println(item.getProductName());
2288
- });
2289
- });
2290
- // Total: 1 + N queries = N+1 problem
2291
- ```
2292
-
2293
- **Solution - Use JOIN FETCH in queries:**
2294
- ```java
2295
- @Query("SELECT o FROM OrderJpa o LEFT JOIN FETCH o.orderItems WHERE o.id = :id")
2296
- OrderJpa findByIdWithItems(@Param("id") String id);
2297
- ```
2298
-
2299
- ---
2300
-
2301
- ### When to manually define inverse relationships?
2302
-
2303
- #### ❌ You DON'T need to define ManyToOne if:
2304
-
2305
- You already defined `OneToMany` with `mappedBy` on the "parent" side. eva4j automatically generates the inverse relationship.
2306
-
2307
- **Example - Only define OneToMany:**
2308
-
2309
- ```yaml
2310
- # ✅ SUFFICIENT: Only define this in Order
2311
- entities:
2312
- - name: order
2313
- isRoot: true
2314
- relationships:
2315
- - type: OneToMany
2316
- target: OrderItem
2317
- mappedBy: order # ← eva4j generates ManyToOne automatically
2318
- cascade: [PERSIST, MERGE, REMOVE]
2319
- fetch: LAZY
2320
-
2321
- # ❌ DON'T NEED this in OrderItem (generated automatically)
2322
- # - name: orderItem
2323
- # relationships:
2324
- # - type: ManyToOne
2325
- # target: Order
2326
- # joinColumn: order_id
2327
- # fetch: LAZY
2328
- ```
2329
-
2330
- **Result:** Complete bidirectional relationship with FK `order_id` generated automatically.
2331
-
2332
- **✅ Advantages:**
2333
- - Less YAML code (only define one side)
2334
- - No duplication or inconsistencies
2335
- - Works the same as defining both sides
2336
- - FK inferred automatically: `{mappedBy}_id`
2337
-
2338
- ---
2339
-
2340
- #### ✅ You SHOULD define ManyToOne manually if:
2341
-
2342
- ##### 1. **You need a specific FK column name**
2343
-
2344
- ```yaml
2345
- # Define both sides to control FK name
2346
- entities:
2347
- - name: order
2348
- isRoot: true
2349
- relationships:
2350
- - type: OneToMany
2351
- target: OrderItem
2352
- mappedBy: order
2353
- cascade: [PERSIST, MERGE]
2354
- fetch: LAZY
2355
-
2356
- - name: orderItem
2357
- relationships:
2358
- - type: ManyToOne
2359
- target: Order
2360
- joinColumn: fk_pedido_uuid # ← Custom name
2361
- fetch: LAZY
2362
- ```
2363
-
2364
- **When to use:**
2365
- - Your DB has specific conventions (`fk_*`, prefixes, etc.)
2366
- - Need to maintain compatibility with existing schema
2367
- - Migration from another tool/framework
2368
-
2369
- ---
2370
-
2371
- ##### 2. **Multiple FKs to the same entity**
2372
-
2373
- ```yaml
2374
- # Transaction has 'from' and 'to' Account
2375
- entities:
2376
- - name: transaction
2377
- tableName: transactions
2378
-
2379
- fields:
2380
- - name: id
2381
- type: String
2382
- - name: amount
2383
- type: BigDecimal
2384
-
2385
- relationships:
2386
- # First relationship
2387
- - type: ManyToOne
2388
- target: Account
2389
- joinColumn: from_account_id # ← Explicit name required
2390
- fetch: LAZY
2391
-
2392
- # Second relationship to same entity
2393
- - type: ManyToOne
2394
- target: Account
2395
- joinColumn: to_account_id # ← Different FK name
2396
- fetch: LAZY
2397
- ```
2398
-
2399
- **When to use:**
2400
- - Self-relationships (category tree, org chart)
2401
- - Multiple relationships to same type (from/to, parent/child)
2402
- - Can't use `mappedBy` (which one would it be?)
2403
-
2404
- ---
2405
-
2406
- ##### 3. **Unidirectional relationship (no inverse side)**
2407
-
2408
- ```yaml
2409
- # OrderItem needs Product, but Product DOESN'T need OrderItems
2410
- entities:
2411
- - name: orderItem
2412
- relationships:
2413
- - type: ManyToOne
2414
- target: Product # Product has NO List<OrderItem>
2415
- joinColumn: product_id
2416
- fetch: LAZY
2417
-
2418
- # In Product DON'T define OneToMany
2419
- - name: product
2420
- isRoot: true
2421
- fields:
2422
- - name: id
2423
- type: String
2424
- - name: name
2425
- type: String
2426
- # No relationships to OrderItem
2427
- ```
2428
-
2429
- **When to use:**
2430
- - Performance: avoid loading unnecessary collections
2431
- - Product is not part of Order aggregate
2432
- - Only need navigation in one direction
2433
-
2434
- ---
2435
-
2436
- #### 📊 Quick Comparison
2437
-
2438
- | Scenario | Define ManyToOne? | Why? |
2439
- |----------|-------------------|------|
2440
- | Standard relationship with `mappedBy` | ❌ No | eva4j generates it automatically |
2441
- | FK with custom name | ✅ Yes | To control `joinColumn` |
2442
- | Multiple FKs to same entity | ✅ Yes | Need explicit names |
2443
- | Unidirectional relationship | ✅ Yes | No inverse side (`mappedBy`) |
2444
- | Specific DB conventions | ✅ Yes | To comply with standards |
2445
- | Simple standard case | ❌ No | Let eva4j generate it |
2446
-
2447
- ---
2448
-
2449
- #### ⚠️ Error Común
2450
-
2451
- **NO hagas esto:**
2452
-
2453
- ```yaml
2454
- # ❌ INCORRECTO: Inconsistencia entre ambos lados
2455
- entities:
2456
- - name: order
2457
- isRoot: true
2458
- relationships:
2459
- - type: OneToMany
2460
- target: OrderItem
2461
- mappedBy: order # ← Espera campo "order" en OrderItem
2462
- fetch: LAZY
2463
-
2464
- - name: orderItem
2465
- relationships:
2466
- - type: ManyToOne
2467
- target: Order
2468
- joinColumn: pedido_id # ← Pero la FK se llama diferente
2469
- fetch: LAZY
2470
- ```
2471
-
2472
- **Problema:** `mappedBy: order` busca un campo llamado `order`, pero `pedido_id` no coincide con la convención de nombres.
2473
-
2474
- **✅ Soluciones:**
2475
-
2476
- **Opción A - Deja que eva4j genere automáticamente:**
2477
- ```yaml
2478
- # Solo define OneToMany, eva4j genera ManyToOne correctamente
2479
- entities:
2480
- - name: order
2481
- isRoot: true
2482
- relationships:
2483
- - type: OneToMany
2484
- target: OrderItem
2485
- mappedBy: order
2486
- fetch: LAZY
2487
- ```
2488
-
2489
- **Opción B - Define ambos lados consistentemente:**
2490
- ```yaml
2491
- entities:
2492
- - name: order
2493
- isRoot: true
2494
- relationships:
2495
- - type: OneToMany
2496
- target: OrderItem
2497
- mappedBy: pedido # ← Coincide con el nombre del campo
2498
- fetch: LAZY
2499
-
2500
- - name: orderItem
2501
- relationships:
2502
- - type: ManyToOne
2503
- target: Order
2504
- joinColumn: pedido_id
2505
- fetch: LAZY
2506
- ```
2507
-
2508
- ---
2509
-
2510
- #### 💡 Recomendación General
2511
-
2512
- **Para el 90% de los casos:**
2513
-
2514
- ```yaml
2515
- # ✅ MEJOR PRÁCTICA: Solo define OneToMany
2516
- entities:
2517
- - name: order
2518
- isRoot: true
2519
- relationships:
2520
- - type: OneToMany
2521
- target: OrderItem
2522
- mappedBy: order
2523
- cascade: [PERSIST, MERGE, REMOVE]
2524
- fetch: LAZY
2525
-
2526
- # NO definas ManyToOne en OrderItem
2527
- # eva4j lo genera automáticamente con:
2528
- # - @JoinColumn(name = "order_id")
2529
- # - @ManyToOne(fetch = FetchType.LAZY)
2530
- ```
2531
-
2532
- **Solo define ambos lados cuando necesites control específico.**
2533
-
2534
- ---
2535
-
2536
- ## Tipos de Datos
2537
-
2538
- ### Tipos primitivos Java
2539
-
2540
- | YAML | Java | JPA | Observaciones |
2541
- |------|------|-----|---------------|
2542
- | `String` | String | VARCHAR | En ID genera UUID |
2543
- | `Integer` | Integer | INTEGER | En ID genera IDENTITY |
2544
- | `Long` | Long | BIGINT | En ID genera IDENTITY |
2545
- | `Double` | Double | DOUBLE | - |
2546
- | `Float` | Float | FLOAT | - |
2547
- | `Boolean` | Boolean | BOOLEAN | - |
2548
- | `BigDecimal` | BigDecimal | DECIMAL | Importa automáticamente |
2549
-
2550
- ### Tipos de fecha/hora
2551
-
2552
- | YAML | Java | Importa automáticamente |
2553
- |------|------|------------------------|
2554
- | `LocalDate` | LocalDate | java.time.LocalDate |
2555
- | `LocalDateTime` | LocalDateTime | java.time.LocalDateTime |
2556
- | `LocalTime` | LocalTime | java.time.LocalTime |
2557
-
2558
- ### Tipos especiales
2559
-
2560
- | YAML | Java | Uso |
2561
- |------|------|-----|
2562
- | `UUID` | UUID | IDs únicos |
2563
- | Cualquier Enum | Enum personalizado | Estados, tipos |
2564
- | Cualquier VO | Value Object | Conceptos de dominio |
2565
-
2566
- ### Colecciones
2567
-
2568
- #### Lista de primitivos
2569
-
2570
- ```yaml
2571
- fields:
2572
- - name: tags
2573
- type: List<String>
2574
- ```
2575
-
2576
- Genera:
2577
- ```java
2578
- @ElementCollection
2579
- @CollectionTable(name = "order_tags", joinColumns = @JoinColumn(name = "order_id"))
2580
- @Column(name = "tags")
2581
- @Builder.Default
2582
- private List<String> tags = new ArrayList<>();
2583
- ```
2584
-
2585
- #### Lista de Value Objects
2586
-
2587
- ```yaml
2588
- fields:
2589
- - name: addresses
2590
- type: List<Address> # Address es un VO definido
2591
- ```
2592
-
2593
- Genera:
2594
- ```java
2595
- @ElementCollection
2596
- @CollectionTable(name = "customer_addresses", joinColumns = @JoinColumn(name = "customer_id"))
2597
- @Builder.Default
2598
- private List<AddressJpa> addresses = new ArrayList<>();
2599
- ```
2600
-
2601
- ---
2602
-
2603
- ## Ejemplos Completos
2604
-
2605
- ### Ejemplo 1: E-Commerce (Order)
2606
-
2607
- ```yaml
2608
- aggregates:
2609
- - name: Order
2610
- entities:
2611
- - name: order
2612
- isRoot: true
2613
- tableName: orders
2614
-
2615
- fields:
2616
- - name: id
2617
- type: String
2618
-
2619
- - name: orderNumber
2620
- type: String
2621
-
2622
- - name: customerId
2623
- type: String
2624
-
2625
- - name: status
2626
- type: OrderStatus
2627
-
2628
- - name: totalAmount
2629
- type: Money
2630
-
2631
- - name: shippingAddress
2632
- type: Address
2633
-
2634
- - name: createdAt
2635
- type: LocalDateTime
2636
-
2637
- - name: updatedAt
2638
- type: LocalDateTime
2639
-
2640
- relationships:
2641
- - type: OneToMany
2642
- target: OrderItem
2643
- mappedBy: order
2644
- cascade: [PERSIST, MERGE, REMOVE]
2645
- fetch: LAZY
2646
-
2647
- - name: orderItem
2648
- tableName: order_items
2649
-
2650
- fields:
2651
- - name: id
2652
- type: Long
2653
-
2654
- - name: productId
2655
- type: String
2656
-
2657
- - name: productName
2658
- type: String
2659
-
2660
- - name: quantity
2661
- type: Integer
2662
-
2663
- - name: unitPrice
2664
- type: Money
2665
-
2666
- - name: subtotal
2667
- type: Money
2668
-
2669
- relationships:
2670
- - type: ManyToOne
2671
- target: Order
2672
- joinColumn: order_id
2673
- fetch: LAZY
2674
-
2675
- valueObjects:
2676
- - name: Money
2677
- fields:
2678
- - name: amount
2679
- type: BigDecimal
2680
- - name: currency
2681
- type: String
2682
-
2683
- - name: Address
2684
- fields:
2685
- - name: street
2686
- type: String
2687
- - name: city
2688
- type: String
2689
- - name: state
2690
- type: String
2691
- - name: zipCode
2692
- type: String
2693
- - name: country
2694
- type: String
2695
-
2696
- enums:
2697
- - name: OrderStatus
2698
- values:
2699
- - PENDING
2700
- - CONFIRMED
2701
- - PROCESSING
2702
- - SHIPPED
2703
- - DELIVERED
2704
- - CANCELLED
2705
- - REFUNDED
2706
- ```
2707
-
2708
- ### Ejemplo 2: Blog (Post)
2709
-
2710
- ```yaml
2711
- aggregates:
2712
- - name: Post
2713
- entities:
2714
- - name: post
2715
- isRoot: true
2716
- tableName: posts
2717
-
2718
- fields:
2719
- - name: id
2720
- type: Long
2721
-
2722
- - name: title
2723
- type: String
2724
-
2725
- - name: slug
2726
- type: String
2727
-
2728
- - name: content
2729
- type: String
2730
-
2731
- - name: authorId
2732
- type: String
2733
-
2734
- - name: status
2735
- type: PostStatus
2736
-
2737
- - name: publishedAt
2738
- type: LocalDateTime
2739
-
2740
- - name: tags
2741
- type: List<String>
2742
-
2743
- - name: metadata
2744
- type: PostMetadata
2745
-
2746
- relationships:
2747
- - type: OneToMany
2748
- target: Comment
2749
- mappedBy: post
2750
- cascade: [PERSIST, MERGE, REMOVE]
2751
- fetch: LAZY
2752
-
2753
- - name: comment
2754
- tableName: comments
2755
-
2756
- fields:
2757
- - name: id
2758
- type: Long
2759
-
2760
- - name: authorId
2761
- type: String
2762
-
2763
- - name: authorName
2764
- type: String
2765
-
2766
- - name: content
2767
- type: String
2768
-
2769
- - name: createdAt
2770
- type: LocalDateTime
2771
-
2772
- - name: approved
2773
- type: Boolean
2774
-
2775
- relationships:
2776
- - type: ManyToOne
2777
- target: Post
2778
- joinColumn: post_id
2779
- fetch: LAZY
2780
-
2781
- valueObjects:
2782
- - name: PostMetadata
2783
- fields:
2784
- - name: viewCount
2785
- type: Integer
2786
- - name: likeCount
2787
- type: Integer
2788
- - name: shareCount
2789
- type: Integer
2790
-
2791
- enums:
2792
- - name: PostStatus
2793
- values: [DRAFT, PUBLISHED, ARCHIVED, DELETED]
2794
- ```
2795
-
2796
- ### Ejemplo 3: Banking (Account)
2797
-
2798
- ```yaml
2799
- aggregates:
2800
- - name: Account
2801
- entities:
2802
- - name: account
2803
- isRoot: true
2804
- tableName: accounts
2805
-
2806
- fields:
2807
- - name: id
2808
- type: String
2809
-
2810
- - name: accountNumber
2811
- type: String
2812
-
2813
- - name: customerId
2814
- type: String
2815
-
2816
- - name: accountType
2817
- type: AccountType
2818
-
2819
- - name: balance
2820
- type: Money
2821
-
2822
- - name: status
2823
- type: AccountStatus
2824
-
2825
- - name: openedAt
2826
- type: LocalDate
2827
-
2828
- relationships:
2829
- - type: OneToMany
2830
- target: Transaction
2831
- mappedBy: account
2832
- cascade: [PERSIST, MERGE]
2833
- fetch: LAZY
2834
-
2835
- - name: transaction
2836
- tableName: transactions
2837
-
2838
- fields:
2839
- - name: id
2840
- type: String
2841
-
2842
- - name: transactionNumber
2843
- type: String
2844
-
2845
- - name: type
2846
- type: TransactionType
2847
-
2848
- - name: amount
2849
- type: Money
2850
-
2851
- - name: description
2852
- type: String
2853
-
2854
- - name: timestamp
2855
- type: LocalDateTime
2856
-
2857
- - name: balanceAfter
2858
- type: Money
2859
-
2860
- relationships:
2861
- - type: ManyToOne
2862
- target: Account
2863
- joinColumn: account_id
2864
- fetch: LAZY
2865
-
2866
- valueObjects:
2867
- - name: Money
2868
- fields:
2869
- - name: amount
2870
- type: BigDecimal
2871
- - name: currency
2872
- type: String
2873
-
2874
- enums:
2875
- - name: AccountType
2876
- values: [CHECKING, SAVINGS, INVESTMENT, CREDIT]
2877
-
2878
- - name: AccountStatus
2879
- values: [ACTIVE, INACTIVE, SUSPENDED, CLOSED]
2880
-
2881
- - name: TransactionType
2882
- values: [DEPOSIT, WITHDRAWAL, TRANSFER, FEE, INTEREST]
2883
- ```
2884
-
2885
- ### Ejemplo 4: Múltiples Agregados en un módulo
2886
-
2887
- ```yaml
2888
- aggregates:
2889
- - name: Customer
2890
- entities:
2891
- - name: customer
2892
- isRoot: true
2893
- fields:
2894
- - name: id
2895
- type: String
2896
- - name: name
2897
- type: String
2898
- - name: email
2899
- type: String
2900
- - name: phone
2901
- type: String
2902
- - name: registeredAt
2903
- type: LocalDateTime
2904
-
2905
- valueObjects:
2906
- - name: ContactInfo
2907
- fields:
2908
- - name: email
2909
- type: String
2910
- - name: phone
2911
- type: String
2912
-
2913
- - name: Product
2914
- entities:
2915
- - name: product
2916
- isRoot: true
2917
- fields:
2918
- - name: id
2919
- type: String
2920
- - name: name
2921
- type: String
2922
- - name: description
2923
- type: String
2924
- - name: price
2925
- type: Money
2926
- - name: stock
2927
- type: Integer
2928
- - name: category
2929
- type: ProductCategory
2930
-
2931
- valueObjects:
2932
- - name: Money
2933
- fields:
2934
- - name: amount
2935
- type: BigDecimal
2936
- - name: currency
2937
- type: String
2938
-
2939
- enums:
2940
- - name: ProductCategory
2941
- values: [ELECTRONICS, CLOTHING, FOOD, BOOKS, TOYS]
2942
- ```
2943
-
2944
- ---
2945
-
2946
- ## Comando de Generación
2947
-
2948
- ```bash
2949
- # Generar todas las entidades del módulo
2950
- eva4j generate entities <module-name>
2951
- ```
2952
-
2953
- ### Salida generada
2954
-
2955
- ```
2956
- ✓ Found 1 aggregate(s) and 1 enum(s)
2957
-
2958
- 📦 Aggregates to generate:
2959
- ├── Order (Root: Order)
2960
- │ ├── OrderItem
2961
- │ └── Money (VO)
2962
-
2963
- ⠋ Generating files...
2964
-
2965
- ✅ Successfully generated 13 files for module 'order'
2966
-
2967
- 📁 Generated Files:
2968
- ✓ Enum: OrderStatus
2969
- ✓ Domain Entity: Order
2970
- ✓ JPA Entity: OrderJpa
2971
- ✓ Domain Entity: OrderItem
2972
- ✓ JPA Entity: OrderItemJpa
2973
- ✓ Domain VO: Money
2974
- ✓ JPA VO: MoneyJpa
2975
- ✓ Mapper: OrderMapper
2976
- ✓ Repository: OrderRepository
2977
- ✓ JPA Repository: OrderJpaRepository
2978
- ✓ Repository Impl: OrderRepositoryImpl
2979
- ```
2980
-
2981
- ---
2982
-
2983
- ## Tips y Mejores Prácticas
2984
-
2985
- ### ✅ Hacer
2986
-
2987
- 1. **Usa nombres descriptivos**: `orderNumber` en lugar de `number`
2988
- 2. **PascalCase para tipos**: `OrderStatus`, `Money`, `Address`
2989
- 3. **camelCase para campos**: `totalAmount`, `createdAt`
2990
- 4. **snake_case para tablas**: `order_items`, `customer_addresses`
2991
- 5. **Define IDs apropiados**: String para UUIDs, Long para secuencias
2992
- 6. **Usa Value Objects**: Para conceptos cohesivos (Money, Address)
2993
- 7. **Cascade apropiado**: PERSIST, MERGE para agregados; evita ALL
2994
-
2995
- ### ❌ Evitar
2996
-
2997
- 1. **Don't use Long for UUIDs**: Use String
2998
- 2. **Don't create bidirectional relationships without mappedBy**: Define the owner
2999
- 3. **Don't use EAGER without reason**: LAZY is better for performance
3000
- 4. **Don't mix concepts**: One aggregate = one transaction
3001
- 5. **Don't use @Column in domain.yaml**: It's for JPA, generated automatically
3002
-
3003
- ---
3004
-
3005
- ## Current Support and Limitations
3006
-
3007
- ### ✅ Supported
3008
-
3009
- - Aggregates with root and secondary entities
3010
- - Embedded Value Objects
3011
- - Enums with values
3012
- - OneToMany, ManyToOne, OneToOne relationships
3013
- - Java primitive and date types
3014
- - Collections of primitives and VOs
3015
- - IDs: String (UUID), Long/Integer (IDENTITY)
3016
- - Custom Cascade and Fetch
3017
-
3018
- ### 🚧 Coming Soon
3019
-
3020
- - JSR-303 validations
3021
- - Automatic auditing
3022
- - Soft delete
3023
- - Custom query methods
3024
- - Indexes and constraints
3025
- - Entity inheritance
3026
-
3027
- ---
3028
-
3029
- ## Frequently Asked Questions
3030
-
3031
- **Q: Can I have multiple aggregates in one domain.yaml?**
3032
- A: Yes, define multiple entries in the `aggregates` array.
3033
-
3034
- **Q: How do I reference an enum from another aggregate?**
3035
- A: Enums are global to the module, just use the name: `type: OrderStatus`
3036
-
3037
- **Q: Can I use a VO in multiple aggregates?**
3038
- A: Yes, but you must define it in each aggregate (for now).
3039
-
3040
- **Q: What happens if I regenerate the code?**
3041
- A: Files are overwritten. Modify only in templates, not in generated code.
3042
-
3043
- **Q: Can I customize generated entities?**
3044
- A: Yes, modify the templates in `templates/aggregate/`.
3045
-
3046
- ---
3047
-
3048
- ## Additional Resources
3049
-
3050
- - [Implementation Guide](IMPLEMENTATION_SUMMARY.md)
3051
- - [Testing Guide](TESTING_GUIDE.md)
3052
- - [Quick Reference](QUICK_REFERENCE.md)
3053
- - [DDD Documentation](https://martinfowler.com/bliki/DomainDrivenDesign.html)
3054
-
3055
- ---
3056
-
3057
- **Ready to start?** Create your `domain.yaml` and run:
3058
-
3059
- ```bash
3060
- eva4j generate entities <your-module>
3061
- ```
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 |