Sahawut Wesaratchakit (
https://hibernate.atlassian.net/secure/ViewProfile.jspa?accountId=557058%...
) *created* an issue
Hibernate ORM (
https://hibernate.atlassian.net/browse/HHH?atlOrigin=eyJpIjoiNjU1ZDRhYmY0...
) / Bug (
https://hibernate.atlassian.net/browse/HHH-16015?atlOrigin=eyJpIjoiNjU1ZD...
) HHH-16015 (
https://hibernate.atlassian.net/browse/HHH-16015?atlOrigin=eyJpIjoiNjU1ZD...
) NPE when using CompositeUserType in Hibernate 6.1.6 (
https://hibernate.atlassian.net/browse/HHH-16015?atlOrigin=eyJpIjoiNjU1ZD...
)
Issue Type: Bug Affects Versions: 6.1.6 Assignee: Unassigned Created: 10/Jan/2023 10:10 AM
Environment: Hibernate-6.1.6, Java-17, Windows-11, PostGreSQL, H2 Priority: Major
Reporter: 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:
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
See entire exception stack trace (
https://github.com/sahawut/hibernate6-issue/blob/master/Test%20case%202%2...
)
I created a minimal sample project (
https://github.com/sahawut/hibernate6-issue ) to
reproduce the problem. @beikov identified it as a bug.
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:
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);
}
}
Here is my CompositeUserType class that maps the VaultValue type:
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);
}
}
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:
@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;
}
}
Here is the Encounter entity class:
@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;
}
}
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):
@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
}
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#100213- sha1:cca7326 )