JavaMoney / jsr354-ri

JSR 354 - Moneta: Reference Implementation
Other
342 stars 101 forks source link

FastMoney or Money CompositeUserType mapping for Hibernate - ideas? #185

Open uded opened 6 years ago

uded commented 6 years ago

OK, I went through the API as much as I could and as I do fully understand why it is created the way it is - it causes a problem for JPA/Hibernate mapping.

The easiest and, possibly, the safest way is to use toString/Parse to represent the amount and currency in the DB. But this is far from perfect if one would like, well I do not know... like operate with SQL on this representation? sum, min, max... none will work on VARCHAR the way I would like to. But I do not see alternatives as I do not have setters to operate by property index on the object. Meaning either I have all data or I won't create an object that I can pass on by reference... or maybe I am missing something.

Any bits of advice? Thoughts on this? I would love to store FastMoney as two separate columns, but I do not see a reasonable option for this...

keilw commented 6 years ago

Which mapping did you try, Jadira?

stokito commented 6 years ago

First of all, SQL has a CAST operator which can be used to convert varchar to number so you’ll be able to use agregated function SUM, MIN etc. But better not to do that and use a number field and then use Jadira Usertypes library to map the field to a Money object. See http://jadira.sourceforge.net/usertype-userguide.html for details. I don’t see that this issue is related to moneta library itself, so let’s close it

keilw commented 6 years ago

Not sure, how you came to SourceForge with Jadira, maybe it still uses that for downloads, but the Issue tracker is https://github.com/JadiraOrg/jadira/issues. Would someone consider filing an issue there?

stokito commented 6 years ago

This is not an issue. It looks like uded wanted to use hibernate’s composition mapping which needs for setters. Jadira will help you.

orousseil commented 5 years ago

Jadira is a good solution. If you don't want to use it, you could just add this class

public class PersistentMoneyAmountAndCurrency implements CompositeUserType {

    public String[] getPropertyNames() {
        // ORDER IS IMPORTANT!  it must match the order the columns are defined in the property mapping
        return new String[]{"currency", "amount"};
    }

    public Type[] getPropertyTypes() {
        return new Type[]{StringType.INSTANCE, BigDecimalType.INSTANCE};
    }

    @Override
    public Class returnedClass() {
        return Money.class;
    }

    public Object getPropertyValue(Object component, int propertyIndex) {
        if (component == null) {
            return null;
        }

        final Money money = (Money) component;
        switch (propertyIndex) {
            case 0:
                return money.getCurrency().getCurrencyCode();
            case 1:
                return money.getNumber().numberValue(BigDecimal.class);
            default:
                throw new HibernateException("Invalid property index [" + propertyIndex + "]");
        }
    }

    public void setPropertyValue(Object component, int propertyIndex, Object value) {
        if (component == null) {
            return;
        }
        throw new HibernateException("Called setPropertyValue on an immutable type {" + component.getClass() + "}");
    }

    @Override
    public Object nullSafeGet(ResultSet resultSet, String[] names, SharedSessionContractImplementor session, Object object) throws SQLException {
        assert names.length == 2;

        //owner here is of type TestUser or the actual owning Object
        Money money = null;
        final String currency = resultSet.getString(names[0]);
        //Deferred check after first read
        if (!resultSet.wasNull()) {
            final BigDecimal amount = resultSet.getBigDecimal(names[1]);
            money = (Money) MoneyUtils.amount(amount, currency);
        }
        return money;
    }

    @Override
    public void nullSafeSet(PreparedStatement preparedStatement, Object value, int property, SharedSessionContractImplementor session) throws SQLException {
        if (null == value) {
            preparedStatement.setNull(property, StringType.INSTANCE.sqlType());
            preparedStatement.setNull(property + 1, BigDecimalType.INSTANCE.sqlType());
        } else {
            final Money amount = (Money) value;
            preparedStatement.setString(property, amount.getCurrency().getCurrencyCode());
            preparedStatement.setBigDecimal(property + 1, amount.getNumber().numberValue(BigDecimal.class));
        }
    }

    /**
     * Used while dirty checking - control passed on to the {@link MonetaryAmount}
     */
    @Override
    public boolean equals(final Object o1, final Object o2) {
        return Objects.equals(o1, o2);
    }

    @Override
    public int hashCode(final Object value) {
        return value.hashCode();
    }

    /**
     * Helps hibernate apply certain optimizations for immutable objects
     */
    @Override
    public boolean isMutable() {
        return false;
    }

    /**
     * Used to create Snapshots of the object
     */
    @Override
    public Object deepCopy(final Object value) {
        return value; //if object was immutable we could return the object as its is
    }

    /**
     * method called when Hibernate puts the data in a second level cache. The data is stored
     * in a serializable form
     */
    @Override
    public Serializable disassemble(final Object value,
                                    final SharedSessionContractImplementor paramSessionImplementor) {
        //Thus the data Types must implement serializable
        return (Serializable) value;
    }

    /**
     * Returns the object from the 2 level cache
     */
    @Override
    public Object assemble(final Serializable cached,
                           final SharedSessionContractImplementor sessionImplementor, final Object owner) {
        //would work as the class is Serializable, and stored in cache as it is - see disassemble
        return cached;
    }

    /**
     * Method is called when merging two objects.
     */
    @Override
    public Object replace(final Object original, final Object target,
                          final SharedSessionContractImplementor paramSessionImplementor, final Object owner) {
        return original; // if immutable use this
    }

}

Then use it like this in your entity

@TypeDef(name = "persistentMoneyAmountAndCurrency", typeClass = PersistentMoneyAmountAndCurrency.class)
@Columns(columns = {@Column(name = "amount_currency", length = 3), @Column(name = "amount_value", precision = 19, scale = 5)})
@Type(type = "persistentMoneyAmountAndCurrency")
private MonetaryAmount amount;
ghost commented 4 years ago

In orousseil's comment. Does anyone know what an equivalent to this statement is

money = (Money) MoneyUtils.amount(amount, currency);

javamoney.moneta.spi.MoneyUtils does not seem to have an amount method?

Edit: Of course this should be:

money = Money.of(amount, currency);

Kindly, Greg

keilw commented 4 years ago

@rollenwiese Would this new method in MoneyUtils help with Hibernate? I'm almost inclined to move it to another repo, especially @orousseil's older comment sounds like something we may cover e.g. in javamoney-lib or a similar place.

orousseil commented 4 years ago

On the code I posted, I was using my own MoneyUtils class. That's why @rollenwiese sayed you have to use money = Money.of(amount, currency) instead. Yes the class PersistentMoneyAmountAndCurrency could be in another lib coz it's specific to Hibernate, and not really java money.

landsman commented 2 years ago

It would be such a great if there will be official Hibernate Custom type implementation in this package. 🙏

keilw commented 2 years ago

At least hibernate-validator already supports validation of MonetaryAmount, so why not ask the makers of hibernate-orm about it, too. Until then Jadira might be the way to go, or ask @vladmihalcea about additional types in his hibernate-types, which seems a little more active than Jadira lately.

vladmihalcea commented 2 years ago

The Hibernate Types project is OSS. Anyone can provide new Types. The same with Hibernate. If you wait for the core maintainers to implement all possible features, you will have to wait a very very long time.

keilw commented 2 years ago

Same goes for the core maintainers of the Money JSR which is effectively in Maintenance mode ;-) So pointing to hibernate-types was mainly a possible alternative to Jadira, if @landsman or others involved in this thread were able to help those libraries, it would be great.