I have the following (vastly simplified) domain object
public class Student {
private Long studentId;
private List<Appointment> appointments;
// Business logic
}
public class Appointment {
private TimeRange timeRange;
private LocalDate date;
// Business logic
}
The aggregate root is Student
which contains a list of appointments. An Appointment
is a sub-entity of Student
.
Now let's say, for whatever reason, the domain object Student
does not perfectly map to my database model. For example, to better perform the business logic, the entity constructed from the database undergoes some transformation. One such transformation is needed because I have a custom TimeRange
class in my Appointment
class which cannot be automatically mapped by Data JDBC.
Therefore I wanted to introduce an indirection which is to be used by Spring Data JDBC:
@Table("student")
public class StudentEntity {
@Id
private Long studentId;
@MappedCollection(idColumn = "student_id")
private Set<AppointmentEntity> appointments;
public StudentEntity(Long studentId, Set<AppointmentEntity> appointments) {
this.studentId = studentId;
this.appointments = appointments;
}
}
@Table("appointment")
public class AppointmentEntity {
@Id
private Long appointmentId;
private LocalTime rangeStart;
private LocalTime rangeEnd;
private LocalDate date;
}
In my repository implementation I do the following
@Repository
public class StudentRepositoryImpl implement StudentRepository {
private final StudenDao studentDao;
public StudentRepositoryImpl(StudentDao studentDao) {
this.studentDao = studentDao;
}
public Student findStudent(Long id) {
Optional<StudentEntity> studentEntity = studentDao.findStudentEntityById(id);
return studentEntity.map(this::toStudent).orElse(null);
}
public void saveStudent(Student student) {
// ???
}
private toAppointment(AppointmentEntity appointmentEntity) {
TimeRange timeRange = new TimeRange(appointmentEntity.rangeStart, appointmentEntity.rangeEnd);
return new Appointment(timeRange, appointmentEntity.getDate());
}
private toStudent(StudentEntity studentEntity) {
List<Appointment> appointments = studentEntity.appointments.map(this::toAppointment);
return new Student(studentEntity.getStudentId(), appointments);
}
}
The flow from Database -> Entity -> Domain
works fine but what about the other direction? Say I perform some actions and the appointments
field of a Student
domain object changes. I want to save it to the database again.
I would have to convert Student
into StudentEntity
, and thus also Appointment
to AppointmentEntity
. But an Appointment
does not have an ID in the domain context as it is not an aggregate root. In my case an Appointment
has the same lifecycle as a Student
and is discarded if a Student
unregisters, for example. So it would not make sense to put it into a separate aggregate.
So my main question is: What is the best way to persist a domain object, including its sub-entities, if your domain objects do not 1:1 map to the database structure?
AppointmentID
is a Surrogate key. Reiterating, The surrogate key is not derived from application data and The only significance of the surrogate key is to act as the primary key. Wikipedia
It follows that we can discard and generate the IDs again and again for the same appointment record when necessary.
So you have three options to choose from:
Delete all appointments from the table and repopulate with new surrogate IDs whenever you persist.
Load the surrogate ID into the domain and hold them as part of appointment data
Construct a hash key to represent an appointment uniquely (from its fields) and use the key as the appointment's unique ID
All three approaches are acceptable, but you can pick the trade-off that best fits your use case. As examples:
And so on.