Mapping concatenated primary keys
JPA and Ebean use @Embeddable
to map concatenated primary keys (via @IdClass
or @EmbeddedId
).
Primary keys are generally unchanging and best represented by an immutable class.
In the example below, the primary key is made up of partId
and brandId
.
@Embeddable
public final class PartBrandKey {
private final long partId;
private final long brandId;
public PartBrandKey(long partId, long brandId) {
this.partId = partId;
this.brandId = brandId;
}
public long partId() {
return partId;
}
public long brandId() {
return brandId;
}
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
PartBrandKey that = (PartBrandKey) o;
return partId == that.partId && brandId == that.brandId;
}
@Override
public int hashCode() {
return Objects.hash(partId, brandId);
}
}
There is a requirement to correctly implement equals() and hashCode() for an @Embeddable that maps a primary key. The implementations must include all components (partId and brandId in this case). The ORM cannot fix your mistakes here, but most IDEs could generate these methods for you.
Adding a default constructor
Some JPA implementations require a default constructor. This forces Java classes to look much less immutable:
- The fields are no longer final
- We have a private constructor that doesn't look like its used - it will be used by the ORM
- The class is now 'effectively immutable' rather than strictly immutable (internally the ORM is going to use that private constructor, set the values, then hand off the fully populated effective immutable instance)
@Embeddable
public final class PartBrandKey {
private long partId; /** no longer final */
private long brandId; /** no longer final */
private PartBrandKey() { } /** for ORM use only */
public PartBrandKey(long partId, long brandId) {
this.partId = partId;
this.brandId = brandId;
}
public long partId() {
return partId;
}
public long brandId() {
return brandId;
}
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
PartBrandKey that = (PartBrandKey) o;
return partId == that.partId && brandId == that.brandId;
}
@Override
public int hashCode() {
return Objects.hash(partId, brandId);
}
}
Java 14 record type
Java 14 records are immutable with correct equals() and hashCode() implementations. They provide a perfect match for the semantics to map a concatenated primary key with @Embeddable.
@Embeddable
public record PartBrandKey(long partId, long brandId) { }
Kotlin data class
Kotlin data class with val
parameters also perfectly match the semantics.
@Embeddable
data class PartBrandKey(val partId: Long, val brandId: Long)
Java Language vs JVM Bytecode
Annotations like @Embeddable
or @Transactional
are markers for transformation to be
applied to the bytecode.
Ebean bytecode transformation applies equally to Java and other JVM languages like Kotlin, Scala, and Groovy. Java 14 record types are a very important (and nice) step forward for the Java language, but the bytecode they compile to isn't much different from immutable Java non-record classes or Kotlin data classes. No changes were necessary to the Ebean code in order to support Java 14 records.
Java Memory Model
In the related SO post we mention "Java Memory Model" which is in reference to the JVM specification and JSR-133.
- JSR 133 (Java Memory Model) FAQ
- pugh - Java Memory Model
- jcp.org - JSR-133
- wikipedia - Java Memory Model
This relates to the point that although we no longer have final fields and hence don't get the memory visibility guarantees that provides. Noting the instances in this case are not accessible by multiple threads until they are 'effectively immutable'.