From b197d8a637c7413c39d68a8053e0cb796182e766 Mon Sep 17 00:00:00 2001 From: Robin Bygrave Date: Mon, 9 May 2016 22:38:51 +1200 Subject: [PATCH] #695 - ENH: Add @DbArray for storing scalar types (UUID, Long, String) to Postgres ARRAY type --- pom.xml | 2 +- .../com/avaje/ebean/ExpressionFactory.java | 32 +++++ .../java/com/avaje/ebean/ExpressionList.java | 29 ++++ .../com/avaje/ebean/annotation/DbArray.java | 40 ++++++ .../ebean/config/dbplatform/DbTypeMap.java | 2 + .../api/SpiExpressionRequest.java | 62 ++++----- .../server/core/DbExpressionHandler.java | 26 ++++ .../server/core/InternalConfiguration.java | 8 +- .../server/core/JsonExpressionHandler.java | 17 --- .../server/core/NotSupportedDbExpression.java | 25 ++++ .../core/NotSupportedJsonExpression.java | 15 --- .../server/core/OracleDbExpression.java | 33 +++++ .../server/core/OracleJsonExpression.java | 24 ---- .../server/core/PostgresCast.java | 52 ++++++++ .../server/core/PostgresJsonExpression.java | 55 ++++---- .../server/core/RelationalQueryRequest.java | 2 +- .../server/deploy/BeanProperty.java | 2 +- .../server/deploy/parse/AnnotationFields.java | 4 + .../deploy/parse/DeployCreateProperties.java | 2 + .../server/deploy/parse/DeployUtil.java | 28 ++++ .../expression/ArrayContainsExpression.java | 93 +++++++++++++ .../expression/ArrayIsEmptyExpression.java | 60 +++++++++ .../expression/DefaultExpressionFactory.java | 20 +++ .../expression/DefaultExpressionList.java | 24 ++++ .../expression/DefaultExpressionRequest.java | 6 +- .../server/expression/JsonPathExpression.java | 2 +- .../server/expression/JunctionExpression.java | 20 +++ .../ebeaninternal/server/persist/Binder.java | 23 ++-- .../server/persist/ExeCallableSql.java | 2 +- .../server/persist/ExeOrmUpdate.java | 2 +- .../server/persist/ExeUpdateSql.java | 2 +- .../server/persist/dml/DmlHandler.java | 2 +- .../ebeaninternal/server/query/CQuery.java | 2 +- .../server/query/CQueryDelete.java | 2 +- .../server/query/CQueryFetchIds.java | 2 +- .../server/query/CQueryPlan.java | 5 +- .../server/query/CQueryPredicates.java | 5 +- .../server/query/CQueryRowCount.java | 2 +- .../server/type/ArrayElementConverter.java | 79 +++++++++++ .../ebeaninternal/server/type/DataBind.java | 12 +- .../server/type/DefaultTypeManager.java | 49 +++++-- .../server/type/ScalarTypeArrayList.java | 125 ++++++++++++++++++ .../server/type/ScalarTypeJsonCollection.java | 7 +- .../server/type/ScalarTypeJsonList.java | 38 +++--- .../type/ScalarTypeJsonObjectMapper.java | 37 +++--- .../server/type/ScalarTypeJsonSet.java | 35 +++-- .../server/type/ScalarTypeUUIDBase.java | 2 +- .../server/type/TypeManager.java | 6 + .../docstore/api/mapping/DocPropertyType.java | 5 + .../java/com/avaje/ebean/BaseTestCase.java | 5 + .../expression/TDSpiExpressionRequest.java | 4 +- .../avaje/tests/model/array/EArrayBean.java | 80 +++++++++++ .../tests/model/array/TestDbArray_basic.java | 113 ++++++++++++++++ 53 files changed, 1102 insertions(+), 229 deletions(-) create mode 100644 src/main/java/com/avaje/ebean/annotation/DbArray.java create mode 100644 src/main/java/com/avaje/ebeaninternal/server/core/DbExpressionHandler.java delete mode 100644 src/main/java/com/avaje/ebeaninternal/server/core/JsonExpressionHandler.java create mode 100644 src/main/java/com/avaje/ebeaninternal/server/core/NotSupportedDbExpression.java delete mode 100644 src/main/java/com/avaje/ebeaninternal/server/core/NotSupportedJsonExpression.java create mode 100644 src/main/java/com/avaje/ebeaninternal/server/core/OracleDbExpression.java delete mode 100644 src/main/java/com/avaje/ebeaninternal/server/core/OracleJsonExpression.java create mode 100644 src/main/java/com/avaje/ebeaninternal/server/core/PostgresCast.java create mode 100644 src/main/java/com/avaje/ebeaninternal/server/expression/ArrayContainsExpression.java create mode 100644 src/main/java/com/avaje/ebeaninternal/server/expression/ArrayIsEmptyExpression.java create mode 100644 src/main/java/com/avaje/ebeaninternal/server/type/ArrayElementConverter.java create mode 100644 src/main/java/com/avaje/ebeaninternal/server/type/ScalarTypeArrayList.java create mode 100644 src/test/java/com/avaje/tests/model/array/EArrayBean.java create mode 100644 src/test/java/com/avaje/tests/model/array/TestDbArray_basic.java diff --git a/pom.xml b/pom.xml index bad54c0ad..968c04176 100644 --- a/pom.xml +++ b/pom.xml @@ -9,7 +9,7 @@ org.avaje.ebeanorm avaje-ebeanorm - 7.10.2-SNAPSHOT + 7.11.1-SNAPSHOT jar avaje-ebeanorm diff --git a/src/main/java/com/avaje/ebean/ExpressionFactory.java b/src/main/java/com/avaje/ebean/ExpressionFactory.java index 89ab15535..264d87312 100644 --- a/src/main/java/com/avaje/ebean/ExpressionFactory.java +++ b/src/main/java/com/avaje/ebean/ExpressionFactory.java @@ -87,6 +87,38 @@ public interface ExpressionFactory { */ Expression jsonBetween(String propertyName, String path, Object lowerValue, Object upperValue); + /** + * Array contains all the given values. + *

+ * Array support is effectively limited to Postgres at this time. + *

+ */ + Expression arrayContains(String propertyName, Object... values); + + /** + * Array does not contain the given values. + *

+ * Array support is effectively limited to Postgres at this time. + *

+ */ + Expression arrayNotContains(String propertyName, Object... values); + + /** + * Array is empty - for the given array property. + *

+ * Array support is effectively limited to Postgres at this time. + *

+ */ + Expression arrayIsEmpty(String propertyName); + + /** + * Array is not empty - for the given array property. + *

+ * Array support is effectively limited to Postgres at this time. + *

+ */ + Expression arrayIsNotEmpty(String propertyName); + /** * Equal To - property equal to the given value. */ diff --git a/src/main/java/com/avaje/ebean/ExpressionList.java b/src/main/java/com/avaje/ebean/ExpressionList.java index 0d9c85c16..f468d698a 100644 --- a/src/main/java/com/avaje/ebean/ExpressionList.java +++ b/src/main/java/com/avaje/ebean/ExpressionList.java @@ -801,6 +801,35 @@ public interface ExpressionList { */ ExpressionList allEq(Map propertyMap); + /** + * Array property contains entries with the given values. + */ + ExpressionList arrayContains(String propertyName, Object... values); + + /** + * Array does not contain the given values. + *

+ * Array support is effectively limited to Postgres at this time. + *

+ */ + ExpressionList arrayNotContains(String propertyName, Object... values); + + /** + * Array is empty - for the given array property. + *

+ * Array support is effectively limited to Postgres at this time. + *

+ */ + ExpressionList arrayIsEmpty(String propertyName); + + /** + * Array is not empty - for the given array property. + *

+ * Array support is effectively limited to Postgres at this time. + *

+ */ + ExpressionList arrayIsNotEmpty(String propertyName); + /** * Add raw expression with a single parameter. *

diff --git a/src/main/java/com/avaje/ebean/annotation/DbArray.java b/src/main/java/com/avaje/ebean/annotation/DbArray.java new file mode 100644 index 000000000..5c81651d5 --- /dev/null +++ b/src/main/java/com/avaje/ebean/annotation/DbArray.java @@ -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. + *

+ * 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. + *

+ * + *

Example:

+ *
{@code
+ *
+ * // Store as ARRAY of UUID on Postgres
+ * @DbArray
+ * List uids = new ArrayList<>();
+ *
+ * // Store as ARRAY on Postgres
+ * @DbArray
+ * List phoneNumbers = new ArrayList<>();
+ *
+ * // Store as ARRAY of INTEGER on Postgres
+ * @DbArray
+ * List someLongs = new ArrayList<>();
+ *
+ * }
+ */ +@Retention(RetentionPolicy.RUNTIME) +@Target(ElementType.FIELD) +public @interface DbArray { + + /** + * For VARCHAR storage specify the column length (defaults to 1000). + */ + int length() default 0; +} diff --git a/src/main/java/com/avaje/ebean/config/dbplatform/DbTypeMap.java b/src/main/java/com/avaje/ebean/config/dbplatform/DbTypeMap.java index 9dbe17b2e..bbf138bdf 100644 --- a/src/main/java/com/avaje/ebean/config/dbplatform/DbTypeMap.java +++ b/src/main/java/com/avaje/ebean/config/dbplatform/DbTypeMap.java @@ -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 diff --git a/src/main/java/com/avaje/ebeaninternal/api/SpiExpressionRequest.java b/src/main/java/com/avaje/ebeaninternal/api/SpiExpressionRequest.java index 98069d29e..3e3d182df 100644 --- a/src/main/java/com/avaje/ebeaninternal/api/SpiExpressionRequest.java +++ b/src/main/java/com/avaje/ebeaninternal/api/SpiExpressionRequest.java @@ -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 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 getBindValues(); /** * Increments the parameter index and returns that value. diff --git a/src/main/java/com/avaje/ebeaninternal/server/core/DbExpressionHandler.java b/src/main/java/com/avaje/ebeaninternal/server/core/DbExpressionHandler.java new file mode 100644 index 000000000..0c1d4bb2b --- /dev/null +++ b/src/main/java/com/avaje/ebeaninternal/server/core/DbExpressionHandler.java @@ -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); + +} diff --git a/src/main/java/com/avaje/ebeaninternal/server/core/InternalConfiguration.java b/src/main/java/com/avaje/ebeaninternal/server/core/InternalConfiguration.java index ed061bb83..214247867 100644 --- a/src/main/java/com/avaje/ebeaninternal/server/core/InternalConfiguration.java +++ b/src/main/java/com/avaje/ebeaninternal/server/core/InternalConfiguration.java @@ -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) { diff --git a/src/main/java/com/avaje/ebeaninternal/server/core/JsonExpressionHandler.java b/src/main/java/com/avaje/ebeaninternal/server/core/JsonExpressionHandler.java deleted file mode 100644 index 67ec32bcb..000000000 --- a/src/main/java/com/avaje/ebeaninternal/server/core/JsonExpressionHandler.java +++ /dev/null @@ -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); - -} diff --git a/src/main/java/com/avaje/ebeaninternal/server/core/NotSupportedDbExpression.java b/src/main/java/com/avaje/ebeaninternal/server/core/NotSupportedDbExpression.java new file mode 100644 index 000000000..17db357a5 --- /dev/null +++ b/src/main/java/com/avaje/ebeaninternal/server/core/NotSupportedDbExpression.java @@ -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"); + } +} diff --git a/src/main/java/com/avaje/ebeaninternal/server/core/NotSupportedJsonExpression.java b/src/main/java/com/avaje/ebeaninternal/server/core/NotSupportedJsonExpression.java deleted file mode 100644 index 840bd237f..000000000 --- a/src/main/java/com/avaje/ebeaninternal/server/core/NotSupportedJsonExpression.java +++ /dev/null @@ -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"); - } -} diff --git a/src/main/java/com/avaje/ebeaninternal/server/core/OracleDbExpression.java b/src/main/java/com/avaje/ebeaninternal/server/core/OracleDbExpression.java new file mode 100644 index 000000000..d3f73cc1e --- /dev/null +++ b/src/main/java/com/avaje/ebeaninternal/server/core/OracleDbExpression.java @@ -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"); + } +} diff --git a/src/main/java/com/avaje/ebeaninternal/server/core/OracleJsonExpression.java b/src/main/java/com/avaje/ebeaninternal/server/core/OracleJsonExpression.java deleted file mode 100644 index c4acf95a9..000000000 --- a/src/main/java/com/avaje/ebeaninternal/server/core/OracleJsonExpression.java +++ /dev/null @@ -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()); - } - - } -} diff --git a/src/main/java/com/avaje/ebeaninternal/server/core/PostgresCast.java b/src/main/java/com/avaje/ebeaninternal/server/core/PostgresCast.java new file mode 100644 index 000000000..3aa592e6b --- /dev/null +++ b/src/main/java/com/avaje/ebeaninternal/server/core/PostgresCast.java @@ -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. + *

+ * This is generally necessary for JSON expressions as text values always returned from the json operators used. + *

+ */ + 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); + } +} diff --git a/src/main/java/com/avaje/ebeaninternal/server/core/PostgresJsonExpression.java b/src/main/java/com/avaje/ebeaninternal/server/core/PostgresJsonExpression.java index c61f21f0a..3e102b132 100644 --- a/src/main/java/com/avaje/ebeaninternal/server/core/PostgresJsonExpression.java +++ b/src/main/java/com/avaje/ebeaninternal/server/core/PostgresJsonExpression.java @@ -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"); + } } } diff --git a/src/main/java/com/avaje/ebeaninternal/server/core/RelationalQueryRequest.java b/src/main/java/com/avaje/ebeaninternal/server/core/RelationalQueryRequest.java index 1eeac35ca..916f429cb 100644 --- a/src/main/java/com/avaje/ebeaninternal/server/core/RelationalQueryRequest.java +++ b/src/main/java/com/avaje/ebeaninternal/server/core/RelationalQueryRequest.java @@ -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()) { diff --git a/src/main/java/com/avaje/ebeaninternal/server/deploy/BeanProperty.java b/src/main/java/com/avaje/ebeaninternal/server/deploy/BeanProperty.java index 04aa3ec51..bf62fa46a 100644 --- a/src/main/java/com/avaje/ebeaninternal/server/deploy/BeanProperty.java +++ b/src/main/java/com/avaje/ebeaninternal/server/deploy/BeanProperty.java @@ -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); } diff --git a/src/main/java/com/avaje/ebeaninternal/server/deploy/parse/AnnotationFields.java b/src/main/java/com/avaje/ebeaninternal/server/deploy/parse/AnnotationFields.java index 8b76eb7f6..ed04e512a 100644 --- a/src/main/java/com/avaje/ebeaninternal/server/deploy/parse/AnnotationFields.java +++ b/src/main/java/com/avaje/ebeaninternal/server/deploy/parse/AnnotationFields.java @@ -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) { diff --git a/src/main/java/com/avaje/ebeaninternal/server/deploy/parse/DeployCreateProperties.java b/src/main/java/com/avaje/ebeaninternal/server/deploy/parse/DeployCreateProperties.java index 8362fdcc5..23ab68dec 100644 --- a/src/main/java/com/avaje/ebeaninternal/server/deploy/parse/DeployCreateProperties.java +++ b/src/main/java/com/avaje/ebeaninternal/server/deploy/parse/DeployCreateProperties.java @@ -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); } diff --git a/src/main/java/com/avaje/ebeaninternal/server/deploy/parse/DeployUtil.java b/src/main/java/com/avaje/ebeaninternal/server/deploy/parse/DeployUtil.java index 0d430f7e7..5887044bb 100644 --- a/src/main/java/com/avaje/ebeaninternal/server/deploy/parse/DeployUtil.java +++ b/src/main/java/com/avaje/ebeaninternal/server/deploy/parse/DeployUtil.java @@ -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. */ diff --git a/src/main/java/com/avaje/ebeaninternal/server/expression/ArrayContainsExpression.java b/src/main/java/com/avaje/ebeaninternal/server/expression/ArrayContainsExpression.java new file mode 100644 index 000000000..a90c13cf7 --- /dev/null +++ b/src/main/java/com/avaje/ebeaninternal/server/expression/ArrayContainsExpression.java @@ -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); + } + } +} diff --git a/src/main/java/com/avaje/ebeaninternal/server/expression/ArrayIsEmptyExpression.java b/src/main/java/com/avaje/ebeaninternal/server/expression/ArrayIsEmptyExpression.java new file mode 100644 index 000000000..741fcf26f --- /dev/null +++ b/src/main/java/com/avaje/ebeaninternal/server/expression/ArrayIsEmptyExpression.java @@ -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 + } +} diff --git a/src/main/java/com/avaje/ebeaninternal/server/expression/DefaultExpressionFactory.java b/src/main/java/com/avaje/ebeaninternal/server/expression/DefaultExpressionFactory.java index 87465295f..f8e6e5729 100644 --- a/src/main/java/com/avaje/ebeaninternal/server/expression/DefaultExpressionFactory.java +++ b/src/main/java/com/avaje/ebeaninternal/server/expression/DefaultExpressionFactory.java @@ -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. */ diff --git a/src/main/java/com/avaje/ebeaninternal/server/expression/DefaultExpressionList.java b/src/main/java/com/avaje/ebeaninternal/server/expression/DefaultExpressionList.java index 413e420ef..8fa03ca5a 100644 --- a/src/main/java/com/avaje/ebeaninternal/server/expression/DefaultExpressionList.java +++ b/src/main/java/com/avaje/ebeaninternal/server/expression/DefaultExpressionList.java @@ -826,6 +826,30 @@ public class DefaultExpressionList implements SpiExpressionList { return this; } + @Override + public ExpressionList arrayContains(String propertyName, Object... elementValue) { + add(expr.arrayContains(propertyName, elementValue)); + return this; + } + + @Override + public ExpressionList arrayNotContains(String propertyName, Object... values) { + add(expr.arrayNotContains(propertyName, values)); + return this; + } + + @Override + public ExpressionList arrayIsEmpty(String propertyName) { + add(expr.arrayIsEmpty(propertyName)); + return this; + } + + @Override + public ExpressionList arrayIsNotEmpty(String propertyName) { + add(expr.arrayIsNotEmpty(propertyName)); + return this; + } + @Override public ExpressionList raw(String raw, Object value) { add(expr.raw(raw, value)); diff --git a/src/main/java/com/avaje/ebeaninternal/server/expression/DefaultExpressionRequest.java b/src/main/java/com/avaje/ebeaninternal/server/expression/DefaultExpressionRequest.java index c87126df4..76f157b43 100644 --- a/src/main/java/com/avaje/ebeaninternal/server/expression/DefaultExpressionRequest.java +++ b/src/main/java/com/avaje/ebeaninternal/server/expression/DefaultExpressionRequest.java @@ -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 diff --git a/src/main/java/com/avaje/ebeaninternal/server/expression/JsonPathExpression.java b/src/main/java/com/avaje/ebeaninternal/server/expression/JsonPathExpression.java index f89905f59..778bbad18 100644 --- a/src/main/java/com/avaje/ebeaninternal/server/expression/JsonPathExpression.java +++ b/src/main/java/com/avaje/ebeaninternal/server/expression/JsonPathExpression.java @@ -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 diff --git a/src/main/java/com/avaje/ebeaninternal/server/expression/JunctionExpression.java b/src/main/java/com/avaje/ebeaninternal/server/expression/JunctionExpression.java index 9b5e61a0a..27f445e83 100644 --- a/src/main/java/com/avaje/ebeaninternal/server/expression/JunctionExpression.java +++ b/src/main/java/com/avaje/ebeaninternal/server/expression/JunctionExpression.java @@ -462,6 +462,26 @@ class JunctionExpression implements SpiJunction, SpiExpression, Expression return exprList.jsonBetween(propertyName, path, lowerValue, upperValue); } + @Override + public ExpressionList arrayContains(String propertyName, Object... values) { + return exprList.arrayContains(propertyName, values); + } + + @Override + public ExpressionList arrayNotContains(String propertyName, Object... values) { + return exprList.arrayNotContains(propertyName, values); + } + + @Override + public ExpressionList arrayIsEmpty(String propertyName) { + return exprList.arrayIsEmpty(propertyName); + } + + @Override + public ExpressionList arrayIsNotEmpty(String propertyName) { + return exprList.arrayIsNotEmpty(propertyName); + } + @Override public ExpressionList ge(String propertyName, Object value) { return exprList.ge(propertyName, value); diff --git a/src/main/java/com/avaje/ebeaninternal/server/persist/Binder.java b/src/main/java/com/avaje/ebeaninternal/server/persist/Binder.java index 5c57c1066..2de9ee82a 100644 --- a/src/main/java/com/avaje/ebeaninternal/server/persist/Binder.java +++ b/src/main/java/com/avaje/ebeaninternal/server/persist/Binder.java @@ -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); } } diff --git a/src/main/java/com/avaje/ebeaninternal/server/persist/ExeCallableSql.java b/src/main/java/com/avaje/ebeaninternal/server/persist/ExeCallableSql.java index 89435be6b..ec565bc9f 100644 --- a/src/main/java/com/avaje/ebeaninternal/server/persist/ExeCallableSql.java +++ b/src/main/java/com/avaje/ebeaninternal/server/persist/ExeCallableSql.java @@ -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); diff --git a/src/main/java/com/avaje/ebeaninternal/server/persist/ExeOrmUpdate.java b/src/main/java/com/avaje/ebeaninternal/server/persist/ExeOrmUpdate.java index 2b3480543..efd238f73 100644 --- a/src/main/java/com/avaje/ebeaninternal/server/persist/ExeOrmUpdate.java +++ b/src/main/java/com/avaje/ebeaninternal/server/persist/ExeOrmUpdate.java @@ -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); diff --git a/src/main/java/com/avaje/ebeaninternal/server/persist/ExeUpdateSql.java b/src/main/java/com/avaje/ebeaninternal/server/persist/ExeUpdateSql.java index 377ce08fa..67cb2ab4f 100644 --- a/src/main/java/com/avaje/ebeaninternal/server/persist/ExeUpdateSql.java +++ b/src/main/java/com/avaje/ebeaninternal/server/persist/ExeUpdateSql.java @@ -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); diff --git a/src/main/java/com/avaje/ebeaninternal/server/persist/dml/DmlHandler.java b/src/main/java/com/avaje/ebeaninternal/server/persist/dml/DmlHandler.java index db0c8261a..b68776381 100644 --- a/src/main/java/com/avaje/ebeaninternal/server/persist/dml/DmlHandler.java +++ b/src/main/java/com/avaje/ebeaninternal/server/persist/dml/DmlHandler.java @@ -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()); } /** diff --git a/src/main/java/com/avaje/ebeaninternal/server/query/CQuery.java b/src/main/java/com/avaje/ebeaninternal/server/query/CQuery.java index e8f96c930..39972cd35 100644 --- a/src/main/java/com/avaje/ebeaninternal/server/query/CQuery.java +++ b/src/main/java/com/avaje/ebeaninternal/server/query/CQuery.java @@ -335,7 +335,7 @@ public class CQuery implements DbReadContext, CancelableQuery { pstmt.setFetchSize(query.getBufferFetchSizeHint()); } - DataBind dataBind = queryPlan.bindEncryptedProperties(pstmt); + DataBind dataBind = queryPlan.bindEncryptedProperties(pstmt, conn); bindLog = predicates.bind(dataBind); // executeQuery diff --git a/src/main/java/com/avaje/ebeaninternal/server/query/CQueryDelete.java b/src/main/java/com/avaje/ebeaninternal/server/query/CQueryDelete.java index f99f4c6f8..5f005ad33 100644 --- a/src/main/java/com/avaje/ebeaninternal/server/query/CQueryDelete.java +++ b/src/main/java/com/avaje/ebeaninternal/server/query/CQueryDelete.java @@ -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; diff --git a/src/main/java/com/avaje/ebeaninternal/server/query/CQueryFetchIds.java b/src/main/java/com/avaje/ebeaninternal/server/query/CQueryFetchIds.java index 7a7a3332e..31da74715 100644 --- a/src/main/java/com/avaje/ebeaninternal/server/query/CQueryFetchIds.java +++ b/src/main/java/com/avaje/ebeaninternal/server/query/CQueryFetchIds.java @@ -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); diff --git a/src/main/java/com/avaje/ebeaninternal/server/query/CQueryPlan.java b/src/main/java/com/avaje/ebeaninternal/server/query/CQueryPlan.java index 2764cfb22..72e7ea4ff 100644 --- a/src/main/java/com/avaje/ebeaninternal/server/query/CQueryPlan.java +++ b/src/main/java/com/avaje/ebeaninternal/server/query/CQueryPlan.java @@ -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(); diff --git a/src/main/java/com/avaje/ebeaninternal/server/query/CQueryPredicates.java b/src/main/java/com/avaje/ebeaninternal/server/query/CQueryPredicates.java index ef8ac205f..4b69efc4b 100644 --- a/src/main/java/com/avaje/ebeaninternal/server/query/CQueryPredicates.java +++ b/src/main/java/com/avaje/ebeaninternal/server/query/CQueryPredicates.java @@ -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 { diff --git a/src/main/java/com/avaje/ebeaninternal/server/query/CQueryRowCount.java b/src/main/java/com/avaje/ebeaninternal/server/query/CQueryRowCount.java index 0a35be81f..9a8d81da1 100644 --- a/src/main/java/com/avaje/ebeaninternal/server/query/CQueryRowCount.java +++ b/src/main/java/com/avaje/ebeaninternal/server/query/CQueryRowCount.java @@ -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()) { diff --git a/src/main/java/com/avaje/ebeaninternal/server/type/ArrayElementConverter.java b/src/main/java/com/avaje/ebeaninternal/server/type/ArrayElementConverter.java new file mode 100644 index 000000000..1065650ff --- /dev/null +++ b/src/main/java/com/avaje/ebeaninternal/server/type/ArrayElementConverter.java @@ -0,0 +1,79 @@ +package com.avaje.ebeaninternal.server.type; + +import java.util.UUID; + +/** + * Type conversion for use with ScalarTypeArrayList. + */ +public interface ArrayElementConverter { + + /** + * 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 { + + @Override + public Long toElement(Object rawValue) { + if (rawValue instanceof Long) { + return (Long)rawValue; + } else { + return ((Number)rawValue).longValue(); + } + } + } + + class IntegerConverter implements ArrayElementConverter { + + @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 { + } + + /** + * UUID converter (noop based). + */ + class UuidConverter extends NoopConverter { + } + + class NoopConverter implements ArrayElementConverter { + + @Override + public T toElement(Object rawValue) { + return (T)rawValue; + } + } + +} diff --git a/src/main/java/com/avaje/ebeaninternal/server/type/DataBind.java b/src/main/java/com/avaje/ebeaninternal/server/type/DataBind.java index 03a8c7d91..20616339b 100644 --- a/src/main/java/com/avaje/ebeaninternal/server/type/DataBind.java +++ b/src/main/java/com/avaje/ebeaninternal/server/type/DataBind.java @@ -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); + } + } \ No newline at end of file diff --git a/src/main/java/com/avaje/ebeaninternal/server/type/DefaultTypeManager.java b/src/main/java/com/avaje/ebeaninternal/server/type/DefaultTypeManager.java index 3da28dbd3..7c7e76203 100644 --- a/src/main/java/com/avaje/ebeaninternal/server/type/DefaultTypeManager.java +++ b/src/main/java/com/avaje/ebeaninternal/server/type/DefaultTypeManager.java @@ -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); } /** diff --git a/src/main/java/com/avaje/ebeaninternal/server/type/ScalarTypeArrayList.java b/src/main/java/com/avaje/ebeaninternal/server/type/ScalarTypeArrayList.java new file mode 100644 index 000000000..73ce0c1c0 --- /dev/null +++ b/src/main/java/com/avaje/ebeaninternal/server/type/ScalarTypeArrayList.java @@ -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 { + + 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); + } + +} diff --git a/src/main/java/com/avaje/ebeaninternal/server/type/ScalarTypeJsonCollection.java b/src/main/java/com/avaje/ebeaninternal/server/type/ScalarTypeJsonCollection.java index d37442385..27cf80165 100644 --- a/src/main/java/com/avaje/ebeaninternal/server/type/ScalarTypeJsonCollection.java +++ b/src/main/java/com/avaje/ebeaninternal/server/type/ScalarTypeJsonCollection.java @@ -8,8 +8,11 @@ import java.io.IOException; abstract class ScalarTypeJsonCollection extends ScalarTypeBase { - public ScalarTypeJsonCollection(Class type, int dbType) { + protected DocPropertyType docPropertyType; + + public ScalarTypeJsonCollection(Class type, int dbType, DocPropertyType docPropertyType) { super(type, false, dbType); + this.docPropertyType = docPropertyType; } /** @@ -41,7 +44,7 @@ abstract class ScalarTypeJsonCollection extends ScalarTypeBase { @Override public DocPropertyType getDocType() { - return DocPropertyType.LIST; + return docPropertyType; } @Override diff --git a/src/main/java/com/avaje/ebeaninternal/server/type/ScalarTypeJsonList.java b/src/main/java/com/avaje/ebeaninternal/server/type/ScalarTypeJsonList.java index 461e2fbd7..64155a6c6 100644 --- a/src/main/java/com/avaje/ebeaninternal/server/type/ScalarTypeJsonList.java +++ b/src/main/java/com/avaje/ebeaninternal/server/type/ScalarTypeJsonList.java @@ -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 VARCHAR = new ScalarTypeJsonList.Varchar(); - - public static final ScalarTypeJsonCollection JSON = new ScalarTypeJsonList.Json(); - - public static final ScalarTypeJsonCollection 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 { - 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; } diff --git a/src/main/java/com/avaje/ebeaninternal/server/type/ScalarTypeJsonObjectMapper.java b/src/main/java/com/avaje/ebeaninternal/server/type/ScalarTypeJsonObjectMapper.java index aba059ef7..587be6fa7 100644 --- a/src/main/java/com/avaje/ebeaninternal/server/type/ScalarTypeJsonObjectMapper.java +++ b/src/main/java/com/avaje/ebeaninternal/server/type/ScalarTypeJsonObjectMapper.java @@ -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 { 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 { - 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 { - 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 { 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 cls, ObjectMapper objectMapper, Type type, int dbType, String pgType) { + public Base(Class 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 diff --git a/src/main/java/com/avaje/ebeaninternal/server/type/ScalarTypeJsonSet.java b/src/main/java/com/avaje/ebeaninternal/server/type/ScalarTypeJsonSet.java index 1cf02d093..b35b352bd 100644 --- a/src/main/java/com/avaje/ebeaninternal/server/type/ScalarTypeJsonSet.java +++ b/src/main/java/com/avaje/ebeaninternal/server/type/ScalarTypeJsonSet.java @@ -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 VARCHAR = new ScalarTypeJsonSet.Varchar(); - - private static final ScalarTypeJsonCollection JSON = new ScalarTypeJsonSet.Json(); - - private static final ScalarTypeJsonCollection 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 { - 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; } diff --git a/src/main/java/com/avaje/ebeaninternal/server/type/ScalarTypeUUIDBase.java b/src/main/java/com/avaje/ebeaninternal/server/type/ScalarTypeUUIDBase.java index 10b0a199b..9c9a18bae 100644 --- a/src/main/java/com/avaje/ebeaninternal/server/type/ScalarTypeUUIDBase.java +++ b/src/main/java/com/avaje/ebeaninternal/server/type/ScalarTypeUUIDBase.java @@ -89,6 +89,6 @@ public abstract class ScalarTypeUUIDBase extends ScalarTypeBase { @Override public DocPropertyType getDocType() { - return DocPropertyType.STRING; + return DocPropertyType.UUID; } } diff --git a/src/main/java/com/avaje/ebeaninternal/server/type/TypeManager.java b/src/main/java/com/avaje/ebeaninternal/server/type/TypeManager.java index bf7ed1b62..ac967797c 100644 --- a/src/main/java/com/avaje/ebeaninternal/server/type/TypeManager.java +++ b/src/main/java/com/avaje/ebeaninternal/server/type/TypeManager.java @@ -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 { *

*/ 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); } diff --git a/src/main/java/com/avaje/ebeanservice/docstore/api/mapping/DocPropertyType.java b/src/main/java/com/avaje/ebeanservice/docstore/api/mapping/DocPropertyType.java index 48e826db2..83ab39c3d 100644 --- a/src/main/java/com/avaje/ebeanservice/docstore/api/mapping/DocPropertyType.java +++ b/src/main/java/com/avaje/ebeanservice/docstore/api/mapping/DocPropertyType.java @@ -10,6 +10,11 @@ public enum DocPropertyType { */ ENUM, + /** + * A UUID is a String Id implying it should not be analysed. + */ + UUID, + /** * String. */ diff --git a/src/test/java/com/avaje/ebean/BaseTestCase.java b/src/test/java/com/avaje/ebean/BaseTestCase.java index c8b31599f..077591532 100644 --- a/src/test/java/com/avaje/ebean/BaseTestCase.java +++ b/src/test/java/com/avaje/ebean/BaseTestCase.java @@ -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. */ diff --git a/src/test/java/com/avaje/ebeaninternal/server/expression/TDSpiExpressionRequest.java b/src/test/java/com/avaje/ebeaninternal/server/expression/TDSpiExpressionRequest.java index 478897e38..33f1f207d 100644 --- a/src/test/java/com/avaje/ebeaninternal/server/expression/TDSpiExpressionRequest.java +++ b/src/test/java/com/avaje/ebeaninternal/server/expression/TDSpiExpressionRequest.java @@ -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; } diff --git a/src/test/java/com/avaje/tests/model/array/EArrayBean.java b/src/test/java/com/avaje/tests/model/array/EArrayBean.java new file mode 100644 index 000000000..a16670a8c --- /dev/null +++ b/src/test/java/com/avaje/tests/model/array/EArrayBean.java @@ -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 phoneNumbers = new ArrayList(); + + @DbArray + List uids = new ArrayList(); + + @DbArray + List otherIds = new ArrayList(); + + @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 getPhoneNumbers() { + return phoneNumbers; + } + + public void setPhoneNumbers(List phoneNumbers) { + this.phoneNumbers = phoneNumbers; + } + + public List getUids() { + return uids; + } + + public void setUids(List uids) { + this.uids = uids; + } + + public List getOtherIds() { + return otherIds; + } + + public void setOtherIds(List otherIds) { + this.otherIds = otherIds; + } + + public Long getVersion() { + return version; + } + + public void setVersion(Long version) { + this.version = version; + } +} diff --git a/src/test/java/com/avaje/tests/model/array/TestDbArray_basic.java b/src/test/java/com/avaje/tests/model/array/TestDbArray_basic.java new file mode 100644 index 000000000..4ca0863e6 --- /dev/null +++ b/src/test/java/com/avaje/tests/model/array/TestDbArray_basic.java @@ -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 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 query = Ebean.find(EArrayBean.class) + .where() + .arrayContains("otherIds", 96L, 97L) + .arrayContains("uids", bean.getUids().get(0)) + .arrayContains("phoneNumbers", "9823") + .arrayIsNotEmpty("phoneNumbers") + .query(); + + List 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 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 sql = LoggedSqlCollector.stop(); + + assertThat(sql.get(0)).contains("update earray_bean set phone_numbers=?, uids=?, version=? where"); + } +} \ No newline at end of file