Sahawut Wesaratchakit (
https://hibernate.atlassian.net/secure/ViewProfile.jspa?accountId=557058%...
) *updated* an issue
Hibernate ORM (
https://hibernate.atlassian.net/browse/HHH?atlOrigin=eyJpIjoiNjIwNjJmMzRk...
) / Bug (
https://hibernate.atlassian.net/browse/HHH-16015?atlOrigin=eyJpIjoiNjIwNj...
) HHH-16015 (
https://hibernate.atlassian.net/browse/HHH-16015?atlOrigin=eyJpIjoiNjIwNj...
) NPE when using CompositeUserType in Hibernate 6.1.6 (
https://hibernate.atlassian.net/browse/HHH-16015?atlOrigin=eyJpIjoiNjIwNj...
)
Change By: Sahawut Wesaratchakit (
https://hibernate.atlassian.net/secure/ViewProfile.jspa?accountId=557058%...
)
I’m migrating my {{UserType}} class from Hibernate 5 to 6. As far as I understand the
rules, I should use {{CompositeUserType}} because my mapped class is rather complex and
encapsulates several properties to be persisted. This can’t be accomplished by
{{UserType}} according to Hibernate 6’s JavaDoc. For the most part, it works. But there is
one particular scenario where it can’t handle the cascading merge; I got a
{{NullPointerException}}, as follows:
{quote}java.lang.NullPointerException: Cannot invoke
“org.hibernate.property.access.spi.Setter.set(Object, Object)” because the return value of
“org.hibernate.property.access.spi.PropertyAccess.getSetter()” is null{quote}
See [entire exception stack
trace|https://github.com/sahawut/hibernate6-issue/blob/master/Test%20case...]
I created a [minimal sample
project|https://github.com/sahawut/hibernate6-issue] to
reproduce the problem. [ @beikov identified it as a bug |https://discourse.
hibernate.org/t/npe-when-using-compositeusertype-in-hibernate-6/7121/2?u=...].
For your convenience, I excerpt the important code from my sample project for a quick
read.
Here is my {{VaultValue}} type encapsulating a piece of data securely stored in a Vault:
{noformat}public class VaultValue {
protected String value = null; // plaintext to be persisted into a secure Vault storage
protected Instant date = null; // the date when plaintext was persisted into the secure
Vault storage
protected Long vaultId = null; // an opaque id used to reference the plaintext persisted
in the secure Vault storage
protected String hashValue = null; // a hash value calculated based on the plaintext
public VaultValue() {}
public VaultValue(Long vaultId, String hashValue) {
this.vaultId = vaultId;
this.hashValue = hashValue;
}
public VaultValue(VaultValue src) {
this.vaultId = src.vaultId;
this.hashValue = src.hashValue;
this.value = src.value;
this.date = src.date;
}
public Long getVaultId() {
return this.vaultId;
}
public String getHashValue() {
return this.hashValue;
}
private boolean setValue(String value, Instant date) {
final var newHash = hash(value);
final boolean changed = !StringUtils.equals(this.hashValue, newHash);
this.value = value;
this.date = date;
this.hashValue = newHash;
return changed;
}
public static VaultValue setValue(VaultValue vaultValue, String value, Instant date) {
VaultValue retval = (vaultValue == null ? new VaultValue() : vaultValue);
retval.setValue(value, date);
return retval;
}
public static String hash(String value) {
if (value == null)
return "";
return DigestUtils.sha256Hex(value.getBytes());
}
@Override public boolean equals(Object o) {
if (o == this) return true;
if (!(o instanceof VaultValue)) return false;
VaultValue other = (VaultValue)o;
// For use by Hibernate dirty-checking, it is important that this test only the
// values that are persisted in the database: vaultId and hashValue.
if (!Objects.equals(this.vaultId, other.vaultId)) return false;
if (!Objects.equals(this.hashValue, other.hashValue)) return false;
return true;
}
@Override public int hashCode() {
return Objects.hashCode(this.vaultId);
}
}
{noformat}
Here is my {{CompositeUserType}} class that maps the {{VaultValue}} type:
{noformat}public class VaultType implements CompositeUserType<VaultValue> {
public static class EmbeddableMapper {
String hash;
Long id;
}
@Override
public Object getPropertyValue(VaultValue component, int property) throws
HibernateException {
return switch (property) { // Hibernate sorts the properties by their names
alphabetically
case 0 -> component.getHashValue();
case 1 -> component.getVaultId();
default -> null;
};
}
@Override
public VaultValue instantiate(ValueAccess values, SessionFactoryImplementor
sessionFactory) {
final String hash = values.getValue(0, String.class);
final Long id = values.getValue(1, Long.class);
return new VaultValue(id, hash);
}
@Override public Class<?> embeddable() { return EmbeddableMapper.class; }
@Override public Class<VaultValue> returnedClass() { return VaultValue.class; }
@Override public boolean equals(VaultValue x, VaultValue y) { return Objects.equals(x, y);
}
@Override public int hashCode(VaultValue x) { return Objects.hashCode(x); }
@Override public boolean isMutable() { return true; } // Yes, the VaultValue internal
state may change
@Override public VaultValue deepCopy(VaultValue value) {
if (value == null) return null;
return new VaultValue(value);
}
@Override public Serializable disassemble(VaultValue value) { throw new
UnsupportedOperationException(); }
@Override public VaultValue assemble(Serializable cached, Object owner) { throw new
UnsupportedOperationException(); }
@Override public VaultValue replace(VaultValue detached, VaultValue managed, Object owner)
{
return deepCopy(detached);
}
}
{noformat}
In my model, I have two entity classes:
* *Encounter* - an encounter in a healthcare setting
* *Observation* - a physician’s observation of the patient
An Encounter has a collection of Observations; basically, *Encounter* and *Observation*
have a cascade one-to-many relation. Both the *Encounter* entity and the *Observation*
entity have a property of my {{VaultValue}} class.
By and large, they seem to work with Hibernate 6.1.6 (via Spring Boot 3.0.1). However,
when an *Encounter* instance is re-saved after some new *Observation* instances are added
to it, I get {{NullPointerException}} caused by
{{PropertyAccessCompositeUserTypeImpl::getSetter()}} which always returns {{null}}.
Here is the {{Observation}} entity class:
{noformat}@Entity
public class Observation extends IdObject {
@ManyToOne(fetch = FetchType.EAGER, optional = false)
@JoinColumn(name = "id_encounter", nullable = false, updatable = false,
foreignKey = @ForeignKey(name = "FK_observation_has_encounter"))
private Encounter encounter;
public void setEncounter(Encounter encounter) {
this.encounter = encounter;
}
public Encounter getEncounter() {
return this.encounter;
}
@Embedded
@AttributeOverride(name = "id", column = @Column(name =
"secure_value_id"))
@AttributeOverride(name = "hash", column = @Column(name =
"secure_value_hash"))
@CompositeType(VaultType.class)
private VaultValue secureValue = new VaultValue();
public void setSecureValue(VaultValue secureValue) {
this.secureValue = secureValue;
}
public void setSecureValue(String value, Instant referenceDate) {
secureValue = VaultValue.setValue(secureValue, StringUtils.trimToNull(value),
referenceDate);
}
public VaultValue getSecureValue() {
return this.secureValue;
}
}
{noformat}
Here is the {{Encounter}} entity class:
{noformat}@Entity
public class Encounter extends IdObject {
@Embedded
@AttributeOverride(name = "id", column = @Column(name =
"identifier_id"))
@AttributeOverride(name = "hash", column = @Column(name =
"identifier_hash"))
@CompositeType(VaultType.class)
private VaultValue identifier = new VaultValue();
public void setIdentifier(String rawValue, Instant referenceDate) {
String value = StringUtils.trimToNull(rawValue);
identifier = VaultValue.setValue(identifier, value, referenceDate);
}
public VaultValue getIdentifier() {
return this.identifier;
}
@OneToMany(fetch = FetchType.LAZY, cascade = CascadeType.ALL, mappedBy =
"encounter")
private Set<Observation> observations = new HashSet<>();
public Set<Observation> getObservations() {
return this.observations;
}
public void setObservations(Set<Observation> observations) {
this.observations = observations;
}
}
{noformat}
The following is the test code that reproduces the {{NullPointerException}} (i.e. the Test
Case 2 in {{HibernateApplicationTests}} code in my minimal sample project, see the link
above):
{noformat}@Test
void saveEncounterWithoutSavingObservationsThenSaveEncounterAgain_NOT_OK() {
// Create and save an Encounter.
final Encounter encounter = new Encounter();
encounter.setIdentifier("Medical-Record-Number-123456", Instant.now());
mockPersistToVault(encounter.getIdentifier());
encounterRepository.save(encounter);
// Create and add an Observation to the Encounter.
final Observation observation = new Observation();
observation.setEncounter(encounter);
encounter.getObservations().add(observation);
observation.setSecureValue("triage notes including patient's private information
blah blah blah...", Instant.now());
mockPersistToVault(observation.getSecureValue());
// Attempt re-saving the Encounter in order to propagate saving of Observations via
one-to-many cascade.
assertThatThrownBy(() -> encounterRepository.save(encounter))
.isInstanceOf(NullPointerException.class); // Why does
PropertyAccessCompositeUserTypeImpl::getSetter() return null?
/* encounterRepository.save(encounter); */ // desired
/* assertThat(observation.getId()).isNotNull(); */ // desired
}
{noformat}
Note:
* {{encounterRepository}} is an injected Spring JPA Repository
* {{mockPersistToVault()}} just assigns {{vaultId}} an arbitrary number for the
{{VaultValue}} argument
I could work around it by saving every {{Observation}} in the collection first before
re-saving the {{Encounter}}. However, this shouldn’t be necessary because I set up cascade
for the one-to-many relation. In fact, I didn’t need to do so with Hibernate 5.6.14.
(
https://hibernate.atlassian.net/browse/HHH-16015#add-comment?atlOrigin=ey...
) Add Comment (
https://hibernate.atlassian.net/browse/HHH-16015#add-comment?atlOrigin=ey...
)
Get Jira notifications on your phone! Download the Jira Cloud app for Android (
https://play.google.com/store/apps/details?id=com.atlassian.android.jira....
) or iOS (
https://itunes.apple.com/app/apple-store/id1006972087?pt=696495&ct=Em...
) This message was sent by Atlassian Jira (v1001.0.0-SNAPSHOT#100214- sha1:b03f6a4 )