This is a request for clarification after watching a video about Spring Data JDBC.
I have an aggregate made of Book (aggregate root), BookCopy and BookCopyQualityControl. A Book has many BookCopy, and a BookCopy has many BookCopyQualityControl. I want only the Book class to have an @Id (Long), because it is the aggregate root (this is suggested here).
In my database:
- BOOK's primary key is ID;
- BOOK_COPY's primary key is the couple (BOOK_ID, COPY_NUMBER), with BOOK_ID referencing BOOK(ID);
- BOOK_COPY_QUALITY_CONTROL's primary key is the triple (BOOK_ID, COPY_NUMBER, QUALITY_CONTROL_NUMBER), with (BOOK_ID, COPY_NUMBER) referencing BOOK_COPY(BOOK_ID, COPY_NUMBER).
I am trying to implement what is explained here. I'll transcribe. "The way I would recommend to do it: the elements inside an aggregate have either, as a primary key, the same value as the aggregate root, in case of a one-to-one relationship, or they have the id of the aggregate root plus an additional column, or, if you have deeper nested elements, multiple additional columns that together make up a primary key, and that is used by Spring Data JDBC".
The deeper nested elements part seems exactly my case, but I don't understand how to do it.
In the Book class, it's easy:
public class Book {
 @Id
 private Long id;
 
 @MappedCollection(idColumn = "BOOK_ID")
 private Set<BookCopy> copies;
 
 // ...stuff...
}
But what should I do in the BookCopy class for the one-to-many relationship with BookCopyQualityControl? I tried this:
public class BookCopy {
 private Long copyNumber;
 
 @MappedCollection(idColumn = "COPY_NUMBER")
 private Set<BookCopyQualityControl> qualityControls;
 // ...stuff...
}
but, when saving the Book, Spring Data JDBC will produce an INSERT statement for the BOOK_COPY_QUALITY_CONTROL table that doesn't include the BOOK_ID column, that is mandatory. And since @MappedCollection accepts a single idColumn, I don't know how to proceed. For completeness, this is the BookCopyQualityControl class:
public class BookCopyQualityControl {
 private Long qualityControlNumber;
 
 // ...stuff...
}
EDIT: I am still stuck after about 2 months and I haven't received any comment or response yet. All the above is still valid, but I will paste below all the code, hoping to get some feedback. I am using spring-boot-starter-data-jdbc from Spring Boot 3.5.5 with Java 17. Thank you in advance.
Database DDL (H2 in memory with MODE=MYSQL)
CREATE TABLE BOOK(
 ID bigint AUTO_INCREMENT NOT NULL,
 TITLE varchar(50) NOT NULL,
 CONSTRAINT PK_BOOK PRIMARY KEY
 (
 ID ASC
 )
);
CREATE TABLE BOOK_COPY(
 COPY_NUMBER bigint AUTO_INCREMENT NOT NULL,
 BOOK_ID bigint NOT NULL,
 AUTHOR_MESSAGE varchar(50) NOT NULL,
 CONSTRAINT PK_BOOK_COPY PRIMARY KEY
 (
 COPY_NUMBER,
 BOOK_ID
 )
);
ALTER TABLE BOOK_COPY ADD CONSTRAINT FK_BOOK_COPY FOREIGN KEY(BOOK_ID) REFERENCES BOOK (ID);
CREATE TABLE BOOK_COPY_QUALITY_CONTROL(
 QUALITY_CONTROL_NUMBER bigint AUTO_INCREMENT NOT NULL,
 COPY_NUMBER bigint NOT NULL,
 BOOK_ID bigint NOT NULL,
 SCORE INT NOT NULL,
 CONSTRAINT UQ_QUALITY_CONTROL PRIMARY KEY
 (
 QUALITY_CONTROL_NUMBER,
 COPY_NUMBER,
 BOOK_ID
 )
);
ALTER TABLE BOOK_COPY_QUALITY_CONTROL ADD CONSTRAINT FK_BOOK_COPY_QUALITY_CONTROL FOREIGN KEY(COPY_NUMBER, BOOK_ID) REFERENCES BOOK_COPY (COPY_NUMBER, BOOK_ID);
BookRepository.java
public interface BookRepository extends CrudRepository<Book, Long>, PagingAndSortingRepository<Book, Long> { }
Book.java
public class Book {
 @Id
 private Long id;
 private String title;
 @MappedCollection(idColumn = "BOOK_ID")
 private Set<BookCopy> copies;
 public Book(String title) {
 this.title = title;
 this.copies = new HashSet<>();
 }
 public BookCopy addCopy(String authorMessage) {
 BookCopy newCopy = new BookCopy(authorMessage);
 this.copies.add(newCopy);
 return newCopy;
 }
 public BookCopyQualityControl addQualityControl(BookCopy copy, Integer score) {
 BookCopyQualityControl newQualityControl = copy.addQualityControl(score);
 return newQualityControl;
 }
 public Long getId() {
 return id;
 }
 public String getTitle() {
 return title;
 }
 public Set<BookCopy> getCopies() {
 return copies;
 }
}
BookCopy.java
public class BookCopy {
 private Long copyNumber;
 private final String authorMessage;
 @MappedCollection(idColumn = "COPY_NUMBER")
 private Set<BookCopyQualityControl> qualityControls;
 BookCopy(String authorMessage) {
 this.authorMessage = authorMessage;
 qualityControls = new HashSet<>();
 }
 BookCopyQualityControl addQualityControl(Integer score) {
 BookCopyQualityControl newQualityControl = new BookCopyQualityControl(score);
 this.qualityControls.add(newQualityControl);
 return newQualityControl;
 }
 public String getAuthorMessage() {
 return authorMessage;
 }
 public Long getCopyNumber() {
 return copyNumber;
 }
 public Set<BookCopyQualityControl> getQualityControls() {
 return qualityControls;
 }
}
BookCopyQualityControl.java
public class BookCopyQualityControl {
 private Long qualityControlNumber;
 private Integer score;
 BookCopyQualityControl(Integer score) {
 this.score = score;
 }
 public Long getQualityControlNumber() {
 return qualityControlNumber;
 }
 public Integer getScore() {
 return score;
 }
}
test
void myTest() {
 Book tales = new Book("Tales of Spring Data JDBC");
 BookCopy newCopy1 = tales.addCopy("I wish you a nice read!");
 tales.addQualityControl(newCopy1, 7);
 BookCopy newCopy2 = tales.addCopy("Hope you enjoy!");
 tales.addQualityControl(newCopy2, 9);
 bookRepository.save(tales);
}
error running the test
Caused by: org.h2.jdbc.JdbcBatchUpdateException: NULL not allowed for column "BOOK_ID"; SQL statement:
INSERT INTO "BOOK_COPY_QUALITY_CONTROL" ("COPY_NUMBER", "QUALITY_CONTROL_NUMBER", "SCORE") VALUES (?, ?, ?) [23502-232]