#695 - ENH: Add @DbArray for storing scalar types (UUID, Long, String) to Postgres ARRAY type

This commit is contained in:
Robin Bygrave
2016-05-09 22:38:51 +12:00
parent 2bb39b02d0
commit b197d8a637
53 changed files with 1102 additions and 229 deletions
+1 -1
View File
@@ -9,7 +9,7 @@
<groupId>org.avaje.ebeanorm</groupId>
<artifactId>avaje-ebeanorm</artifactId>
<version>7.10.2-SNAPSHOT</version>
<version>7.11.1-SNAPSHOT</version>
<packaging>jar</packaging>
<name>avaje-ebeanorm</name>
@@ -87,6 +87,38 @@ public interface ExpressionFactory {
*/
Expression jsonBetween(String propertyName, String path, Object lowerValue, Object upperValue);
/**
* Array contains all the given values.
* <p>
* Array support is effectively limited to Postgres at this time.
* </p>
*/
Expression arrayContains(String propertyName, Object... values);
/**
* Array does not contain the given values.
* <p>
* Array support is effectively limited to Postgres at this time.
* </p>
*/
Expression arrayNotContains(String propertyName, Object... values);
/**
* Array is empty - for the given array property.
* <p>
* Array support is effectively limited to Postgres at this time.
* </p>
*/
Expression arrayIsEmpty(String propertyName);
/**
* Array is not empty - for the given array property.
* <p>
* Array support is effectively limited to Postgres at this time.
* </p>
*/
Expression arrayIsNotEmpty(String propertyName);
/**
* Equal To - property equal to the given value.
*/
@@ -801,6 +801,35 @@ public interface ExpressionList<T> {
*/
ExpressionList<T> allEq(Map<String, Object> propertyMap);
/**
* Array property contains entries with the given values.
*/
ExpressionList<T> arrayContains(String propertyName, Object... values);
/**
* Array does not contain the given values.
* <p>
* Array support is effectively limited to Postgres at this time.
* </p>
*/
ExpressionList<T> arrayNotContains(String propertyName, Object... values);
/**
* Array is empty - for the given array property.
* <p>
* Array support is effectively limited to Postgres at this time.
* </p>
*/
ExpressionList<T> arrayIsEmpty(String propertyName);
/**
* Array is not empty - for the given array property.
* <p>
* Array support is effectively limited to Postgres at this time.
* </p>
*/
ExpressionList<T> arrayIsNotEmpty(String propertyName);
/**
* Add raw expression with a single parameter.
* <p>
@@ -0,0 +1,40 @@
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;
/**
* Specify a collection property that will be stored into a DB ARRAY type.
* <p>
* If the target database does not support ARRAY type (so not Postgres)
* then the collection will be stored in JSON format into a VARCHAR column.
* </p>
*
* <h3>Example:</h3>
* <pre>{@code
*
* // Store as ARRAY of UUID on Postgres
* @DbArray
* List<UUID> uids = new ArrayList<>();
*
* // Store as ARRAY on Postgres
* @DbArray
* List<String> phoneNumbers = new ArrayList<>();
*
* // Store as ARRAY of INTEGER on Postgres
* @DbArray
* List<Long> someLongs = new ArrayList<>();
*
* }</pre>
*/
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.FIELD)
public @interface DbArray {
/**
* For VARCHAR storage specify the column length (defaults to 1000).
*/
int length() default 0;
}
@@ -49,6 +49,7 @@ public class DbTypeMap {
lookup.put("TIME", Types.TIME);
lookup.put("TIMESTAMP", Types.TIMESTAMP);
lookup.put("ARRAY", Types.ARRAY);
lookup.put("UUID", DbType.UUID);
// Not standard java.sql.Types
@@ -105,6 +106,7 @@ public class DbTypeMap {
// DB native UUID support (H2 and Postgres)
put(DbType.UUID, new DbType("uuid"));
put(Types.ARRAY, new DbType("array"));
if (logicalTypes) {
// keep it logical for 2 layer DDL generation
@@ -1,6 +1,6 @@
package com.avaje.ebeaninternal.api;
import com.avaje.ebeaninternal.server.core.JsonExpressionHandler;
import com.avaje.ebeaninternal.server.core.DbExpressionHandler;
import com.avaje.ebeaninternal.server.core.SpiOrmQueryRequest;
import com.avaje.ebeaninternal.server.deploy.BeanDescriptor;
@@ -12,49 +12,49 @@ import java.util.List;
public interface SpiExpressionRequest {
/**
* Return the DB specific JSON expression handler.
* Return the DB specific handler for JSON and ARRAY expressions.
*/
JsonExpressionHandler getJsonHandler();
DbExpressionHandler getDbPlatformHandler();
/**
* Parse the logical property name to the deployment name.
*/
String parseDeploy(String logicalProp);
/**
* Return the bean descriptor for the root type.
*/
BeanDescriptor<?> getBeanDescriptor();
/**
* Return the associated QueryRequest.
*/
SpiOrmQueryRequest<?> getQueryRequest();
/**
* Append to the expression sql.
*/
SpiExpressionRequest append(String sql);
/**
* Return the bean descriptor for the root type.
*/
BeanDescriptor<?> getBeanDescriptor();
/**
* Return the associated QueryRequest.
*/
SpiOrmQueryRequest<?> getQueryRequest();
/**
* Append to the expression sql.
*/
SpiExpressionRequest append(String sql);
/**
* Add an encryption key to bind to this request.
*/
void addBindEncryptKey(Object encryptKey);
/**
* Add a bind value to this request.
*/
void addBindValue(Object bindValue);
/**
* Return the accumulated expression sql for all expressions in this request.
*/
String getSql();
/**
* Return the ordered list of bind values for all expressions in this request.
*/
List<Object> getBindValues();
/**
* Add a bind value to this request.
*/
void addBindValue(Object bindValue);
/**
* Return the accumulated expression sql for all expressions in this request.
*/
String getSql();
/**
* Return the ordered list of bind values for all expressions in this request.
*/
List<Object> getBindValues();
/**
* Increments the parameter index and returns that value.
@@ -0,0 +1,26 @@
package com.avaje.ebeaninternal.server.core;
import com.avaje.ebeaninternal.api.SpiExpressionRequest;
import com.avaje.ebeaninternal.server.expression.Op;
/**
* Adds the db platform specific json expression.
*/
public interface DbExpressionHandler {
/**
* Write the db platform specific json expression.
*/
void json(SpiExpressionRequest request, String propName, String path, Op operator, Object value);
/**
* Add SQL for ARRAY CONTAINS expression.
*/
void arrayContains(SpiExpressionRequest request, String propName, boolean contains, Object... values);
/**
* Add SQL for ARRAY IS EMPTY expression.
*/
void arrayIsEmpty(SpiExpressionRequest request, String propName, boolean empty);
}
@@ -235,7 +235,7 @@ public class InternalConfiguration {
*/
private Binder getBinder(TypeManager typeManager, DatabasePlatform databasePlatform, DataTimeZone dataTimeZone) {
JsonExpressionHandler jsonHandler = getJsonExpressionHandler(databasePlatform);
DbExpressionHandler jsonHandler = getDbExpressionHandler(databasePlatform);
DbHistorySupport historySupport = databasePlatform.getHistorySupport();
if (historySupport == null) {
@@ -247,16 +247,16 @@ public class InternalConfiguration {
/**
* Return the JSON expression handler for the given database platform.
*/
private JsonExpressionHandler getJsonExpressionHandler(DatabasePlatform databasePlatform) {
private DbExpressionHandler getDbExpressionHandler(DatabasePlatform databasePlatform) {
String name = databasePlatform.getName();
if ("postgres".equalsIgnoreCase(name)) {
return new PostgresJsonExpression();
}
if ("oracle".equalsIgnoreCase(name)) {
return new OracleJsonExpression();
return new OracleDbExpression();
}
return new NotSupportedJsonExpression();
return new NotSupportedDbExpression();
}
public JsonContext createJsonContext(SpiEbeanServer server) {
@@ -1,17 +0,0 @@
package com.avaje.ebeaninternal.server.core;
import com.avaje.ebeaninternal.api.SpiExpressionRequest;
import com.avaje.ebeaninternal.server.expression.Op;
/**
* Adds the db platform specific json expression.
*/
public interface JsonExpressionHandler {
/**
* Write the db platform specific json expression.
*/
void addSql(SpiExpressionRequest request, String propName, String path, Op operator, Object value);
}
@@ -0,0 +1,25 @@
package com.avaje.ebeaninternal.server.core;
import com.avaje.ebeaninternal.api.SpiExpressionRequest;
import com.avaje.ebeaninternal.server.expression.Op;
/**
* Not supported JSON or ARRAY expression handler.
*/
public class NotSupportedDbExpression implements DbExpressionHandler {
@Override
public void json(SpiExpressionRequest request, String propName, String path, Op operator, Object value) {
throw new RuntimeException("JSON expressions only supported on Postgres and Oracle");
}
@Override
public void arrayContains(SpiExpressionRequest request, String propName, boolean contains, Object... values) {
throw new RuntimeException("ARRAY expressions only supported on Postgres");
}
@Override
public void arrayIsEmpty(SpiExpressionRequest request, String propName, boolean empty) {
throw new RuntimeException("ARRAY expressions only supported on Postgres");
}
}
@@ -1,15 +0,0 @@
package com.avaje.ebeaninternal.server.core;
import com.avaje.ebeaninternal.api.SpiExpressionRequest;
import com.avaje.ebeaninternal.server.expression.Op;
/**
* Not supported JSON expression handler.
*/
public class NotSupportedJsonExpression implements JsonExpressionHandler {
@Override
public void addSql(SpiExpressionRequest request, String propName, String path, Op operator, Object value) {
throw new RuntimeException("JSON expressions only supported on Postgres and Oracle");
}
}
@@ -0,0 +1,33 @@
package com.avaje.ebeaninternal.server.core;
import com.avaje.ebeaninternal.api.SpiExpressionRequest;
import com.avaje.ebeaninternal.server.expression.Op;
/**
* Oracle JSON expression handler, ARRAY expressions not supported.
*/
public class OracleDbExpression implements DbExpressionHandler {
@Override
public void json(SpiExpressionRequest request, String propName, String path, Op operator, Object value) {
if (operator == Op.EXISTS) {
request.append("json_exists(").append(propName).append(", '$.").append(path).append("')");
} else if (operator == Op.NOT_EXISTS) {
request.append("not json_exists(").append(propName).append(", '$.").append(path).append("')");
} else {
request.append("json_value(").append(propName).append(", '$.").append(path).append("')");
request.append(operator.bind());
}
}
@Override
public void arrayContains(SpiExpressionRequest request, String propName, boolean contains, Object... values) {
throw new IllegalStateException("ARRAY expressions not supported on Oracle");
}
@Override
public void arrayIsEmpty(SpiExpressionRequest request, String propName, boolean empty) {
throw new IllegalStateException("ARRAY expressions not supported on Oracle");
}
}
@@ -1,24 +0,0 @@
package com.avaje.ebeaninternal.server.core;
import com.avaje.ebeaninternal.api.SpiExpressionRequest;
import com.avaje.ebeaninternal.server.expression.Op;
/**
* Postgres JSON expression handler
*/
public class OracleJsonExpression implements JsonExpressionHandler {
@Override
public void addSql(SpiExpressionRequest request, String propName, String path, Op operator, Object value) {
if (operator == Op.EXISTS) {
request.append("json_exists(").append(propName).append(", '$.").append(path).append("')");
} else if (operator == Op.NOT_EXISTS) {
request.append("not json_exists(").append(propName).append(", '$.").append(path).append("')");
} else {
request.append("json_value(").append(propName).append(", '$.").append(path).append("')");
request.append(operator.bind());
}
}
}
@@ -0,0 +1,52 @@
package com.avaje.ebeaninternal.server.core;
/**
* Helper for determining type casting for JSON and ARRAY expressions.
*/
public class PostgresCast {
/**
* Postgres CAST the type if necessary.
* <p>
* This is generally necessary for JSON expressions as text values always returned from the json operators used.
* </p>
*/
protected static String cast(Object value) {
return cast(value, false);
}
/**
* Postgres CAST the type if necessary additionally specify if DB ARRAY is used.
*/
protected static String cast(Object value, boolean asArray) {
if (value == null) {
// for exists and not-exists expressions
return "";
}
if (isIntegerType(value)) {
return asArray ? "::integer[]" : "::integer";
}
if (isNumberType(value)) {
return asArray ? "::decimal[]" : "::decimal";
}
if (isBooleanType(value)) {
return asArray ? "::boolean[]" : "::boolean";
}
return "";
}
private static boolean isBooleanType(Object value) {
return (value instanceof Boolean);
}
private static boolean isIntegerType(Object value) {
return (value instanceof Integer) || (value instanceof Long);
}
private static boolean isNumberType(Object value) {
return (value instanceof Number);
}
}
@@ -4,12 +4,12 @@ import com.avaje.ebeaninternal.api.SpiExpressionRequest;
import com.avaje.ebeaninternal.server.expression.Op;
/**
* Postgres JSON expression handler
* Postgres JSON and ARRAY expression handler
*/
public class PostgresJsonExpression implements JsonExpressionHandler {
public class PostgresJsonExpression implements DbExpressionHandler {
@Override
public void addSql(SpiExpressionRequest request, String propName, String path, Op operator, Object value) {
public void json(SpiExpressionRequest request, String propName, String path, Op operator, Object value) {
StringBuilder sb = new StringBuilder(50);
String[] paths = path.split("\\.");
@@ -29,43 +29,36 @@ public class PostgresJsonExpression implements JsonExpressionHandler {
sb.append("}')");
}
request.append(castType(sb.toString(), value));
request.append(sb.toString());
request.append(PostgresCast.cast(value));
request.append(operator.bind());
}
/**
* Postgres CAST the type if necessary as text values always returned from the json operators used.
*/
private String castType(String expression, Object value) {
@Override
public void arrayContains(SpiExpressionRequest request, String propName, boolean contains, Object... values) {
if (value == null) {
// for exists and not-exists expressions
return expression;
if (!contains) {
request.append("not (");
}
// Postgres cast of returned text value
if (isIntegerType(value)) {
return expression+"::INTEGER";
request.append(propName).append(" @> array[?");
for (int i = 1; i < values.length; i++) {
request.append(",?");
}
if (isNumberType(value)) {
return expression+"::DECIMAL";
request.append("]");
request.append(PostgresCast.cast(values[0], true));
if (!contains) {
request.append(")");
}
if (isBooleanType(value)) {
return expression+"::BOOLEAN";
}
return expression;
}
private boolean isBooleanType(Object value) {
return (value instanceof Boolean);
}
@Override
public void arrayIsEmpty(SpiExpressionRequest request, String propName, boolean empty) {
private boolean isIntegerType(Object value) {
return (value instanceof Integer) || (value instanceof Long);
}
private boolean isNumberType(Object value) {
return (value instanceof Number);
request.append("coalesce(cardinality(").append(propName).append("),0)");
if (empty) {
request.append(" = 0");
} else {
request.append(" <> 0");
}
}
}
@@ -235,7 +235,7 @@ public final class RelationalQueryRequest {
BindParams bindParams = query.getBindParams();
if (!bindParams.isEmpty()) {
this.bindLog = binder.bind(bindParams, pstmt);
this.bindLog = binder.bind(bindParams, pstmt, conn);
}
if (isLogSql()) {
@@ -1367,7 +1367,7 @@ public class BeanProperty implements ElPropertyValue, Property {
DocPropertyType type = scalarType.getDocType();
DocPropertyOptions options = docOptions.copy();
if (DocPropertyType.ENUM == type || isStringId(type)) {
if (DocPropertyType.UUID == type || DocPropertyType.ENUM == type ||isStringId(type)) {
options.setCode(true);
}
@@ -187,6 +187,10 @@ public class AnnotationFields extends AnnotationParser {
util.setDbJsonBType(prop, dbJsonB);
}
}
DbArray dbArray = get(prop, DbArray.class);
if (dbArray != null) {
util.setDbArray(prop, dbArray);
}
DocCode docCode = get(prop, DocCode.class);
if (docCode != null) {
@@ -1,5 +1,6 @@
package com.avaje.ebeaninternal.server.deploy.parse;
import com.avaje.ebean.annotation.DbArray;
import com.avaje.ebean.annotation.DbHstore;
import com.avaje.ebean.annotation.DbJson;
import com.avaje.ebean.annotation.DbJsonB;
@@ -299,6 +300,7 @@ public class DeployCreateProperties {
private boolean isSpecialScalarType(Field field) {
return (field.getAnnotation(DbJson.class) != null)
|| (field.getAnnotation(DbJsonB.class) != null)
|| (field.getAnnotation(DbArray.class) != null)
|| (field.getAnnotation(DbHstore.class) != null);
}
@@ -5,6 +5,7 @@ import javax.persistence.EnumType;
import javax.persistence.Enumerated;
import javax.persistence.PersistenceException;
import com.avaje.ebean.annotation.DbArray;
import com.avaje.ebean.annotation.DbJson;
import com.avaje.ebean.annotation.DbJsonB;
import com.avaje.ebean.annotation.DbJsonType;
@@ -21,6 +22,7 @@ import com.avaje.ebeaninternal.server.deploy.meta.DeployBeanProperty;
import com.avaje.ebeaninternal.server.deploy.meta.DeployBeanPropertyCompound;
import com.avaje.ebeaninternal.server.type.DataEncryptSupport;
import com.avaje.ebeaninternal.server.type.ScalarType;
import com.avaje.ebeaninternal.server.type.ScalarTypeArrayList;
import com.avaje.ebeaninternal.server.type.ScalarTypeEnumStandard;
import com.avaje.ebeaninternal.server.type.SimpleAesEncryptor;
import com.avaje.ebeaninternal.server.type.TypeManager;
@@ -47,6 +49,8 @@ public class DeployUtil {
private static final int DEFAULT_JSON_VARCHAR_LENGTH = 3000;
private static final int DEFAULT_ARRAY_VARCHAR_LENGTH = 1000;
private final NamingConvention namingConvention;
private final TypeManager typeManager;
@@ -210,6 +214,30 @@ public class DeployUtil {
prop.setScalarType(scalarType);
}
/**
* Set the DbArray type (effectively Postgres only).
*/
public void setDbArray(DeployBeanProperty prop, DbArray dbArray) {
Class<?> type = prop.getPropertyType();
ScalarType<?> scalarType = typeManager.getArrayScalarType(type, dbArray, prop.getGenericType());
if (scalarType == null) {
throw new RuntimeException("No ScalarType for @DbArray type for [" + prop.getFullBeanName()+ "]");
}
int dbType = scalarType.getJdbcType();
prop.setDbType(dbType);
prop.setScalarType(scalarType);
if (scalarType instanceof ScalarTypeArrayList) {
prop.setDbColumnDefn(((ScalarTypeArrayList)scalarType).getDbColumnDefn());
}
if (dbType == Types.VARCHAR) {
// determine the db column size
int dbLength = dbArray.length();
int columnLength = (dbLength > 0) ? dbLength : DEFAULT_ARRAY_VARCHAR_LENGTH;
prop.setDbLength(columnLength);
}
}
/**
* This property is marked as a Lob object.
*/
@@ -0,0 +1,93 @@
package com.avaje.ebeaninternal.server.expression;
import com.avaje.ebeaninternal.api.HashQueryPlanBuilder;
import com.avaje.ebeaninternal.api.SpiExpression;
import com.avaje.ebeaninternal.api.SpiExpressionRequest;
import java.io.IOException;
/**
* Contains expression for ARRAY type.
*/
public class ArrayContainsExpression extends AbstractExpression {
private final boolean contains;
private final Object[] values;
protected ArrayContainsExpression(String propName, boolean contains, Object... values) {
super(propName);
this.contains = contains;
this.values = values;
if (values == null || values.length == 0) {
throw new IllegalArgumentException("values must not be null or empty");
}
}
@Override
public void writeDocQuery(DocQueryContext context) throws IOException {
if (values.length == 1) {
context.writeEqualTo(propName, values[0]);
} else {
if (contains) {
context.startBoolMust();
} else {
context.startBoolMustNot();
}
for (int i = 0; i < values.length; i++) {
context.writeEqualTo(propName, values[i]);
}
context.endBool();
}
}
@Override
public void queryPlanHash(HashQueryPlanBuilder builder) {
builder.add(ArrayContainsExpression.class).add(propName).add(contains);
builder.bind(values.length);
}
@Override
public int queryBindHash() {
int hc = values[0].hashCode();
for (int i = 1; i < values.length; i++) {
hc = hc * 31 + values[i].hashCode();
}
return hc;
}
@Override
public boolean isSameByPlan(SpiExpression other) {
if (!(other instanceof ArrayContainsExpression)) {
return false;
}
ArrayContainsExpression that = (ArrayContainsExpression) other;
return this.propName.equals(that.propName)
&& this.contains == that.contains
&& this.values.length == that.values.length;
}
@Override
public boolean isSameByBind(SpiExpression other) {
ArrayContainsExpression that = (ArrayContainsExpression) other;
for (int i = 0; i < this.values.length; i++) {
if (!this.values[i].equals(that.values[i])) {
return false;
}
}
return true;
}
@Override
public void addSql(SpiExpressionRequest request) {
request.getDbPlatformHandler().arrayContains(request, propName, contains, values);
}
@Override
public void addBindValues(SpiExpressionRequest request) {
for (Object value : values) {
request.addBindValue(value);
}
}
}
@@ -0,0 +1,60 @@
package com.avaje.ebeaninternal.server.expression;
import com.avaje.ebeaninternal.api.HashQueryPlanBuilder;
import com.avaje.ebeaninternal.api.SpiExpression;
import com.avaje.ebeaninternal.api.SpiExpressionRequest;
import java.io.IOException;
/**
* IsEmpty expression for ARRAY type.
*/
public class ArrayIsEmptyExpression extends AbstractExpression {
private final boolean empty;
protected ArrayIsEmptyExpression(String propName, boolean empty) {
super(propName);
this.empty = empty;
}
@Override
public void writeDocQuery(DocQueryContext context) throws IOException {
context.writeExists(!empty, propName);
}
@Override
public void queryPlanHash(HashQueryPlanBuilder builder) {
builder.add(ArrayIsEmptyExpression.class).add(propName);
}
@Override
public int queryBindHash() {
return empty ? 0 : 31;
}
@Override
public boolean isSameByPlan(SpiExpression other) {
if (!(other instanceof ArrayIsEmptyExpression)) {
return false;
}
ArrayIsEmptyExpression that = (ArrayIsEmptyExpression) other;
return this.propName.equals(that.propName)
&& this.empty == that.empty;
}
@Override
public boolean isSameByBind(SpiExpression other) {
return true;
}
@Override
public void addSql(SpiExpressionRequest request) {
request.getDbPlatformHandler().arrayIsEmpty(request, propName, empty);
}
@Override
public void addBindValues(SpiExpressionRequest request) {
// nothing to bind
}
}
@@ -106,6 +106,26 @@ public class DefaultExpressionFactory implements SpiExpressionFactory {
return new JsonPathExpression(propertyName, path, lowerValue, upperValue);
}
@Override
public Expression arrayContains(String propertyName, Object... values) {
return new ArrayContainsExpression(propertyName, true, values);
}
@Override
public Expression arrayNotContains(String propertyName, Object... values) {
return new ArrayContainsExpression(propertyName, false, values);
}
@Override
public Expression arrayIsEmpty(String propertyName) {
return new ArrayIsEmptyExpression(propertyName, true);
}
@Override
public Expression arrayIsNotEmpty(String propertyName) {
return new ArrayIsEmptyExpression(propertyName, false);
}
/**
* Equal To - property equal to the given value.
*/
@@ -826,6 +826,30 @@ public class DefaultExpressionList<T> implements SpiExpressionList<T> {
return this;
}
@Override
public ExpressionList<T> arrayContains(String propertyName, Object... elementValue) {
add(expr.arrayContains(propertyName, elementValue));
return this;
}
@Override
public ExpressionList<T> arrayNotContains(String propertyName, Object... values) {
add(expr.arrayNotContains(propertyName, values));
return this;
}
@Override
public ExpressionList<T> arrayIsEmpty(String propertyName) {
add(expr.arrayIsEmpty(propertyName));
return this;
}
@Override
public ExpressionList<T> arrayIsNotEmpty(String propertyName) {
add(expr.arrayIsNotEmpty(propertyName));
return this;
}
@Override
public ExpressionList<T> raw(String raw, Object value) {
add(expr.raw(raw, value));
@@ -6,7 +6,7 @@ import java.util.List;
import com.avaje.ebeaninternal.api.SpiExpressionList;
import com.avaje.ebeaninternal.api.SpiExpressionRequest;
import com.avaje.ebeaninternal.server.core.JsonExpressionHandler;
import com.avaje.ebeaninternal.server.core.DbExpressionHandler;
import com.avaje.ebeaninternal.server.core.SpiOrmQueryRequest;
import com.avaje.ebeaninternal.server.deploy.BeanDescriptor;
import com.avaje.ebeaninternal.server.deploy.DeployParser;
@@ -73,8 +73,8 @@ public class DefaultExpressionRequest implements SpiExpressionRequest {
}
@Override
public JsonExpressionHandler getJsonHandler() {
return binder.getJsonExpressionHandler();
public DbExpressionHandler getDbPlatformHandler() {
return binder.getDbExpressionHandler();
}
@Override
@@ -108,7 +108,7 @@ class JsonPathExpression extends AbstractExpression {
public void addSql(SpiExpressionRequest request) {
// Use DB specific expression handling (Postgres and Oracle supported)
request.getJsonHandler().addSql(request, propName, path, operator, value);
request.getDbPlatformHandler().json(request, propName, path, operator, value);
}
@Override
@@ -462,6 +462,26 @@ class JunctionExpression<T> implements SpiJunction<T>, SpiExpression, Expression
return exprList.jsonBetween(propertyName, path, lowerValue, upperValue);
}
@Override
public ExpressionList<T> arrayContains(String propertyName, Object... values) {
return exprList.arrayContains(propertyName, values);
}
@Override
public ExpressionList<T> arrayNotContains(String propertyName, Object... values) {
return exprList.arrayNotContains(propertyName, values);
}
@Override
public ExpressionList<T> arrayIsEmpty(String propertyName) {
return exprList.arrayIsEmpty(propertyName);
}
@Override
public ExpressionList<T> arrayIsNotEmpty(String propertyName) {
return exprList.arrayIsNotEmpty(propertyName);
}
@Override
public ExpressionList<T> ge(String propertyName, Object value) {
return exprList.ge(propertyName, value);
@@ -2,6 +2,7 @@ package com.avaje.ebeaninternal.server.persist;
import java.math.BigDecimal;
import java.sql.CallableStatement;
import java.sql.Connection;
import java.sql.PreparedStatement;
import java.sql.SQLException;
import java.sql.Types;
@@ -12,7 +13,7 @@ import javax.persistence.PersistenceException;
import com.avaje.ebean.config.dbplatform.DbType;
import com.avaje.ebeaninternal.api.BindParams;
import com.avaje.ebeaninternal.server.core.JsonExpressionHandler;
import com.avaje.ebeaninternal.server.core.DbExpressionHandler;
import com.avaje.ebeaninternal.server.core.Message;
import com.avaje.ebeaninternal.server.core.timezone.DataTimeZone;
import com.avaje.ebeaninternal.server.type.DataBind;
@@ -34,7 +35,7 @@ public class Binder {
private final boolean bindAsOfWithFromClause;
private final JsonExpressionHandler jsonExpressionHandler;
private final DbExpressionHandler dbExpressionHandler;
private final DataTimeZone dataTimeZone;
@@ -42,12 +43,12 @@ public class Binder {
* Set the PreparedStatement with which to bind variables to.
*/
public Binder(TypeManager typeManager, int asOfBindCount, boolean bindAsOfWithFromClause,
JsonExpressionHandler jsonExpressionHandler, DataTimeZone dataTimeZone) {
DbExpressionHandler dbExpressionHandler, DataTimeZone dataTimeZone) {
this.typeManager = typeManager;
this.asOfBindCount = asOfBindCount;
this.bindAsOfWithFromClause = bindAsOfWithFromClause;
this.jsonExpressionHandler = jsonExpressionHandler;
this.dbExpressionHandler = dbExpressionHandler;
this.dataTimeZone = dataTimeZone;
}
@@ -101,8 +102,8 @@ public class Binder {
/**
* Bind the parameters to the preparedStatement returning the bind log.
*/
public String bind(BindParams bindParams, PreparedStatement statement) throws SQLException {
return bind(bindParams, new DataBind(dataTimeZone, statement));
public String bind(BindParams bindParams, PreparedStatement statement, Connection connection) throws SQLException {
return bind(bindParams, new DataBind(dataTimeZone, statement, connection));
}
/**
@@ -401,16 +402,16 @@ public class Binder {
}
/**
* Return the JsonExpressionHandler specific to the database.
* Return the DB platform specific expression handler (for JSON and ARRAY types).
*/
public JsonExpressionHandler getJsonExpressionHandler() {
return jsonExpressionHandler;
public DbExpressionHandler getDbExpressionHandler() {
return dbExpressionHandler;
}
/**
* Create and return a DataBind for the statement.
*/
public DataBind dataBind(PreparedStatement stmt) {
return new DataBind(dataTimeZone, stmt);
public DataBind dataBind(PreparedStatement stmt, Connection connection) {
return new DataBind(dataTimeZone, stmt, connection);
}
}
@@ -95,7 +95,7 @@ public class ExeCallableSql {
String bindLog = null;
if (!bindParams.isEmpty()) {
bindLog = binder.bind(bindParams, cstmt);
bindLog = binder.bind(bindParams, cstmt, t.getInternalConnection());
}
request.setBindLog(bindLog);
@@ -113,7 +113,7 @@ public class ExeOrmUpdate {
String bindLog = null;
if (!bindParams.isEmpty()) {
bindLog = binder.bind(bindParams, pstmt);
bindLog = binder.bind(bindParams, pstmt, t.getInternalConnection());
}
request.setBindLog(bindLog);
@@ -100,7 +100,7 @@ public class ExeUpdateSql {
String bindLog = null;
if (!bindParams.isEmpty()) {
bindLog = binder.bind(bindParams, pstmt);
bindLog = binder.bind(bindParams, pstmt, t.getInternalConnection());
}
request.setBindLog(bindLog);
@@ -78,7 +78,7 @@ public abstract class DmlHandler implements PersistHandler, BindableRequest {
* Bind to the statement returning the DataBind.
*/
protected DataBind bind(PreparedStatement stmt) {
return new DataBind(persistRequest.getDataTimeZone(), stmt);
return new DataBind(persistRequest.getDataTimeZone(), stmt, transaction.getInternalConnection());
}
/**
@@ -335,7 +335,7 @@ public class CQuery<T> implements DbReadContext, CancelableQuery {
pstmt.setFetchSize(query.getBufferFetchSizeHint());
}
DataBind dataBind = queryPlan.bindEncryptedProperties(pstmt);
DataBind dataBind = queryPlan.bindEncryptedProperties(pstmt, conn);
bindLog = predicates.bind(dataBind);
// executeQuery
@@ -105,7 +105,7 @@ public class CQueryDelete {
pstmt.setQueryTimeout(query.getTimeout());
}
bindLog = predicates.bind(pstmt);
bindLog = predicates.bind(pstmt, conn);
rowCount = pstmt.executeUpdate();
long exeNano = System.nanoTime() - startNano;
@@ -143,7 +143,7 @@ public class CQueryFetchIds {
pstmt.setQueryTimeout(query.getTimeout());
}
bindLog = predicates.bind(pstmt);
bindLog = predicates.bind(pstmt, conn);
ResultSet rset = pstmt.executeQuery();
dataReader = new RsetDataReader(request.getDataTimeZone(), rset);
@@ -15,6 +15,7 @@ import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.security.MessageDigest;
import java.sql.Connection;
import java.sql.PreparedStatement;
import java.sql.ResultSet;
import java.sql.SQLException;
@@ -139,8 +140,8 @@ public class CQueryPlan {
/**
* Bind keys for encrypted properties if necessary returning the DataBind.
*/
public DataBind bindEncryptedProperties(PreparedStatement stmt) throws SQLException {
DataBind dataBind = new DataBind(dataTimeZone, stmt);
public DataBind bindEncryptedProperties(PreparedStatement stmt, Connection conn) throws SQLException {
DataBind dataBind = new DataBind(dataTimeZone, stmt, conn);
if (encryptedProps != null) {
for (int i = 0; i < encryptedProps.length; i++) {
String key = encryptedProps[i].getEncryptKey().getStringValue();
@@ -17,6 +17,7 @@ import com.avaje.ebeaninternal.server.expression.DefaultExpressionRequest;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.sql.Connection;
import java.sql.PreparedStatement;
import java.sql.SQLException;
import java.sql.Timestamp;
@@ -126,8 +127,8 @@ public class CQueryPredicates {
this.idValue = query.getId();
}
public String bind(PreparedStatement stmt) throws SQLException {
return bind(binder.dataBind(stmt));
public String bind(PreparedStatement stmt, Connection connection) throws SQLException {
return bind(binder.dataBind(stmt, connection));
}
public String bind(DataBind dataBind) throws SQLException {
@@ -116,7 +116,7 @@ public class CQueryRowCount {
pstmt.setQueryTimeout(query.getTimeout());
}
bindLog = predicates.bind(pstmt);
bindLog = predicates.bind(pstmt, conn);
rset = pstmt.executeQuery();
if (!rset.next()) {
@@ -0,0 +1,79 @@
package com.avaje.ebeaninternal.server.type;
import java.util.UUID;
/**
* Type conversion for use with ScalarTypeArrayList.
*/
public interface ArrayElementConverter<T> {
/**
* Convert the array element to the logical type.
*/
T toElement(Object rawValue);
/**
* The UUID converter implementation.
*/
ArrayElementConverter UUID = new UuidConverter();
/**
* The String converter implementation.
*/
ArrayElementConverter STRING = new StringConverter();
/**
* The Long converter implementation.
*/
ArrayElementConverter LONG = new LongConverter();
/**
* The Integer converter implementation.
*/
ArrayElementConverter INTEGER = new IntegerConverter();
class LongConverter implements ArrayElementConverter<Long> {
@Override
public Long toElement(Object rawValue) {
if (rawValue instanceof Long) {
return (Long)rawValue;
} else {
return ((Number)rawValue).longValue();
}
}
}
class IntegerConverter implements ArrayElementConverter<Integer> {
@Override
public Integer toElement(Object rawValue) {
if (rawValue instanceof Integer) {
return (Integer)rawValue;
} else {
return ((Number)rawValue).intValue();
}
}
}
/**
* String converter (noop based).
*/
class StringConverter extends NoopConverter<String> {
}
/**
* UUID converter (noop based).
*/
class UuidConverter extends NoopConverter<UUID> {
}
class NoopConverter<T> implements ArrayElementConverter<T> {
@Override
public T toElement(Object rawValue) {
return (T)rawValue;
}
}
}
@@ -7,6 +7,8 @@ import java.io.InputStream;
import java.io.Reader;
import java.io.StringReader;
import java.math.BigDecimal;
import java.sql.Array;
import java.sql.Connection;
import java.sql.PreparedStatement;
import java.sql.SQLException;
import java.sql.Time;
@@ -19,13 +21,16 @@ public class DataBind {
private final PreparedStatement pstmt;
private final Connection connection;
private final StringBuilder bindLog = new StringBuilder();
private int pos;
public DataBind(DataTimeZone dataTimeZone, PreparedStatement pstmt) {
public DataBind(DataTimeZone dataTimeZone, PreparedStatement pstmt, Connection connection) {
this.dataTimeZone = dataTimeZone;
this.pstmt = pstmt;
this.connection = connection;
}
/**
@@ -156,4 +161,9 @@ public class DataBind {
pstmt.setCharacterStream(++pos, reader, content.length());
}
public void setArray(String arrayType, Object[] elements) throws SQLException {
Array array = connection.createArrayOf(arrayType, elements);
pstmt.setArray(++pos, array);
}
}
@@ -1,5 +1,6 @@
package com.avaje.ebeaninternal.server.type;
import com.avaje.ebean.annotation.DbArray;
import com.avaje.ebean.annotation.DbEnumType;
import com.avaje.ebean.annotation.DbEnumValue;
import com.avaje.ebean.annotation.EnumValue;
@@ -19,6 +20,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 com.avaje.ebeanservice.docstore.api.mapping.DocPropertyType;
import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.ObjectMapper;
import org.joda.time.DateMidnight;
@@ -357,22 +359,38 @@ public final class DefaultTypeManager implements TypeManager, KnownImmutable {
return reader;
}
@Override
public ScalarType<?> getArrayScalarType(Class<?> type, DbArray dbArray, Type genericType) {
if (type.equals(List.class)) {
if (postgres) {
Type valueType = getValueType(genericType);
return ScalarTypeArrayList.typeFor(valueType);
}
// fallback to JSON storage in VARCHAR column
return new ScalarTypeJsonList.Varchar(getDocType(getValueType(genericType)));
}
throw new IllegalStateException("Type ["+type+"] not supported for @DbArray");
}
@Override
public ScalarType<?> getJsonScalarType(Class<?> type, int dbType, int dbLength, Type genericType) {
if (type.equals(List.class)) {
DocPropertyType docType = getDocType(genericType);
if (isValueTypeSimple(genericType)) {
return ScalarTypeJsonList.typeFor(postgres, dbType);
return ScalarTypeJsonList.typeFor(postgres, dbType, docType);
} else {
return createJsonObjectMapperType(type, genericType, dbType);
return createJsonObjectMapperType(type, genericType, dbType, docType);
}
}
if (type.equals(Set.class)) {
DocPropertyType docType = getDocType(genericType);
if (isValueTypeSimple(genericType)) {
return ScalarTypeJsonSet.typeFor(postgres, dbType);
return ScalarTypeJsonSet.typeFor(postgres, dbType, docType);
} else {
return createJsonObjectMapperType(type, genericType, dbType);
return createJsonObjectMapperType(type, genericType, dbType, docType);
}
}
@@ -380,7 +398,7 @@ public final class DefaultTypeManager implements TypeManager, KnownImmutable {
if (isMapValueTypeObject(genericType)) {
return ScalarTypeJsonMap.typeFor(postgres, dbType);
} else {
return createJsonObjectMapperType(type, genericType, dbType);
return createJsonObjectMapperType(type, genericType, dbType, DocPropertyType.OBJECT);
}
}
@@ -396,7 +414,17 @@ public final class DefaultTypeManager implements TypeManager, KnownImmutable {
}
}
return createJsonObjectMapperType(type, type, dbType);
return createJsonObjectMapperType(type, type, dbType, DocPropertyType.OBJECT);
}
private DocPropertyType getDocType(Type genericType) {
if (genericType instanceof Class<?>) {
ScalarType<?> found = typeMap.get(genericType);
if (found != null) {
return found.getDocType();
}
}
return DocPropertyType.OBJECT;
}
/**
@@ -407,6 +435,11 @@ public final class DefaultTypeManager implements TypeManager, KnownImmutable {
return String.class.equals(typeArgs[0]) || Long.class.equals(typeArgs[0]);
}
private Type getValueType(Type genericType) {
Type[] typeArgs = ((ParameterizedType) genericType).getActualTypeArguments();
return typeArgs[0];
}
/**
* Return true if value parameter type of the map is Object.
*/
@@ -415,11 +448,11 @@ public final class DefaultTypeManager implements TypeManager, KnownImmutable {
return Object.class.equals(typeArgs[1]);
}
private ScalarType<?> createJsonObjectMapperType(Class<?> type, Type genericType, int dbType) {
private ScalarType<?> createJsonObjectMapperType(Class<?> type, Type genericType, int dbType, DocPropertyType docType) {
if (objectMapper == null) {
throw new IllegalArgumentException("Type [" + type + "] unsupported for @DbJson mapping - Jackson ObjectMapper not present");
}
return ScalarTypeJsonObjectMapper.createTypeFor(postgres, type, (ObjectMapper) objectMapper, genericType, dbType);
return ScalarTypeJsonObjectMapper.createTypeFor(postgres, type, (ObjectMapper) objectMapper, genericType, dbType, docType);
}
/**
@@ -0,0 +1,125 @@
package com.avaje.ebeaninternal.server.type;
import com.avaje.ebean.text.json.EJson;
import com.avaje.ebeanservice.docstore.api.mapping.DocPropertyType;
import com.fasterxml.jackson.core.JsonGenerator;
import com.fasterxml.jackson.core.JsonParser;
import javax.persistence.PersistenceException;
import java.io.IOException;
import java.lang.reflect.Type;
import java.sql.Array;
import java.sql.SQLException;
import java.sql.Types;
import java.util.ArrayList;
import java.util.List;
import java.util.UUID;
/**
* Type mapped for DB ARRAY type (Postgres only effectively).
*/
public class ScalarTypeArrayList extends ScalarTypeJsonCollection<List> {
private static ScalarTypeArrayList UUID = new ScalarTypeArrayList("uuid", DocPropertyType.UUID, ArrayElementConverter.UUID);
private static ScalarTypeArrayList LONG = new ScalarTypeArrayList("integer", DocPropertyType.LONG, ArrayElementConverter.LONG);
private static ScalarTypeArrayList INTEGER = new ScalarTypeArrayList("integer", DocPropertyType.INTEGER, ArrayElementConverter.INTEGER);
private static ScalarTypeArrayList STRING = new ScalarTypeArrayList("varchar", DocPropertyType.STRING, ArrayElementConverter.STRING);
/**
* Return the ScalarType to use based on the List's generic parameter type.
*/
public static ScalarTypeArrayList typeFor(Type valueType) {
if (valueType.equals(UUID.class)) {
return UUID;
}
if (valueType.equals(Long.class)) {
return LONG;
}
if (valueType.equals(Integer.class)) {
return INTEGER;
}
if (valueType.equals(String.class)) {
return STRING;
}
throw new IllegalArgumentException("Type ["+valueType+"] not supported for @DbArray mapping");
}
private final String arrayType;
private final ArrayElementConverter converter;
public ScalarTypeArrayList(String arrayType, DocPropertyType docPropertyType, ArrayElementConverter converter) {
super(List.class, Types.ARRAY, docPropertyType);
this.arrayType = arrayType;
this.converter = converter;
}
@Override
public DocPropertyType getDocType() {
return docPropertyType;
}
/**
* Return the DB column definition for DDL generation.
*/
public String getDbColumnDefn() {
return arrayType+"[]";
}
@SuppressWarnings("unchecked")
private List fromArray(Object[] array1) {
List list = new ArrayList();
for (Object element : array1) {
list.add(converter.toElement(element));
}
return new ModifyAwareList(list);
}
private Object[] toArray(List value) {
return value.toArray();
}
@Override
public List read(DataReader reader) throws SQLException {
Array array = reader.getArray();
return fromArray((Object[])array.getArray());
}
@Override
public void bind(DataBind bind, List value) throws SQLException {
bind.setArray(arrayType, toArray(value));
}
@Override
public String formatValue(List value) {
try {
return EJson.write(value);
} catch (IOException e) {
throw new PersistenceException("Failed to format List into JSON content", e);
}
}
@Override
public List parse(String value) {
try {
return EJson.parseList(value, false);
} catch (IOException e) {
throw new PersistenceException("Failed to parse JSON content as List: ["+value+"]", e);
}
}
@Override
public List jsonRead(JsonParser parser) throws IOException {
return EJson.parseList(parser, parser.getCurrentToken());
}
@Override
public void jsonWrite(JsonGenerator writer, List value) throws IOException {
EJson.write(value, writer);
}
}
@@ -8,8 +8,11 @@ import java.io.IOException;
abstract class ScalarTypeJsonCollection<T> extends ScalarTypeBase<T> {
public ScalarTypeJsonCollection(Class<T> type, int dbType) {
protected DocPropertyType docPropertyType;
public ScalarTypeJsonCollection(Class<T> type, int dbType, DocPropertyType docPropertyType) {
super(type, false, dbType);
this.docPropertyType = docPropertyType;
}
/**
@@ -41,7 +44,7 @@ abstract class ScalarTypeJsonCollection<T> extends ScalarTypeBase<T> {
@Override
public DocPropertyType getDocType() {
return DocPropertyType.LIST;
return docPropertyType;
}
@Override
@@ -2,6 +2,7 @@ package com.avaje.ebeaninternal.server.type;
import com.avaje.ebean.config.dbplatform.DbType;
import com.avaje.ebean.text.json.EJson;
import com.avaje.ebeanservice.docstore.api.mapping.DocPropertyType;
import com.fasterxml.jackson.core.JsonGenerator;
import com.fasterxml.jackson.core.JsonParser;
@@ -15,32 +16,25 @@ import java.util.List;
* Types for mapping List in JSON format to DB types VARCHAR, JSON and JSONB.
*/
public class ScalarTypeJsonList {
public static final ScalarTypeJsonCollection<List> VARCHAR = new ScalarTypeJsonList.Varchar();
public static final ScalarTypeJsonCollection<List> JSON = new ScalarTypeJsonList.Json();
public static final ScalarTypeJsonCollection<List> JSONB = new ScalarTypeJsonList.JsonB();
/**
* Return the appropriate ScalarType based requested dbType and if Postgres.
*/
public static ScalarType<?> typeFor(boolean postgres, int dbType) {
public static ScalarType<?> typeFor(boolean postgres, int dbType, DocPropertyType docType) {
if (postgres) {
switch (dbType) {
case DbType.JSONB: return ScalarTypeJsonList.JSONB;
case DbType.JSON: return ScalarTypeJsonList.JSON;
case DbType.JSONB: return new ScalarTypeJsonList.JsonB(docType);
case DbType.JSON: return new ScalarTypeJsonList.Json(docType);
}
}
return ScalarTypeJsonList.VARCHAR;
return new ScalarTypeJsonList.Varchar(docType);
}
/**
* List mapped to DB VARCHAR.
*/
private static class Varchar extends ScalarTypeJsonList.Base {
public Varchar() {
super(Types.VARCHAR);
public static class Varchar extends ScalarTypeJsonList.Base {
public Varchar(DocPropertyType docType) {
super(Types.VARCHAR, docType);
}
}
@@ -48,8 +42,8 @@ public class ScalarTypeJsonList {
* List mapped to Postgres JSON.
*/
private static class Json extends ScalarTypeJsonList.PgBase {
public Json() {
super(DbType.JSON, PostgresHelper.JSON_TYPE);
public Json(DocPropertyType docType) {
super(DbType.JSON, PostgresHelper.JSON_TYPE, docType);
}
}
@@ -57,8 +51,8 @@ public class ScalarTypeJsonList {
* List mapped to Postgres JSONB.
*/
private static class JsonB extends ScalarTypeJsonList.PgBase {
public JsonB() {
super(DbType.JSONB, PostgresHelper.JSONB_TYPE);
public JsonB(DocPropertyType docType) {
super(DbType.JSONB, PostgresHelper.JSONB_TYPE, docType);
}
}
@@ -67,8 +61,8 @@ public class ScalarTypeJsonList {
*/
private abstract static class Base extends ScalarTypeJsonCollection<List> {
public Base(int dbType) {
super(List.class, dbType);
public Base(int dbType, DocPropertyType docType) {
super(List.class, dbType, docType);
}
@Override
@@ -134,8 +128,8 @@ public class ScalarTypeJsonList {
final String pgType;
PgBase(int jdbcType, String pgType) {
super(jdbcType);
PgBase(int jdbcType, String pgType, DocPropertyType docType) {
super(jdbcType, docType);
this.pgType = pgType;
}
@@ -25,21 +25,16 @@ public class ScalarTypeJsonObjectMapper {
/**
* Create and return the appropriate ScalarType.
*
* @param postgres
* @param type The field/property type
* @param objectMapper The Jackson ObjectMapper to use for marshalling
* @param genericType The generic type
* @param dbType The DB storage type to use
*/
public static ScalarType<?> createTypeFor(boolean postgres, Class<?> type, ObjectMapper objectMapper, Type genericType, int dbType) {
public static ScalarType<?> createTypeFor(boolean postgres, Class<?> type, ObjectMapper objectMapper,
Type genericType, int dbType, DocPropertyType docType) {
String pgType = getPostgresType(postgres, dbType);
if (Set.class.equals(type)) {
return new OmSet(objectMapper, genericType, dbType, pgType);
return new OmSet(objectMapper, genericType, dbType, pgType, docType);
}
if (List.class.equals(type)) {
return new OmList(objectMapper, genericType, dbType, pgType);
return new OmList(objectMapper, genericType, dbType, pgType, docType);
}
if (Map.class.equals(type)) {
return new OmMap(objectMapper, genericType, dbType, pgType);
@@ -63,7 +58,7 @@ public class ScalarTypeJsonObjectMapper {
private static class GenericObject extends Base<Object> {
public GenericObject(ObjectMapper objectMapper, Type type, int dbType, String pgType) {
super(Object.class, objectMapper, type, dbType, pgType);
super(Object.class, objectMapper, type, dbType, pgType, DocPropertyType.OBJECT);
}
}
@@ -72,8 +67,8 @@ public class ScalarTypeJsonObjectMapper {
*/
private static class OmSet extends Base<Set> {
public OmSet(ObjectMapper objectMapper, Type type, int dbType, String pgType) {
super(Set.class, objectMapper, type, dbType, pgType);
public OmSet(ObjectMapper objectMapper, Type type, int dbType, String pgType, DocPropertyType docType) {
super(Set.class, objectMapper, type, dbType, pgType, docType);
}
@Override
@@ -89,8 +84,8 @@ public class ScalarTypeJsonObjectMapper {
*/
private static class OmList extends Base<List> {
public OmList(ObjectMapper objectMapper, Type type, int dbType, String pgType) {
super(List.class, objectMapper, type, dbType, pgType);
public OmList(ObjectMapper objectMapper, Type type, int dbType, String pgType, DocPropertyType docType) {
super(List.class, objectMapper, type, dbType, pgType, docType);
}
@Override
@@ -107,7 +102,7 @@ public class ScalarTypeJsonObjectMapper {
private static class OmMap extends Base<Map> {
public OmMap(ObjectMapper objectMapper, Type type, int dbType, String pgType) {
super(Map.class, objectMapper, type, dbType, pgType);
super(Map.class, objectMapper, type, dbType, pgType, DocPropertyType.OBJECT);
}
@Override
@@ -130,16 +125,15 @@ public class ScalarTypeJsonObjectMapper {
private final String pgType;
private final DocPropertyType docType;
/**
* Construct given the object mapper, property type and DB type for storage.
*
* @param objectMapper Jackson object mapper for JSON marshalling/unmarshalling
* @param type The property type (ie. type of field with @DbJson)
* @param dbType The DB type used for storage (JSON, JSONB, VARCHAR, CLOB or BLOB)
*/
public Base(Class<T> cls, ObjectMapper objectMapper, Type type, int dbType, String pgType) {
public Base(Class<T> cls, ObjectMapper objectMapper, Type type, int dbType, String pgType, DocPropertyType docType) {
super(cls, false, dbType);
this.pgType = pgType;
this.docType = docType;
this.objectMapper = objectMapper;
this.javaType = objectMapper.getTypeFactory().constructType(type);
}
@@ -199,6 +193,7 @@ public class ScalarTypeJsonObjectMapper {
}
@Override
@SuppressWarnings("unchecked")
public T toBeanType(Object value) {
// no type conversion supported
return (T) value;
@@ -224,7 +219,7 @@ public class ScalarTypeJsonObjectMapper {
@Override
public DocPropertyType getDocType() {
return DocPropertyType.OBJECT;
return docType;
}
@Override
@@ -2,6 +2,7 @@ package com.avaje.ebeaninternal.server.type;
import com.avaje.ebean.config.dbplatform.DbType;
import com.avaje.ebean.text.json.EJson;
import com.avaje.ebeanservice.docstore.api.mapping.DocPropertyType;
import com.fasterxml.jackson.core.JsonGenerator;
import com.fasterxml.jackson.core.JsonParser;
@@ -18,31 +19,25 @@ import java.util.Set;
*/
public class ScalarTypeJsonSet {
private static final ScalarTypeJsonCollection<Set> VARCHAR = new ScalarTypeJsonSet.Varchar();
private static final ScalarTypeJsonCollection<Set> JSON = new ScalarTypeJsonSet.Json();
private static final ScalarTypeJsonCollection<Set> JSONB = new ScalarTypeJsonSet.JsonB();
/**
* Return the appropriate ScalarType for the requested dbType and Postgres.
*/
public static ScalarType<?> typeFor(boolean postgres, int dbType) {
public static ScalarType<?> typeFor(boolean postgres, int dbType, DocPropertyType docPropertyType) {
if (postgres) {
switch (dbType) {
case DbType.JSONB: return ScalarTypeJsonSet.JSONB;
case DbType.JSON: return ScalarTypeJsonSet.JSON;
case DbType.JSONB: return new ScalarTypeJsonSet.JsonB(docPropertyType);
case DbType.JSON: return new ScalarTypeJsonSet.Json(docPropertyType);
}
}
return ScalarTypeJsonSet.VARCHAR;
return new ScalarTypeJsonSet.Varchar(docPropertyType);
}
/**
* List mapped to DB VARCHAR.
*/
private static class Varchar extends ScalarTypeJsonSet.Base {
public Varchar() {
super(Types.VARCHAR);
public Varchar(DocPropertyType docPropertyType) {
super(Types.VARCHAR, docPropertyType);
}
}
@@ -50,8 +45,8 @@ public class ScalarTypeJsonSet {
* List mapped to Postgres JSON.
*/
private static class Json extends ScalarTypeJsonSet.PgBase {
public Json() {
super(DbType.JSON, PostgresHelper.JSON_TYPE);
public Json(DocPropertyType docPropertyType) {
super(DbType.JSON, PostgresHelper.JSON_TYPE, docPropertyType);
}
}
@@ -59,8 +54,8 @@ public class ScalarTypeJsonSet {
* List mapped to Postgres JSONB.
*/
private static class JsonB extends ScalarTypeJsonSet.PgBase {
public JsonB() {
super(DbType.JSONB, PostgresHelper.JSONB_TYPE);
public JsonB(DocPropertyType docPropertyType) {
super(DbType.JSONB, PostgresHelper.JSONB_TYPE, docPropertyType);
}
}
@@ -69,8 +64,8 @@ public class ScalarTypeJsonSet {
*/
private abstract static class Base extends ScalarTypeJsonCollection<Set> {
public Base(int dbType) {
super(Set.class, dbType);
public Base(int dbType, DocPropertyType docPropertyType) {
super(Set.class, dbType, docPropertyType);
}
@Override
@@ -142,8 +137,8 @@ public class ScalarTypeJsonSet {
final String pgType;
PgBase(int jdbcType, String pgType) {
super(jdbcType);
PgBase(int jdbcType, String pgType, DocPropertyType docPropertyType) {
super(jdbcType, docPropertyType);
this.pgType = pgType;
}
@@ -89,6 +89,6 @@ public abstract class ScalarTypeUUIDBase extends ScalarTypeBase<UUID> {
@Override
public DocPropertyType getDocType() {
return DocPropertyType.STRING;
return DocPropertyType.UUID;
}
}
@@ -1,5 +1,6 @@
package com.avaje.ebeaninternal.server.type;
import com.avaje.ebean.annotation.DbArray;
import com.avaje.ebeaninternal.server.type.reflect.CheckImmutableResponse;
import java.lang.reflect.Type;
@@ -66,4 +67,9 @@ public interface TypeManager {
* </p>
*/
ScalarType<?> getJsonScalarType(Class<?> type, int dbType, int dbLength, Type genericType);
/**
* Return the ScalarType used to handle DB ARRAY.
*/
ScalarType<?> getArrayScalarType(Class<?> type, DbArray dbArray, Type genericType);
}
@@ -10,6 +10,11 @@ public enum DocPropertyType {
*/
ENUM,
/**
* A UUID is a String Id implying it should not be analysed.
*/
UUID,
/**
* String.
*/
@@ -32,6 +32,11 @@ public class BaseTestCase {
return spi.getDatabasePlatform().getName().equals("h2");
}
public boolean isPostgres() {
SpiEbeanServer spi = (SpiEbeanServer)Ebean.getDefaultServer();
return spi.getDatabasePlatform().getName().equals("postgres");
}
/**
* Wait for the L2 cache to propagate changes post-commit.
*/
@@ -1,7 +1,7 @@
package com.avaje.ebeaninternal.server.expression;
import com.avaje.ebeaninternal.api.SpiExpressionRequest;
import com.avaje.ebeaninternal.server.core.JsonExpressionHandler;
import com.avaje.ebeaninternal.server.core.DbExpressionHandler;
import com.avaje.ebeaninternal.server.core.SpiOrmQueryRequest;
import com.avaje.ebeaninternal.server.deploy.BeanDescriptor;
@@ -22,7 +22,7 @@ public class TDSpiExpressionRequest implements SpiExpressionRequest {
}
@Override
public JsonExpressionHandler getJsonHandler() {
public DbExpressionHandler getDbPlatformHandler() {
return null;
}
@@ -0,0 +1,80 @@
package com.avaje.tests.model.array;
import com.avaje.ebean.annotation.DbArray;
import javax.persistence.Entity;
import javax.persistence.Id;
import javax.persistence.Version;
import java.util.ArrayList;
import java.util.List;
import java.util.UUID;
@Entity
public class EArrayBean {
@Id
Long id;
String name;
@DbArray(length = 300)
List<String> phoneNumbers = new ArrayList<String>();
@DbArray
List<UUID> uids = new ArrayList<UUID>();
@DbArray
List<Long> otherIds = new ArrayList<Long>();
@Version
Long version;
public Long getId() {
return id;
}
public void setId(Long id) {
this.id = id;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public List<String> getPhoneNumbers() {
return phoneNumbers;
}
public void setPhoneNumbers(List<String> phoneNumbers) {
this.phoneNumbers = phoneNumbers;
}
public List<UUID> getUids() {
return uids;
}
public void setUids(List<UUID> uids) {
this.uids = uids;
}
public List<Long> getOtherIds() {
return otherIds;
}
public void setOtherIds(List<Long> otherIds) {
this.otherIds = otherIds;
}
public Long getVersion() {
return version;
}
public void setVersion(Long version) {
this.version = version;
}
}
@@ -0,0 +1,113 @@
package com.avaje.tests.model.array;
import com.avaje.ebean.BaseTestCase;
import com.avaje.ebean.Ebean;
import com.avaje.ebean.Query;
import org.avaje.ebeantest.LoggedSqlCollector;
import org.junit.Test;
import java.util.List;
import java.util.UUID;
import static org.assertj.core.api.Assertions.assertThat;
import static org.junit.Assert.assertEquals;
public class TestDbArray_basic extends BaseTestCase {
private EArrayBean bean = new EArrayBean();
private EArrayBean found;
@Test
public void insert() {
bean.setName("some stuff");
List<String> phNumbers = bean.getPhoneNumbers();
phNumbers.add("4321");
phNumbers.add("9823");
bean.getUids().add(UUID.randomUUID());
bean.getUids().add(UUID.randomUUID());
bean.getOtherIds().add(95L);
bean.getOtherIds().add(96L);
bean.getOtherIds().add(97L);
Ebean.save(bean);
found = Ebean.find(EArrayBean.class, bean.getId());
assertThat(found.getPhoneNumbers()).containsExactly("4321", "9823");
if (isPostgres()) {
Query<EArrayBean> query = Ebean.find(EArrayBean.class)
.where()
.arrayContains("otherIds", 96L, 97L)
.arrayContains("uids", bean.getUids().get(0))
.arrayContains("phoneNumbers", "9823")
.arrayIsNotEmpty("phoneNumbers")
.query();
List<EArrayBean> list = query.findList();
assertThat(query.getGeneratedSql()).contains(" t0.other_ids @> array[?,?]::integer[] ");
assertThat(query.getGeneratedSql()).contains(" t0.uids @> array[?] ");
assertThat(query.getGeneratedSql()).contains(" t0.phone_numbers @> array[?] ");
assertThat(query.getGeneratedSql()).contains(" coalesce(cardinality(t0.phone_numbers),0) <> 0");
assertThat(list).hasSize(1);
query = Ebean.find(EArrayBean.class)
.where()
.arrayIsEmpty("otherIds")
.arrayNotContains("uids", bean.getUids().get(0))
.query();
query.findList();
assertThat(query.getGeneratedSql()).contains(" coalesce(cardinality(t0.other_ids),0) = 0");
assertThat(query.getGeneratedSql()).contains(" not (t0.uids @> array[?])");
}
json_parse_format();
update_when_notDirty();
update_when_dirty();
}
//@Test//(dependsOnMethods = "insert")
public void json_parse_format() {
String asJson = Ebean.json().toJson(found);
assertThat(asJson).contains("\"phoneNumbers\":[\"4321\",\"9823\"]");
assertThat(asJson).contains("\"id\":");
EArrayBean fromJson = Ebean.json().toBean(EArrayBean.class, asJson);
assertEquals(found.getId(), fromJson.getId());
assertEquals(found.getId(), fromJson.getId());
assertEquals(found.getName(), fromJson.getName());
assertThat(fromJson.getPhoneNumbers()).containsExactly("4321", "9823");
}
//@Test//(dependsOnMethods = "insert")
public void update_when_notDirty() {
found.setName("jack");
LoggedSqlCollector.start();
Ebean.save(found);
List<String> sql = LoggedSqlCollector.stop();
// we don't update the phone numbers (as they are not dirty)
assertThat(sql.get(0)).contains("update earray_bean set name=?, version=? where");
}
//@Test//(dependsOnMethods = "update_when_notDirty")
public void update_when_dirty() {
found.getPhoneNumbers().add("9987");
found.getUids().add(UUID.randomUUID());
LoggedSqlCollector.start();
Ebean.save(found);
List<String> sql = LoggedSqlCollector.stop();
assertThat(sql.get(0)).contains("update earray_bean set phone_numbers=?, uids=?, version=? where");
}
}