Given there are two tables client
and device
with a one(client)-to-many(devices) relation. The simplified schemas of the table are:
TABLE client (
id,
import_date,
PRIMARY_KEY (id, import_date)
)
and
TABLE device (
id,
import_date,
client_id,
PRIMARY_KEY (id, import_date),
FOREIGN_KEY (client_id -> client.id, import_date -> client.import_date)
)
Note: The column device.import_date
is part of the primary key as well as part of the foreign key.
Unfortunately I fail to describe the above structure in JPA.
This is my humble attempt (getters, setters, implemented interfaces, etc. omitted):
public class ImportId {
private Integer id;
private LocalDate importDate;
}
@Entity
@IdClass(ImportId.class)
public class Client {
@Id
private Integer id;
@Id
private LocalDate importDate;
}
@Entity
@IdClass(ImportId.class)
public class Device {
@Id
private Integer id;
@Id
private LocalDate importDate;
@ManyToOne
//@MapsId("importDate") <-- does not work
//@JoinColumn
//@JoinColumn <-- also not working
private Client client;
}
Using @MapsId
together with @IdClass
does nothing special resulting in an extra column client_import_date
in the device
table, which is undesirable.
I've also tried using @MapsId
in conjunction with @Embeddable
without success. It fails with
org.hibernate.PropertyNotFoundException: Could not locate field name [id] on class [java.time.LocalDate]
Can someone help me with this issue?
Thanks a lot!
Update 1 (responding to Phil Ku's approach)
At first glance, this looks promising. Hibernate does not create the "client_import_date" column and sets the foreign key correctly. However, I can not save a device entity. Executing this code
// var client = clientRepository.save(new Client(1234, LocalDate.now());
deviceRepository.save(new Device(567, LocalDate.now(), client));
Hibernate generates the following SQL:
INSERT INTO device(client_id, import_date, id) VALUES(?,?,?);
then tries to bind the following parameters:
[1] as [INTEGER] - [1234]
[2] as [DATE] - [2023-03-22]
[3] as [INTEGER] - [567]
[4] as [DATE] - [2023-03-22]
and then throws the exception Invalid value "4" for parameter "parameterIndex"
. Of course it fails, because there is no fourth parameter to bind to. Any thoughts why Hibernate's parameter list does not match the insert statement?
Update 1.1
The Hibernate exception described in Update 1 appears to occur only in conjunction with the H2 platform. So integration testing might be broken in this scenario. Tested versions: Hibernate 6.1.7 and H2 2.1.214, both shipped with Spring Boot 3.0.4.
Using MariaDB driver everything of Phil Ku's approach works fine. Tracing the Hibernate output reveals that there are still four parameter candidates, selecting the first three doesn't seem to be a problem though.
Update 2
The next transaction related issue arises.
When I execute the following code
var importDate = LocalDate.now();
var client = clientRepository.save(new Client(100, importDate));
var device1 = new Device(1, importDate, client);
var device1 = new Device(2, importDate, client);
deviceRepository.saveAll(List.of(device1, device2));
the exception
SQLIntegrityConstraintViolationException: Cannot add or update a child row: a foreign key constraint fails (`device`, CONSTRAINT `...` FOREIGN KEY (`client_id`, `import_date`) REFERENCES `client` (`id`, `import_date`))
is thrown.
When I instead call deviceRepository.save
for each device, everything works just fine.
I'm not able to mimic this behavior outside of JPA/Hibernate context. Executing the appropriate INSERT statements directly on the database – either in one or in two transactions – never results in a constraint violation.
Any fresh ideas on that?
Update 3
Based on K.Nicholas' and Phil Ku's comments I have re-designed my database schemas. Now, the client
table has an ordinary numeric single column primary key and the import_date
column is just part of a unique constraint key with the (client-)id
column. The primary key is then referenced by the device
table.
Feels a bit like a workaround, but it works perfectly fine though.
Thanks everybody for helping.