spring-datadomain-driven-designaggregateroot

Manually mapping model to database - Spring Data JDBC


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?


Solution

  • 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:

    1. Delete all appointments from the table and repopulate with new surrogate IDs whenever you persist.

    2. Load the surrogate ID into the domain and hold them as part of appointment data

    3. 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.