mirror of
https://github.com/google/nomulus.git
synced 2025-05-21 19:59:34 +02:00
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:
parent
4a34369ba9
commit
bac1998d6a
12 changed files with 333 additions and 220 deletions
|
@ -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. */
|
||||||
|
|
|
@ -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());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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);
|
||||||
|
}
|
||||||
|
}
|
|
@ -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()));
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -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;
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -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;
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -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));
|
||||||
|
}
|
||||||
|
}
|
|
@ -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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -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>
|
||||||
|
|
|
@ -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();
|
|
@ -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() {}
|
|
@ -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;
|
||||||
}
|
}
|
||||||
}
|
}
|
Loading…
Add table
Add a link
Reference in a new issue