From 43d76a0a9924ca0fa1985366b8f085eb34b59420 Mon Sep 17 00:00:00 2001 From: Ulrich Grave Date: Wed, 3 Aug 2022 08:19:21 +0200 Subject: [PATCH] MonetaryAmountType throws NullPointerException when reading a null column value #465 --- .../type/ImmutableCompositeType.java | 328 +++++++++++++++++ .../type/money/MonetaryAmountType.java | 75 ++-- .../money/MySQLMonetaryAmountTypeTest.java | 36 ++ .../PostgreSQLMonetaryAmountTypeTest.java | 36 ++ .../type/ImmutableCompositeType.java | 329 ++++++++++++++++++ .../type/money/MonetaryAmountType.java | 75 ++-- .../money/MySQLMonetaryAmountTypeTest.java | 36 ++ .../PostgreSQLMonetaryAmountTypeTest.java | 36 ++ .../money/MySQLMonetaryAmountTypeTest.java | 36 ++ .../PostgreSQLMonetaryAmountTypeTest.java | 36 ++ 10 files changed, 915 insertions(+), 108 deletions(-) create mode 100644 hibernate-types-52/src/main/java/com/vladmihalcea/hibernate/type/ImmutableCompositeType.java create mode 100644 hibernate-types-55/src/main/java/com/vladmihalcea/hibernate/type/ImmutableCompositeType.java diff --git a/hibernate-types-52/src/main/java/com/vladmihalcea/hibernate/type/ImmutableCompositeType.java b/hibernate-types-52/src/main/java/com/vladmihalcea/hibernate/type/ImmutableCompositeType.java new file mode 100644 index 000000000..8d3ddab99 --- /dev/null +++ b/hibernate-types-52/src/main/java/com/vladmihalcea/hibernate/type/ImmutableCompositeType.java @@ -0,0 +1,328 @@ +package com.vladmihalcea.hibernate.type; + +import com.vladmihalcea.hibernate.type.util.Configuration; +import org.hibernate.HibernateException; +import org.hibernate.MappingException; +import org.hibernate.engine.jdbc.Size; +import org.hibernate.engine.spi.Mapping; +import org.hibernate.engine.spi.SessionFactoryImplementor; +import org.hibernate.engine.spi.SharedSessionContractImplementor; +import org.hibernate.internal.util.collections.ArrayHelper; +import org.hibernate.type.ForeignKeyDirection; +import org.hibernate.type.Type; +import org.hibernate.type.descriptor.java.IncomparableComparator; +import org.hibernate.usertype.CompositeUserType; + +import java.io.Serializable; +import java.sql.PreparedStatement; +import java.sql.ResultSet; +import java.sql.SQLException; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; +import java.util.Map; +import java.util.stream.Collectors; + +/** + * Very convenient base class for implementing immutable object types using Hibernate {@link CompositeUserType}. + *

+ * The {@link ImmutableCompositeType} implements the {@link Type} interface too, so you can pass all + * types extending the {@link ImmutableCompositeType} to the {@link org.hibernate.query.NativeQuery#addScalar(String, Type)} + * method to fix the No Dialect mapping for JDBC type issues. + * + * @author Vlad Mihalcea + */ +public abstract class ImmutableCompositeType implements CompositeUserType, Type { + + private final Configuration configuration; + + private final Class clazz; + + /** + * Initialization constructor taking the {@link Class} + * and using the default {@link Configuration} object. + * + * @param clazz the entity attribute {@link Class} type to be handled + */ + protected ImmutableCompositeType(Class clazz) { + this.clazz = clazz; + this.configuration = Configuration.INSTANCE; + } + + /** + * Initialization constructor taking the {@link Class} and {@link Configuration} objects. + * + * @param clazz the entity attribute {@link Class} type to be handled + * @param configuration custom {@link Configuration} object. + */ + protected ImmutableCompositeType(Class clazz, Configuration configuration) { + this.clazz = clazz; + this.configuration = configuration; + } + + /** + * Get the current {@link Configuration} object. + * + * @return the current {@link Configuration} object. + */ + protected Configuration getConfiguration() { + return configuration; + } + + /** + * Get the column value from the JDBC {@link ResultSet}. + * + * @param rs JDBC {@link ResultSet} + * @param names database column name + * @param session current Hibernate {@link org.hibernate.Session} + * @param owner current Hibernate {@link SessionFactoryImplementor} + * @return column value + * @throws SQLException in case of failure + */ + protected abstract T get(ResultSet rs, String[] names, + SharedSessionContractImplementor session, Object owner) throws SQLException; + + /** + * Set the column value on the provided JDBC {@link PreparedStatement}. + * + * @param st JDBC {@link PreparedStatement} + * @param value database column value + * @param index database column index + * @param session current Hibernate {@link org.hibernate.Session} + * @throws SQLException in case of failure + */ + protected abstract void set(PreparedStatement st, T value, int index, + SharedSessionContractImplementor session) throws SQLException; + + /* Methods inherited from the {@link UserType} interface */ + + @Override + public Object nullSafeGet(ResultSet rs, String[] names, + SharedSessionContractImplementor session, Object owner) throws SQLException { + return get(rs, names, session, owner); + } + + @Override + public void nullSafeSet(PreparedStatement st, Object value, int index, + SharedSessionContractImplementor session) throws SQLException { + set(st, clazz.cast(value), index, session); + } + + @Override + public Class returnedClass() { + return clazz; + } + + @Override + public boolean equals(Object x, Object y) { + return (x == y) || (x != null && x.equals(y)); + } + + @Override + public int hashCode(Object x) { + return x.hashCode(); + } + + @Override + public Object deepCopy(Object value) { + return value; + } + + @Override + public boolean isMutable() { + return false; + } + + @Override + public Serializable disassemble(Object o, SharedSessionContractImplementor session) { + return (Serializable) o; + } + + @Override + public Object assemble(Serializable cached, SharedSessionContractImplementor session, Object owner) throws HibernateException { + return cached; + } + + @Override + public Object replace(Object o, Object target, SharedSessionContractImplementor session, Object owner) { + return o; + } + + /* Methods inherited from the {@link Type} interface */ + + @Override + public boolean isAssociationType() { + return false; + } + + @Override + public boolean isCollectionType() { + return false; + } + + @Override + public boolean isEntityType() { + return false; + } + + @Override + public boolean isAnyType() { + return false; + } + + @Override + public boolean isComponentType() { + return false; + } + + @Override + public int getColumnSpan(Mapping mapping) throws MappingException { + return getPropertyTypes().length; + } + + @Override + public int[] sqlTypes(Mapping mapping) throws MappingException { + List sqlTypes= new ArrayList<>(); + Type[] types = getPropertyTypes(); + for (int i = 0; i < types.length; i++) { + sqlTypes.addAll( + Arrays.stream(types[i].sqlTypes(mapping)).boxed().collect(Collectors.toList()) + ); + } + return sqlTypes.stream().mapToInt(i->i).toArray(); + } + + @Override + public Size[] dictatedSizes(Mapping mapping) throws MappingException { + return new Size[]{new Size()}; + } + + @Override + public Size[] defaultSizes(Mapping mapping) throws MappingException { + return dictatedSizes(mapping); + } + + @Override + public Class getReturnedClass() { + return returnedClass(); + } + + @Override + public boolean isSame(Object x, Object y) throws HibernateException { + return equals(x, y); + } + + @Override + public boolean isEqual(Object x, Object y) throws HibernateException { + return equals(x, y); + } + + @Override + public boolean isEqual(Object x, Object y, SessionFactoryImplementor factory) throws HibernateException { + return equals(x, y); + } + + @Override + public int getHashCode(Object x) throws HibernateException { + return hashCode(x); + } + + @Override + public int getHashCode(Object x, SessionFactoryImplementor factory) throws HibernateException { + return hashCode(x); + } + + @Override + public int compare(Object x, Object y) { + return IncomparableComparator.INSTANCE.compare(x, y); + } + + @Override + public final boolean isDirty(Object old, Object current, SharedSessionContractImplementor session) { + return isDirty(old, current); + } + + @Override + public final boolean isDirty(Object old, Object current, boolean[] checkable, SharedSessionContractImplementor session) { + return checkable[0] && isDirty(old, current); + } + + protected final boolean isDirty(Object old, Object current) { + return !isSame(old, current); + } + + @Override + public boolean isModified(Object dbState, Object currentState, boolean[] checkable, SharedSessionContractImplementor session) throws HibernateException { + return isDirty(dbState, currentState); + } + + @Override + public Object nullSafeGet(ResultSet rs, String name, SharedSessionContractImplementor session, Object owner) throws HibernateException, SQLException { + return get(rs, new String[]{name}, session, owner); + } + + @Override + public void nullSafeSet(PreparedStatement st, Object value, int index, boolean[] settable, SharedSessionContractImplementor session) throws HibernateException, SQLException { + set(st, returnedClass().cast(value), index, session); + } + + @Override + public String toLoggableString(Object value, SessionFactoryImplementor factory) throws HibernateException { + return String.valueOf(value); + } + + @Override + public String getName() { + return getClass().getSimpleName(); + } + + @Override + public Object deepCopy(Object value, SessionFactoryImplementor factory) throws HibernateException { + return deepCopy(value); + } + + @Override + public Serializable disassemble(Object value, SharedSessionContractImplementor session, Object owner) throws HibernateException { + return disassemble(value, session); + } + + @Override + public void beforeAssemble(Serializable cached, SharedSessionContractImplementor session) { + + } + + @Override + public Object hydrate(ResultSet rs, String[] names, SharedSessionContractImplementor session, Object owner) throws HibernateException, SQLException { + return nullSafeGet(rs, names, session, owner); + } + + @Override + public Object resolve(Object value, SharedSessionContractImplementor session, Object owner) throws HibernateException { + return value; + } + + @Override + public Object semiResolve(Object value, SharedSessionContractImplementor session, Object owner) throws HibernateException { + return value; + } + + @Override + public Type getSemiResolvedType(SessionFactoryImplementor factory) { + return this; + } + + @Override + public Object replace(Object original, Object target, SharedSessionContractImplementor session, Object owner, Map copyCache) throws HibernateException { + return replace(original, target, session, owner); + } + + @Override + public Object replace(Object original, Object target, SharedSessionContractImplementor session, Object owner, Map copyCache, ForeignKeyDirection foreignKeyDirection) throws HibernateException { + return replace(original, target, session, owner); + } + + @Override + public boolean[] toColumnNullness(Object value, Mapping mapping) { + return value == null ? ArrayHelper.FALSE : ArrayHelper.TRUE; + } +} \ No newline at end of file diff --git a/hibernate-types-52/src/main/java/com/vladmihalcea/hibernate/type/money/MonetaryAmountType.java b/hibernate-types-52/src/main/java/com/vladmihalcea/hibernate/type/money/MonetaryAmountType.java index ac0afb7b7..86b72525b 100644 --- a/hibernate-types-52/src/main/java/com/vladmihalcea/hibernate/type/money/MonetaryAmountType.java +++ b/hibernate-types-52/src/main/java/com/vladmihalcea/hibernate/type/money/MonetaryAmountType.java @@ -1,20 +1,19 @@ package com.vladmihalcea.hibernate.type.money; +import com.vladmihalcea.hibernate.type.ImmutableCompositeType; +import com.vladmihalcea.hibernate.type.util.Configuration; import org.hibernate.HibernateException; import org.hibernate.engine.spi.SharedSessionContractImplementor; import org.hibernate.type.BigDecimalType; import org.hibernate.type.StringType; import org.hibernate.type.Type; -import org.hibernate.usertype.CompositeUserType; import org.javamoney.moneta.Money; import javax.money.MonetaryAmount; -import java.io.Serializable; import java.math.BigDecimal; import java.sql.PreparedStatement; import java.sql.ResultSet; import java.sql.SQLException; -import java.util.Objects; import static java.sql.Types.DECIMAL; import static java.sql.Types.VARCHAR; @@ -29,7 +28,15 @@ * * @author Piotr Olaszewski */ -public class MonetaryAmountType implements CompositeUserType { +public class MonetaryAmountType extends ImmutableCompositeType { + + public MonetaryAmountType() { + super(MonetaryAmount.class); + } + + public MonetaryAmountType(Configuration configuration) { + super(MonetaryAmount.class, configuration); + } @Override public String[] getPropertyNames() { @@ -59,75 +66,35 @@ public void setPropertyValue(Object component, int property, Object value) throw } @Override - public Class returnedClass() { - return MonetaryAmount.class; - } - - @Override - public boolean equals(Object x, Object y) throws HibernateException { - return Objects.equals(x, y); - } - - @Override - public int hashCode(Object x) throws HibernateException { - return x.hashCode(); - } - - @Override - public Object nullSafeGet(ResultSet rs, String[] names, SharedSessionContractImplementor session, Object owner) throws HibernateException, SQLException { - if (rs.wasNull()) { - return null; - } - + protected MonetaryAmount get(ResultSet rs, String[] names, SharedSessionContractImplementor session, Object owner) throws SQLException { String amountColumnName = names[0]; String currencyColumnName = names[1]; BigDecimal amount = rs.getBigDecimal(amountColumnName); + if(amount == null) { + return null; + } String currency = rs.getString(currencyColumnName); + if(currency == null) { + return null; + } return Money.of(amount, currency); } @Override - public void nullSafeSet(PreparedStatement st, Object value, int amountColumnIndex, SharedSessionContractImplementor session) throws HibernateException, SQLException { + protected void set(PreparedStatement st, MonetaryAmount value, int amountColumnIndex, SharedSessionContractImplementor session) throws SQLException { int currencyColumnIndex = amountColumnIndex + 1; if (value == null) { st.setNull(amountColumnIndex, DECIMAL); st.setNull(currencyColumnIndex, VARCHAR); } else { - MonetaryAmount monetaryAmount = (MonetaryAmount) value; - - BigDecimal amount = monetaryAmount.getNumber().numberValue(BigDecimal.class); - String currency = monetaryAmount.getCurrency().getCurrencyCode(); + BigDecimal amount = value.getNumber().numberValue(BigDecimal.class); + String currency = value.getCurrency().getCurrencyCode(); st.setBigDecimal(amountColumnIndex, amount); st.setString(currencyColumnIndex, currency); } } - - @Override - public Object deepCopy(Object value) throws HibernateException { - return value; - } - - @Override - public boolean isMutable() { - return false; - } - - @Override - public Serializable disassemble(Object value, SharedSessionContractImplementor session) throws HibernateException { - return (Serializable) value; - } - - @Override - public Object assemble(Serializable cached, SharedSessionContractImplementor session, Object owner) throws HibernateException { - return cached; - } - - @Override - public Object replace(Object original, Object target, SharedSessionContractImplementor session, Object owner) throws HibernateException { - return original; - } } diff --git a/hibernate-types-52/src/test/java/com/vladmihalcea/hibernate/type/money/MySQLMonetaryAmountTypeTest.java b/hibernate-types-52/src/test/java/com/vladmihalcea/hibernate/type/money/MySQLMonetaryAmountTypeTest.java index 34ce9c1ce..c6ea0cd2b 100644 --- a/hibernate-types-52/src/test/java/com/vladmihalcea/hibernate/type/money/MySQLMonetaryAmountTypeTest.java +++ b/hibernate-types-52/src/test/java/com/vladmihalcea/hibernate/type/money/MySQLMonetaryAmountTypeTest.java @@ -12,6 +12,7 @@ import java.math.BigDecimal; import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNull; /** * @author Piotr Olaszewski @@ -64,6 +65,31 @@ public void testSearchByMoney() { }); } + @Test + public void testReturnNullMoney() { + Long _id = doInJPA(entityManager -> { + Salary salary = new Salary(); + entityManager.persist(salary); + return salary.getId(); + }); + + doInJPA(entityManager -> { + Salary salary = entityManager.createQuery("select s from Salary s where s.id = :id", Salary.class) + .setParameter("id", _id) + .getSingleResult(); + + assertNull(salary.getSalary()); + }); + + doInJPA(entityManager -> { + MonetaryAmount money = entityManager.createQuery("select s.salary from Salary s where s.id = :id", MonetaryAmount.class) + .setParameter("id", _id) + .getSingleResult(); + + assertNull(money); + }); + } + @Entity(name = "Salary") @Table(name = "salary") @TypeDef(name = "monetary-amount-currency", typeClass = MonetaryAmountType.class, defaultForType = MonetaryAmount.class) @@ -72,6 +98,8 @@ public static class Salary { @GeneratedValue private Long id; + private String other; + @Columns(columns = { @Column(name = "salary_amount"), @Column(name = "salary_currency") @@ -94,5 +122,13 @@ public MonetaryAmount getSalary() { public void setSalary(MonetaryAmount salary) { this.salary = salary; } + + public String getOther() { + return other; + } + + public void setOther(String other) { + this.other = other; + } } } diff --git a/hibernate-types-52/src/test/java/com/vladmihalcea/hibernate/type/money/PostgreSQLMonetaryAmountTypeTest.java b/hibernate-types-52/src/test/java/com/vladmihalcea/hibernate/type/money/PostgreSQLMonetaryAmountTypeTest.java index c537e68b5..784823073 100644 --- a/hibernate-types-52/src/test/java/com/vladmihalcea/hibernate/type/money/PostgreSQLMonetaryAmountTypeTest.java +++ b/hibernate-types-52/src/test/java/com/vladmihalcea/hibernate/type/money/PostgreSQLMonetaryAmountTypeTest.java @@ -12,6 +12,7 @@ import java.math.BigDecimal; import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNull; /** * @author Piotr Olaszewski @@ -64,6 +65,31 @@ public void testSearchByMoney() { }); } + @Test + public void testReturnNullMoney() { + Long _id = doInJPA(entityManager -> { + Salary salary = new Salary(); + entityManager.persist(salary); + return salary.getId(); + }); + + doInJPA(entityManager -> { + Salary salary = entityManager.createQuery("select s from Salary s where s.id = :id", Salary.class) + .setParameter("id", _id) + .getSingleResult(); + + assertNull(salary.getSalary()); + }); + + doInJPA(entityManager -> { + MonetaryAmount money = entityManager.createQuery("select s.salary from Salary s where s.id = :id", MonetaryAmount.class) + .setParameter("id", _id) + .getSingleResult(); + + assertNull(money); + }); + } + @Entity(name = "Salary") @Table(name = "salary") @TypeDef(name = "monetary-amount-currency", typeClass = MonetaryAmountType.class, defaultForType = MonetaryAmount.class) @@ -72,6 +98,8 @@ public static class Salary { @GeneratedValue private Long id; + private String other; + @Columns(columns = { @Column(name = "salary_amount"), @Column(name = "salary_currency") @@ -94,5 +122,13 @@ public MonetaryAmount getSalary() { public void setSalary(MonetaryAmount salary) { this.salary = salary; } + + public String getOther() { + return other; + } + + public void setOther(String other) { + this.other = other; + } } } diff --git a/hibernate-types-55/src/main/java/com/vladmihalcea/hibernate/type/ImmutableCompositeType.java b/hibernate-types-55/src/main/java/com/vladmihalcea/hibernate/type/ImmutableCompositeType.java new file mode 100644 index 000000000..a2f20c1d6 --- /dev/null +++ b/hibernate-types-55/src/main/java/com/vladmihalcea/hibernate/type/ImmutableCompositeType.java @@ -0,0 +1,329 @@ +package com.vladmihalcea.hibernate.type; + +import com.vladmihalcea.hibernate.type.util.Configuration; +import org.hibernate.HibernateException; +import org.hibernate.MappingException; +import org.hibernate.engine.jdbc.Size; +import org.hibernate.engine.spi.Mapping; +import org.hibernate.engine.spi.SessionFactoryImplementor; +import org.hibernate.engine.spi.SharedSessionContractImplementor; +import org.hibernate.internal.util.collections.ArrayHelper; +import org.hibernate.type.ForeignKeyDirection; +import org.hibernate.type.Type; +import org.hibernate.type.descriptor.java.IncomparableComparator; +import org.hibernate.usertype.CompositeUserType; +import org.hibernate.usertype.UserType; + +import java.io.Serializable; +import java.sql.PreparedStatement; +import java.sql.ResultSet; +import java.sql.SQLException; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; +import java.util.Map; +import java.util.stream.Collectors; + +/** + * Very convenient base class for implementing immutable object types using Hibernate {@link CompositeUserType}. + *

+ * The {@link ImmutableCompositeType} implements the {@link Type} interface too, so you can pass all + * types extending the {@link ImmutableCompositeType} to the {@link org.hibernate.query.NativeQuery#addScalar(String, Type)} + * method to fix the No Dialect mapping for JDBC type issues. + * + * @author Vlad Mihalcea + */ +public abstract class ImmutableCompositeType implements CompositeUserType, Type { + + private final Configuration configuration; + + private final Class clazz; + + /** + * Initialization constructor taking the {@link Class} + * and using the default {@link Configuration} object. + * + * @param clazz the entity attribute {@link Class} type to be handled + */ + protected ImmutableCompositeType(Class clazz) { + this.clazz = clazz; + this.configuration = Configuration.INSTANCE; + } + + /** + * Initialization constructor taking the {@link Class} and {@link Configuration} objects. + * + * @param clazz the entity attribute {@link Class} type to be handled + * @param configuration custom {@link Configuration} object. + */ + protected ImmutableCompositeType(Class clazz, Configuration configuration) { + this.clazz = clazz; + this.configuration = configuration; + } + + /** + * Get the current {@link Configuration} object. + * + * @return the current {@link Configuration} object. + */ + protected Configuration getConfiguration() { + return configuration; + } + + /** + * Get the column value from the JDBC {@link ResultSet}. + * + * @param rs JDBC {@link ResultSet} + * @param names database column name + * @param session current Hibernate {@link org.hibernate.Session} + * @param owner current Hibernate {@link SessionFactoryImplementor} + * @return column value + * @throws SQLException in case of failure + */ + protected abstract T get(ResultSet rs, String[] names, + SharedSessionContractImplementor session, Object owner) throws SQLException; + + /** + * Set the column value on the provided JDBC {@link PreparedStatement}. + * + * @param st JDBC {@link PreparedStatement} + * @param value database column value + * @param index database column index + * @param session current Hibernate {@link org.hibernate.Session} + * @throws SQLException in case of failure + */ + protected abstract void set(PreparedStatement st, T value, int index, + SharedSessionContractImplementor session) throws SQLException; + + /* Methods inherited from the {@link UserType} interface */ + + @Override + public Object nullSafeGet(ResultSet rs, String[] names, + SharedSessionContractImplementor session, Object owner) throws SQLException { + return get(rs, names, session, owner); + } + + @Override + public void nullSafeSet(PreparedStatement st, Object value, int index, + SharedSessionContractImplementor session) throws SQLException { + set(st, clazz.cast(value), index, session); + } + + @Override + public Class returnedClass() { + return clazz; + } + + @Override + public boolean equals(Object x, Object y) { + return (x == y) || (x != null && x.equals(y)); + } + + @Override + public int hashCode(Object x) { + return x.hashCode(); + } + + @Override + public Object deepCopy(Object value) { + return value; + } + + @Override + public boolean isMutable() { + return false; + } + + @Override + public Serializable disassemble(Object o, SharedSessionContractImplementor session) { + return (Serializable) o; + } + + @Override + public Object assemble(Serializable cached, SharedSessionContractImplementor session, Object owner) throws HibernateException { + return cached; + } + + @Override + public Object replace(Object o, Object target, SharedSessionContractImplementor session, Object owner) { + return o; + } + + /* Methods inherited from the {@link Type} interface */ + + @Override + public boolean isAssociationType() { + return false; + } + + @Override + public boolean isCollectionType() { + return false; + } + + @Override + public boolean isEntityType() { + return false; + } + + @Override + public boolean isAnyType() { + return false; + } + + @Override + public boolean isComponentType() { + return false; + } + + @Override + public int getColumnSpan(Mapping mapping) throws MappingException { + return getPropertyTypes().length; + } + + @Override + public int[] sqlTypes(Mapping mapping) throws MappingException { + List sqlTypes= new ArrayList<>(); + Type[] types = getPropertyTypes(); + for (int i = 0; i < types.length; i++) { + sqlTypes.addAll( + Arrays.stream(types[i].sqlTypes(mapping)).boxed().collect(Collectors.toList()) + ); + } + return sqlTypes.stream().mapToInt(i->i).toArray(); + } + + @Override + public Size[] dictatedSizes(Mapping mapping) throws MappingException { + return new Size[]{new Size()}; + } + + @Override + public Size[] defaultSizes(Mapping mapping) throws MappingException { + return dictatedSizes(mapping); + } + + @Override + public Class getReturnedClass() { + return returnedClass(); + } + + @Override + public boolean isSame(Object x, Object y) throws HibernateException { + return equals(x, y); + } + + @Override + public boolean isEqual(Object x, Object y) throws HibernateException { + return equals(x, y); + } + + @Override + public boolean isEqual(Object x, Object y, SessionFactoryImplementor factory) throws HibernateException { + return equals(x, y); + } + + @Override + public int getHashCode(Object x) throws HibernateException { + return hashCode(x); + } + + @Override + public int getHashCode(Object x, SessionFactoryImplementor factory) throws HibernateException { + return hashCode(x); + } + + @Override + public int compare(Object x, Object y) { + return IncomparableComparator.INSTANCE.compare(x, y); + } + + @Override + public final boolean isDirty(Object old, Object current, SharedSessionContractImplementor session) { + return isDirty(old, current); + } + + @Override + public final boolean isDirty(Object old, Object current, boolean[] checkable, SharedSessionContractImplementor session) { + return checkable[0] && isDirty(old, current); + } + + protected final boolean isDirty(Object old, Object current) { + return !isSame(old, current); + } + + @Override + public boolean isModified(Object dbState, Object currentState, boolean[] checkable, SharedSessionContractImplementor session) throws HibernateException { + return isDirty(dbState, currentState); + } + + @Override + public Object nullSafeGet(ResultSet rs, String name, SharedSessionContractImplementor session, Object owner) throws HibernateException, SQLException { + return get(rs, new String[]{name}, session, owner); + } + + @Override + public void nullSafeSet(PreparedStatement st, Object value, int index, boolean[] settable, SharedSessionContractImplementor session) throws HibernateException, SQLException { + set(st, returnedClass().cast(value), index, session); + } + + @Override + public String toLoggableString(Object value, SessionFactoryImplementor factory) throws HibernateException { + return String.valueOf(value); + } + + @Override + public String getName() { + return getClass().getSimpleName(); + } + + @Override + public Object deepCopy(Object value, SessionFactoryImplementor factory) throws HibernateException { + return deepCopy(value); + } + + @Override + public Serializable disassemble(Object value, SharedSessionContractImplementor session, Object owner) throws HibernateException { + return disassemble(value, session); + } + + @Override + public void beforeAssemble(Serializable cached, SharedSessionContractImplementor session) { + + } + + @Override + public Object hydrate(ResultSet rs, String[] names, SharedSessionContractImplementor session, Object owner) throws HibernateException, SQLException { + return nullSafeGet(rs, names, session, owner); + } + + @Override + public Object resolve(Object value, SharedSessionContractImplementor session, Object owner) throws HibernateException { + return value; + } + + @Override + public Object semiResolve(Object value, SharedSessionContractImplementor session, Object owner) throws HibernateException { + return value; + } + + @Override + public Type getSemiResolvedType(SessionFactoryImplementor factory) { + return this; + } + + @Override + public Object replace(Object original, Object target, SharedSessionContractImplementor session, Object owner, Map copyCache) throws HibernateException { + return replace(original, target, session, owner); + } + + @Override + public Object replace(Object original, Object target, SharedSessionContractImplementor session, Object owner, Map copyCache, ForeignKeyDirection foreignKeyDirection) throws HibernateException { + return replace(original, target, session, owner); + } + + @Override + public boolean[] toColumnNullness(Object value, Mapping mapping) { + return value == null ? ArrayHelper.FALSE : ArrayHelper.TRUE; + } +} \ No newline at end of file diff --git a/hibernate-types-55/src/main/java/com/vladmihalcea/hibernate/type/money/MonetaryAmountType.java b/hibernate-types-55/src/main/java/com/vladmihalcea/hibernate/type/money/MonetaryAmountType.java index ac0afb7b7..86b72525b 100644 --- a/hibernate-types-55/src/main/java/com/vladmihalcea/hibernate/type/money/MonetaryAmountType.java +++ b/hibernate-types-55/src/main/java/com/vladmihalcea/hibernate/type/money/MonetaryAmountType.java @@ -1,20 +1,19 @@ package com.vladmihalcea.hibernate.type.money; +import com.vladmihalcea.hibernate.type.ImmutableCompositeType; +import com.vladmihalcea.hibernate.type.util.Configuration; import org.hibernate.HibernateException; import org.hibernate.engine.spi.SharedSessionContractImplementor; import org.hibernate.type.BigDecimalType; import org.hibernate.type.StringType; import org.hibernate.type.Type; -import org.hibernate.usertype.CompositeUserType; import org.javamoney.moneta.Money; import javax.money.MonetaryAmount; -import java.io.Serializable; import java.math.BigDecimal; import java.sql.PreparedStatement; import java.sql.ResultSet; import java.sql.SQLException; -import java.util.Objects; import static java.sql.Types.DECIMAL; import static java.sql.Types.VARCHAR; @@ -29,7 +28,15 @@ * * @author Piotr Olaszewski */ -public class MonetaryAmountType implements CompositeUserType { +public class MonetaryAmountType extends ImmutableCompositeType { + + public MonetaryAmountType() { + super(MonetaryAmount.class); + } + + public MonetaryAmountType(Configuration configuration) { + super(MonetaryAmount.class, configuration); + } @Override public String[] getPropertyNames() { @@ -59,75 +66,35 @@ public void setPropertyValue(Object component, int property, Object value) throw } @Override - public Class returnedClass() { - return MonetaryAmount.class; - } - - @Override - public boolean equals(Object x, Object y) throws HibernateException { - return Objects.equals(x, y); - } - - @Override - public int hashCode(Object x) throws HibernateException { - return x.hashCode(); - } - - @Override - public Object nullSafeGet(ResultSet rs, String[] names, SharedSessionContractImplementor session, Object owner) throws HibernateException, SQLException { - if (rs.wasNull()) { - return null; - } - + protected MonetaryAmount get(ResultSet rs, String[] names, SharedSessionContractImplementor session, Object owner) throws SQLException { String amountColumnName = names[0]; String currencyColumnName = names[1]; BigDecimal amount = rs.getBigDecimal(amountColumnName); + if(amount == null) { + return null; + } String currency = rs.getString(currencyColumnName); + if(currency == null) { + return null; + } return Money.of(amount, currency); } @Override - public void nullSafeSet(PreparedStatement st, Object value, int amountColumnIndex, SharedSessionContractImplementor session) throws HibernateException, SQLException { + protected void set(PreparedStatement st, MonetaryAmount value, int amountColumnIndex, SharedSessionContractImplementor session) throws SQLException { int currencyColumnIndex = amountColumnIndex + 1; if (value == null) { st.setNull(amountColumnIndex, DECIMAL); st.setNull(currencyColumnIndex, VARCHAR); } else { - MonetaryAmount monetaryAmount = (MonetaryAmount) value; - - BigDecimal amount = monetaryAmount.getNumber().numberValue(BigDecimal.class); - String currency = monetaryAmount.getCurrency().getCurrencyCode(); + BigDecimal amount = value.getNumber().numberValue(BigDecimal.class); + String currency = value.getCurrency().getCurrencyCode(); st.setBigDecimal(amountColumnIndex, amount); st.setString(currencyColumnIndex, currency); } } - - @Override - public Object deepCopy(Object value) throws HibernateException { - return value; - } - - @Override - public boolean isMutable() { - return false; - } - - @Override - public Serializable disassemble(Object value, SharedSessionContractImplementor session) throws HibernateException { - return (Serializable) value; - } - - @Override - public Object assemble(Serializable cached, SharedSessionContractImplementor session, Object owner) throws HibernateException { - return cached; - } - - @Override - public Object replace(Object original, Object target, SharedSessionContractImplementor session, Object owner) throws HibernateException { - return original; - } } diff --git a/hibernate-types-55/src/test/java/com/vladmihalcea/hibernate/type/money/MySQLMonetaryAmountTypeTest.java b/hibernate-types-55/src/test/java/com/vladmihalcea/hibernate/type/money/MySQLMonetaryAmountTypeTest.java index 34ce9c1ce..c6ea0cd2b 100644 --- a/hibernate-types-55/src/test/java/com/vladmihalcea/hibernate/type/money/MySQLMonetaryAmountTypeTest.java +++ b/hibernate-types-55/src/test/java/com/vladmihalcea/hibernate/type/money/MySQLMonetaryAmountTypeTest.java @@ -12,6 +12,7 @@ import java.math.BigDecimal; import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNull; /** * @author Piotr Olaszewski @@ -64,6 +65,31 @@ public void testSearchByMoney() { }); } + @Test + public void testReturnNullMoney() { + Long _id = doInJPA(entityManager -> { + Salary salary = new Salary(); + entityManager.persist(salary); + return salary.getId(); + }); + + doInJPA(entityManager -> { + Salary salary = entityManager.createQuery("select s from Salary s where s.id = :id", Salary.class) + .setParameter("id", _id) + .getSingleResult(); + + assertNull(salary.getSalary()); + }); + + doInJPA(entityManager -> { + MonetaryAmount money = entityManager.createQuery("select s.salary from Salary s where s.id = :id", MonetaryAmount.class) + .setParameter("id", _id) + .getSingleResult(); + + assertNull(money); + }); + } + @Entity(name = "Salary") @Table(name = "salary") @TypeDef(name = "monetary-amount-currency", typeClass = MonetaryAmountType.class, defaultForType = MonetaryAmount.class) @@ -72,6 +98,8 @@ public static class Salary { @GeneratedValue private Long id; + private String other; + @Columns(columns = { @Column(name = "salary_amount"), @Column(name = "salary_currency") @@ -94,5 +122,13 @@ public MonetaryAmount getSalary() { public void setSalary(MonetaryAmount salary) { this.salary = salary; } + + public String getOther() { + return other; + } + + public void setOther(String other) { + this.other = other; + } } } diff --git a/hibernate-types-55/src/test/java/com/vladmihalcea/hibernate/type/money/PostgreSQLMonetaryAmountTypeTest.java b/hibernate-types-55/src/test/java/com/vladmihalcea/hibernate/type/money/PostgreSQLMonetaryAmountTypeTest.java index c537e68b5..784823073 100644 --- a/hibernate-types-55/src/test/java/com/vladmihalcea/hibernate/type/money/PostgreSQLMonetaryAmountTypeTest.java +++ b/hibernate-types-55/src/test/java/com/vladmihalcea/hibernate/type/money/PostgreSQLMonetaryAmountTypeTest.java @@ -12,6 +12,7 @@ import java.math.BigDecimal; import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNull; /** * @author Piotr Olaszewski @@ -64,6 +65,31 @@ public void testSearchByMoney() { }); } + @Test + public void testReturnNullMoney() { + Long _id = doInJPA(entityManager -> { + Salary salary = new Salary(); + entityManager.persist(salary); + return salary.getId(); + }); + + doInJPA(entityManager -> { + Salary salary = entityManager.createQuery("select s from Salary s where s.id = :id", Salary.class) + .setParameter("id", _id) + .getSingleResult(); + + assertNull(salary.getSalary()); + }); + + doInJPA(entityManager -> { + MonetaryAmount money = entityManager.createQuery("select s.salary from Salary s where s.id = :id", MonetaryAmount.class) + .setParameter("id", _id) + .getSingleResult(); + + assertNull(money); + }); + } + @Entity(name = "Salary") @Table(name = "salary") @TypeDef(name = "monetary-amount-currency", typeClass = MonetaryAmountType.class, defaultForType = MonetaryAmount.class) @@ -72,6 +98,8 @@ public static class Salary { @GeneratedValue private Long id; + private String other; + @Columns(columns = { @Column(name = "salary_amount"), @Column(name = "salary_currency") @@ -94,5 +122,13 @@ public MonetaryAmount getSalary() { public void setSalary(MonetaryAmount salary) { this.salary = salary; } + + public String getOther() { + return other; + } + + public void setOther(String other) { + this.other = other; + } } } diff --git a/hibernate-types-60/src/test/java/com/vladmihalcea/hibernate/type/money/MySQLMonetaryAmountTypeTest.java b/hibernate-types-60/src/test/java/com/vladmihalcea/hibernate/type/money/MySQLMonetaryAmountTypeTest.java index 7da4bd3a0..49aa32a0c 100644 --- a/hibernate-types-60/src/test/java/com/vladmihalcea/hibernate/type/money/MySQLMonetaryAmountTypeTest.java +++ b/hibernate-types-60/src/test/java/com/vladmihalcea/hibernate/type/money/MySQLMonetaryAmountTypeTest.java @@ -10,6 +10,7 @@ import java.math.BigDecimal; import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNull; /** * @author Piotr Olaszewski @@ -62,6 +63,31 @@ public void testSearchByMoney() { }); } + @Test + public void testReturnNullMoney() { + Long _id = doInJPA(entityManager -> { + Salary salary = new Salary(); + entityManager.persist(salary); + return salary.getId(); + }); + + doInJPA(entityManager -> { + Salary salary = entityManager.createQuery("select s from Salary s where s.id = :id", Salary.class) + .setParameter("id", _id) + .getSingleResult(); + + assertNull(salary.getSalary()); + }); + + doInJPA(entityManager -> { + MonetaryAmount money = entityManager.createQuery("select s.salary from Salary s where s.id = :id", MonetaryAmount.class) + .setParameter("id", _id) + .getSingleResult(); + + assertNull(money); + }); + } + @Entity(name = "Salary") @Table(name = "salary") public static class Salary { @@ -69,6 +95,8 @@ public static class Salary { @GeneratedValue private Long id; + private String other; + @AttributeOverride(name = "amount", column = @Column(name = "salary_amount")) @AttributeOverride(name = "currency", column = @Column(name = "salary_currency")) @CompositeType(MonetaryAmountType.class) @@ -89,5 +117,13 @@ public MonetaryAmount getSalary() { public void setSalary(MonetaryAmount salary) { this.salary = salary; } + + public String getOther() { + return other; + } + + public void setOther(String other) { + this.other = other; + } } } diff --git a/hibernate-types-60/src/test/java/com/vladmihalcea/hibernate/type/money/PostgreSQLMonetaryAmountTypeTest.java b/hibernate-types-60/src/test/java/com/vladmihalcea/hibernate/type/money/PostgreSQLMonetaryAmountTypeTest.java index 7778c7794..558f8b0d8 100644 --- a/hibernate-types-60/src/test/java/com/vladmihalcea/hibernate/type/money/PostgreSQLMonetaryAmountTypeTest.java +++ b/hibernate-types-60/src/test/java/com/vladmihalcea/hibernate/type/money/PostgreSQLMonetaryAmountTypeTest.java @@ -10,6 +10,7 @@ import java.math.BigDecimal; import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNull; /** * @author Piotr Olaszewski @@ -62,6 +63,31 @@ public void testSearchByMoney() { }); } + @Test + public void testReturnNullMoney() { + Long _id = doInJPA(entityManager -> { + Salary salary = new Salary(); + entityManager.persist(salary); + return salary.getId(); + }); + + doInJPA(entityManager -> { + Salary salary = entityManager.createQuery("select s from Salary s where s.id = :id", Salary.class) + .setParameter("id", _id) + .getSingleResult(); + + assertNull(salary.getSalary()); + }); + + doInJPA(entityManager -> { + MonetaryAmount money = entityManager.createQuery("select s.salary from Salary s where s.id = :id", MonetaryAmount.class) + .setParameter("id", _id) + .getSingleResult(); + + assertNull(money); + }); + } + @Entity(name = "Salary") @Table(name = "salary") public static class Salary { @@ -69,6 +95,8 @@ public static class Salary { @GeneratedValue private Long id; + private String other; + @AttributeOverride(name = "amount", column = @Column(name = "salary_amount")) @AttributeOverride(name = "currency", column = @Column(name = "salary_currency")) @CompositeType(MonetaryAmountType.class) @@ -89,5 +117,13 @@ public MonetaryAmount getSalary() { public void setSalary(MonetaryAmount salary) { this.salary = salary; } + + public String getOther() { + return other; + } + + public void setOther(String other) { + this.other = other; + } } }