Auto-apply JPA converters for map type (#520)

* Add map converter

* Delete old map usertype

* Refactor bind

* Change to use map entry

* Use Map.Entry
This commit is contained in:
Shicong Huang 2020-04-02 16:43:08 -04:00 committed by GitHub
parent 4a34369ba9
commit bac1998d6a
12 changed files with 333 additions and 220 deletions

View file

@ -392,8 +392,6 @@ public class Registrar extends ImmutableObject implements Buildable, Jsonifiable
*/ */
@Nullable @Nullable
@Mapify(CurrencyMapper.class) @Mapify(CurrencyMapper.class)
@org.hibernate.annotations.Type(
type = "google.registry.persistence.converter.CurrencyToBillingMapUserType")
Map<CurrencyUnit, BillingAccountEntry> billingAccountMap; Map<CurrencyUnit, BillingAccountEntry> billingAccountMap;
/** A billing account entry for this registrar, consisting of a currency and an account Id. */ /** A billing account entry for this registrar, consisting of a currency and an account Id. */

View file

@ -14,6 +14,7 @@
package google.registry.persistence; package google.registry.persistence;
import google.registry.persistence.converter.StringCollectionDescriptor; import google.registry.persistence.converter.StringCollectionDescriptor;
import google.registry.persistence.converter.StringMapDescriptor;
import java.sql.Types; import java.sql.Types;
import org.hibernate.boot.model.TypeContributions; import org.hibernate.boot.model.TypeContributions;
import org.hibernate.dialect.PostgreSQL95Dialect; import org.hibernate.dialect.PostgreSQL95Dialect;
@ -26,7 +27,7 @@ public class NomulusPostgreSQLDialect extends PostgreSQL95Dialect {
registerColumnType(Types.VARCHAR, "text"); registerColumnType(Types.VARCHAR, "text");
registerColumnType(Types.TIMESTAMP_WITH_TIMEZONE, "timestamptz"); registerColumnType(Types.TIMESTAMP_WITH_TIMEZONE, "timestamptz");
registerColumnType(Types.TIMESTAMP, "timestamptz"); registerColumnType(Types.TIMESTAMP, "timestamptz");
registerColumnType(Types.OTHER, "hstore"); registerColumnType(StringMapDescriptor.COLUMN_TYPE, StringMapDescriptor.COLUMN_NAME);
registerColumnType( registerColumnType(
StringCollectionDescriptor.COLUMN_TYPE, StringCollectionDescriptor.COLUMN_DDL_NAME); StringCollectionDescriptor.COLUMN_TYPE, StringCollectionDescriptor.COLUMN_DDL_NAME);
} }
@ -37,5 +38,7 @@ public class NomulusPostgreSQLDialect extends PostgreSQL95Dialect {
super.contributeTypes(typeContributions, serviceRegistry); super.contributeTypes(typeContributions, serviceRegistry);
typeContributions.contributeJavaTypeDescriptor(StringCollectionDescriptor.getInstance()); typeContributions.contributeJavaTypeDescriptor(StringCollectionDescriptor.getInstance());
typeContributions.contributeSqlTypeDescriptor(StringCollectionDescriptor.getInstance()); typeContributions.contributeSqlTypeDescriptor(StringCollectionDescriptor.getInstance());
typeContributions.contributeJavaTypeDescriptor(StringMapDescriptor.getInstance());
typeContributions.contributeSqlTypeDescriptor(StringMapDescriptor.getInstance());
} }
} }

View file

@ -0,0 +1,43 @@
// Copyright 2020 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 static google.registry.model.registrar.Registrar.BillingAccountEntry;
import com.google.common.collect.Maps;
import java.util.Map;
import javax.persistence.Converter;
import org.joda.money.CurrencyUnit;
/** JPA converter for storing/retrieving {@link Map <CurrencyUnit, BillingAccountEntry>} objects. */
@Converter(autoApply = true)
public class CurrencyToBillingConverter
extends StringMapConverterBase<CurrencyUnit, BillingAccountEntry> {
@Override
Map.Entry<String, String> convertToDatabaseMapEntry(
Map.Entry<CurrencyUnit, BillingAccountEntry> entry) {
return Maps.immutableEntry(entry.getKey().getCode(), entry.getValue().getAccountId());
}
@Override
Map.Entry<CurrencyUnit, BillingAccountEntry> convertToEntityMapEntry(
Map.Entry<String, String> entry) {
CurrencyUnit currencyUnit = CurrencyUnit.of(entry.getKey());
BillingAccountEntry billingAccountEntry =
new BillingAccountEntry(currencyUnit, entry.getValue());
return Maps.immutableEntry(currencyUnit, billingAccountEntry);
}
}

View file

@ -1,54 +0,0 @@
// Copyright 2020 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 static com.google.common.collect.ImmutableMap.toImmutableMap;
import google.registry.model.registrar.Registrar.BillingAccountEntry;
import java.util.Map;
import org.hibernate.usertype.UserType;
import org.joda.money.CurrencyUnit;
/**
* A custom {@link UserType} for storing/retrieving {@link Map<CurrencyUnit, BillingAccountEntry>}
* objects.
*/
public class CurrencyToBillingMapUserType extends MapUserType {
@Override
public Object toEntityTypeMap(Map<String, String> map) {
return map == null
? null
: map.entrySet().stream()
.collect(
toImmutableMap(
entry -> CurrencyUnit.of(entry.getKey()),
entry ->
new BillingAccountEntry(
CurrencyUnit.of(entry.getKey()), entry.getValue())));
}
@Override
public Map<String, String> toDbSupportedMap(Object map) {
return map == null
? null
: ((Map<CurrencyUnit, BillingAccountEntry>) map)
.entrySet().stream()
.collect(
toImmutableMap(
entry -> entry.getKey().getCode(),
entry -> entry.getValue().getAccountId()));
}
}

View file

@ -1,73 +0,0 @@
// Copyright 2020 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 java.sql.PreparedStatement;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.sql.Types;
import java.util.Map;
import org.hibernate.HibernateException;
import org.hibernate.engine.spi.SharedSessionContractImplementor;
import org.hibernate.usertype.UserType;
/**
* A custom {@link UserType} used to convert a Java {@link Map<String, String>} to/from PostgreSQL
* hstore type. Per this <a href="https://www.postgresql.org/docs/current/hstore.html">doc</a>, as
* hstore keys and values are simply text strings, the type of key and value in the Java map has to
* be {@link String} as well.
*/
public class MapUserType extends MutableUserType {
@Override
public int[] sqlTypes() {
return new int[] {Types.OTHER};
}
@Override
public Class returnedClass() {
return Map.class;
}
@Override
public Object nullSafeGet(
ResultSet rs, String[] names, SharedSessionContractImplementor session, Object owner)
throws HibernateException, SQLException {
return toEntityTypeMap((Map<String, String>) rs.getObject(names[0]));
}
@Override
public void nullSafeSet(
PreparedStatement st, Object value, int index, SharedSessionContractImplementor session)
throws HibernateException, SQLException {
st.setObject(index, toDbSupportedMap(value));
}
/**
* Subclass can override this method to convert the {@link Map<String, String>} to a {@link Map}
* of specific type defined in the entity class.
*/
public Object toEntityTypeMap(Map<String, String> map) {
return map;
}
/**
* Subclass can override this method to convert the {@link Map} of specific type to a {@link
* Map<String, String>} that can be stored in the hstore type column.
*/
public Map<String, String> toDbSupportedMap(Object map) {
return (Map<String, String>) map;
}
}

View file

@ -1,63 +0,0 @@
// Copyright 2020 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 java.io.Serializable;
import java.util.Objects;
import org.hibernate.HibernateException;
import org.hibernate.usertype.UserType;
/**
* An abstract class represents a mutable Hibernate user type which implements related methods
* defined in {@link UserType}.
*/
public abstract class MutableUserType implements UserType {
@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 == null ? 0 : x.hashCode();
}
// TODO(b/147489651): Investigate how to properly implement the below methods.
@Override
public Object deepCopy(Object value) throws HibernateException {
return value;
}
@Override
public boolean isMutable() {
return false;
}
@Override
public Serializable disassemble(Object value) throws HibernateException {
return (Serializable) value;
}
@Override
public Object assemble(Serializable cached, Object owner) throws HibernateException {
return cached;
}
@Override
public Object replace(Object original, Object target, Object owner) throws HibernateException {
return original;
}
}

View file

@ -0,0 +1,52 @@
// Copyright 2020 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 static com.google.common.collect.ImmutableMap.toImmutableMap;
import google.registry.persistence.converter.StringMapDescriptor.StringMap;
import java.util.Map;
import javax.persistence.AttributeConverter;
/**
* Base JPA converter for {@link Map} objects that are stored in a column with data type of hstore
* in the database.
*/
public abstract class StringMapConverterBase<K, V>
implements AttributeConverter<Map<K, V>, StringMap> {
abstract Map.Entry<String, String> convertToDatabaseMapEntry(Map.Entry<K, V> entry);
abstract Map.Entry<K, V> convertToEntityMapEntry(Map.Entry<String, String> entry);
@Override
public StringMap convertToDatabaseColumn(Map<K, V> attribute) {
return attribute == null
? null
: StringMap.create(
attribute.entrySet().stream()
.map(this::convertToDatabaseMapEntry)
.collect(toImmutableMap(Map.Entry::getKey, Map.Entry::getValue)));
}
@Override
public Map<K, V> convertToEntityAttribute(StringMap dbData) {
return dbData == null
? null
: dbData.getMap().entrySet().stream()
.map(this::convertToEntityMapEntry)
.collect(toImmutableMap(Map.Entry::getKey, Map.Entry::getValue));
}
}

View file

@ -0,0 +1,177 @@
// Copyright 2020 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 static google.registry.persistence.converter.StringMapDescriptor.StringMap;
import com.google.common.collect.ImmutableMap;
import java.sql.CallableStatement;
import java.sql.PreparedStatement;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.sql.Types;
import java.util.Map;
import org.hibernate.type.descriptor.ValueBinder;
import org.hibernate.type.descriptor.ValueExtractor;
import org.hibernate.type.descriptor.WrapperOptions;
import org.hibernate.type.descriptor.java.AbstractTypeDescriptor;
import org.hibernate.type.descriptor.java.JavaTypeDescriptor;
import org.hibernate.type.descriptor.spi.JdbcRecommendedSqlTypeMappingContext;
import org.hibernate.type.descriptor.sql.BasicBinder;
import org.hibernate.type.descriptor.sql.BasicExtractor;
import org.hibernate.type.descriptor.sql.SqlTypeDescriptor;
/**
* The {@link JavaTypeDescriptor} and {@link SqlTypeDescriptor} for {@link StringMap}.
*
* <p>A {@link StringMap} object is a simple wrapper for a {@link Map <String, String>} which can be
* stored in a column with data type of hstore in the database. The {@link JavaTypeDescriptor} and
* {@link SqlTypeDescriptor} is used by JPA/Hibernate to map between the map and hstore which is the
* actual type that JDBC uses to read from and write to the database.
*
* @see <a
* href="https://docs.jboss.org/hibernate/orm/current/userguide/html_single/Hibernate_User_Guide.html#basic-jpa-convert">JPA
* 2.1 AttributeConverters</a>
* @see <a href="https://www.postgresql.org/docs/current/hstore.html">hstore</a>
*/
public class StringMapDescriptor extends AbstractTypeDescriptor<StringMap>
implements SqlTypeDescriptor {
public static final int COLUMN_TYPE = Types.OTHER;
public static final String COLUMN_NAME = "hstore";
private static final StringMapDescriptor INSTANCE = new StringMapDescriptor();
protected StringMapDescriptor() {
super(StringMap.class);
}
public static StringMapDescriptor getInstance() {
return INSTANCE;
}
@Override
public StringMap fromString(String string) {
throw new UnsupportedOperationException(
"Constructing StringMapDescriptor from string is not allowed");
}
@Override
public <X> X unwrap(StringMap value, Class<X> type, WrapperOptions options) {
if (value == null) {
return null;
}
if (Map.class.isAssignableFrom(type)) {
return (X) value.getMap();
}
throw unknownUnwrap(type);
}
@Override
public <X> StringMap wrap(X value, WrapperOptions options) {
if (value == null) {
return null;
}
if (value instanceof Map) {
return StringMap.create((Map<String, String>) value);
}
throw unknownWrap(value.getClass());
}
@Override
public SqlTypeDescriptor getJdbcRecommendedSqlType(JdbcRecommendedSqlTypeMappingContext context) {
return this;
}
@Override
public int getSqlType() {
return COLUMN_TYPE;
}
@Override
public boolean canBeRemapped() {
return false;
}
@Override
public <X> ValueBinder<X> getBinder(JavaTypeDescriptor<X> javaTypeDescriptor) {
return new BasicBinder<X>(javaTypeDescriptor, this) {
@Override
protected void doBind(PreparedStatement st, X value, int index, WrapperOptions options)
throws SQLException {
st.setObject(index, getStringMap(value));
}
@Override
protected void doBind(CallableStatement st, X value, String name, WrapperOptions options)
throws SQLException {
st.setObject(name, getStringMap(value));
}
private Map<String, String> getStringMap(X value) {
if (value == null) {
return null;
}
if (value instanceof StringMap) {
return ((StringMap) value).getMap();
} else {
throw new UnsupportedOperationException(
String.format(
"Binding type %s is not supported by StringMapDescriptor",
value.getClass().getName()));
}
}
};
}
@Override
public <X> ValueExtractor<X> getExtractor(JavaTypeDescriptor<X> javaTypeDescriptor) {
return new BasicExtractor<X>(javaTypeDescriptor, this) {
@Override
protected X doExtract(ResultSet rs, String name, WrapperOptions options) throws SQLException {
return javaTypeDescriptor.wrap(rs.getObject(name), options);
}
@Override
protected X doExtract(CallableStatement statement, int index, WrapperOptions options)
throws SQLException {
return javaTypeDescriptor.wrap(statement.getObject(index), options);
}
@Override
protected X doExtract(CallableStatement statement, String name, WrapperOptions options)
throws SQLException {
return javaTypeDescriptor.wrap(statement.getObject(name), options);
}
};
}
/** A simple wrapper class for {@link Map<String, String>}. */
public static class StringMap {
private Map<String, String> map;
private StringMap(Map<String, String> map) {
this.map = map;
}
/** Constructs an instance of {@link StringMap} from the given map. */
public static StringMap create(Map<String, String> map) {
return new StringMap(ImmutableMap.copyOf(map));
}
/** Returns the underlying {@link Map<String, String>} object. */
public Map<String, String> getMap() {
return map;
}
}
}

View file

@ -36,6 +36,7 @@
<class>google.registry.persistence.converter.BloomFilterConverter</class> <class>google.registry.persistence.converter.BloomFilterConverter</class>
<class>google.registry.persistence.converter.CidrAddressBlockListConverter</class> <class>google.registry.persistence.converter.CidrAddressBlockListConverter</class>
<class>google.registry.persistence.converter.CreateAutoTimestampConverter</class> <class>google.registry.persistence.converter.CreateAutoTimestampConverter</class>
<class>google.registry.persistence.converter.CurrencyToBillingConverter</class>
<class>google.registry.persistence.converter.CurrencyUnitConverter</class> <class>google.registry.persistence.converter.CurrencyUnitConverter</class>
<class>google.registry.persistence.converter.DateTimeConverter</class> <class>google.registry.persistence.converter.DateTimeConverter</class>
<class>google.registry.persistence.converter.DurationConverter</class> <class>google.registry.persistence.converter.DurationConverter</class>

View file

@ -32,7 +32,7 @@ import org.junit.runners.JUnit4;
/** Unit tests for {@link CidrAddressBlockListConverter}. */ /** Unit tests for {@link CidrAddressBlockListConverter}. */
@RunWith(JUnit4.class) @RunWith(JUnit4.class)
public class CidrAddressBlockListUserTypeTest { public class CidrAddressBlockListConverterTest {
@Rule @Rule
public final JpaUnitTestRule jpaRule = public final JpaUnitTestRule jpaRule =
new JpaTestRules.Builder().withEntityClass(TestEntity.class).buildUnitTestRule(); new JpaTestRules.Builder().withEntityClass(TestEntity.class).buildUnitTestRule();

View file

@ -25,16 +25,15 @@ import google.registry.persistence.transaction.JpaTestRules.JpaUnitTestRule;
import java.util.Map; import java.util.Map;
import javax.persistence.Entity; import javax.persistence.Entity;
import javax.persistence.Id; import javax.persistence.Id;
import org.hibernate.annotations.Type;
import org.joda.money.CurrencyUnit; import org.joda.money.CurrencyUnit;
import org.junit.Rule; import org.junit.Rule;
import org.junit.Test; import org.junit.Test;
import org.junit.runner.RunWith; import org.junit.runner.RunWith;
import org.junit.runners.JUnit4; import org.junit.runners.JUnit4;
/** Unit tests for {@link CurrencyToBillingMapUserType}. */ /** Unit tests for {@link CurrencyToBillingConverter}. */
@RunWith(JUnit4.class) @RunWith(JUnit4.class)
public class CurrencyToBillingMapUserTypeTest { public class CurrencyToBillingConverterTest {
@Rule @Rule
public final JpaUnitTestRule jpaRule = public final JpaUnitTestRule jpaRule =
new JpaTestRules.Builder() new JpaTestRules.Builder()
@ -62,7 +61,6 @@ public class CurrencyToBillingMapUserTypeTest {
@Id String name = "id"; @Id String name = "id";
@Type(type = "google.registry.persistence.converter.CurrencyToBillingMapUserType")
Map<CurrencyUnit, BillingAccountEntry> currencyToBilling; Map<CurrencyUnit, BillingAccountEntry> currencyToBilling;
private TestEntity() {} private TestEntity() {}

View file

@ -19,54 +19,56 @@ import static google.registry.persistence.transaction.TransactionManagerFactory.
import static org.junit.Assert.assertThrows; import static org.junit.Assert.assertThrows;
import com.google.common.collect.ImmutableMap; import com.google.common.collect.ImmutableMap;
import com.google.common.collect.Maps;
import google.registry.model.ImmutableObject; import google.registry.model.ImmutableObject;
import google.registry.persistence.transaction.JpaTestRules; import google.registry.persistence.transaction.JpaTestRules;
import google.registry.persistence.transaction.JpaTestRules.JpaUnitTestRule;
import java.util.Map; import java.util.Map;
import javax.persistence.Converter;
import javax.persistence.Entity; import javax.persistence.Entity;
import javax.persistence.Id; import javax.persistence.Id;
import javax.persistence.NoResultException; import javax.persistence.NoResultException;
import org.hibernate.annotations.Type;
import org.junit.Rule; import org.junit.Rule;
import org.junit.Test; import org.junit.Test;
import org.junit.runner.RunWith; import org.junit.runner.RunWith;
import org.junit.runners.JUnit4; import org.junit.runners.JUnit4;
/** Unit tests for {@link MapUserType}. */ /** Unit tests for {@link StringMapConverterBase}. */
@RunWith(JUnit4.class) @RunWith(JUnit4.class)
public class MapUserTypeTest { public class StringMapConverterBaseTest {
// Reusing production script sql/flyway/V14__load_extension_for_hstore.sql, which loads the
// hstore extension but nothing else.
@Rule @Rule
public final JpaUnitTestRule jpaRule = public final JpaTestRules.JpaUnitTestRule jpaRule =
new JpaTestRules.Builder() new JpaTestRules.Builder()
.withInitScript("sql/flyway/V14__load_extension_for_hstore.sql") .withInitScript("sql/flyway/V14__load_extension_for_hstore.sql")
.withEntityClass(TestEntity.class) .withEntityClass(TestStringMapConverter.class, TestEntity.class)
.buildUnitTestRule(); .buildUnitTestRule();
private static final ImmutableMap<Key, Value> MAP =
ImmutableMap.of(
new Key("key1"), new Value("value1"),
new Key("key2"), new Value("value2"),
new Key("key3"), new Value("value3"));
@Test @Test
public void roundTripConversion_returnsSameMap() { public void roundTripConversion_returnsSameMap() {
Map<String, String> map = ImmutableMap.of("key1", "value1", "key2", "value2"); TestEntity testEntity = new TestEntity(MAP);
TestEntity testEntity = new TestEntity(map);
jpaTm().transact(() -> jpaTm().getEntityManager().persist(testEntity)); jpaTm().transact(() -> jpaTm().getEntityManager().persist(testEntity));
TestEntity persisted = TestEntity persisted =
jpaTm().transact(() -> jpaTm().getEntityManager().find(TestEntity.class, "id")); jpaTm().transact(() -> jpaTm().getEntityManager().find(TestEntity.class, "id"));
assertThat(persisted.map).containsExactly("key1", "value1", "key2", "value2"); assertThat(persisted.map).containsExactlyEntriesIn(MAP);
} }
@Test @Test
public void testMerge_succeeds() { public void testUpdateColumn_succeeds() {
Map<String, String> map = ImmutableMap.of("key1", "value1", "key2", "value2"); TestEntity testEntity = new TestEntity(MAP);
TestEntity testEntity = new TestEntity(map);
jpaTm().transact(() -> jpaTm().getEntityManager().persist(testEntity)); jpaTm().transact(() -> jpaTm().getEntityManager().persist(testEntity));
TestEntity persisted = TestEntity persisted =
jpaTm().transact(() -> jpaTm().getEntityManager().find(TestEntity.class, "id")); jpaTm().transact(() -> jpaTm().getEntityManager().find(TestEntity.class, "id"));
persisted.map = ImmutableMap.of("key3", "value3"); assertThat(persisted.map).containsExactlyEntriesIn(MAP);
persisted.map = ImmutableMap.of(new Key("key4"), new Value("value4"));
jpaTm().transact(() -> jpaTm().getEntityManager().merge(persisted)); jpaTm().transact(() -> jpaTm().getEntityManager().merge(persisted));
TestEntity updated = TestEntity updated =
jpaTm().transact(() -> jpaTm().getEntityManager().find(TestEntity.class, "id")); jpaTm().transact(() -> jpaTm().getEntityManager().find(TestEntity.class, "id"));
assertThat(updated.map).containsExactly("key3", "value3"); assertThat(updated.map).containsExactly(new Key("key4"), new Value("value4"));
} }
@Test @Test
@ -79,7 +81,7 @@ public class MapUserTypeTest {
} }
@Test @Test
public void testEmptyCollection_writesAndReadsEmptyCollectionSuccessfully() { public void testEmptyMap_writesAndReadsEmptyCollectionSuccessfully() {
TestEntity testEntity = new TestEntity(ImmutableMap.of()); TestEntity testEntity = new TestEntity(ImmutableMap.of());
jpaTm().transact(() -> jpaTm().getEntityManager().persist(testEntity)); jpaTm().transact(() -> jpaTm().getEntityManager().persist(testEntity));
TestEntity persisted = TestEntity persisted =
@ -88,7 +90,7 @@ public class MapUserTypeTest {
} }
@Test @Test
public void testNativeQuery_succeeds() throws Exception { public void testNativeQuery_succeeds() {
executeNativeQuery( executeNativeQuery(
"INSERT INTO \"TestEntity\" (name, map) VALUES ('id', 'key1=>value1, key2=>value2')"); "INSERT INTO \"TestEntity\" (name, map) VALUES ('id', 'key1=>value1, key2=>value2')");
@ -126,17 +128,46 @@ public class MapUserTypeTest {
.transact(() -> jpaTm().getEntityManager().createNativeQuery(sql).executeUpdate()); .transact(() -> jpaTm().getEntityManager().createNativeQuery(sql).executeUpdate());
} }
private static class Key extends ImmutableObject {
private String key;
private Key(String key) {
this.key = key;
}
}
private static class Value extends ImmutableObject {
private String value;
private Value(String value) {
this.value = value;
}
}
@Converter(autoApply = true)
private static class TestStringMapConverter extends StringMapConverterBase<Key, Value> {
@Override
Map.Entry<String, String> convertToDatabaseMapEntry(Map.Entry<Key, Value> entry) {
return Maps.immutableEntry(entry.getKey().key, entry.getValue().value);
}
@Override
Map.Entry<Key, Value> convertToEntityMapEntry(Map.Entry<String, String> entry) {
return Maps.immutableEntry(new Key(entry.getKey()), new Value(entry.getValue()));
}
}
@Entity(name = "TestEntity") // Override entity name to avoid the nested class reference. @Entity(name = "TestEntity") // Override entity name to avoid the nested class reference.
private static class TestEntity extends ImmutableObject { private static class TestEntity extends ImmutableObject {
@Id String name = "id"; @Id String name = "id";
@Type(type = "google.registry.persistence.converter.MapUserType") Map<Key, Value> map;
Map<String, String> map;
private TestEntity() {} private TestEntity() {}
private TestEntity(Map<String, String> map) { private TestEntity(Map<Key, Value> map) {
this.map = map; this.map = map;
} }
} }