diff --git a/src/main/java/com/avaje/ebean/EbeanServer.java b/src/main/java/com/avaje/ebean/EbeanServer.java index ab4df20f9..3f19306c8 100644 --- a/src/main/java/com/avaje/ebean/EbeanServer.java +++ b/src/main/java/com/avaje/ebean/EbeanServer.java @@ -1,13 +1,5 @@ package com.avaje.ebean; -import java.util.Collection; -import java.util.List; -import java.util.Map; -import java.util.Set; - -import javax.persistence.OptimisticLockException; -import javax.persistence.PersistenceException; - import com.avaje.ebean.annotation.CacheStrategy; import com.avaje.ebean.cache.ServerCacheManager; import com.avaje.ebean.config.ServerConfig; @@ -17,6 +9,13 @@ import com.avaje.ebean.text.csv.CsvReader; import com.avaje.ebean.text.json.JsonContext; import org.jetbrains.annotations.Nullable; +import javax.persistence.OptimisticLockException; +import javax.persistence.PersistenceException; +import java.util.Collection; +import java.util.List; +import java.util.Map; +import java.util.Set; + /** * Provides the API for fetching and saving beans to a particular DataSource. *

@@ -1114,6 +1113,21 @@ public interface EbeanServer { @Nullable T findUnique(Query query, Transaction transaction); + /** + * Execute as a delete query deleting the 'root level' beans that match the predicates + * in the query. + *

+ * Note that if the query includes joins then the generated delete statement may not be + * optimal depending on the database platform. + *

+ * + * @param query the query used for the delete + * @param transaction the transaction to use (can be null) + * @param the type of entity bean to fetch. + * @return the number of beans/rows that were deleted + */ + int delete(Query query, Transaction transaction); + /** * Execute the sql query returning a list of MapBean. *

diff --git a/src/main/java/com/avaje/ebean/ExpressionList.java b/src/main/java/com/avaje/ebean/ExpressionList.java index 0a8ba54c2..bbec05217 100644 --- a/src/main/java/com/avaje/ebean/ExpressionList.java +++ b/src/main/java/com/avaje/ebean/ExpressionList.java @@ -107,6 +107,18 @@ public interface ExpressionList extends Serializable { */ Query asOf(Timestamp asOf); + /** + * Execute as a delete query deleting the 'root level' beans that match the predicates + * in the query. + *

+ * Note that if the query includes joins then the generated delete statement may not be + * optimal depending on the database platform. + *

+ * + * @return the number of beans/rows that were deleted. + */ + int delete(); + /** * Execute the query iterating over the results. * diff --git a/src/main/java/com/avaje/ebean/Query.java b/src/main/java/com/avaje/ebean/Query.java index 63f27f1df..ce1582255 100644 --- a/src/main/java/com/avaje/ebean/Query.java +++ b/src/main/java/com/avaje/ebean/Query.java @@ -743,6 +743,18 @@ public interface Query extends Serializable { */ List> findVersionsBetween(Timestamp start, Timestamp end); + /** + * Execute as a delete query deleting the 'root level' beans that match the predicates + * in the query. + *

+ * Note that if the query includes joins then the generated delete statement may not be + * optimal depending on the database platform. + *

+ * + * @return the number of beans/rows that were deleted. + */ + int delete(); + /** * Return the count of entities this query should return. *

diff --git a/src/main/java/com/avaje/ebeaninternal/api/SpiQuery.java b/src/main/java/com/avaje/ebeaninternal/api/SpiQuery.java index ec54a63a1..1f496d319 100644 --- a/src/main/java/com/avaje/ebeaninternal/api/SpiQuery.java +++ b/src/main/java/com/avaje/ebeaninternal/api/SpiQuery.java @@ -84,7 +84,12 @@ public interface SpiQuery extends Query { /** * A subquery used as part of a where clause. */ - SUBQUERY + SUBQUERY, + + /** + * Delete query. + */ + DELETE, } enum TemporalMode { @@ -272,6 +277,11 @@ public interface SpiQuery extends Query { */ NaturalKeyBindParam getNaturalKeyBindParam(); + /** + * Set the query to be a delete query. + */ + void setDelete(); + /** * Set the query to select the id property only. */ diff --git a/src/main/java/com/avaje/ebeaninternal/server/core/DefaultServer.java b/src/main/java/com/avaje/ebeaninternal/server/core/DefaultServer.java index e38a555de..a716516c5 100644 --- a/src/main/java/com/avaje/ebeaninternal/server/core/DefaultServer.java +++ b/src/main/java/com/avaje/ebeaninternal/server/core/DefaultServer.java @@ -1338,6 +1338,17 @@ public final class DefaultServer implements SpiServer, SpiEbeanServer { } } + public int delete(Query query, Transaction t) { + + SpiOrmQueryRequest request = createQueryRequest(Type.DELETE, query, t); + try { + request.initTransIfRequired(); + return request.delete(); + } finally { + request.endTransIfRequired(); + } + } + public FutureRowCount findFutureRowCount(Query q, Transaction t) { SpiQuery copy = ((SpiQuery) q).copy(); diff --git a/src/main/java/com/avaje/ebeaninternal/server/core/OrmQueryEngine.java b/src/main/java/com/avaje/ebeaninternal/server/core/OrmQueryEngine.java index 425685f3e..75b04fa35 100644 --- a/src/main/java/com/avaje/ebeaninternal/server/core/OrmQueryEngine.java +++ b/src/main/java/com/avaje/ebeaninternal/server/core/OrmQueryEngine.java @@ -39,4 +39,8 @@ public interface OrmQueryEngine { */ BeanIdList findIds(OrmQueryRequest request); + /** + * Execute the query as a delete statement. + */ + int delete(OrmQueryRequest request); } diff --git a/src/main/java/com/avaje/ebeaninternal/server/core/OrmQueryRequest.java b/src/main/java/com/avaje/ebeaninternal/server/core/OrmQueryRequest.java index aff6460bb..00ec3564d 100644 --- a/src/main/java/com/avaje/ebeaninternal/server/core/OrmQueryRequest.java +++ b/src/main/java/com/avaje/ebeaninternal/server/core/OrmQueryRequest.java @@ -218,7 +218,14 @@ public final class OrmQueryRequest extends BeanRequest implements BeanQueryRe public boolean isFindById() { return query.getType() == Type.BEAN; } - + + /** + * Execute the query as a delete. + */ + public int delete() { + return queryEngine.delete(this); + } + /** * Execute the query as findById. */ diff --git a/src/main/java/com/avaje/ebeaninternal/server/core/SpiOrmQueryRequest.java b/src/main/java/com/avaje/ebeaninternal/server/core/SpiOrmQueryRequest.java index 1204b3335..b463f2d13 100644 --- a/src/main/java/com/avaje/ebeaninternal/server/core/SpiOrmQueryRequest.java +++ b/src/main/java/com/avaje/ebeaninternal/server/core/SpiOrmQueryRequest.java @@ -47,6 +47,11 @@ public interface SpiOrmQueryRequest { */ void endTransIfRequired(); + /** + * Execute the query as a delete. + */ + int delete(); + /** * Execute the query as findById. */ diff --git a/src/main/java/com/avaje/ebeaninternal/server/deploy/BeanDescriptor.java b/src/main/java/com/avaje/ebeaninternal/server/deploy/BeanDescriptor.java index 22cf7c42f..e73218217 100644 --- a/src/main/java/com/avaje/ebeaninternal/server/deploy/BeanDescriptor.java +++ b/src/main/java/com/avaje/ebeaninternal/server/deploy/BeanDescriptor.java @@ -677,6 +677,13 @@ public class BeanDescriptor implements MetaBeanInfo, SpiBeanType { } } + /** + * Return the "delete by id" sql. + */ + public String getDeleteByIdInSql() { + return deleteByIdInSql; + } + /** * Return SQL that can be used to delete a list of Id's without any optimistic * concurrency checking. 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 2312dc3c1..7c6abbb94 100644 --- a/src/main/java/com/avaje/ebeaninternal/server/expression/JunctionExpression.java +++ b/src/main/java/com/avaje/ebeaninternal/server/expression/JunctionExpression.java @@ -218,6 +218,11 @@ abstract class JunctionExpression implements Junction, SpiExpression, Expr throw new RuntimeException("filterMany not allowed on Junction expression list"); } + @Override + public int delete() { + return exprList.delete(); + } + @Override public Query asOf(Timestamp asOf) { return exprList.asOf(asOf); diff --git a/src/main/java/com/avaje/ebeaninternal/server/query/CQueryBuilder.java b/src/main/java/com/avaje/ebeaninternal/server/query/CQueryBuilder.java index 5cb8ed158..40350fa77 100644 --- a/src/main/java/com/avaje/ebeaninternal/server/query/CQueryBuilder.java +++ b/src/main/java/com/avaje/ebeaninternal/server/query/CQueryBuilder.java @@ -83,6 +83,47 @@ public class CQueryBuilder { return sb.toString(); } + /** + * Build the delete query. + */ + public CQueryDelete buildDeleteQuery(OrmQueryRequest request) { + + SpiQuery query = request.getQuery(); + query.setDelete(); + + CQueryPredicates predicates = new CQueryPredicates(binder, request); + CQueryPlan queryPlan = request.getQueryPlan(); + if (queryPlan != null) { + // skip building the SqlTree and Sql string + predicates.prepare(false); + String sql = queryPlan.getSql(); + return new CQueryDelete(request, predicates, sql); + } + + predicates.prepare(true); + + SqlTree sqlTree = createSqlTree(request, predicates, getHistorySupport(query)); + + boolean includeJoins = sqlTree.isIncludeJoins(); + + String sql; + if (!includeJoins) { + // simple - delete from table ... + sql = buildSql("delete", request, predicates, sqlTree).getSql(); + + } else { + // wrap as - delete from table where id in (select id ...) + sql = buildSql(null, request, predicates, sqlTree).getSql(); + sql = request.getBeanDescriptor().getDeleteByIdInSql() + "in (" + sql + ")"; + } + + // cache the query plan + queryPlan = new CQueryPlan(request, sql, sqlTree, false, false, predicates.getLogWhereSql()); + + request.putQueryPlan(queryPlan); + return new CQueryDelete(request, predicates, sql); + } + /** * Build the row count query. */ diff --git a/src/main/java/com/avaje/ebeaninternal/server/query/CQueryDelete.java b/src/main/java/com/avaje/ebeaninternal/server/query/CQueryDelete.java new file mode 100644 index 000000000..a83362ffd --- /dev/null +++ b/src/main/java/com/avaje/ebeaninternal/server/query/CQueryDelete.java @@ -0,0 +1,135 @@ +package com.avaje.ebeaninternal.server.query; + +import com.avaje.ebeaninternal.api.SpiQuery; +import com.avaje.ebeaninternal.api.SpiTransaction; +import com.avaje.ebeaninternal.server.core.OrmQueryRequest; +import com.avaje.ebeaninternal.server.deploy.BeanDescriptor; +import com.avaje.ebeaninternal.server.type.DataBind; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.sql.Connection; +import java.sql.PreparedStatement; +import java.sql.SQLException; + +/** + * Executes the delete query. + */ +public class CQueryDelete { + + private static final Logger logger = LoggerFactory.getLogger(CQueryDelete.class); + + private final OrmQueryRequest request; + + private final BeanDescriptor desc; + + private final SpiQuery query; + + /** + * Where clause predicates. + */ + private final CQueryPredicates predicates; + + /** + * The final sql that is generated. + */ + private final String sql; + + /** + * The statement used to create the resultSet. + */ + private PreparedStatement pstmt; + + private String bindLog; + + private int executionTimeMicros; + + private int rowCount; + + /** + * Create the Sql select based on the request. + */ + public CQueryDelete(OrmQueryRequest request, CQueryPredicates predicates, String sql) { + + this.request = request; + this.query = request.getQuery(); + this.sql = sql; + query.setGeneratedSql(sql); + this.desc = request.getBeanDescriptor(); + this.predicates = predicates; + } + + /** + * Return a summary description of this query. + */ + public String getSummary() { + //noinspection StringBufferReplaceableByString + StringBuilder sb = new StringBuilder(80); + sb.append("Delete exeMicros[").append(executionTimeMicros) + .append("] rows[").append(rowCount) + .append("] type[").append(desc.getName()) + .append("] predicates[").append(predicates.getLogWhereSql()) + .append("] bind[").append(bindLog).append("]"); + + return sb.toString(); + } + + /** + * Return the bind log. + */ + public String getBindLog() { + return bindLog; + } + + /** + * Return the generated sql. + */ + public String getGeneratedSql() { + return sql; + } + + /** + * Execute the query returning the row count. + */ + public int delete() throws SQLException { + + long startNano = System.nanoTime(); + + try { + + SpiTransaction t = request.getTransaction(); + Connection conn = t.getInternalConnection(); + pstmt = conn.prepareStatement(sql); + + if (query.getTimeout() > 0) { + pstmt.setQueryTimeout(query.getTimeout()); + } + + bindLog = predicates.bind(new DataBind(pstmt)); + rowCount = pstmt.executeUpdate(); + + long exeNano = System.nanoTime() - startNano; + executionTimeMicros = (int) exeNano / 1000; + + return rowCount; + + } finally { + close(); + } + } + + /** + * Close the resources. + */ + private void close() { + try { + if (pstmt != null) { + pstmt.close(); + pstmt = null; + } + } catch (SQLException e) { + logger.error(null, e); + } + } + +} diff --git a/src/main/java/com/avaje/ebeaninternal/server/query/CQueryEngine.java b/src/main/java/com/avaje/ebeaninternal/server/query/CQueryEngine.java index 73bd7cf49..49fa7e74a 100644 --- a/src/main/java/com/avaje/ebeaninternal/server/query/CQueryEngine.java +++ b/src/main/java/com/avaje/ebeaninternal/server/query/CQueryEngine.java @@ -53,6 +53,27 @@ public class CQueryEngine { return queryBuilder.buildQuery(request); } + public int delete(OrmQueryRequest request) { + + CQueryDelete query = queryBuilder.buildDeleteQuery(request); + try { + int rows = query.delete(); + + if (request.isLogSql()) { + String logSql = query.getGeneratedSql(); + if (TransactionManager.SQL_LOGGER.isTraceEnabled()) { + logSql = Str.add(logSql, "; --bind(", query.getBindLog(), ") rows:", String.valueOf(rows)); + } + request.logSql(logSql); + } + + return rows; + + } catch (SQLException e) { + throw CQuery.createPersistenceException(e, request.getTransaction(), query.getBindLog(), query.getGeneratedSql()); + } + } + /** * Build and execute the find Id's query. */ diff --git a/src/main/java/com/avaje/ebeaninternal/server/query/DefaultOrmQueryEngine.java b/src/main/java/com/avaje/ebeaninternal/server/query/DefaultOrmQueryEngine.java index 55eecd392..2f70f8516 100644 --- a/src/main/java/com/avaje/ebeaninternal/server/query/DefaultOrmQueryEngine.java +++ b/src/main/java/com/avaje/ebeaninternal/server/query/DefaultOrmQueryEngine.java @@ -47,6 +47,12 @@ public class DefaultOrmQueryEngine implements OrmQueryEngine { } } + public int delete(OrmQueryRequest request) { + + flushJdbcBatchOnQuery(request); + return queryEngine.delete(request); + } + public int findRowCount(OrmQueryRequest request) { flushJdbcBatchOnQuery(request); diff --git a/src/main/java/com/avaje/ebeaninternal/server/query/SqlTree.java b/src/main/java/com/avaje/ebeaninternal/server/query/SqlTree.java index a278c2463..06b2e6e40 100644 --- a/src/main/java/com/avaje/ebeaninternal/server/query/SqlTree.java +++ b/src/main/java/com/avaje/ebeaninternal/server/query/SqlTree.java @@ -41,11 +41,13 @@ public class SqlTree { */ private final String inheritanceWhereSql; + private final boolean includeJoins; + /** * Create the SqlSelectClause. */ public SqlTree(String summary, SqlTreeNode rootNode, String selectSql, String fromSql, String inheritanceWhereSql, - BeanProperty[] encryptedProps, BeanPropertyAssocMany manyProperty, Set includes) { + BeanProperty[] encryptedProps, BeanPropertyAssocMany manyProperty, Set includes, boolean includeJoins) { this.summary = summary; this.rootNode = rootNode; @@ -55,6 +57,7 @@ public class SqlTree { this.encryptedProps = encryptedProps; this.manyProperty = manyProperty; this.includes = includes; + this.includeJoins = includeJoins; } /** @@ -69,6 +72,14 @@ public class SqlTree { this.encryptedProps = null; this.manyProperty = null; this.includes = null; + this.includeJoins = false; //not valid for rawSql + } + + /** + * Return true if the query includes joins (not valid for rawSql). + */ + public boolean isIncludeJoins() { + return includeJoins; } /** diff --git a/src/main/java/com/avaje/ebeaninternal/server/query/SqlTreeAlias.java b/src/main/java/com/avaje/ebeaninternal/server/query/SqlTreeAlias.java index 986845232..483c6ca66 100644 --- a/src/main/java/com/avaje/ebeaninternal/server/query/SqlTreeAlias.java +++ b/src/main/java/com/avaje/ebeaninternal/server/query/SqlTreeAlias.java @@ -205,4 +205,11 @@ public class SqlTreeAlias { private String nextManyWhereTableAlias() { return "u" + (++manyWhereCounter); } + + /** + * Return true if there are joins included in the query. + */ + public boolean isIncludeJoins() { + return !aliasMap.isEmpty() || !manyWhereAliasMap.isEmpty(); + } } diff --git a/src/main/java/com/avaje/ebeaninternal/server/query/SqlTreeBuilder.java b/src/main/java/com/avaje/ebeaninternal/server/query/SqlTreeBuilder.java index 51f94ce4c..68304fffe 100644 --- a/src/main/java/com/avaje/ebeaninternal/server/query/SqlTreeBuilder.java +++ b/src/main/java/com/avaje/ebeaninternal/server/query/SqlTreeBuilder.java @@ -1,17 +1,5 @@ package com.avaje.ebeaninternal.server.query; -import java.util.ArrayList; -import java.util.Arrays; -import java.util.Collection; -import java.util.HashMap; -import java.util.HashSet; -import java.util.List; -import java.util.Map; -import java.util.Set; - -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; - import com.avaje.ebeaninternal.api.ManyWhereJoins; import com.avaje.ebeaninternal.api.PropertyJoin; import com.avaje.ebeaninternal.api.SpiQuery; @@ -27,6 +15,17 @@ import com.avaje.ebeaninternal.server.deploy.TableJoin; import com.avaje.ebeaninternal.server.el.ElPropertyValue; import com.avaje.ebeaninternal.server.querydefn.OrmQueryDetail; import com.avaje.ebeaninternal.server.querydefn.OrmQueryProperties; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collection; +import java.util.HashMap; +import java.util.HashSet; +import java.util.List; +import java.util.Map; +import java.util.Set; /** * Factory for SqlTree. @@ -138,8 +137,10 @@ public class SqlTreeBuilder { encryptedProps = ctx.getEncryptedProps(); } + boolean includeJoins = (alias == null) ? false : alias.isIncludeJoins(); + return new SqlTree(summary.toString(), rootNode, selectSql, fromSql, inheritanceWhereSql, encryptedProps, - manyProperty, queryDetail.getIncludes()); + manyProperty, queryDetail.getIncludes(), includeJoins); } private String buildSelectClause() { diff --git a/src/main/java/com/avaje/ebeaninternal/server/querydefn/DefaultOrmQuery.java b/src/main/java/com/avaje/ebeaninternal/server/querydefn/DefaultOrmQuery.java index a313db2e5..db2dfb342 100644 --- a/src/main/java/com/avaje/ebeaninternal/server/querydefn/DefaultOrmQuery.java +++ b/src/main/java/com/avaje/ebeaninternal/server/querydefn/DefaultOrmQuery.java @@ -413,6 +413,17 @@ public class DefaultOrmQuery implements SpiQuery { detail.convertManyFetchJoinsToQueryJoins(beanDescriptor, lazyLoadManyPath, allowOne, queryBatch); } + /** + * Setup to be a delete query. + */ + public void setDelete() { + // unset any paging and select on the id in the case where the query + // includes joins and we use - delete ... where id in (...) + maxRows = 0; + firstRow = 0; + setSelectId(); + } + /** * Set the select clause to select the Id property. */ @@ -948,6 +959,11 @@ public class DefaultOrmQuery implements SpiQuery { return this; } + @Override + public int delete() { + return server.delete(this, null); + } + public List findIds() { // a copy of this query is made in the server // as the query needs to modified (so we modify diff --git a/src/main/java/com/avaje/ebeaninternal/util/DefaultExpressionList.java b/src/main/java/com/avaje/ebeaninternal/util/DefaultExpressionList.java index eb0147d1c..9384aa2ee 100644 --- a/src/main/java/com/avaje/ebeaninternal/util/DefaultExpressionList.java +++ b/src/main/java/com/avaje/ebeaninternal/util/DefaultExpressionList.java @@ -158,6 +158,11 @@ public class DefaultExpressionList implements SpiExpressionList { return query.apply(pathProperties); } + @Override + public int delete() { + return query.delete(); + } + @Override public FutureIds findFutureIds() { return query.findFutureIds(); diff --git a/src/test/java/com/avaje/ebeaninternal/api/TDSpiEbeanServer.java b/src/test/java/com/avaje/ebeaninternal/api/TDSpiEbeanServer.java index 65d3ebdbe..9304cf8ec 100644 --- a/src/test/java/com/avaje/ebeaninternal/api/TDSpiEbeanServer.java +++ b/src/test/java/com/avaje/ebeaninternal/api/TDSpiEbeanServer.java @@ -161,6 +161,11 @@ public class TDSpiEbeanServer implements SpiEbeanServer { return null; } + @Override + public int delete(Query query, Transaction t) { + return 0; + } + @Override public List> findVersions(Query query, Transaction transaction) { return null; diff --git a/src/test/java/com/avaje/tests/delete/TestDeleteByQuery.java b/src/test/java/com/avaje/tests/delete/TestDeleteByQuery.java new file mode 100644 index 000000000..e9bb80bb0 --- /dev/null +++ b/src/test/java/com/avaje/tests/delete/TestDeleteByQuery.java @@ -0,0 +1,48 @@ +package com.avaje.tests.delete; + +import com.avaje.ebean.BaseTestCase; +import com.avaje.ebean.Ebean; +import com.avaje.ebean.EbeanServer; +import com.avaje.ebean.Query; +import com.avaje.tests.model.basic.Contact; +import org.avaje.ebeantest.LoggedSqlCollector; +import org.junit.Test; + +import java.util.List; + +import static org.assertj.core.api.Assertions.assertThat; + +public class TestDeleteByQuery extends BaseTestCase { + + @Test + public void test() { + + EbeanServer server = Ebean.getDefaultServer(); + + Query query = server.find(Contact.class).where().eq("group.name", "NahYeahMaybe").query(); + + LoggedSqlCollector.start(); + server.delete(query, null); + + List loggedSql = LoggedSqlCollector.stop(); + assertThat(loggedSql).hasSize(1); + assertThat(loggedSql.get(0)).contains("delete from contact where id in (select t0.id c0 from contact t0 left outer join"); + + Query query2 = server.find(Contact.class).where().eq("firstName", "NotARealFirstName").query(); + + LoggedSqlCollector.start(); + server.delete(query2, null); + + loggedSql = LoggedSqlCollector.stop(); + assertThat(loggedSql).hasSize(1); + assertThat(loggedSql.get(0)).contains("delete from contact t0 where t0.first_name ="); + + + server.find(Contact.class).select("id").where().eq("firstName", "NotARealFirstName").delete(); + server.find(Contact.class).select("id").where().eq("firstName", "TwoAlsoNotRealFirstName").query().delete(); + + List list = server.find(Contact.class).select("id").where().eq("firstName", "NotARealFirstName").findList(); + assertThat(list).isEmpty(); + } + +}