Value Objects with @Embedded and @Embeddable Annotations
Consider the following Student
entity
1@Entity 2@DynamicInsert 3@GenerateDTO 4@Table( 5 name = "student", 6 schema = "public" 7) 8class Student( 9 @Id 10 @Column(name = "id") 11 @GeneratedValue(generator = "ulid_as_uuid") 12 var id: UUID? = null, 13 @Column(name = "first_name", nullable = false) 14 var firstName: String, 15 @Column(name = "last_name", nullable = false) 16 var lastName: String, 17 @Column(name = "chinese_first_name") 18 var chineseFirstName: String? = null, 19 @Column(name = "chinese_last_name") 20 var chineseLastName: String? = null, 21 @Column(name = "school_name", nullable = false) 22 var schoolName: String, 23 @Column(name = "student_code") 24 var studentCode: String? = null, 25 @Column(name = "grade", nullable = false) 26 var grade: String, 27 @Column(name = "phone_number") 28 var phoneNumber: String? = null, 29 @Column(name = "wechat_id") 30 var wechatId: String? = null, 31 //@Column(name = "birthdate", nullable = false) 32 //var birthdate: Double, 33 @Embedded 34 var birthdate: Birthdate,
Here we have simply changed the primitive type to a data class Birthdate
, with the value object being defined by:
package dev.james.alicetimetable.domain.valueObject import jakarta.persistence.Column import jakarta.persistence.Embeddable @Embeddable data class Birthdate( @Column(name = "birthdate", nullable = false) val value: Double, ) { init { require(value > 0) { "Birthdate must be positive" } } }
Now the validation logic will take place automatically. Moreover, when we save the entity JPA will extract the @Column
-annotate fields from the @Embedded
fields.
This is a very powerful technique in DDD and JPA, now we can validate Email
, Address
, PhoneNumber
etc value objects within the class itself and the data-persistence from JPA will automatically save the annotated fields into the database.
35 @Column(name = "parent_email", nullable = false) 36 var parentEmail: String, 37 @Column(name = "created_at") 38 var createdAt: Double? = null, 39 @Column(name = "created_at_hk") 40 var createdAtHk: String? = null, 41 @Column(name = "parent_id") 42 var parentId: UUID? = null, 43 @Column(name = "gender", nullable = false) 44 @Enumerated(EnumType.STRING) 45 @JdbcType(PostgreSQLEnumJdbcType::class) 46 var gender: Gender, 47 @Column(name = "should_auto_renew_package") 48 var shouldAutoRenewPackage: Boolean? = null, 49 @Column(name = "is_active") 50 @Suppress("INAPPLICABLE_JVM_NAME") 51 @set:JvmName("setIsActive") 52 var isActive: Boolean? = null, 53 @Column(name = "preferred_name") 54 var preferredName: String? = null, 55 @Column(name = "remark") 56 var remark: String? = null, 57) : AbstractAggregateRoot<Student>() { 58 ... 59}
Interaction with toDTO()
Method generated by kspKotlin
toDTO()
Method generated by kspKotlinWhen we convert an entity class into a plain DTO object, there are two approches to instruct kspKotlin
for data transform:
- Keep it as
{ ..., brithday: { value: ... }}
- Use the name in
@Column(name="some_name")
and convert it into a plain object withsomeName
as one of the fields
We adopt the second approach as it makes no change to our codebase when we do the refactoring from a String
-field to a Birthday
-field.
The following article
has updated the GenerateDTOProcessor
class to scan all the @Embedded
-annotated fields, and then scan all @Column
-annotated fields contained in it throughout the toDTO()
code generation process.