diff --git a/core/src/main/java/google/registry/beam/comparedb/ValidateSqlUtils.java b/core/src/main/java/google/registry/beam/comparedb/ValidateSqlUtils.java index 130f2de51..2c7ea5619 100644 --- a/core/src/main/java/google/registry/beam/comparedb/ValidateSqlUtils.java +++ b/core/src/main/java/google/registry/beam/comparedb/ValidateSqlUtils.java @@ -19,12 +19,9 @@ import static google.registry.persistence.transaction.TransactionManagerFactory. import com.google.common.base.Preconditions; import com.google.common.collect.ImmutableList; -import com.google.common.collect.ImmutableSortedMap; -import com.google.common.collect.Maps; import com.google.common.flogger.FluentLogger; import google.registry.model.EppResource; import google.registry.model.ImmutableObject; -import google.registry.model.billing.BillingEvent.OneTime; import google.registry.model.contact.ContactBase; import google.registry.model.contact.ContactHistory; import google.registry.model.domain.DomainContent; @@ -34,7 +31,6 @@ import google.registry.model.host.HostHistory; import google.registry.model.poll.PollMessage; import google.registry.model.replay.SqlEntity; import google.registry.model.reporting.HistoryEntry; -import google.registry.model.tld.Registry; import java.lang.reflect.Field; import java.math.BigInteger; import java.util.HashMap; @@ -48,7 +44,6 @@ import org.apache.beam.sdk.metrics.Metrics; import org.apache.beam.sdk.transforms.DoFn; import org.apache.beam.sdk.values.KV; import org.apache.beam.sdk.values.TupleTag; -import org.joda.money.Money; /** Helpers for use by {@link ValidateSqlPipeline}. */ final class ValidateSqlUtils { @@ -195,12 +190,6 @@ final class ValidateSqlUtils { if (sqlEntity instanceof HistoryEntry) { return (SqlEntity) normalizeHistoryEntry((HistoryEntry) sqlEntity); } - if (sqlEntity instanceof Registry) { - return normalizeRegistry((Registry) sqlEntity); - } - if (sqlEntity instanceof OneTime) { - return normalizeOnetime((OneTime) sqlEntity); - } return sqlEntity; } @@ -278,44 +267,4 @@ final class ValidateSqlUtils { throw new RuntimeException(e); } } - - static Registry normalizeRegistry(Registry registry) { - if (registry.getStandardCreateCost().getAmount().scale() == 0) { - return registry; - } - return registry - .asBuilder() - .setCreateBillingCost(normalizeMoney(registry.getStandardCreateCost())) - .setRestoreBillingCost(normalizeMoney(registry.getStandardRestoreCost())) - .setServerStatusChangeBillingCost(normalizeMoney(registry.getServerStatusChangeCost())) - .setRegistryLockOrUnlockBillingCost( - normalizeMoney(registry.getRegistryLockOrUnlockBillingCost())) - .setRenewBillingCostTransitions( - ImmutableSortedMap.copyOf( - Maps.transformValues( - registry.getRenewBillingCostTransitions(), ValidateSqlUtils::normalizeMoney))) - .setEapFeeSchedule( - ImmutableSortedMap.copyOf( - Maps.transformValues( - registry.getEapFeeScheduleAsMap(), ValidateSqlUtils::normalizeMoney))) - .build(); - } - - /** Normalizes an {@link OneTime} instance for comparison. */ - static OneTime normalizeOnetime(OneTime oneTime) { - Money cost = oneTime.getCost(); - if (cost.getAmount().scale() == 0) { - return oneTime; - } - try { - return oneTime.asBuilder().setCost(normalizeMoney(oneTime.getCost())).build(); - } catch (Exception e) { - throw new RuntimeException(e); - } - } - - static Money normalizeMoney(Money original) { - // Strips ".00" from the amount. - return Money.of(original.getCurrencyUnit(), original.getAmount().stripTrailingZeros()); - } } diff --git a/core/src/main/java/google/registry/model/billing/BillingEvent.java b/core/src/main/java/google/registry/model/billing/BillingEvent.java index af436282d..1fe08e7a2 100644 --- a/core/src/main/java/google/registry/model/billing/BillingEvent.java +++ b/core/src/main/java/google/registry/model/billing/BillingEvent.java @@ -53,6 +53,7 @@ import google.registry.persistence.BillingVKey.BillingEventVKey; import google.registry.persistence.BillingVKey.BillingRecurrenceVKey; import google.registry.persistence.VKey; import google.registry.persistence.WithLongVKey; +import google.registry.persistence.converter.JodaMoneyType; import java.util.Objects; import java.util.Optional; import java.util.Set; @@ -66,6 +67,8 @@ import javax.persistence.Enumerated; import javax.persistence.MappedSuperclass; import javax.persistence.PostLoad; import javax.persistence.Transient; +import org.hibernate.annotations.Columns; +import org.hibernate.annotations.Type; import org.joda.money.Money; import org.joda.time.DateTime; @@ -312,10 +315,8 @@ public abstract class BillingEvent extends ImmutableObject public static class OneTime extends BillingEvent implements DatastoreAndSqlEntity { /** The billable value. */ - @AttributeOverrides({ - @AttributeOverride(name = "money.amount", column = @Column(name = "cost_amount")), - @AttributeOverride(name = "money.currency", column = @Column(name = "cost_currency")) - }) + @Type(type = JodaMoneyType.TYPE_NAME) + @Columns(columns = {@Column(name = "cost_amount"), @Column(name = "cost_currency")}) Money cost; /** When the cost should be billed. */ diff --git a/core/src/main/java/google/registry/model/tld/Registry.java b/core/src/main/java/google/registry/model/tld/Registry.java index faa81b26a..fdf06d601 100644 --- a/core/src/main/java/google/registry/model/tld/Registry.java +++ b/core/src/main/java/google/registry/model/tld/Registry.java @@ -64,6 +64,7 @@ import google.registry.model.replay.DatastoreAndSqlEntity; import google.registry.model.tld.label.PremiumList; import google.registry.model.tld.label.ReservedList; import google.registry.persistence.VKey; +import google.registry.persistence.converter.JodaMoneyType; import google.registry.util.Idn; import java.util.Map; import java.util.Optional; @@ -72,13 +73,13 @@ import java.util.concurrent.ExecutionException; import java.util.function.Predicate; import java.util.regex.Pattern; import javax.annotation.Nullable; -import javax.persistence.AttributeOverride; -import javax.persistence.AttributeOverrides; import javax.persistence.Column; import javax.persistence.EnumType; import javax.persistence.Enumerated; import javax.persistence.PostLoad; import javax.persistence.Transient; +import org.hibernate.annotations.Columns; +import org.hibernate.annotations.Type; import org.joda.money.CurrencyUnit; import org.joda.money.Money; import org.joda.time.DateTime; @@ -466,47 +467,39 @@ public class Registry extends ImmutableObject CurrencyUnit currency = DEFAULT_CURRENCY; /** The per-year billing cost for registering a new domain name. */ - @AttributeOverrides({ - @AttributeOverride( - name = "money.amount", - column = @Column(name = "create_billing_cost_amount")), - @AttributeOverride( - name = "money.currency", - column = @Column(name = "create_billing_cost_currency")) - }) + @Type(type = JodaMoneyType.TYPE_NAME) + @Columns( + columns = { + @Column(name = "create_billing_cost_amount"), + @Column(name = "create_billing_cost_currency") + }) Money createBillingCost = DEFAULT_CREATE_BILLING_COST; /** The one-time billing cost for restoring a domain name from the redemption grace period. */ - @AttributeOverrides({ - @AttributeOverride( - name = "money.amount", - column = @Column(name = "restore_billing_cost_amount")), - @AttributeOverride( - name = "money.currency", - column = @Column(name = "restore_billing_cost_currency")) - }) + @Type(type = JodaMoneyType.TYPE_NAME) + @Columns( + columns = { + @Column(name = "restore_billing_cost_amount"), + @Column(name = "restore_billing_cost_currency") + }) Money restoreBillingCost = DEFAULT_RESTORE_BILLING_COST; /** The one-time billing cost for changing the server status (i.e. lock). */ - @AttributeOverrides({ - @AttributeOverride( - name = "money.amount", - column = @Column(name = "server_status_change_billing_cost_amount")), - @AttributeOverride( - name = "money.currency", - column = @Column(name = "server_status_change_billing_cost_currency")) - }) + @Type(type = JodaMoneyType.TYPE_NAME) + @Columns( + columns = { + @Column(name = "server_status_change_billing_cost_amount"), + @Column(name = "server_status_change_billing_cost_currency") + }) Money serverStatusChangeBillingCost = DEFAULT_SERVER_STATUS_CHANGE_BILLING_COST; /** The one-time billing cost for a registry lock/unlock action initiated by a registrar. */ - @AttributeOverrides({ - @AttributeOverride( - name = "money.amount", - column = @Column(name = "registry_lock_or_unlock_cost_amount")), - @AttributeOverride( - name = "money.currency", - column = @Column(name = "registry_lock_or_unlock_cost_currency")) - }) + @Type(type = JodaMoneyType.TYPE_NAME) + @Columns( + columns = { + @Column(name = "registry_lock_or_unlock_cost_amount"), + @Column(name = "registry_lock_or_unlock_cost_currency") + }) Money registryLockOrUnlockBillingCost = DEFAULT_REGISTRY_LOCK_OR_UNLOCK_BILLING_COST; /** diff --git a/core/src/main/java/google/registry/persistence/NomulusPostgreSQLDialect.java b/core/src/main/java/google/registry/persistence/NomulusPostgreSQLDialect.java index f90123113..2f7b0af1b 100644 --- a/core/src/main/java/google/registry/persistence/NomulusPostgreSQLDialect.java +++ b/core/src/main/java/google/registry/persistence/NomulusPostgreSQLDialect.java @@ -14,6 +14,7 @@ package google.registry.persistence; import google.registry.persistence.converter.IntervalDescriptor; +import google.registry.persistence.converter.JodaMoneyType; import google.registry.persistence.converter.StringCollectionDescriptor; import google.registry.persistence.converter.StringMapDescriptor; import java.sql.Types; @@ -34,6 +35,7 @@ public class NomulusPostgreSQLDialect extends PostgreSQL95Dialect { registerColumnType(IntervalDescriptor.COLUMN_TYPE, IntervalDescriptor.COLUMN_NAME); } + @SuppressWarnings("deprecation") // See comments below on JodaMoneyType. @Override public void contributeTypes( TypeContributions typeContributions, ServiceRegistry serviceRegistry) { @@ -44,5 +46,8 @@ public class NomulusPostgreSQLDialect extends PostgreSQL95Dialect { typeContributions.contributeSqlTypeDescriptor(StringMapDescriptor.getInstance()); typeContributions.contributeJavaTypeDescriptor(IntervalDescriptor.getInstance()); typeContributions.contributeSqlTypeDescriptor(IntervalDescriptor.getInstance()); + // Below method (contributing CompositeUserType) is deprecated. Please see javadoc of + // JodaMoneyType for reasons. + typeContributions.contributeType(JodaMoneyType.INSTANCE, JodaMoneyType.TYPE_NAME); } } diff --git a/core/src/main/java/google/registry/persistence/converter/JodaMoneyType.java b/core/src/main/java/google/registry/persistence/converter/JodaMoneyType.java new file mode 100644 index 000000000..681ce0a57 --- /dev/null +++ b/core/src/main/java/google/registry/persistence/converter/JodaMoneyType.java @@ -0,0 +1,178 @@ +// Copyright 2021 The Nomulus Authors. All Rights Reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package google.registry.persistence.converter; + +import com.google.common.collect.ImmutableList; +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 org.hibernate.HibernateException; +import org.hibernate.engine.spi.SharedSessionContractImplementor; +import org.hibernate.type.StandardBasicTypes; +import org.hibernate.type.Type; +import org.hibernate.usertype.CompositeUserType; +import org.joda.money.CurrencyUnit; +import org.joda.money.Money; + +/** + * Defines JPA mapping for {@link Money Joda Money type}. + * + *

{@code Money} is mapped to two table columns, a text {@code currency} column that stores the + * currency code, and a numeric {@code amount} column that stores the amount. + * + *

The main purpose of this class is to normalize the amount loaded from the database. To support + * all currency types, the scale of the numeric column is set to 2. As a result, the {@link + * BigDecimal} instances obtained from query ResultSets all have their scale at 2. However, some + * currency types, e.g., JPY requires that the scale be zero. This class strips trailing zeros from + * each loaded BigDecimal, then calls the appropriate factory method for Money, which will adjust + * the scale appropriately. + * + *

Although {@link CompositeUserType} is likely to suffer breaking change in Hibernate 6, it is + * the only option. The suggested alternatives such as Hibernate component or Java Embeddable do not + * work in this case. Hibernate component (our previous solution that is replaced by this class) + * does not allow manipulation of the loaded amount objects. Java Embeddable is not applicable since + * we do not own the Joda money classes. + * + *

Usage: + * + *

{@code
+ * '@'Type(type = JodaMoneyType.TYPE_NAME)
+ * '@'Columns(
+ *   columns = {
+ *     '@'Column(name = "cost_amount"),
+ *     '@'Column(name = "cost_currency")
+ *   }
+ * )
+ * Money cost;
+ * }
+ */ +public class JodaMoneyType implements CompositeUserType { + + public static final JodaMoneyType INSTANCE = new JodaMoneyType(); + + /** The name of this type registered with JPA. See the example in class doc. */ + public static final String TYPE_NAME = "JodaMoney"; + + // JPA property names that can be used in JPQL queries. + private static final ImmutableList JPA_PROPERTY_NAMES = + ImmutableList.of("amount", "currency"); + private static final ImmutableList PROPERTY_TYPES = + ImmutableList.of(StandardBasicTypes.BIG_DECIMAL, StandardBasicTypes.STRING); + private static final int AMOUNT_ID = JPA_PROPERTY_NAMES.indexOf("amount"); + private static final int CURRENCY_ID = JPA_PROPERTY_NAMES.indexOf("currency"); + + @Override + public String[] getPropertyNames() { + return JPA_PROPERTY_NAMES.toArray(new String[0]); + } + + @Override + public Type[] getPropertyTypes() { + return PROPERTY_TYPES.toArray(new Type[0]); + } + + @Override + public Object getPropertyValue(Object component, int property) throws HibernateException { + if (property >= JPA_PROPERTY_NAMES.size()) { + throw new HibernateException("Property index too large: " + property); + } + Money money = (Money) component; + return property == AMOUNT_ID ? money.getAmount() : money.getCurrencyUnit().getCode(); + } + + @Override + public void setPropertyValue(Object component, int property, Object value) + throws HibernateException { + throw new HibernateException("Money is immutable"); + } + + @Override + public Class returnedClass() { + return Money.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 Objects.hashCode(x); + } + + @Override + public Object nullSafeGet( + ResultSet rs, String[] names, SharedSessionContractImplementor session, Object owner) + throws HibernateException, SQLException { + BigDecimal amount = StandardBasicTypes.BIG_DECIMAL.nullSafeGet(rs, names[AMOUNT_ID], session); + CurrencyUnit currencyUnit = + CurrencyUnit.of(StandardBasicTypes.STRING.nullSafeGet(rs, names[CURRENCY_ID], session)); + if (amount != null && currencyUnit != null) { + return Money.of(currencyUnit, amount.stripTrailingZeros()); + } + if (amount == null && currencyUnit == null) { + return null; + } + throw new HibernateException("Mismatching null state between currency and amount."); + } + + @Override + public void nullSafeSet( + PreparedStatement st, Object value, int index, SharedSessionContractImplementor session) + throws HibernateException, SQLException { + BigDecimal amount = value == null ? null : ((Money) value).getAmount(); + String currencyUnit = value == null ? null : ((Money) value).getCurrencyUnit().getCode(); + + if ((amount == null && currencyUnit != null) || (amount != null && currencyUnit == null)) { + throw new HibernateException("Mismatching null state between currency and amount."); + } + StandardBasicTypes.BIG_DECIMAL.nullSafeSet(st, amount, index, session); + StandardBasicTypes.STRING.nullSafeSet(st, currencyUnit, index + 1, session); + } + + @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 ((Money) 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/core/src/main/resources/META-INF/orm.xml b/core/src/main/resources/META-INF/orm.xml index bc3187cdd..e2c310b5e 100644 --- a/core/src/main/resources/META-INF/orm.xml +++ b/core/src/main/resources/META-INF/orm.xml @@ -4,13 +4,6 @@ xsi:schemaLocation="http://xmlns.jcp.org/xml/ns/persistence/orm http://xmlns.jcp.org/xml/ns/persistence/orm_2_2.xsd" version="2.2"> - - - - - - - diff --git a/core/src/test/java/google/registry/persistence/converter/JodaMoneyConverterTest.java b/core/src/test/java/google/registry/persistence/converter/JodaMoneyConverterTest.java index 3592bb012..e3d767570 100644 --- a/core/src/test/java/google/registry/persistence/converter/JodaMoneyConverterTest.java +++ b/core/src/test/java/google/registry/persistence/converter/JodaMoneyConverterTest.java @@ -26,8 +26,6 @@ import java.math.BigDecimal; import java.util.Arrays; import java.util.List; import java.util.Map; -import javax.persistence.AttributeOverride; -import javax.persistence.AttributeOverrides; import javax.persistence.CollectionTable; import javax.persistence.Column; import javax.persistence.ElementCollection; @@ -36,30 +34,14 @@ import javax.persistence.FetchType; import javax.persistence.Id; import javax.persistence.JoinColumn; import javax.persistence.MapKeyColumn; -import javax.persistence.PostLoad; +import org.hibernate.annotations.Columns; +import org.hibernate.annotations.Type; import org.joda.money.CurrencyUnit; import org.joda.money.Money; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.RegisterExtension; -/** - * Unit tests for embeddable {@link Money}. - * - *

{@link Money} is a wrapper around {@link org.joda.money.BigMoney} which itself contains two - * fields: a {@link BigDecimal} {@code amount} and a {@link CurrencyUnit} {@code currency}. When we - * store an entity with a {@link Money} field, we would like to store it in two columns, for the - * amount and the currency separately, so that it is easily queryable. This requires that we make - * {@link Money} a nested embeddable object. - * - *

However becaues {@link Money} is not a class that we control, we cannot use annotation-based - * mapping. Therefore there is no {@code JodaMoneyConverter} class. Instead, we define the mapping - * in {@code META-INF/orm.xml}. - * - *

Also note that any entity that contains a {@link Money} should should implement a - * {@link @PostLoad} callback that converts the amount in the {@link Money} to a scale that is - * appropriate for the currency. This is espcially necessary for currencies like JPY where the scale - * is 0, which is different from the default scale that {@link BigDecimal} is persisted in database. - */ +/** Unit tests for embeddable {@link JodaMoneyType}. */ public class JodaMoneyConverterTest { @RegisterExtension @@ -70,7 +52,8 @@ public class JodaMoneyConverterTest { @Test void roundTripConversion() { - Money money = Money.of(CurrencyUnit.USD, 100); + Money money = Money.of(CurrencyUnit.USD, 100.12); + assertThat(money.getAmount().scale()).isEqualTo(2); TestEntity entity = new TestEntity(money); insertInDb(entity); List result = @@ -83,15 +66,55 @@ public class JodaMoneyConverterTest { "SELECT amount, currency FROM \"TestEntity\" WHERE name = 'id'") .getResultList()); assertThat(result.size()).isEqualTo(1); + // The amount property, when loaded as a raw value, has the same scale as the table column, + // which is 2. assertThat(Arrays.asList((Object[]) result.get(0))) - .containsExactly( - BigDecimal.valueOf(100).setScale(CurrencyUnit.USD.getDecimalPlaces()), "USD") + .containsExactly(BigDecimal.valueOf(100.12).setScale(2), "USD") .inOrder(); TestEntity persisted = jpaTm().transact(() -> jpaTm().getEntityManager().find(TestEntity.class, "id")); assertThat(persisted.money).isEqualTo(money); } + @Test + void roundTripConversion_scale0() { + Money money = Money.ofMajor(CurrencyUnit.JPY, 100); + assertThat(money.getAmount().scale()).isEqualTo(0); // JPY's amount has scale at 0. + TestEntity entity = new TestEntity(money); + insertInDb(entity); + List result = + jpaTm() + .transact( + () -> + jpaTm() + .getEntityManager() + .createNativeQuery( + "SELECT amount, currency FROM \"TestEntity\" WHERE name = 'id'") + .getResultList()); + assertThat(result.size()).isEqualTo(1); + /* The amount property, when loaded as a raw value, has the same scale as the table column, + which is 2. */ + assertThat(Arrays.asList((Object[]) result.get(0))) + .containsExactly(BigDecimal.valueOf(100).setScale(2), "JPY") + .inOrder(); + + result = + jpaTm() + .transact( + () -> + jpaTm() + .getEntityManager() + .createQuery("SELECT money FROM TestEntity WHERE name" + " = 'id'") + .getResultList()); + + // When the money field is loaded as an embedded entity, it has the desired scale (0). + assertThat(result.get(0)).isEqualTo(money); + + TestEntity persisted = + jpaTm().transact(() -> jpaTm().getEntityManager().find(TestEntity.class, "id")); + assertThat(persisted.money).isEqualTo(money); + } + @Test void roundTripConversionWithComplexEntity() { Money myMoney = Money.of(CurrencyUnit.USD, 100); @@ -132,11 +155,6 @@ public class JodaMoneyConverterTest { jpaTm().transact(() -> jpaTm().getEntityManager().find(ComplexTestEntity.class, "id")); assertThat(result.size()).isEqualTo(1); - // Note that the amount has two decimal places even though JPY is supposed to have scale 0. - // This is due to the unfournate fact that we need to accommodate differet currencies stored - // in the same table so that the scale has to be set to the largest (2). When a Money field is - // persisted in an entity, the entity should always have a @PostLoad callback to convert the - // Money to the correct scale. assertThat(Arrays.asList((Object[]) result.get(0))) .containsExactly(BigDecimal.valueOf(2000).setScale(2), "JPY") .inOrder(); @@ -147,13 +165,15 @@ public class JodaMoneyConverterTest { } // Override entity name to exclude outer-class name in table name. Not necessary if class is not - // inner class. The double quotes are added to conform to our schema generation convention. - @Entity(name = "\"TestEntity\"") + // inner class. + @Entity(name = "TestEntity") @EntityForTesting public static class TestEntity extends ImmutableObject { @Id String name = "id"; + @Type(type = JodaMoneyType.TYPE_NAME) + @Columns(columns = {@Column(name = "amount"), @Column(name = "currency")}) Money money; public TestEntity() {} @@ -164,50 +184,26 @@ public class JodaMoneyConverterTest { } // See comments on the annotation for TestEntity above for reason. - @Entity(name = "\"ComplexTestEntity\"") + @Entity(name = "ComplexTestEntity") @EntityForTesting // This entity is used to test column override for embedded fields and collections. public static class ComplexTestEntity extends ImmutableObject { - // After the entity is loaded from the database, go through the money map and make sure that - // the scale is consistent with the currency. This is necessary for currency like JPY where - // the scale is 0 but the amount is persisteted as BigDecimal with scale 2. - @PostLoad - void setCurrencyScale() { - moneyMap - .entrySet() - .forEach( - entry -> { - Money money = entry.getValue(); - if (!money.toBigMoney().isCurrencyScale()) { - CurrencyUnit currency = money.getCurrencyUnit(); - BigDecimal amount = money.getAmount().setScale(currency.getDecimalPlaces()); - entry.setValue(Money.of(currency, amount)); - } - }); - } - @Id String name = "id"; @ElementCollection(fetch = FetchType.EAGER) @CollectionTable(name = "MoneyMap", joinColumns = @JoinColumn(name = "entity_name")) @MapKeyColumn(name = "map_key") - @AttributeOverrides({ - @AttributeOverride(name = "value.money.amount", column = @Column(name = "map_amount")), - @AttributeOverride(name = "value.money.currency", column = @Column(name = "map_currency")) - }) + @Type(type = JodaMoneyType.TYPE_NAME) + @Columns(columns = {@Column(name = "map_amount"), @Column(name = "map_currency")}) Map moneyMap; - @AttributeOverrides({ - @AttributeOverride(name = "money.amount", column = @Column(name = "my_amount")), - @AttributeOverride(name = "money.currency", column = @Column(name = "my_currency")) - }) + @Type(type = JodaMoneyType.TYPE_NAME) + @Columns(columns = {@Column(name = "my_amount"), @Column(name = "my_currency")}) Money myMoney; - @AttributeOverrides({ - @AttributeOverride(name = "money.amount", column = @Column(name = "your_amount")), - @AttributeOverride(name = "money.currency", column = @Column(name = "your_currency")) - }) + @Type(type = JodaMoneyType.TYPE_NAME) + @Columns(columns = {@Column(name = "your_amount"), @Column(name = "your_currency")}) Money yourMoney; public ComplexTestEntity() {}