diff --git a/src/main/java/io/ebean/event/BeanQueryRequest.java b/src/main/java/io/ebean/event/BeanQueryRequest.java index 4ba1e395e..ce327e67c 100644 --- a/src/main/java/io/ebean/event/BeanQueryRequest.java +++ b/src/main/java/io/ebean/event/BeanQueryRequest.java @@ -24,4 +24,13 @@ public interface BeanQueryRequest { */ Query getQuery(); + /** + * Return true if multi-value binding using Array or Table Values is supported. + */ + boolean isMultiValueIdSupported(); + + /** + * Return true if multi-value binding is supported for this value type. + */ + boolean isMultiValueSupported(Class valueType); } diff --git a/src/main/java/io/ebeaninternal/api/LoadBeanRequest.java b/src/main/java/io/ebeaninternal/api/LoadBeanRequest.java index eaa2acb49..88b3881bd 100644 --- a/src/main/java/io/ebeaninternal/api/LoadBeanRequest.java +++ b/src/main/java/io/ebeaninternal/api/LoadBeanRequest.java @@ -103,7 +103,8 @@ public class LoadBeanRequest extends LoadRequest { idList.add(desc.getId(bean)); } - if (!idList.isEmpty()) { + + if (!desc.isMultiValueIdSupported() && !idList.isEmpty()) { int extraIds = batchSize - batch.size(); if (extraIds > 0) { // for performance make up the Id's to the batch size diff --git a/src/main/java/io/ebeaninternal/api/LoadManyRequest.java b/src/main/java/io/ebeaninternal/api/LoadManyRequest.java index f73af11aa..d92ce3222 100644 --- a/src/main/java/io/ebeaninternal/api/LoadManyRequest.java +++ b/src/main/java/io/ebeaninternal/api/LoadManyRequest.java @@ -107,11 +107,13 @@ public class LoadManyRequest extends LoadRequest { for (BeanCollection bc : batch) { idList.add(many.getParentId(bc.getOwnerBean())); } - int extraIds = batchSize - batch.size(); - if (extraIds > 0) { - Object firstId = idList.get(0); - for (int i = 0; i < extraIds; i++) { - idList.add(firstId); + if (!many.getTargetDescriptor().isMultiValueIdSupported()) { + int extraIds = batchSize - batch.size(); + if (extraIds > 0) { + Object firstId = idList.get(0); + for (int i = 0; i < extraIds; i++) { + idList.add(firstId); + } } } diff --git a/src/main/java/io/ebeaninternal/api/SpiExpressionRequest.java b/src/main/java/io/ebeaninternal/api/SpiExpressionRequest.java index 09487e833..79ecbd46a 100644 --- a/src/main/java/io/ebeaninternal/api/SpiExpressionRequest.java +++ b/src/main/java/io/ebeaninternal/api/SpiExpressionRequest.java @@ -70,4 +70,9 @@ public interface SpiExpressionRequest { * Escapes a string to use it as exact match in Like clause. */ String escapeLikeString(String value); + + /** + * Append IN expression taking into account platform and type support for Multi-value. + */ + void appendInExpression(Object[] bindValues); } diff --git a/src/main/java/io/ebeaninternal/server/core/BeanRequest.java b/src/main/java/io/ebeaninternal/server/core/BeanRequest.java index 909ba0ecf..f3a3e5d2a 100644 --- a/src/main/java/io/ebeaninternal/server/core/BeanRequest.java +++ b/src/main/java/io/ebeaninternal/server/core/BeanRequest.java @@ -12,7 +12,7 @@ import org.slf4j.LoggerFactory; */ public abstract class BeanRequest { - private static final Logger log = LoggerFactory.getLogger(BeanRequest.class); + static final Logger log = LoggerFactory.getLogger(BeanRequest.class); /** * The server processing the request. diff --git a/src/main/java/io/ebeaninternal/server/core/DefaultBeanLoader.java b/src/main/java/io/ebeaninternal/server/core/DefaultBeanLoader.java index 9bc4b2ab1..828bfd361 100644 --- a/src/main/java/io/ebeaninternal/server/core/DefaultBeanLoader.java +++ b/src/main/java/io/ebeaninternal/server/core/DefaultBeanLoader.java @@ -182,7 +182,6 @@ public class DefaultBeanLoader { } } - /** * Load a batch of beans for +query or +lazy loading. */ diff --git a/src/main/java/io/ebeaninternal/server/core/InternalConfiguration.java b/src/main/java/io/ebeaninternal/server/core/InternalConfiguration.java index 68e8f1a21..030615494 100644 --- a/src/main/java/io/ebeaninternal/server/core/InternalConfiguration.java +++ b/src/main/java/io/ebeaninternal/server/core/InternalConfiguration.java @@ -7,9 +7,9 @@ import io.ebean.cache.ServerCacheManager; import io.ebean.config.ExternalTransactionManager; import io.ebean.config.ProfilingConfig; import io.ebean.config.ServerConfig; +import io.ebean.config.SlowQueryListener; import io.ebean.config.dbplatform.DatabasePlatform; import io.ebean.config.dbplatform.DbHistorySupport; -import io.ebean.config.SlowQueryListener; import io.ebean.event.changelog.ChangeLogListener; import io.ebean.event.changelog.ChangeLogPrepare; import io.ebean.event.changelog.ChangeLogRegister; @@ -43,6 +43,9 @@ import io.ebeaninternal.server.deploy.parse.DeployUtil; import io.ebeaninternal.server.expression.DefaultExpressionFactory; import io.ebeaninternal.server.persist.Binder; import io.ebeaninternal.server.persist.DefaultPersister; +import io.ebeaninternal.server.persist.platform.H2MultiValueBind; +import io.ebeaninternal.server.persist.platform.MultiValueBind; +import io.ebeaninternal.server.persist.platform.PostgresMultiValueBind; import io.ebeaninternal.server.query.CQueryEngine; import io.ebeaninternal.server.query.DefaultOrmQueryEngine; import io.ebeaninternal.server.query.DefaultRelationalQueryEngine; @@ -122,6 +125,8 @@ public class InternalConfiguration { */ private final List plugins = new ArrayList<>(); + private final MultiValueBind multiValueBind; + public InternalConfiguration(ClusterManager clusterManager, SpiCacheManager cacheManager, SpiBackgroundExecutor backgroundExecutor, ServerConfig serverConfig, BootupClasses bootupClasses) { @@ -138,6 +143,7 @@ public class InternalConfiguration { this.expressionFactory = initExpressionFactory(serverConfig, databasePlatform); this.typeManager = new DefaultTypeManager(serverConfig, bootupClasses); + this.multiValueBind = createMultiValueBind(databasePlatform.getPlatform()); this.deployInherit = new DeployInherit(bootupClasses); this.deployCreateProperties = new DeployCreateProperties(typeManager); @@ -245,9 +251,9 @@ public class InternalConfiguration { DbHistorySupport historySupport = databasePlatform.getHistorySupport(); if (historySupport == null) { - return new Binder(typeManager, 0, false, jsonHandler, dataTimeZone); + return new Binder(typeManager, 0, false, jsonHandler, dataTimeZone, multiValueBind); } - return new Binder(typeManager, historySupport.getBindCount(), historySupport.isStandardsBased(), jsonHandler, dataTimeZone); + return new Binder(typeManager, historySupport.getBindCount(), historySupport.isStandardsBased(), jsonHandler, dataTimeZone, multiValueBind); } /** @@ -267,6 +273,21 @@ public class InternalConfiguration { } } + private MultiValueBind createMultiValueBind(Platform platform) { + switch (platform) { + case H2: + return new H2MultiValueBind(); + case POSTGRES: + return new PostgresMultiValueBind(); +// case SQLSERVER: +// return new SqlServerTvpMultiValueHelp(); +// case ORACLE: +// return new OracleTvpMultiValueHelp(); + default: + return new MultiValueBind(); + } + } + public SpiJsonContext createJsonContext(SpiEbeanServer server) { return new DJsonContext(server, jsonFactory, typeManager); } @@ -280,7 +301,7 @@ public class InternalConfiguration { } public OrmQueryEngine createOrmQueryEngine() { - return new DefaultOrmQueryEngine(cQueryEngine); + return new DefaultOrmQueryEngine(cQueryEngine, binder); } public Persister createPersister(SpiEbeanServer server) { @@ -478,4 +499,11 @@ public class InternalConfiguration { } return listener; } + + /** + * Return the platform specific MultiValue bind support. + */ + public MultiValueBind getMultiValueBind() { + return multiValueBind; + } } diff --git a/src/main/java/io/ebeaninternal/server/core/OrmQueryEngine.java b/src/main/java/io/ebeaninternal/server/core/OrmQueryEngine.java index 76c5875c8..aeda6a8ad 100644 --- a/src/main/java/io/ebeaninternal/server/core/OrmQueryEngine.java +++ b/src/main/java/io/ebeaninternal/server/core/OrmQueryEngine.java @@ -62,4 +62,9 @@ public interface OrmQueryEngine { * Translate the SQLException to a specific persistence exception type if possible. */ PersistenceException translate(OrmQueryRequest request, String bindLog, String sql, SQLException e); + + /** + * Return true if multi-value bind is supported for this type (and current platform). + */ + boolean isMultiValueSupported(Class valueType); } diff --git a/src/main/java/io/ebeaninternal/server/core/OrmQueryRequest.java b/src/main/java/io/ebeaninternal/server/core/OrmQueryRequest.java index dc76fe050..2b4c2f958 100644 --- a/src/main/java/io/ebeaninternal/server/core/OrmQueryRequest.java +++ b/src/main/java/io/ebeaninternal/server/core/OrmQueryRequest.java @@ -85,6 +85,16 @@ public final class OrmQueryRequest extends BeanRequest implements BeanQueryRe return queryEngine.translate(this, bindLog, sql, e); } + @Override + public boolean isMultiValueIdSupported() { + return beanDescriptor.isMultiValueIdSupported(); + } + + @Override + public boolean isMultiValueSupported(Class valueType) { + return queryEngine.isMultiValueSupported(valueType); + } + /** * Mark the transaction as not being query only. */ @@ -232,6 +242,22 @@ public final class OrmQueryRequest extends BeanRequest implements BeanQueryRe loadContext = new DLoadContext(this, secondaryQueries); } + /** + * Rollback the transaction if it was created for this request. + */ + public void rollbackTransIfRequired() { + if (createdTransaction) { + try { + transaction.end(); + } catch (Exception e) { + // Just log this and carry on. A previous exception has been + // thrown and if this rollback throws exception it likely means + // that the connection is broken (and the dataSource and db will cleanup) + log.error("Error trying to rollback a transaction (after a prior exception thrown)", e); + } + } + } + /** * Return the JsonReadOptions taking into account lazy loading and persistence context. */ diff --git a/src/main/java/io/ebeaninternal/server/deploy/BeanDescriptor.java b/src/main/java/io/ebeaninternal/server/deploy/BeanDescriptor.java index 11c559c50..dcb92c73a 100644 --- a/src/main/java/io/ebeaninternal/server/deploy/BeanDescriptor.java +++ b/src/main/java/io/ebeaninternal/server/deploy/BeanDescriptor.java @@ -49,6 +49,7 @@ import io.ebeaninternal.server.core.InternString; import io.ebeaninternal.server.core.PersistRequest; import io.ebeaninternal.server.core.PersistRequestBean; import io.ebeaninternal.server.deploy.id.IdBinder; +import io.ebeaninternal.server.deploy.id.IdBinderSimple; import io.ebeaninternal.server.deploy.id.ImportedId; import io.ebeaninternal.server.deploy.meta.DeployBeanDescriptor; import io.ebeaninternal.server.deploy.meta.DeployBeanPropertyLists; @@ -116,6 +117,8 @@ public class BeanDescriptor implements MetaBeanInfo, BeanType { private final short profileBeanId; + private final boolean multiValueSupported; + public enum EntityType { ORM, EMBEDDED, VIEW, SQL, DOC } @@ -402,6 +405,7 @@ public class BeanDescriptor implements MetaBeanInfo, BeanType { public BeanDescriptor(BeanDescriptorMap owner, DeployBeanDescriptor deploy) { this.owner = owner; + this.multiValueSupported = owner.isMultiValueSupported(); this.serverName = owner.getServerName(); this.entityType = deploy.getEntityType(); this.properties = deploy.getProperties(); @@ -935,9 +939,7 @@ public class BeanDescriptor implements MetaBeanInfo, BeanType { sb.append(inClause); DefaultSqlUpdate delete = new DefaultSqlUpdate(sb.toString()); - for (Object anIdList : idList) { - idBinder.bindId(delete, anIdList); - } + idBinder.addIdInBindValues(delete, idList); return delete; } @@ -1672,6 +1674,20 @@ public class BeanDescriptor implements MetaBeanInfo, BeanType { return idBinder; } + /** + * Return true if this bean type has a simple single Id property. + */ + public boolean isSimpleId() { + return idBinder instanceof IdBinderSimple; + } + + /** + * Return true if this type has a simple Id and the platform supports mutli-value binding. + */ + public boolean isMultiValueIdSupported() { + return multiValueSupported && isSimpleId(); + } + /** * Return the sql for binding an id. This is the columns with table alias that * make up the id. diff --git a/src/main/java/io/ebeaninternal/server/deploy/BeanDescriptorManager.java b/src/main/java/io/ebeaninternal/server/deploy/BeanDescriptorManager.java index 75cecdcef..72d29e20d 100644 --- a/src/main/java/io/ebeaninternal/server/deploy/BeanDescriptorManager.java +++ b/src/main/java/io/ebeaninternal/server/deploy/BeanDescriptorManager.java @@ -45,6 +45,7 @@ import io.ebeaninternal.server.deploy.parse.DeployInherit; import io.ebeaninternal.server.deploy.parse.DeployUtil; import io.ebeaninternal.server.deploy.parse.ReadAnnotations; import io.ebeaninternal.server.deploy.parse.TransientProperties; +import io.ebeaninternal.server.persist.platform.MultiValueBind; import io.ebeaninternal.server.properties.BeanPropertiesReader; import io.ebeaninternal.server.properties.BeanPropertyAccess; import io.ebeaninternal.server.properties.EnhanceBeanPropertyAccess; @@ -131,6 +132,8 @@ public class BeanDescriptorManager implements BeanDescriptorMap { private final DocStoreFactory docStoreFactory; + private final MultiValueBind multiValueBind; + private int entityBeanCount; private final boolean updateChangesOnly; @@ -201,7 +204,8 @@ public class BeanDescriptorManager implements BeanDescriptorMap { this.dataSource = serverConfig.getDataSource(); this.encryptKeyManager = serverConfig.getEncryptKeyManager(); this.databasePlatform = serverConfig.getDatabasePlatform(); - this.idBinderFactory = new IdBinderFactory(databasePlatform.isIdInExpandedForm()); + this.multiValueBind = config.getMultiValueBind(); + this.idBinderFactory = new IdBinderFactory(databasePlatform.isIdInExpandedForm(), multiValueBind); this.eagerFetchLobs = serverConfig.isEagerFetchLobs(); this.asOfViewSuffix = getAsOfViewSuffix(databasePlatform, serverConfig); @@ -252,6 +256,11 @@ public class BeanDescriptorManager implements BeanDescriptorMap { return (historySupport == null) ? serverConfig.getAsOfViewSuffix() : historySupport.getVersionsBetweenSuffix(serverConfig.getAsOfViewSuffix()); } + @Override + public boolean isMultiValueSupported() { + return multiValueBind.isSupported(); + } + @Override public ServerConfig getServerConfig() { return serverConfig; diff --git a/src/main/java/io/ebeaninternal/server/deploy/BeanDescriptorMap.java b/src/main/java/io/ebeaninternal/server/deploy/BeanDescriptorMap.java index d01664bc2..2cb357a2e 100644 --- a/src/main/java/io/ebeaninternal/server/deploy/BeanDescriptorMap.java +++ b/src/main/java/io/ebeaninternal/server/deploy/BeanDescriptorMap.java @@ -36,6 +36,11 @@ public interface BeanDescriptorMap { */ NamingConvention getNamingConvention(); + /** + * Return true if multiple values can be bound as Array or Table Value and hence share the same query plan. + */ + boolean isMultiValueSupported(); + /** * Return the BeanDescriptor for a given class. */ diff --git a/src/main/java/io/ebeaninternal/server/deploy/BeanPropertyAssocMany.java b/src/main/java/io/ebeaninternal/server/deploy/BeanPropertyAssocMany.java index fd7024ac6..52844c8f1 100644 --- a/src/main/java/io/ebeaninternal/server/deploy/BeanPropertyAssocMany.java +++ b/src/main/java/io/ebeaninternal/server/deploy/BeanPropertyAssocMany.java @@ -18,6 +18,7 @@ import io.ebeaninternal.server.deploy.id.ImportedId; import io.ebeaninternal.server.deploy.meta.DeployBeanPropertyAssocMany; import io.ebeaninternal.server.el.ElPropertyChainBuilder; import io.ebeaninternal.server.el.ElPropertyValue; +import io.ebeaninternal.server.persist.MultiValueWrapper; import io.ebeaninternal.server.query.SqlBeanLoad; import io.ebeaninternal.server.text.json.ReadJson; import io.ebeaninternal.server.text.json.SpiJsonWriter; @@ -366,8 +367,11 @@ public class BeanPropertyAssocMany extends BeanPropertyAssoc { // Flatten the bind values if needed (embeddedId) List bindValues = getBindParentIds(parentIds); - - query.where().raw(expr, bindValues.toArray()); + if (descriptor.isSimpleId()) { + query.where().raw(expr, new MultiValueWrapper(bindValues)); + } else { + query.where().raw(expr, bindValues.toArray()); + } } private List findIdsByParentIdList(List parentIdList, Transaction t, ArrayList excludeDetailIds) { @@ -383,10 +387,12 @@ public class BeanPropertyAssocMany extends BeanPropertyAssoc { } EbeanServer server = getBeanDescriptor().getEbeanServer(); - Query q = server.find(getPropertyType()) - .where() - .raw(expr, bindValues.toArray()) - .query(); + Query q = server.find(getPropertyType()); + if (descriptor.isSimpleId()) { + q.where().raw(expr, new MultiValueWrapper(bindValues)); + } else { + q.where().raw(expr, bindValues.toArray()); + } if (excludeDetailIds != null && !excludeDetailIds.isEmpty()) { Expression idIn = q.getExpressionFactory().idIn(excludeDetailIds); @@ -405,10 +411,13 @@ public class BeanPropertyAssocMany extends BeanPropertyAssoc { sb.append(inClause); DefaultSqlUpdate delete = new DefaultSqlUpdate(sb.toString()); - for (Object aParentIdist : parentIdist) { - bindWhereParendId(delete, aParentIdist); + if (exportedProperties.length == 1) { + bindWhereParendId(delete, new MultiValueWrapper(parentIdist)); + } else { + for (Object aParentIdist : parentIdist) { + bindWhereParendId(delete, aParentIdist); + } } - return delete; } @@ -430,9 +439,11 @@ public class BeanPropertyAssocMany extends BeanPropertyAssoc { private String buildInClauseBinding(int size, String bindProto) { + if (descriptor.isSimpleId()) { + return descriptor.getIdBinder().getIdInValueExpr(size); + } StringBuilder sb = new StringBuilder(10 + (size * (bindProto.length() + 1))); sb.append(" in"); - sb.append(" ("); for (int i = 0; i < size; i++) { if (i > 0) { diff --git a/src/main/java/io/ebeaninternal/server/deploy/id/IdBinder.java b/src/main/java/io/ebeaninternal/server/deploy/id/IdBinder.java index 95212c98a..bce0e892d 100644 --- a/src/main/java/io/ebeaninternal/server/deploy/id/IdBinder.java +++ b/src/main/java/io/ebeaninternal/server/deploy/id/IdBinder.java @@ -12,6 +12,7 @@ import java.io.DataInput; import java.io.DataOutput; import java.io.IOException; import java.sql.SQLException; +import java.util.Collection; import java.util.List; /** @@ -134,7 +135,15 @@ public interface IdBinder { */ void bindId(DefaultSqlUpdate sqlUpdate, Object value); - void addIdInBindValue(SpiExpressionRequest request, Object value); + /** + * Binds multiple id value to an update. + */ + void addIdInBindValues(DefaultSqlUpdate sqlUpdate, Collection ids); + + /** + * Binds multiple id value to a request. + */ + void addIdInBindValues(SpiExpressionRequest request, Collection ids); /** * Return the sql for binding the id using an IN clause. diff --git a/src/main/java/io/ebeaninternal/server/deploy/id/IdBinderEmbedded.java b/src/main/java/io/ebeaninternal/server/deploy/id/IdBinderEmbedded.java index caf0fcbe1..7a5b4d276 100644 --- a/src/main/java/io/ebeaninternal/server/deploy/id/IdBinderEmbedded.java +++ b/src/main/java/io/ebeaninternal/server/deploy/id/IdBinderEmbedded.java @@ -1,6 +1,7 @@ package io.ebeaninternal.server.deploy.id; import io.ebean.bean.EntityBean; +import io.ebean.util.SplitName; import io.ebeaninternal.api.SpiExpressionRequest; import io.ebeaninternal.server.core.DefaultSqlUpdate; import io.ebeaninternal.server.deploy.BeanDescriptor; @@ -8,13 +9,13 @@ import io.ebeaninternal.server.deploy.BeanProperty; import io.ebeaninternal.server.deploy.BeanPropertyAssocOne; import io.ebeaninternal.server.deploy.DbReadContext; import io.ebeaninternal.server.deploy.DbSqlContext; -import io.ebean.util.SplitName; import io.ebeaninternal.server.type.DataBind; import java.io.DataInput; import java.io.DataOutput; import java.io.IOException; import java.sql.SQLException; +import java.util.Collection; import java.util.LinkedHashMap; import java.util.List; import java.util.Map; @@ -163,13 +164,6 @@ public final class IdBinderEmbedded implements IdBinder { return props; } - @Override - public void addIdInBindValue(SpiExpressionRequest request, Object value) { - for (BeanProperty prop : props) { - request.addBindValue(prop.getValue((EntityBean) value)); - } - } - @Override public String getIdInValueExprDelete(int size) { if (size <= 0) { @@ -294,6 +288,22 @@ public final class IdBinderEmbedded implements IdBinder { } } + @Override + public void addIdInBindValues(DefaultSqlUpdate sqlUpdate, Collection values) { + for (Object value : values) { + bindId(sqlUpdate, value); + } + } + + @Override + public void addIdInBindValues(SpiExpressionRequest request, Collection values) { + for (Object value : values) { + for (BeanProperty prop : props) { + request.addBindValue(prop.getValue((EntityBean) value)); + } + } + } + @Override public Object readData(DataInput dataInput) throws IOException { diff --git a/src/main/java/io/ebeaninternal/server/deploy/id/IdBinderEmpty.java b/src/main/java/io/ebeaninternal/server/deploy/id/IdBinderEmpty.java index 03ff6ecb0..5a04f0d91 100644 --- a/src/main/java/io/ebeaninternal/server/deploy/id/IdBinderEmpty.java +++ b/src/main/java/io/ebeaninternal/server/deploy/id/IdBinderEmpty.java @@ -12,6 +12,7 @@ import java.io.DataInput; import java.io.DataOutput; import java.io.IOException; import java.sql.SQLException; +import java.util.Collection; import java.util.List; /** @@ -85,11 +86,6 @@ public final class IdBinderEmpty implements IdBinder { return null; } - @Override - public void addIdInBindValue(SpiExpressionRequest request, Object value) { - - } - @Override public String getIdInValueExprDelete(int size) { return getIdInValueExpr(size); @@ -135,6 +131,16 @@ public final class IdBinderEmpty implements IdBinder { } + @Override + public void addIdInBindValues(DefaultSqlUpdate sqlUpdate, Collection ids) { + + } + + @Override + public void addIdInBindValues(SpiExpressionRequest request, Collection ids) { + + } + @Override public void loadIgnore(DbReadContext ctx) { } diff --git a/src/main/java/io/ebeaninternal/server/deploy/id/IdBinderFactory.java b/src/main/java/io/ebeaninternal/server/deploy/id/IdBinderFactory.java index 00793db0d..d5140f589 100644 --- a/src/main/java/io/ebeaninternal/server/deploy/id/IdBinderFactory.java +++ b/src/main/java/io/ebeaninternal/server/deploy/id/IdBinderFactory.java @@ -2,6 +2,7 @@ package io.ebeaninternal.server.deploy.id; import io.ebeaninternal.server.deploy.BeanProperty; import io.ebeaninternal.server.deploy.BeanPropertyAssocOne; +import io.ebeaninternal.server.persist.platform.MultiValueBind; /** * Creates the appropriate IdConvertSet depending on the type of Id property(s). @@ -12,8 +13,11 @@ public class IdBinderFactory { private final boolean idInExpandedForm; - public IdBinderFactory(boolean idInExpandedForm) { + private final MultiValueBind multiValueBind; + + public IdBinderFactory(boolean idInExpandedForm, MultiValueBind multiValueBind) { this.idInExpandedForm = idInExpandedForm; + this.multiValueBind = multiValueBind; } /** @@ -29,7 +33,7 @@ public class IdBinderFactory { if (id.isEmbedded()) { return new IdBinderEmbedded(idInExpandedForm, (BeanPropertyAssocOne) id); } else { - return new IdBinderSimple(id); + return new IdBinderSimple(id, multiValueBind); } } diff --git a/src/main/java/io/ebeaninternal/server/deploy/id/IdBinderSimple.java b/src/main/java/io/ebeaninternal/server/deploy/id/IdBinderSimple.java index c87eee837..ac97ea374 100644 --- a/src/main/java/io/ebeaninternal/server/deploy/id/IdBinderSimple.java +++ b/src/main/java/io/ebeaninternal/server/deploy/id/IdBinderSimple.java @@ -7,6 +7,8 @@ import io.ebeaninternal.server.core.InternString; import io.ebeaninternal.server.deploy.BeanProperty; import io.ebeaninternal.server.deploy.DbReadContext; import io.ebeaninternal.server.deploy.DbSqlContext; +import io.ebeaninternal.server.persist.MultiValueWrapper; +import io.ebeaninternal.server.persist.platform.MultiValueBind; import io.ebeaninternal.server.type.DataBind; import io.ebeaninternal.server.type.ScalarType; @@ -14,6 +16,8 @@ import java.io.DataInput; import java.io.DataOutput; import java.io.IOException; import java.sql.SQLException; +import java.util.ArrayList; +import java.util.Collection; import java.util.List; /** @@ -27,14 +31,18 @@ public final class IdBinderSimple implements IdBinder { private final Class expectedType; + private final MultiValueBind multiValueBind; + @SuppressWarnings("rawtypes") private final ScalarType scalarType; - public IdBinderSimple(BeanProperty idProperty) { + + public IdBinderSimple(BeanProperty idProperty, MultiValueBind multiValueBind) { this.idProperty = idProperty; this.scalarType = idProperty.getScalarType(); this.expectedType = idProperty.getPropertyType(); bindIdSql = InternString.intern(idProperty.getDbColumn() + " = ? "); + this.multiValueBind = multiValueBind; } @Override @@ -133,20 +141,21 @@ public final class IdBinderSimple implements IdBinder { if (size <= 0) { throw new IndexOutOfBoundsException("The size must be at least 1"); } - StringBuilder sb = new StringBuilder(2 * size + 10); - sb.append(" in"); - sb.append(" (?"); - for (int i = 1; i < size; i++) { - sb.append(",?"); - } - sb.append(") "); - return sb.toString(); + return multiValueBind.getInExpression(scalarType, size); } @Override - public void addIdInBindValue(SpiExpressionRequest request, Object value) { - value = convertSetId(value, null); - request.addBindValue(value); + public void addIdInBindValues(DefaultSqlUpdate sqlUpdate, Collection ids) { + sqlUpdate.addParameter(new MultiValueWrapper(ids)); + } + + @Override + public void addIdInBindValues(SpiExpressionRequest request, Collection values) { + List copy = new ArrayList<>(values); + for (int i = 0; i < copy.size(); i++) { + copy.set(i, convertSetId(copy.get(i), null)); + } + request.addBindValue(new MultiValueWrapper(copy)); } @Override diff --git a/src/main/java/io/ebeaninternal/server/expression/DefaultExpressionRequest.java b/src/main/java/io/ebeaninternal/server/expression/DefaultExpressionRequest.java index e15ab261a..e2e7851db 100644 --- a/src/main/java/io/ebeaninternal/server/expression/DefaultExpressionRequest.java +++ b/src/main/java/io/ebeaninternal/server/expression/DefaultExpressionRequest.java @@ -160,4 +160,8 @@ public class DefaultExpressionRequest implements SpiExpressionRequest { return bindValues; } + @Override + public void appendInExpression(Object[] bindValues) { + append(binder.getInExpression(bindValues)); + } } diff --git a/src/main/java/io/ebeaninternal/server/expression/IdInExpression.java b/src/main/java/io/ebeaninternal/server/expression/IdInExpression.java index 436be9d54..aa76daebe 100644 --- a/src/main/java/io/ebeaninternal/server/expression/IdInExpression.java +++ b/src/main/java/io/ebeaninternal/server/expression/IdInExpression.java @@ -1,5 +1,6 @@ package io.ebeaninternal.server.expression; +import io.ebean.event.BeanQueryRequest; import io.ebeaninternal.api.ManyWhereJoins; import io.ebeaninternal.api.SpiExpression; import io.ebeaninternal.api.SpiExpressionRequest; @@ -18,10 +19,17 @@ public class IdInExpression extends NonPrepareExpression { private final Collection idCollection; + private boolean multiValueIdSupported; + public IdInExpression(Collection idCollection) { this.idCollection = idCollection; } + @Override + public void prepareExpression(BeanQueryRequest request) { + multiValueIdSupported = request.isMultiValueIdSupported(); + } + @Override public String nestedPath(BeanDescriptor desc) { return null; @@ -49,10 +57,7 @@ public class IdInExpression extends NonPrepareExpression { DefaultExpressionRequest r = (DefaultExpressionRequest) request; BeanDescriptor descriptor = r.getBeanDescriptor(); IdBinder idBinder = descriptor.getIdBinder(); - - for (Object id : idCollection) { - idBinder.addIdInBindValue(request, id); - } + idBinder.addIdInBindValues(request, idCollection); } /** @@ -92,7 +97,12 @@ public class IdInExpression extends NonPrepareExpression { */ @Override public void queryPlanHash(StringBuilder builder) { - builder.append("IdIn[").append("?").append(idCollection.size()).append("]"); + builder.append("IdIn[?"); + if (!multiValueIdSupported) { + // query plan specific to the number of parameters in the IN clause + builder.append(idCollection.size()); + } + builder.append("]"); } @Override diff --git a/src/main/java/io/ebeaninternal/server/expression/InExpression.java b/src/main/java/io/ebeaninternal/server/expression/InExpression.java index 3409e0ce0..73da10629 100644 --- a/src/main/java/io/ebeaninternal/server/expression/InExpression.java +++ b/src/main/java/io/ebeaninternal/server/expression/InExpression.java @@ -5,11 +5,13 @@ import io.ebean.event.BeanQueryRequest; import io.ebeaninternal.api.SpiExpression; import io.ebeaninternal.api.SpiExpressionRequest; import io.ebeaninternal.server.el.ElPropertyValue; +import io.ebeaninternal.server.persist.MultiValueWrapper; import java.io.IOException; import java.util.ArrayList; import java.util.Arrays; import java.util.Collection; +import java.util.Collections; import java.util.List; class InExpression extends AbstractExpression { @@ -20,6 +22,8 @@ class InExpression extends AbstractExpression { private Object[] bindValues; + private boolean multiValueSupported; + InExpression(String propertyName, Collection sourceValues, boolean not) { super(propertyName); this.sourceValues = sourceValues; @@ -43,6 +47,9 @@ class InExpression extends AbstractExpression { @Override public void prepareExpression(BeanQueryRequest request) { bindValues = values(); + if (bindValues.length > 0) { + multiValueSupported = request.isMultiValueSupported((bindValues[0]).getClass()); + } } @Override @@ -58,19 +65,24 @@ class InExpression extends AbstractExpression { prop = null; } - for (Object bindValue : bindValues) { - if (prop == null) { - request.addBindValue(bindValue); - - } else { + if (prop == null) { + if (bindValues.length > 0) { + // if we have no property, we wrap them in a multi value wrapper. + // later the binder will decide, which bind strategy to use. + request.addBindValue(new MultiValueWrapper(Arrays.asList(bindValues))); + } + } else { + List idList = new ArrayList<>(); + for (Object bindValue : bindValues) { // extract the id values from the bean Object[] ids = prop.getAssocIdValues((EntityBean) bindValue); if (ids != null) { - for (Object id : ids) { - request.addBindValue(id); - } + Collections.addAll(idList, ids); } } + if (!idList.isEmpty()) { + request.addBindValue(new MultiValueWrapper(idList)); + } } } @@ -101,12 +113,7 @@ class InExpression extends AbstractExpression { if (not) { request.append(" not"); } - request.append(" in (?"); - for (int i = 1; i < bindValues.length; i++) { - request.append(", ").append("?"); - } - - request.append(" ) "); + request.appendInExpression(bindValues); } } @@ -121,7 +128,12 @@ class InExpression extends AbstractExpression { builder.append("In["); } builder.append(propName); - builder.append(" ?").append(bindValues.length).append("]"); + builder.append(" ?"); + if (!multiValueSupported) { + // query plan specific to the number of parameters in the IN clause + builder.append(bindValues.length); + } + builder.append("]"); } @Override diff --git a/src/main/java/io/ebeaninternal/server/persist/Binder.java b/src/main/java/io/ebeaninternal/server/persist/Binder.java index 945495483..d031adf47 100644 --- a/src/main/java/io/ebeaninternal/server/persist/Binder.java +++ b/src/main/java/io/ebeaninternal/server/persist/Binder.java @@ -5,6 +5,7 @@ import io.ebeaninternal.api.BindParams; import io.ebeaninternal.server.core.DbExpressionHandler; import io.ebeaninternal.server.core.Message; import io.ebeaninternal.server.core.timezone.DataTimeZone; +import io.ebeaninternal.server.persist.platform.MultiValueBind; import io.ebeaninternal.server.type.DataBind; import io.ebeaninternal.server.type.ScalarType; import io.ebeaninternal.server.type.TypeManager; @@ -19,6 +20,7 @@ import java.sql.PreparedStatement; import java.sql.SQLException; import java.sql.Types; import java.util.ArrayList; +import java.util.Collection; import java.util.List; /** @@ -38,17 +40,20 @@ public class Binder { private final DataTimeZone dataTimeZone; + private final MultiValueBind multiValueBind; + /** * Set the PreparedStatement with which to bind variables to. */ public Binder(TypeManager typeManager, int asOfBindCount, boolean asOfStandardsBased, - DbExpressionHandler dbExpressionHandler, DataTimeZone dataTimeZone) { + DbExpressionHandler dbExpressionHandler, DataTimeZone dataTimeZone, MultiValueBind multiValueBind) { this.typeManager = typeManager; this.asOfBindCount = asOfBindCount; this.asOfStandardsBased = asOfStandardsBased; this.dbExpressionHandler = dbExpressionHandler; this.dataTimeZone = dataTimeZone; + this.multiValueBind = multiValueBind; } /** @@ -168,6 +173,26 @@ public class Binder { } } + /** + * Return true if MultiValue binding is supported for the given type. + */ + public boolean isMultiValueSupported(Class cls) { + try { + ScalarType scalarType = getScalarType(cls); + return multiValueBind.isTypeSupported(scalarType.getJdbcType()); + } catch (PersistenceException e) { + return false; + } + } + + private ScalarType getScalarType(Class clazz) { + ScalarType type = typeManager.getScalarType(clazz); + if (type == null) { + throw new PersistenceException("No ScalarType registered for " + clazz); + } + return type; + } + /** * Bind an Object with unknown data type. */ @@ -178,15 +203,19 @@ public class Binder { bindObject(dataBind, null, Types.OTHER); return null; + } else if (value instanceof MultiValueWrapper) { + MultiValueWrapper wrapper = (MultiValueWrapper) value; + Collection values = wrapper.getValues(); + + ScalarType type = getScalarType(wrapper.getType()); + int dbType = type.getJdbcType(); + // let the multiValueBind decide what to do with the value + multiValueBind.bindMultiValues(dataBind, values, type, one -> bindObject(dataBind, one, dbType)); + return values; + } else { - - ScalarType type = typeManager.getScalarType(value.getClass()); - if (type == null) { - // the type is not registered with the TypeManager. - String msg = "No ScalarType registered for " + value.getClass(); - throw new PersistenceException(msg); - - } else if (!type.isJdbcNative()) { + ScalarType type = getScalarType(value.getClass()); + if (!type.isJdbcNative()) { // convert to a JDBC native type value = type.toJdbcType(value); } @@ -197,6 +226,14 @@ public class Binder { } } + /** + * Return the SQL in clause taking into account Multi-value support. + */ + public String getInExpression(Object[] bindValues) { + ScalarType type = getScalarType(bindValues[0].getClass()); + return multiValueBind.getInExpression(type, bindValues.length); + } + /** * bind a single value. *

diff --git a/src/main/java/io/ebeaninternal/server/persist/MultiValueWrapper.java b/src/main/java/io/ebeaninternal/server/persist/MultiValueWrapper.java new file mode 100644 index 000000000..4eaee90ed --- /dev/null +++ b/src/main/java/io/ebeaninternal/server/persist/MultiValueWrapper.java @@ -0,0 +1,40 @@ +package io.ebeaninternal.server.persist; + +import java.util.Collection; + +/** + * Wraps the multi values that are used for "property in (...)" queries + * @author Roland Praml, FOCONIS AG + */ +public class MultiValueWrapper { + private final Collection values; + private Class type; + + public MultiValueWrapper(Collection values) { + this.values = values; + this.type = values.iterator().next().getClass(); + } + + public Collection getValues() { + return values; + } + + public Class getType() { + return type; + } + + @Override + public String toString() { + StringBuilder sb = new StringBuilder("Array[" + values.size() + "]={"); + for (Object value : values) { + sb.append(value).append(','); + if (sb.length() > 50) { + sb.append("...}"); + return sb.toString(); + } + } + sb.setLength(sb.length() - 1); + sb.append('}'); + return sb.toString(); + } +} diff --git a/src/main/java/io/ebeaninternal/server/persist/platform/AbstractMultiValueBind.java b/src/main/java/io/ebeaninternal/server/persist/platform/AbstractMultiValueBind.java new file mode 100644 index 000000000..0079ca957 --- /dev/null +++ b/src/main/java/io/ebeaninternal/server/persist/platform/AbstractMultiValueBind.java @@ -0,0 +1,93 @@ +package io.ebeaninternal.server.persist.platform; + +import io.ebean.config.dbplatform.ExtraDbTypes; +import io.ebeaninternal.server.type.DataBind; +import io.ebeaninternal.server.type.ScalarType; + +import java.sql.SQLException; +import java.util.Collection; + +import static java.sql.Types.BIGINT; +import static java.sql.Types.BIT; +import static java.sql.Types.BOOLEAN; +import static java.sql.Types.CHAR; +import static java.sql.Types.DATE; +import static java.sql.Types.DECIMAL; +import static java.sql.Types.DOUBLE; +import static java.sql.Types.FLOAT; +import static java.sql.Types.INTEGER; +import static java.sql.Types.NCHAR; +import static java.sql.Types.NUMERIC; +import static java.sql.Types.NVARCHAR; +import static java.sql.Types.REAL; +import static java.sql.Types.SMALLINT; +import static java.sql.Types.TIMESTAMP; +import static java.sql.Types.TIMESTAMP_WITH_TIMEZONE; +import static java.sql.Types.TIME_WITH_TIMEZONE; +import static java.sql.Types.TINYINT; +import static java.sql.Types.VARCHAR; + +/** + * Base MultiValueBind for platform specific support. + */ +abstract class AbstractMultiValueBind extends MultiValueBind { + + @Override + public boolean isSupported() { + return true; + } + + @Override + public boolean isTypeSupported(int jdbcType) { + return getArrayType(jdbcType) != null; + } + + @Override + public void bindMultiValues(DataBind dataBind, Collection values, ScalarType type, BindOne bindOne) throws SQLException { + String arrayType = getArrayType(type.getJdbcType()); + if (arrayType == null) { + super.bindMultiValues(dataBind, values, type, bindOne); + } else { + dataBind.setArray(arrayType, toArray(values, type)); + } + } + + protected String getArrayType(int dbType) { + switch(dbType) { + case TINYINT: + case SMALLINT: + case INTEGER: + case BIGINT: + case DECIMAL: // TODO: we have no info about precision here + case NUMERIC: + return "bigint"; + case REAL: + case FLOAT: + case DOUBLE: + return "float"; + case BIT: + case BOOLEAN: + return "bit"; + case DATE: + return "date"; + case TIMESTAMP: + case TIME_WITH_TIMEZONE: + case TIMESTAMP_WITH_TIMEZONE: + return "timestamp"; + //case LONGVARCHAR: + //case CLOB: + case CHAR: + case VARCHAR: + //case LONGNVARCHAR: + //case NCLOB: + case NCHAR: + case NVARCHAR: + return "varchar"; + case ExtraDbTypes.UUID: // Db Native UUID + return "varchar"; + + default: + return null; + } + } +} diff --git a/src/main/java/io/ebeaninternal/server/persist/platform/H2MultiValueBind.java b/src/main/java/io/ebeaninternal/server/persist/platform/H2MultiValueBind.java new file mode 100644 index 000000000..30b56a194 --- /dev/null +++ b/src/main/java/io/ebeaninternal/server/persist/platform/H2MultiValueBind.java @@ -0,0 +1,23 @@ +package io.ebeaninternal.server.persist.platform; + +import io.ebeaninternal.server.type.ScalarType; + +/** + * Multi value binder that uses SqlServers Table-value parameters + * @author Roland Praml, FOCONIS AG + * + */ +public class H2MultiValueBind extends AbstractMultiValueBind { + + @Override + public String getInExpression(ScalarType type, int size) { + String arrayType = getArrayType(type.getJdbcType()); + if (arrayType == null) { + return super.getInExpression(type, size); + } else { + StringBuilder sb = new StringBuilder(50); + sb.append(" in (select * from table(x ").append(arrayType).append(" = ?)) "); + return sb.toString(); + } + } +} diff --git a/src/main/java/io/ebeaninternal/server/persist/platform/MultiValueBind.java b/src/main/java/io/ebeaninternal/server/persist/platform/MultiValueBind.java new file mode 100644 index 000000000..342651273 --- /dev/null +++ b/src/main/java/io/ebeaninternal/server/persist/platform/MultiValueBind.java @@ -0,0 +1,67 @@ +package io.ebeaninternal.server.persist.platform; + +import io.ebeaninternal.server.type.DataBind; +import io.ebeaninternal.server.type.ScalarType; + +import java.sql.SQLException; +import java.util.Collection; + +/** + * Default implementation for multi value help. + */ +public class MultiValueBind { + + + @FunctionalInterface + public interface BindOne { + void bind(Object value) throws SQLException; + } + + protected Object[] toArray(Collection values, ScalarType type) { + Object[] array = new Object[values.size()]; + int i = 0; + for (Object value : values) { + array[i++] = type.toJdbcType(value); + } + return array; + } + + /** + * Defaults to not supported and using a bind value per element. + */ + public boolean isSupported() { + return false; + } + + /** + * Defaults to not supported and using a bind value per element. + */ + public boolean isTypeSupported(int jdbcType) { + return false; + } + + /** + * Default for multi values. They are appended one by one. + */ + public void bindMultiValues(DataBind dataBind, Collection values, ScalarType type, BindOne bindOne) throws SQLException { + for (Object value : values) { + if (!type.isJdbcNative()) { + value = type.toJdbcType(value); + } + bindOne.bind(value); + } + } + + /** + * Appends the 'in' expression to the request. Must add leading & trailing space! + */ + public String getInExpression(ScalarType type, int size) { + StringBuilder sb = new StringBuilder(); + sb.append(" in (?"); + for (int i = 1; i < size; i++) { + sb.append(", ").append("?"); + } + sb.append(" ) "); + return sb.toString(); + } +} diff --git a/src/main/java/io/ebeaninternal/server/persist/platform/PostgresMultiValueBind.java b/src/main/java/io/ebeaninternal/server/persist/platform/PostgresMultiValueBind.java new file mode 100644 index 000000000..418f92587 --- /dev/null +++ b/src/main/java/io/ebeaninternal/server/persist/platform/PostgresMultiValueBind.java @@ -0,0 +1,34 @@ +package io.ebeaninternal.server.persist.platform; + +import io.ebean.config.dbplatform.ExtraDbTypes; +import io.ebeaninternal.server.type.ScalarType; + +/** + * Multi value binder that uses Postgres Array and unnest. + * + * @author Roland Praml, FOCONIS AG + */ +public class PostgresMultiValueBind extends AbstractMultiValueBind { + + @Override + public String getInExpression(ScalarType type, int size) { + int dbType = type.getJdbcType(); + if (dbType == ExtraDbTypes.UUID) { + return " in (select(unnest(?))::uuid) "; + } + String arrayType = getArrayType(dbType); + if (arrayType == null) { + return super.getInExpression(type, size); + } else { + return " in (select(unnest(?))) "; + } + } + + @Override + protected String getArrayType(int dbType) { + if (dbType == ExtraDbTypes.UUID) { + return "uuid"; + } + return super.getArrayType(dbType); + } +} diff --git a/src/main/java/io/ebeaninternal/server/query/CQueryEngine.java b/src/main/java/io/ebeaninternal/server/query/CQueryEngine.java index 51f53eb55..de0b5cc1c 100644 --- a/src/main/java/io/ebeaninternal/server/query/CQueryEngine.java +++ b/src/main/java/io/ebeaninternal/server/query/CQueryEngine.java @@ -331,7 +331,7 @@ public class CQueryEngine { * deemed to be a be a paging query - check that the order by contains the id * property to ensure unique row ordering for predicable paging but only in * case, this is not a distinct query - * + * * @param request */ private void prepareForPaging(OrmQueryRequest request) { diff --git a/src/main/java/io/ebeaninternal/server/query/DefaultOrmQueryEngine.java b/src/main/java/io/ebeaninternal/server/query/DefaultOrmQueryEngine.java index 01d81a454..a327f3882 100644 --- a/src/main/java/io/ebeaninternal/server/query/DefaultOrmQueryEngine.java +++ b/src/main/java/io/ebeaninternal/server/query/DefaultOrmQueryEngine.java @@ -10,6 +10,7 @@ import io.ebeaninternal.api.SpiTransaction; import io.ebeaninternal.server.core.OrmQueryEngine; import io.ebeaninternal.server.core.OrmQueryRequest; import io.ebeaninternal.server.deploy.BeanDescriptor; +import io.ebeaninternal.server.persist.Binder; import javax.persistence.PersistenceException; import java.sql.SQLException; @@ -28,11 +29,14 @@ public class DefaultOrmQueryEngine implements OrmQueryEngine { */ private final CQueryEngine queryEngine; + private final Binder binder; + /** * Create the Finder. */ - public DefaultOrmQueryEngine(CQueryEngine queryEngine) { + public DefaultOrmQueryEngine(CQueryEngine queryEngine, Binder binder) { this.queryEngine = queryEngine; + this.binder = binder; } @Override @@ -40,6 +44,11 @@ public class DefaultOrmQueryEngine implements OrmQueryEngine { return queryEngine.translate(request, bindLog, sql, e); } + @Override + public boolean isMultiValueSupported(Class cls) { + return binder.isMultiValueSupported(cls); + } + /** * Flushes the jdbc batch by default unless explicitly turned off on the transaction. */ diff --git a/src/main/java/io/ebeaninternal/server/querydefn/OrmQueryPlanKey.java b/src/main/java/io/ebeaninternal/server/querydefn/OrmQueryPlanKey.java index 88e060e85..314996b56 100644 --- a/src/main/java/io/ebeaninternal/server/querydefn/OrmQueryPlanKey.java +++ b/src/main/java/io/ebeaninternal/server/querydefn/OrmQueryPlanKey.java @@ -2,12 +2,12 @@ package io.ebeaninternal.server.querydefn; import io.ebean.OrderBy; import io.ebean.Query; -import io.ebeaninternal.server.rawsql.SpiRawSql; import io.ebeaninternal.api.BindParams; import io.ebeaninternal.api.CQueryPlanKey; import io.ebeaninternal.api.SpiExpression; import io.ebeaninternal.api.SpiQuery; import io.ebeaninternal.server.deploy.TableJoin; +import io.ebeaninternal.server.rawsql.SpiRawSql; /** * Query plan key for ORM queries. @@ -109,6 +109,10 @@ class OrmQueryPlanKey implements CQueryPlanKey { return planHash; } + public String toString() { + return description + " maxRows:" + maxRows + " firstRow:" + firstRow + " rawSqlKey:" + rawSqlKey + " planHash:" + planHash; + } + @Override public boolean equals(Object o) { if (this == o) return true; diff --git a/src/test/java/io/ebean/BaseTestCase.java b/src/test/java/io/ebean/BaseTestCase.java index d05d97ce3..66b163495 100644 --- a/src/test/java/io/ebean/BaseTestCase.java +++ b/src/test/java/io/ebean/BaseTestCase.java @@ -3,12 +3,15 @@ package io.ebean; import io.ebean.annotation.Platform; import io.ebean.util.StringHelper; import io.ebeaninternal.api.SpiEbeanServer; +import io.ebeaninternal.api.SpiQuery; +import io.ebeaninternal.server.core.HelpCreateQueryRequest; +import io.ebeaninternal.server.core.OrmQueryRequest; import io.ebeaninternal.server.deploy.BeanDescriptor; -import org.tests.model.basic.Country; import org.avaje.agentloader.AgentLoader; import org.junit.runner.RunWith; import org.slf4j.Logger; import org.slf4j.LoggerFactory; +import org.tests.model.basic.Country; import java.sql.Types; @@ -81,7 +84,7 @@ public abstract class BaseTestCase { public boolean isDb2() { return Platform.DB2 == platform(); } - + public boolean isPostgres() { return Platform.POSTGRES == platform(); } @@ -127,4 +130,8 @@ public abstract class BaseTestCase { .setLoadBeanCache(true) .findList(); } + + protected OrmQueryRequest createQueryRequest(SpiQuery.Type type, Query query, Transaction t) { + return HelpCreateQueryRequest.create(server(), type, query, t); + } } diff --git a/src/test/java/io/ebean/EbeanServer_deleteAllByIdTest.java b/src/test/java/io/ebean/EbeanServer_deleteAllByIdTest.java index 1de9a7d55..5eb6a4c2f 100644 --- a/src/test/java/io/ebean/EbeanServer_deleteAllByIdTest.java +++ b/src/test/java/io/ebean/EbeanServer_deleteAllByIdTest.java @@ -1,8 +1,8 @@ package io.ebean; -import org.tests.model.basic.EBasicVer; import org.ebeantest.LoggedSqlCollector; import org.junit.Test; +import org.tests.model.basic.EBasicVer; import java.util.ArrayList; import java.util.List; @@ -29,7 +29,11 @@ public class EbeanServer_deleteAllByIdTest extends BaseTestCase { List loggedSql = LoggedSqlCollector.stop(); assertThat(loggedSql).hasSize(1); - assertThat(loggedSql.get(0)).contains("delete from e_basicver where id in (?,?,?)"); + if (isH2()) { + assertThat(loggedSql.get(0)).contains("delete from e_basicver where id in (select * from table(x bigint = ?))"); + } else { + assertThat(loggedSql.get(0)).contains("delete from e_basicver where id in "); + } } @Test @@ -55,7 +59,9 @@ public class EbeanServer_deleteAllByIdTest extends BaseTestCase { } List loggedSql = LoggedSqlCollector.stop(); assertThat(loggedSql).hasSize(1); - assertThat(loggedSql.get(0)).contains("delete from e_basicver where id in (?,?,?)"); + if (isH2()) { + assertThat(loggedSql.get(0)).contains("delete from e_basicver where id in (select * from table(x bigint = ?))"); + } } @Test @@ -75,7 +81,9 @@ public class EbeanServer_deleteAllByIdTest extends BaseTestCase { List loggedSql = LoggedSqlCollector.stop(); assertThat(loggedSql).hasSize(1); - assertThat(loggedSql.get(0)).contains("delete from e_basicver where id in (?,?,?)"); + if (isH2()) { + assertThat(loggedSql.get(0)).contains("delete from e_basicver where id in (select * from table(x bigint = ?))"); + } } @@ -102,7 +110,9 @@ public class EbeanServer_deleteAllByIdTest extends BaseTestCase { } List loggedSql = LoggedSqlCollector.stop(); assertThat(loggedSql).hasSize(1); - assertThat(loggedSql.get(0)).contains("delete from e_basicver where id in (?,?,?)"); + if (isH2()) { + assertThat(loggedSql.get(0)).contains("delete from e_basicver where id in (select * from table(x bigint = ?))"); + } } private List beans(int count) { diff --git a/src/test/java/io/ebeaninternal/server/core/HelpCreateQueryRequest.java b/src/test/java/io/ebeaninternal/server/core/HelpCreateQueryRequest.java new file mode 100644 index 000000000..805ca2982 --- /dev/null +++ b/src/test/java/io/ebeaninternal/server/core/HelpCreateQueryRequest.java @@ -0,0 +1,15 @@ +package io.ebeaninternal.server.core; + +import io.ebean.EbeanServer; +import io.ebean.Query; +import io.ebean.Transaction; +import io.ebeaninternal.api.SpiQuery; + +public class HelpCreateQueryRequest { + + public static OrmQueryRequest create(EbeanServer server, SpiQuery.Type type, Query query, Transaction t) { + + DefaultServer defaultServer = (DefaultServer) server; + return (OrmQueryRequest)defaultServer.createQueryRequest(type, query, t); + } +} diff --git a/src/test/java/io/ebeaninternal/server/expression/IdInExpressionTest.java b/src/test/java/io/ebeaninternal/server/expression/IdInExpressionTest.java index 253fd0851..44aa34dff 100644 --- a/src/test/java/io/ebeaninternal/server/expression/IdInExpressionTest.java +++ b/src/test/java/io/ebeaninternal/server/expression/IdInExpressionTest.java @@ -28,7 +28,7 @@ public class IdInExpressionTest extends BaseExpressionTest { } @Test - public void isSameByPlan_when_diffBindCount() { + public void isSameByPlan_when_diffBindCount_notPrepared() { different(exp(10), exp(10, 20)); } diff --git a/src/test/java/io/ebeaninternal/server/expression/InExpressionTest.java b/src/test/java/io/ebeaninternal/server/expression/InExpressionTest.java index b2a4c17b8..e5fc5cab2 100644 --- a/src/test/java/io/ebeaninternal/server/expression/InExpressionTest.java +++ b/src/test/java/io/ebeaninternal/server/expression/InExpressionTest.java @@ -1,6 +1,11 @@ package io.ebeaninternal.server.expression; +import io.ebean.EbeanServer; +import io.ebean.Query; +import io.ebean.Transaction; +import io.ebean.event.BeanQueryRequest; import org.junit.Test; +import org.tests.model.basic.Customer; import java.util.ArrayList; import java.util.Arrays; @@ -10,6 +15,19 @@ import static org.assertj.core.api.StrictAssertions.assertThat; public class InExpressionTest extends BaseExpressionTest { + /** + * Request with Multi-Value support. + */ + private TDQueryRequest multi() { + return MULTI_VALUE; + } + + /** + * Request with NO Multi-Value support. + */ + private TDQueryRequest noMulti() { + return NO_MULTI_VALUE; + } @Test public void queryPlanHash_given_diffPropertyName_should_differentPlanHash() throws Exception { @@ -19,8 +37,8 @@ public class InExpressionTest extends BaseExpressionTest { InExpression ex1 = new InExpression("foo", values, false); InExpression ex2 = new InExpression("bar", values, false); - ex1.prepareExpression(null); - ex2.prepareExpression(null); + ex1.prepareExpression(multi()); + ex2.prepareExpression(multi()); different(ex1, ex2); } @@ -34,12 +52,25 @@ public class InExpressionTest extends BaseExpressionTest { InExpression ex1 = new InExpression("foo", values1, false); InExpression ex2 = new InExpression("foo", values2, false); - ex1.prepareExpression(null); - ex2.prepareExpression(null); - + ex1.prepareExpression(noMulti()); + ex2.prepareExpression(noMulti()); different(ex1, ex2); } + @Test + public void queryPlanHash_given_diffBindCount_withMultiSupport_samePlanHash() throws Exception { + + List values1 = values(42, 92); + List values2 = values(42, 92, 82); + + InExpression ex1 = new InExpression("foo", values1, false); + InExpression ex2 = new InExpression("foo", values2, false); + + ex1.prepareExpression(multi()); + ex2.prepareExpression(multi()); + same(ex1, ex2); + } + @Test public void queryPlanHash_given_diffNotFlag_should_differentPlanHash() throws Exception { @@ -48,8 +79,8 @@ public class InExpressionTest extends BaseExpressionTest { InExpression ex1 = new InExpression("foo", values, true); InExpression ex2 = new InExpression("foo", values, false); - ex1.prepareExpression(null); - ex2.prepareExpression(null); + ex1.prepareExpression(multi()); + ex2.prepareExpression(multi()); different(ex1, ex2); } @@ -62,8 +93,8 @@ public class InExpressionTest extends BaseExpressionTest { InExpression ex1 = new InExpression("foo", values, true); InExpression ex2 = new InExpression("foo", values, true); - ex1.prepareExpression(null); - ex2.prepareExpression(null); + ex1.prepareExpression(multi()); + ex2.prepareExpression(multi()); same(ex1, ex2); } @@ -78,7 +109,13 @@ public class InExpressionTest extends BaseExpressionTest { private InExpression exp(String propName, boolean not, Object... values) { InExpression ex = new InExpression(propName, Arrays.asList(values), not); - ex.prepareExpression(null); + ex.prepareExpression(multi()); + return ex; + } + + private InExpression expNoMulti(String propName, boolean not, Object... values) { + InExpression ex = new InExpression(propName, Arrays.asList(values), not); + ex.prepareExpression(noMulti()); return ex; } @@ -103,13 +140,15 @@ public class InExpressionTest extends BaseExpressionTest { @Test public void isSameByPlan_when_diffBind_same() { - different(exp("a", false, 10), exp("a", false, 10, 20)); + same(exp("a", false, 10), exp("a", false, 10, 20)); + different(expNoMulti("a", false, 10), expNoMulti("a", false, 10, 20)); } @Test public void isSameByPlan_when_diffBindCount() { - different(exp("a", false, 10), exp("a", false, 10, 20)); + same(exp("a", false, 10), exp("a", false, 10, 20)); + different(expNoMulti("a", false, 10), expNoMulti("a", false, 10, 20)); } @Test @@ -142,4 +181,42 @@ public class InExpressionTest extends BaseExpressionTest { assertThat(exp("a", false, 10, "ABC").isSameByBind(exp("a", false, 10, "ABC", 30))).isFalse(); } + + private static final TDQueryRequest MULTI_VALUE= new TDQueryRequest<>(true); + private static final TDQueryRequest NO_MULTI_VALUE = new TDQueryRequest<>(false); + + static class TDQueryRequest implements BeanQueryRequest { + + final boolean supported; + + TDQueryRequest(boolean supported) { + this.supported = supported; + } + + @Override + public EbeanServer getEbeanServer() { + return null; + } + + @Override + public Transaction getTransaction() { + return null; + } + + @Override + public Query getQuery() { + return null; + } + + @Override + public boolean isMultiValueIdSupported() { + return supported; + } + + @Override + public boolean isMultiValueSupported(Class valueType) { + return supported; + } + } + } diff --git a/src/test/java/io/ebeaninternal/server/expression/TDSpiExpressionRequest.java b/src/test/java/io/ebeaninternal/server/expression/TDSpiExpressionRequest.java index 55641a1a8..da204459a 100644 --- a/src/test/java/io/ebeaninternal/server/expression/TDSpiExpressionRequest.java +++ b/src/test/java/io/ebeaninternal/server/expression/TDSpiExpressionRequest.java @@ -78,9 +78,14 @@ public class TDSpiExpressionRequest implements SpiExpressionRequest { public void appendLike() { } - + @Override public String escapeLikeString(String value) { return value; } + + @Override + public void appendInExpression(Object[] bindValues) { + + } } diff --git a/src/test/java/io/ebeaninternal/server/grammer/EqlParserTest.java b/src/test/java/io/ebeaninternal/server/grammer/EqlParserTest.java index 03f043a8c..ea259390b 100644 --- a/src/test/java/io/ebeaninternal/server/grammer/EqlParserTest.java +++ b/src/test/java/io/ebeaninternal/server/grammer/EqlParserTest.java @@ -167,7 +167,7 @@ public class EqlParserTest extends BaseTestCase { Query query = parse("where name in ('Rob','Jim')"); query.findList(); - assertThat(query.getGeneratedSql()).contains("where t0.name in (?, ? )"); + assertThat(query.getGeneratedSql()).contains("where t0.name in "); } @Test @@ -178,7 +178,12 @@ public class EqlParserTest extends BaseTestCase { query.setParameter("two", "Bar"); query.findList(); - assertThat(query.getGeneratedSql()).contains("where t0.name in (?, ? )"); + + if (isH2()) { + assertThat(query.getGeneratedSql()).contains("where t0.name in (select * from table(x varchar = ?)"); + } else { + assertThat(query.getGeneratedSql()).contains("where t0.name in "); + } } @Test @@ -189,7 +194,11 @@ public class EqlParserTest extends BaseTestCase { query.setParameter("two", "Bar"); query.findList(); - assertThat(query.getGeneratedSql()).contains("where t0.name in (?, ? )"); + if (isH2()) { + assertThat(query.getGeneratedSql()).contains("where t0.name in (select * from table(x varchar = ?)"); + } else { + assertThat(query.getGeneratedSql()).contains("where t0.name in "); + } } @Test @@ -200,7 +209,11 @@ public class EqlParserTest extends BaseTestCase { query.setParameter("two", "Bar"); query.findList(); - assertThat(query.getGeneratedSql()).contains("where t0.name in (?, ? )"); + if (isH2()) { + assertThat(query.getGeneratedSql()).contains("where t0.name in (select * from table(x varchar = ?)"); + } else { + assertThat(query.getGeneratedSql()).contains("where t0.name in "); + } } @Test @@ -210,7 +223,12 @@ public class EqlParserTest extends BaseTestCase { query.setParameter("names", Arrays.asList("Baz", "Maz", "Jim")); query.findList(); - assertThat(query.getGeneratedSql()).contains("where t0.name in (?, ?, ? )"); + if (isH2()) { + assertThat(query.getGeneratedSql()).contains("where t0.name in (select * from table(x varchar = ?)"); + + } else { + assertThat(query.getGeneratedSql()).contains("where t0.name in "); + } } @Test diff --git a/src/test/java/io/ebeaninternal/server/querydefn/DefaultOrmQueryTest.java b/src/test/java/io/ebeaninternal/server/querydefn/DefaultOrmQueryTest.java index 427539c80..a92ccf92a 100644 --- a/src/test/java/io/ebeaninternal/server/querydefn/DefaultOrmQueryTest.java +++ b/src/test/java/io/ebeaninternal/server/querydefn/DefaultOrmQueryTest.java @@ -3,9 +3,11 @@ package io.ebeaninternal.server.querydefn; import io.ebean.BaseTestCase; import io.ebean.Ebean; +import io.ebeaninternal.api.SpiQuery; +import io.ebeaninternal.server.core.OrmQueryRequest; +import org.junit.Test; import org.tests.model.basic.Customer; import org.tests.model.basic.Order; -import org.junit.Test; import static org.assertj.core.api.Assertions.assertThat; @@ -91,7 +93,11 @@ public class DefaultOrmQueryTest extends BaseTestCase { } private void prepare(DefaultOrmQuery q1, DefaultOrmQuery q2) { - q1.prepare(null); - q2.prepare(null); + + OrmQueryRequest r1 = createQueryRequest(SpiQuery.Type.LIST, q1, null); + q1.prepare(r1); + + OrmQueryRequest r2 = createQueryRequest(SpiQuery.Type.LIST, q2, null); + q2.prepare(r2); } } diff --git a/src/test/java/org/tests/batchload/TestBatchLazyWithCacheHits.java b/src/test/java/org/tests/batchload/TestBatchLazyWithCacheHits.java index 6246e4212..90ba66020 100644 --- a/src/test/java/org/tests/batchload/TestBatchLazyWithCacheHits.java +++ b/src/test/java/org/tests/batchload/TestBatchLazyWithCacheHits.java @@ -79,7 +79,7 @@ public class TestBatchLazyWithCacheHits extends BaseTestCase { // batch lazy loading into cache assertThat(sql).hasSize(2); assertThat(sql.get(0)).contains("from uuone t0 where t0.name like "); - assertThat(sql.get(1)).contains("from uuone t0 where t0.id in (?,"); + assertThat(sql.get(1)).contains("from uuone t0 where t0.id in "); statistics = beanCache.getStatistics(true); assertThat(statistics.getSize()).isGreaterThan(3); diff --git a/src/test/java/org/tests/batchload/TestQueryJoinToAssocOne.java b/src/test/java/org/tests/batchload/TestQueryJoinToAssocOne.java index d01d2ef78..541a6b88a 100644 --- a/src/test/java/org/tests/batchload/TestQueryJoinToAssocOne.java +++ b/src/test/java/org/tests/batchload/TestQueryJoinToAssocOne.java @@ -36,7 +36,7 @@ public class TestQueryJoinToAssocOne extends BaseTestCase { String secondaryQuery = trimSql(loggedSql.get(1), 1); assertThat(secondaryQuery).contains("select t0.order_id, t0.id,"); assertThat(secondaryQuery).contains(" from o_order_detail t0 left join o_product t1"); - assertThat(secondaryQuery).contains(" (t0.order_id) in (?"); + assertThat(secondaryQuery).contains(" (t0.order_id) in "); assertThat(secondaryQuery).contains(" order by t0.order_id, t0.id"); } @@ -63,7 +63,7 @@ public class TestQueryJoinToAssocOne extends BaseTestCase { String secondaryQuery = trimSql(loggedSql.get(1), 1); assertThat(secondaryQuery).contains("select t0.order_id, t0.id,"); assertThat(secondaryQuery).contains(" from o_order_detail t0 left join o_product t1"); - assertThat(secondaryQuery).contains(" (t0.order_id) in (?"); + assertThat(secondaryQuery).contains(" (t0.order_id) in "); assertThat(secondaryQuery).contains(" order by t0.order_id, t0.id"); } @@ -103,7 +103,7 @@ public class TestQueryJoinToAssocOne extends BaseTestCase { String secondaryQuery = trimSql(loggedSql.get(1), 1); assertThat(secondaryQuery).contains("select t0.order_id, t0.id,"); assertThat(secondaryQuery).contains(" from o_order_detail t0 left join o_product t1"); - assertThat(secondaryQuery).contains(" (t0.order_id) in (?"); + assertThat(secondaryQuery).contains(" (t0.order_id) in "); assertThat(secondaryQuery).contains(" order by t0.order_id, t0.id"); } diff --git a/src/test/java/org/tests/batchload/TestSecondaryQueries.java b/src/test/java/org/tests/batchload/TestSecondaryQueries.java index 99881043c..2804fd13f 100644 --- a/src/test/java/org/tests/batchload/TestSecondaryQueries.java +++ b/src/test/java/org/tests/batchload/TestSecondaryQueries.java @@ -123,7 +123,7 @@ public class TestSecondaryQueries extends BaseTestCase { assertThat(generatedSql).contains("from o_customer t0 where t0.id = ?"); assertEquals(2, sql.size()); - assertThat(sql.get(1)).contains("from contact t0 where (t0.customer_id) in (?)"); + assertThat(sql.get(1)).contains("from contact t0 where (t0.customer_id) in "); } @@ -179,7 +179,7 @@ public class TestSecondaryQueries extends BaseTestCase { // from o_order_detail t0 // where (t0.order_id) in (?,?,?,?,?) ; --bind(1,4,1,1,1) - assertThat(ordSecondarySql).contains(" from o_order_detail t0 where t0.id > 0 and (t0.order_id) in (?"); + assertThat(ordSecondarySql).contains(" from o_order_detail t0 where t0.id > 0 and (t0.order_id) in "); } } diff --git a/src/test/java/org/tests/model/onetoone/album/DeleteById_SoftDelete_Tests.java b/src/test/java/org/tests/model/onetoone/album/DeleteById_SoftDelete_Tests.java index 97c53d326..9ee6b7ff6 100644 --- a/src/test/java/org/tests/model/onetoone/album/DeleteById_SoftDelete_Tests.java +++ b/src/test/java/org/tests/model/onetoone/album/DeleteById_SoftDelete_Tests.java @@ -118,9 +118,9 @@ public class DeleteById_SoftDelete_Tests extends BaseTestCase { List sql = LoggedSqlCollector.stop(); assertThat(sql).hasSize(1); if (isPlatformBooleanNative()) { - assertThat(sql.get(0)).contains("update cover set deleted=true where id in (?,?)"); + assertThat(sql.get(0)).contains("update cover set deleted=true where id in "); } else { - assertThat(sql.get(0)).contains("update cover set deleted=1 where id in (?,?)"); + assertThat(sql.get(0)).contains("update cover set deleted=1 where id in "); } } @@ -144,9 +144,9 @@ public class DeleteById_SoftDelete_Tests extends BaseTestCase { List sql = LoggedSqlCollector.stop(); assertThat(sql).hasSize(1); if (isPlatformBooleanNative()) { - assertThat(sql.get(0)).contains("update cover set deleted=true where id in (?,?)"); + assertThat(sql.get(0)).contains("update cover set deleted=true where id in "); } else { - assertThat(sql.get(0)).contains("update cover set deleted=1 where id in (?,?)"); + assertThat(sql.get(0)).contains("update cover set deleted=1 where id in "); } } @@ -162,7 +162,7 @@ public class DeleteById_SoftDelete_Tests extends BaseTestCase { List sql = LoggedSqlCollector.stop(); assertThat(sql).hasSize(1); - assertThat(sql.get(0)).contains("delete from cover where id in (?,?)"); + assertThat(sql.get(0)).contains("delete from cover where id in "); } @Test @@ -178,7 +178,7 @@ public class DeleteById_SoftDelete_Tests extends BaseTestCase { List sql = LoggedSqlCollector.stop(); assertThat(sql).hasSize(1); - assertThat(sql.get(0)).contains("delete from cover where id in (?,?)"); + assertThat(sql.get(0)).contains("delete from cover where id in "); } @@ -201,7 +201,7 @@ public class DeleteById_SoftDelete_Tests extends BaseTestCase { List sql = LoggedSqlCollector.stop(); assertThat(sql).hasSize(1); - assertThat(sql.get(0)).contains("delete from cover where id in (?,?)"); + assertThat(sql.get(0)).contains("delete from cover where id in "); } @Test diff --git a/src/test/java/org/tests/query/TestQueryInAssocOne.java b/src/test/java/org/tests/query/TestQueryInAssocOne.java index d95030ac2..7218f2570 100644 --- a/src/test/java/org/tests/query/TestQueryInAssocOne.java +++ b/src/test/java/org/tests/query/TestQueryInAssocOne.java @@ -27,7 +27,7 @@ public class TestQueryInAssocOne extends BaseTestCase { String sql = query.getGeneratedSql(); assertThat(sql).contains("join o_customer t1 on t1.id = t0.kcustomer_id"); - assertThat(sql).contains("t0.kcustomer_id in (?"); + assertThat(sql).contains("t0.kcustomer_id in "); } @@ -44,7 +44,7 @@ public class TestQueryInAssocOne extends BaseTestCase { String sql = query.getGeneratedSql(); assertThat(sql).contains("join o_customer t1 on t1.id = t0.kcustomer_id"); - assertThat(sql).contains("t0.kcustomer_id in (?"); + assertThat(sql).contains("t0.kcustomer_id in "); } @@ -61,6 +61,6 @@ public class TestQueryInAssocOne extends BaseTestCase { String sql = query.getGeneratedSql(); assertThat(sql).contains("join o_customer t1 on t1.id = t0.kcustomer_id"); - assertThat(sql).contains("t0.kcustomer_id not in (?"); + assertThat(sql).contains("t0.kcustomer_id not in "); } }