Fix for #108 - ENH (443) : Postgres HStore support

This commit is contained in:
Rob Bygrave
2014-06-19 01:35:07 +12:00
parent 7d83e3f49e
commit 548fa745d6
21 changed files with 590 additions and 3 deletions
@@ -0,0 +1,15 @@
package com.avaje.ebean.annotation;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
/**
*
*/
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.FIELD)
public @interface ColumnHstore {
}
@@ -4,6 +4,7 @@ import com.avaje.ebean.BackgroundExecutor;
import com.avaje.ebean.config.GlobalProperties;
import javax.sql.DataSource;
import java.sql.Types;
/**
@@ -14,6 +15,11 @@ import java.sql.Types;
*/
public class PostgresPlatform extends DatabasePlatform {
/**
* Unique jdbc type id defined for hstore type.
*/
public static final int TYPE_HSTORE = 4001;
public PostgresPlatform() {
super();
this.name = "postgres";
@@ -40,6 +46,8 @@ public class PostgresPlatform extends DatabasePlatform {
this.openQuote = "\"";
this.closeQuote = "\"";
dbTypeMap.put(TYPE_HSTORE, new DbType("hstore"));
dbTypeMap.put(Types.INTEGER, new DbType("integer", false));
dbTypeMap.put(Types.DOUBLE, new DbType("float"));
dbTypeMap.put(Types.TINYINT, new DbType("smallint"));
@@ -122,6 +122,10 @@ public final class PersistRequestBean<T> extends PersistRequest implements BeanP
this.bean = bean;
this.parentBean = parentBean;
this.controller = beanDescriptor.getPersistController();
if (Type.UPDATE == type) {
// Mark Mutable scalar properties (like Hstore) as dirty where necessary
beanDescriptor.checkMutableProperties(intercept);
}
this.concurrencyMode = beanDescriptor.getConcurrencyMode(intercept);
this.dirty = intercept.isDirty();
}
@@ -220,6 +220,12 @@ public class BeanDescriptor<T> implements MetaBeanInfo {
*/
private final BeanProperty[] propertiesLocal;
/**
* Scalar mutable properties (need to dirty check on update).
*/
private final BeanProperty[] propertiesMutable;
private final BeanPropertyAssocOne<?> unidirectional;
/**
@@ -391,6 +397,7 @@ public class BeanDescriptor<T> implements MetaBeanInfo {
this.propertiesBaseCompound = listHelper.getBaseCompound();
this.propertiesEmbedded = listHelper.getEmbedded();
this.propertiesLocal = listHelper.getLocal();
this.propertiesMutable = listHelper.getMutable();
this.unidirectional = listHelper.getUnidirectional();
this.propertiesOne = listHelper.getOnes();
this.propertiesOneExported = listHelper.getOneExported();
@@ -1918,7 +1925,27 @@ public class BeanDescriptor<T> implements MetaBeanInfo {
return false;
}
/**
* Check for mutable scalar types and mark as dirty if necessary.
*/
public void checkMutableProperties(EntityBeanIntercept ebi) {
for (int i = 0; i < propertiesMutable.length; i++) {
BeanProperty beanProperty = propertiesMutable[i];
if (ebi.isDirtyProperty(beanProperty.getPropertyIndex())) {
// already marked as dirty
} else if (ebi.isLoadedProperty(beanProperty.getPropertyIndex())) {
Object value = beanProperty.getValue(ebi.getOwner());
if (value == null || beanProperty.isDirtyValue(value)) {
// mutable scalar value which is considered dirty so mark
// it as such so that it is included in an update
ebi.markPropertyAsChanged(beanProperty.getPropertyIndex());
}
}
}
}
public ConcurrencyMode getConcurrencyMode(EntityBeanIntercept ebi) {
if (!hasVersionProperty(ebi)) {
return ConcurrencyMode.NONE;
} else {
@@ -477,6 +477,13 @@ public class BeanProperty implements ElPropertyValue {
public boolean isDiscriminator() {
return discriminator;
}
/**
* Return true if the underlying type is mutable.
*/
public boolean isMutableScalarType() {
return scalarType.isMutable();
}
public void copyProperty(EntityBean sourceBean, EntityBean destBean) {
Object value = getValue(sourceBean);
@@ -868,6 +875,14 @@ public class BeanProperty implements ElPropertyValue {
return descriptor.getFullName() + "." + name;
}
/**
* Return true if the mutable value is considered dirty.
* This is only used for 'mutable' scalar types like hstore etc.
*/
public boolean isDirtyValue(Object value) {
return scalarType.isDirty(value);
}
/**
* Return the scalarType.
*/
@@ -37,6 +37,8 @@ public class DeployBeanPropertyLists {
private final ArrayList<BeanProperty> local = new ArrayList<BeanProperty>();
private final ArrayList<BeanProperty> mutable = new ArrayList<BeanProperty>();
private final ArrayList<BeanProperty> manys = new ArrayList<BeanProperty>();
private final ArrayList<BeanProperty> nonManys = new ArrayList<BeanProperty>();
@@ -135,6 +137,10 @@ public class DeployBeanPropertyLists {
nonTransients.add(prop);
}
if (prop.isMutableScalarType()) {
mutable.add(prop);
}
if (desc.getInheritInfo() != null && prop.isLocal()) {
local.add(prop);
}
@@ -197,7 +203,7 @@ public class DeployBeanPropertyLists {
public BeanProperty getId() {
if (ids.size() > 1) {
String msg = "Ebean does not support multiple @Id properties. You need to convert to using an @EmbeddedId."
String msg = "Issue with bean "+desc+". Ebean does not support multiple @Id properties. You need to convert to using an @EmbeddedId."
+" Please email the ebean google group if you need further clarification.";
throw new IllegalStateException(msg);
}
@@ -223,6 +229,10 @@ public class DeployBeanPropertyLists {
return (BeanProperty[]) local.toArray(new BeanProperty[local.size()]);
}
public BeanProperty[] getMutable() {
return (BeanProperty[]) mutable.toArray(new BeanProperty[mutable.size()]);
}
public BeanPropertyAssocOne<?>[] getEmbedded() {
return (BeanPropertyAssocOne[]) embedded.toArray(new BeanPropertyAssocOne[embedded.size()]);
}
@@ -10,6 +10,7 @@ import java.util.Iterator;
import javax.persistence.PersistenceException;
import javax.persistence.Transient;
import com.avaje.ebean.annotation.ColumnHstore;
import com.avaje.ebeaninternal.server.core.Message;
import com.avaje.ebeaninternal.server.deploy.DetermineManyType;
import com.avaje.ebeaninternal.server.deploy.ManyType;
@@ -21,8 +22,10 @@ import com.avaje.ebeaninternal.server.deploy.meta.DeployBeanPropertyCompound;
import com.avaje.ebeaninternal.server.deploy.meta.DeployBeanPropertySimpleCollection;
import com.avaje.ebeaninternal.server.type.CtCompoundType;
import com.avaje.ebeaninternal.server.type.ScalarType;
import com.avaje.ebeaninternal.server.type.ScalarTypePostgresHstore;
import com.avaje.ebeaninternal.server.type.TypeManager;
import com.avaje.ebeaninternal.server.type.reflect.CheckImmutableResponse;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
@@ -276,6 +279,16 @@ public class DeployCreateProperties {
Class<?> propertyType = field.getType();
Class<?> innerType = propertyType;
String specialTypeKey = getSpecialScalarType(field);
if (specialTypeKey != null) {
ScalarType<?> scalarType = typeManager.getScalarTypeFromKey(specialTypeKey);
if (scalarType == null) {
logger.error("Could not find ScalarType to match key ["+specialTypeKey+"]");
} else {
return new DeployBeanProperty(desc, propertyType, scalarType, null);
}
}
// check for Collection type (list, set or map)
ManyType manyType = determineManyType.getManyType(propertyType);
@@ -336,6 +349,15 @@ public class DeployCreateProperties {
}
}
private String getSpecialScalarType(Field field) {
if (field.getAnnotation(ColumnHstore.class) != null) {
return ScalarTypePostgresHstore.KEY;
};
return null;
}
private boolean isTransientField(Field field) {
Transient t = field.getAnnotation(Transient.class);
@@ -45,6 +45,7 @@ import com.avaje.ebeaninternal.server.type.reflect.KnownImmutable;
import com.avaje.ebeaninternal.server.type.reflect.ReflectionBasedCompoundType;
import com.avaje.ebeaninternal.server.type.reflect.ReflectionBasedCompoundTypeProperty;
import com.avaje.ebeaninternal.server.type.reflect.ReflectionBasedTypeBuilder;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
@@ -64,6 +65,8 @@ public final class DefaultTypeManager implements TypeManager, KnownImmutable {
private final ConcurrentHashMap<Integer, ScalarType<?>> nativeMap;
private final ConcurrentHashMap<String, ScalarType<?>> customTypeMap;
private final DefaultTypeFactory extraTypeFactory;
private final ScalarType<?> charType = new ScalarTypeChar();
@@ -136,6 +139,9 @@ public final class DefaultTypeManager implements TypeManager, KnownImmutable {
this.compoundTypeMap = new ConcurrentHashMap<Class<?>, CtCompoundType<?>>();
this.typeMap = new ConcurrentHashMap<Class<?>, ScalarType<?>>();
this.nativeMap = new ConcurrentHashMap<Integer, ScalarType<?>>();
this.customTypeMap = new ConcurrentHashMap<String, ScalarType<?>>();
this.customTypeMap.put(ScalarTypePostgresHstore.KEY, new ScalarTypePostgresHstore());
this.extraTypeFactory = new DefaultTypeFactory(config);
@@ -149,6 +155,14 @@ public final class DefaultTypeManager implements TypeManager, KnownImmutable {
}
}
/**
* Lookup a special or custom scalar type by key.
*/
@Override
public ScalarType<?> getScalarTypeFromKey(String specialTypeKey) {
return customTypeMap.get(specialTypeKey);
}
public boolean isKnownImmutable(Class<?> cls) {
if (cls == null) {
@@ -613,7 +627,7 @@ public final class DefaultTypeManager implements TypeManager, KnownImmutable {
* plus some other common types such as java.util.Date and java.util.Calendar.
*/
protected void initialiseStandard(int platformClobType, int platformBlobType, boolean binaryUUID) {
ScalarType<?> utilDateType = extraTypeFactory.createUtilDate();
typeMap.put(java.util.Date.class, utilDateType);
@@ -0,0 +1,121 @@
package com.avaje.ebeaninternal.server.type;
import java.util.Collection;
import java.util.Iterator;
/**
* Wraps a collection for the purposes of detecting modifications.
*/
public class ModifyAwareCollection<E> implements Collection<E> {
protected final ModifyAwareOwner owner;
protected final Collection<E> c;
/**
* Create with an Owner and the underlying collection this wraps.
* <p>
* The owner is notified of the additions and removals.
* </p>
*/
public ModifyAwareCollection(ModifyAwareOwner owner, Collection<E> c) {
this.owner = owner;
this.c = c;
}
public String toString() {
return c.toString();
}
public boolean add(E o) {
if (c.add(o)) {
owner.markAsModified();
return true;
}
return false;
}
public boolean addAll(Collection<? extends E> collection) {
boolean changed = false;
Iterator<? extends E> it = collection.iterator();
while (it.hasNext()) {
E o = it.next();
if (c.add(o)) {
owner.markAsModified();
changed = true;
}
}
return changed;
}
public void clear() {
if (!c.isEmpty()) {
owner.markAsModified();
}
c.clear();
}
public boolean contains(Object o) {
return c.contains(o);
}
public boolean containsAll(Collection<?> collection) {
return c.containsAll(collection);
}
public boolean isEmpty() {
return c.isEmpty();
}
public Iterator<E> iterator() {
return new ModifyAwareIterator<E>(owner, c.iterator());
}
public boolean remove(Object o) {
if (c.remove(o)) {
owner.markAsModified();
return true;
}
return false;
}
public boolean removeAll(Collection<?> collection) {
boolean changed = false;
Iterator<?> it = collection.iterator();
while (it.hasNext()) {
Object o = (Object) it.next();
if (c.remove(o)) {
owner.markAsModified();
changed = true;
}
}
return changed;
}
public boolean retainAll(Collection<?> collection) {
boolean changed = false;
Iterator<?> it = c.iterator();
while (it.hasNext()) {
Object o = (Object) it.next();
if (!collection.contains(o)) {
it.remove();
owner.markAsModified();
changed = true;
}
}
return changed;
}
public int size() {
return c.size();
}
public Object[] toArray() {
return c.toArray();
}
public <T> T[] toArray(T[] a) {
return c.toArray(a);
}
}
@@ -0,0 +1,38 @@
package com.avaje.ebeaninternal.server.type;
import java.util.Iterator;
/**
* Wraps an iterator for the purposes of detecting modifications.
*/
public class ModifyAwareIterator<E> implements Iterator<E> {
private final ModifyAwareOwner owner;
private final Iterator<E> it;
/**
* Create with an Owner and the underlying Iterator this wraps.
* <p>
* The owner is notified of the removals.
* </p>
*/
public ModifyAwareIterator(ModifyAwareOwner owner, Iterator<E> it) {
this.owner = owner;
this.it = it;
}
public boolean hasNext() {
return it.hasNext();
}
public E next() {
return it.next();
}
public void remove() {
owner.markAsModified();
it.remove();
}
}
@@ -0,0 +1,111 @@
package com.avaje.ebeaninternal.server.type;
import java.util.Collection;
import java.util.Map;
import java.util.Set;
/**
* Map that is wraps an underlying map for the purpose of detecting changes.
*/
public class ModifyAwareMap<K,V> implements Map<K,V>, ModifyAwareOwner {
/**
* Dirty flag set when the map has been modified.
*/
private boolean dirty;
/**
* The underlying map.
*/
private Map<K,V> map;
public ModifyAwareMap(Map<K,V> underyling) {
this.map = underyling;
}
public String toString() {
return map.toString();
}
@Override
public boolean isMarkedDirty() {
return dirty;
}
@Override
public void markAsModified() {
dirty = true;
}
@Override
public int size() {
return map.size();
}
@Override
public boolean isEmpty() {
return map.isEmpty();
}
@Override
public boolean containsKey(Object key) {
return map.containsKey(key);
}
@Override
public boolean containsValue(Object value) {
return map.containsValue(value);
}
@Override
public V get(Object key) {
return map.get(key);
}
@Override
public V put(K key, V value) {
markAsModified();
return map.put(key, value);
}
@Override
public V remove(Object key) {
V value = map.remove(key);
if (value != null) {
markAsModified();
}
return value;
}
@Override
public void putAll(Map<? extends K, ? extends V> m) {
markAsModified();
map.putAll(m);
}
@Override
public void clear() {
if (!map.isEmpty()) {
markAsModified();
}
map.clear();
}
@Override
public Set<K> keySet() {
return new ModifyAwareSet<K>(this, map.keySet());
}
@Override
public Collection<V> values() {
return new ModifyAwareCollection<V>(this, map.values());
}
@Override
public Set<Map.Entry<K, V>> entrySet() {
return new ModifyAwareSet<Map.Entry<K, V>>(this, map.entrySet());
}
}
@@ -0,0 +1,17 @@
package com.avaje.ebeaninternal.server.type;
/**
* Owner object notified when a modification is detected.
*/
public interface ModifyAwareOwner {
/**
* Return true if the value is considered dirty.
*/
public boolean isMarkedDirty();
/**
* Marks the object as modified.
*/
public void markAsModified();
}
@@ -0,0 +1,17 @@
package com.avaje.ebeaninternal.server.type;
import java.util.Set;
/**
* Wraps a Set for the purposes of detecting modifications.
*/
public class ModifyAwareSet<E> extends ModifyAwareCollection<E> implements Set<E> {
/**
* Create with an Owner that is notified of modifications.
*/
public ModifyAwareSet(ModifyAwareOwner owner, Set<E> s) {
super(owner, s);
}
}
@@ -0,0 +1,6 @@
package com.avaje.ebeaninternal.server.type;
public interface ModifyAwareType {
public boolean isDirty();
}
@@ -34,6 +34,17 @@ import com.avaje.ebeaninternal.server.text.json.WriteJsonBuffer;
*/
public interface ScalarType<T> extends StringParser, StringFormatter, ScalarDataReader<T> {
/**
* Return true if this is a mutable scalar type (like hstore).
*/
public boolean isMutable();
/**
* For mutable scalarType's return true if the value is dirty.
* Non-dirty properties may be excluded from updates.
*/
public boolean isDirty(Object value);
/**
* Return the default DB column length for this type.
* <p>
@@ -21,6 +21,22 @@ public abstract class ScalarTypeBase<T> implements ScalarType<T> {
}
/**
* Default implementation of mutable false.
*/
@Override
public boolean isMutable() {
return false;
}
/**
* Default to true.
*/
@Override
public boolean isDirty(Object value) {
return true;
}
/**
* Just return 0.
*/
public int getLength() {
@@ -26,6 +26,16 @@ public class ScalarTypeBytesEncrypted implements ScalarType<byte[]> {
this.dataEncryptSupport = dataEncryptSupport;
}
@Override
public boolean isMutable() {
return false;
}
@Override
public boolean isDirty(Object value) {
return false;
}
public void bind(DataBind b, byte[] value) throws SQLException {
value = dataEncryptSupport.encrypt(value);
baseType.bind(b, value);
@@ -22,6 +22,16 @@ public class ScalarTypeEncryptedWrapper<T> implements ScalarType<T> {
this.dataEncryptSupport = dataEncryptSupport;
}
@Override
public boolean isMutable() {
return wrapped.isMutable();
}
@Override
public boolean isDirty(Object value) {
return wrapped.isDirty(value);
}
public Object readData(DataInput dataInput) throws IOException {
return wrapped.readData(dataInput);
}
@@ -0,0 +1,100 @@
package com.avaje.ebeaninternal.server.type;
import java.io.DataInput;
import java.io.DataOutput;
import java.io.IOException;
import java.sql.SQLException;
import java.util.Map;
import com.avaje.ebean.config.dbplatform.PostgresPlatform;
/**
* Postgres Hstore type which maps Map<String,String> to a single 'HStore column' in the DB.
*/
@SuppressWarnings("rawtypes")
public class ScalarTypePostgresHstore extends ScalarTypeBase<Map> {
public static final String KEY = "hstore";
public static final int HSTORE_TYPE = PostgresPlatform.TYPE_HSTORE;
public ScalarTypePostgresHstore() {
super(Map.class, false, HSTORE_TYPE);
}
@Override
public boolean isMutable() {
return true;
}
@Override
public boolean isDirty(Object value) {
if (value instanceof ModifyAwareOwner) {
return ((ModifyAwareOwner)value).isMarkedDirty();
}
return true;
}
@SuppressWarnings("unchecked")
@Override
public Map read(DataReader dataReader) throws SQLException {
Object value = dataReader.getObject();
if (value == null) {
return null;
}
if (value instanceof Map == false) {
throw new RuntimeException("Expecting Hstore to return as Map but got type "+value.getClass());
}
return new ModifyAwareMap((Map)value);
}
@Override
public void bind(DataBind b, Map value) throws SQLException {
b.setObject(value);
}
@Override
public Object toJdbcType(Object value) {
return value;
}
@Override
public Map toBeanType(Object value) {
return (Map)value;
}
@Override
public String formatValue(Map v) {
// TODO format as json
return null;
}
@Override
public Map parse(String value) {
// TODO parse json into map
return null;
}
@Override
public Map parseDateTime(long dateTime) {
throw new RuntimeException("Should never be called");
}
@Override
public boolean isDateTimeCapable() {
return false;
}
@Override
public Object readData(DataInput dataInput) throws IOException {
return null;
}
@Override
public void writeData(DataOutput dataOutput, Object v) throws IOException {
}
}
@@ -37,11 +37,21 @@ public class ScalarTypeWrapper<B, S> implements ScalarType<B> {
this.nullValue = converter.getNullValue();
this.wrapperType = wrapperType;
}
public String toString() {
return "ScalarTypeWrapper " + wrapperType + " to " + scalarType.getType();
}
@Override
public boolean isMutable() {
return scalarType.isMutable();
}
@Override
public boolean isDirty(Object value) {
return scalarType.isDirty(value);
}
@SuppressWarnings("unchecked")
public Object readData(DataInput dataInput) throws IOException {
Object v = scalarType.readData(dataInput);
@@ -57,4 +57,9 @@ public interface TypeManager {
* or String which has limitations).
*/
public ScalarType<?> createEnumScalarType(Class<?> enumType);
/**
* Find a scalarType using a custom type key. Used for Hstore and similar special types.
*/
public ScalarType<?> getScalarTypeFromKey(String specialTypeKey);
}