mirror of
https://github.com/ebean-orm/ebean.git
synced 2024-04-21 10:51:47 +00:00
Merge branch 'experimental-elliminate-not-in-with-too-many-params' of github.com:FOCONIS/ebean into FOCONIS-experimental-elliminate-not-in-with-too-many-params
This commit is contained in:
@@ -1,6 +1,8 @@
|
||||
package io.ebeaninternal.server.deploy;
|
||||
|
||||
import io.ebean.Query;
|
||||
import io.ebean.SqlUpdate;
|
||||
import io.ebean.Transaction;
|
||||
import io.ebean.bean.EntityBean;
|
||||
import io.ebean.core.type.DocPropertyType;
|
||||
import io.ebean.text.PathProperties;
|
||||
@@ -570,4 +572,26 @@ public abstract class BeanPropertyAssoc<T> extends BeanProperty implements STree
|
||||
+ " or a @JoinColumn needs an explicit referencedColumnName specified?";
|
||||
throw new PersistenceException(msg);
|
||||
}
|
||||
|
||||
/**
|
||||
* Create SqlUpdate statement to delete all child beans of the parent <code>id</code>.
|
||||
*/
|
||||
public abstract SqlUpdate deleteByParentId(Object id);
|
||||
|
||||
/**
|
||||
* Create SqlUpdate statement to delete all child beans of the parent ids in <code>idList</code>.
|
||||
*/
|
||||
public abstract SqlUpdate deleteByParentIdList(List<Object> idList);
|
||||
|
||||
/**
|
||||
* Find child beans of the parent <code>id</code>.
|
||||
*/
|
||||
public abstract List<Object> findIdsByParentId(Object id, Transaction transaction, boolean includeSoftDeletes);
|
||||
|
||||
/**
|
||||
* Find child beans of the parent ids in <code>idList</code>.
|
||||
*/
|
||||
public abstract List<Object> findIdsByParentIdList(List<Object> idList, Transaction transaction, boolean includeSoftDeletes);
|
||||
|
||||
}
|
||||
|
||||
|
||||
+29
-14
@@ -296,23 +296,38 @@ public class BeanPropertyAssocMany<T> extends BeanPropertyAssoc<T> implements ST
|
||||
// do not add to the selectChain at the top level of the Many bean
|
||||
}
|
||||
|
||||
public SpiSqlUpdate deleteByParentId(Object parentId, List<Object> parentIdist) {
|
||||
if (parentId != null) {
|
||||
return sqlHelp.deleteByParentId(parentId);
|
||||
} else {
|
||||
return sqlHelp.deleteByParentIdList(parentIdist);
|
||||
}
|
||||
@Override
|
||||
public SpiSqlUpdate deleteByParentId(Object parentId) {
|
||||
return sqlHelp.deleteByParentId(parentId);
|
||||
}
|
||||
|
||||
@Override
|
||||
public SpiSqlUpdate deleteByParentIdList(List<Object> parentIdist) {
|
||||
return sqlHelp.deleteByParentIdList(parentIdist);
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Find the Id's of detail beans given a parent Id
|
||||
*/
|
||||
@Override
|
||||
public List<Object> findIdsByParentId(Object parentId, Transaction t, boolean includeSoftDeletes) {
|
||||
return sqlHelp.findIdsByParentId(parentId, t, includeSoftDeletes, null);
|
||||
}
|
||||
|
||||
/**
|
||||
* Find the Id's of detail beans given a parent Id or list of parent Id's.
|
||||
* Find the Id's of detail beans given a parent Id and optionally exclude detail IDs
|
||||
*/
|
||||
public List<Object> findIdsByParentId(Object parentId, List<Object> parentIdList, Transaction t, List<Object> excludeDetailIds, boolean hard) {
|
||||
if (parentId != null) {
|
||||
return sqlHelp.findIdsByParentId(parentId, t, excludeDetailIds, hard);
|
||||
} else {
|
||||
return sqlHelp.findIdsByParentIdList(parentIdList, t, excludeDetailIds, hard);
|
||||
}
|
||||
public List<Object> findIdsByParentId(Object parentId, Transaction t, boolean includeSoftDeletes, Set<Object> excludeDetailIds) {
|
||||
return sqlHelp.findIdsByParentId(parentId, t, includeSoftDeletes, excludeDetailIds);
|
||||
}
|
||||
|
||||
/**
|
||||
* Find the Id's of detail beans given a list of parent Id's.
|
||||
*/
|
||||
@Override
|
||||
public List<Object> findIdsByParentIdList(List<Object> parentIdList, Transaction t, boolean includeSoftDeletes) {
|
||||
return sqlHelp.findIdsByParentIdList(parentIdList, t, includeSoftDeletes);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -740,7 +755,7 @@ public class BeanPropertyAssocMany<T> extends BeanPropertyAssoc<T> implements ST
|
||||
throw new PersistenceException(from + ": Could not find mapKey property " + mapKey + " on " + to);
|
||||
}
|
||||
|
||||
public IntersectionRow buildManyDeleteChildren(EntityBean parentBean, List<Object> excludeDetailIds) {
|
||||
public IntersectionRow buildManyDeleteChildren(EntityBean parentBean, Set<Object> excludeDetailIds) {
|
||||
IntersectionRow row = new IntersectionRow(tableJoin.getTable(), targetDescriptor);
|
||||
if (excludeDetailIds != null && !excludeDetailIds.isEmpty()) {
|
||||
row.setExcludeIds(excludeDetailIds, targetDescriptor());
|
||||
|
||||
+7
-8
@@ -4,12 +4,13 @@ import io.ebean.Transaction;
|
||||
import io.ebeaninternal.api.SpiEbeanServer;
|
||||
import io.ebeaninternal.api.SpiQuery;
|
||||
import io.ebeaninternal.api.SpiSqlUpdate;
|
||||
import io.ebeaninternal.server.core.DefaultSqlUpdate;
|
||||
import io.ebeaninternal.server.deploy.visitor.BaseTablePropertyVisitor;
|
||||
import io.ebeaninternal.server.deploy.visitor.VisitProperties;
|
||||
import io.ebeaninternal.server.core.DefaultSqlUpdate;
|
||||
import io.ebeaninternal.server.util.Str;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.Set;
|
||||
|
||||
class BeanPropertyAssocManySqlHelp<T> {
|
||||
|
||||
@@ -123,29 +124,27 @@ class BeanPropertyAssocManySqlHelp<T> {
|
||||
many.bindParentIdsIn(rawWhere, parentIds, query);
|
||||
}
|
||||
|
||||
List<Object> findIdsByParentId(Object parentId, Transaction t, List<Object> excludeDetailIds, boolean hard) {
|
||||
List<Object> findIdsByParentId(Object parentId, Transaction t, boolean includeSoftDeletes, Set<Object> excludeDetailIds) {
|
||||
final SpiEbeanServer server = descriptor.ebeanServer();
|
||||
final SpiQuery<?> query = many.newQuery(server);
|
||||
many.bindParentIdEq(rawParentIdEQ(""), parentId, query);
|
||||
if (hard) {
|
||||
if (includeSoftDeletes) {
|
||||
query.setIncludeSoftDeletes();
|
||||
}
|
||||
|
||||
if (excludeDetailIds != null && !excludeDetailIds.isEmpty()) {
|
||||
query.where().not(query.getExpressionFactory().idIn(excludeDetailIds));
|
||||
}
|
||||
return server.findIds(query, t);
|
||||
}
|
||||
|
||||
List<Object> findIdsByParentIdList(List<Object> parentIds, Transaction t, List<Object> excludeDetailIds, boolean hard) {
|
||||
List<Object> findIdsByParentIdList(List<Object> parentIds, Transaction t, boolean includeSoftDeletes) {
|
||||
final SpiEbeanServer server = descriptor.ebeanServer();
|
||||
final SpiQuery<?> query = many.newQuery(server);
|
||||
many.bindParentIdsIn(rawParentIdIN("", parentIds.size()), parentIds, query);
|
||||
if (hard) {
|
||||
if (includeSoftDeletes) {
|
||||
query.setIncludeSoftDeletes();
|
||||
}
|
||||
if (excludeDetailIds != null && !excludeDetailIds.isEmpty()) {
|
||||
query.where().not(query.getExpressionFactory().idIn(excludeDetailIds));
|
||||
}
|
||||
return server.findIds(query, t);
|
||||
}
|
||||
|
||||
|
||||
@@ -237,50 +237,46 @@ public class BeanPropertyAssocOne<T> extends BeanPropertyAssoc<T> implements STr
|
||||
return encrypted ? elPlaceHolderEncrypted : elPlaceHolder;
|
||||
}
|
||||
|
||||
public SqlUpdate deleteByParentId(Object parentId, List<Object> parentIdist) {
|
||||
if (parentId != null) {
|
||||
return deleteByParentId(parentId);
|
||||
} else {
|
||||
return deleteByParentIdList(parentIdist);
|
||||
}
|
||||
}
|
||||
|
||||
private SqlUpdate deleteByParentIdList(List<Object> parentIds) {
|
||||
@Override
|
||||
public SqlUpdate deleteByParentIdList(List<Object> parentIds) {
|
||||
String sql = deleteByParentIdInSql + targetIdBinder.idInValueExpr(false, parentIds.size());
|
||||
DefaultSqlUpdate delete = new DefaultSqlUpdate(sql);
|
||||
bindParentIds(delete, parentIds);
|
||||
return delete;
|
||||
}
|
||||
|
||||
private SqlUpdate deleteByParentId(Object parentId) {
|
||||
@Override
|
||||
public SqlUpdate deleteByParentId(Object parentId) {
|
||||
DefaultSqlUpdate delete = new DefaultSqlUpdate(deleteByParentIdSql);
|
||||
bindParentId(delete, parentId);
|
||||
return delete;
|
||||
}
|
||||
|
||||
public List<Object> findIdsByParentId(Object parentId, List<Object> parentIds, Transaction t) {
|
||||
if (parentId != null) {
|
||||
return findIdsByParentId(parentId, t);
|
||||
} else {
|
||||
return findIdsByParentIdList(parentIds, t);
|
||||
}
|
||||
}
|
||||
|
||||
private List<Object> findIdsByParentId(Object parentId, Transaction t) {
|
||||
@Override
|
||||
public List<Object> findIdsByParentId(Object parentId, Transaction t, boolean includeSoftDeletes) {
|
||||
String rawWhere = deriveWhereParentIdSql(false);
|
||||
SpiEbeanServer server = server();
|
||||
Query<?> q = server.find(type());
|
||||
bindParentIdEq(rawWhere, parentId, q);
|
||||
if (includeSoftDeletes) {
|
||||
q.setIncludeSoftDeletes();
|
||||
}
|
||||
return server.findIds(q, t);
|
||||
}
|
||||
|
||||
private List<Object> findIdsByParentIdList(List<Object> parentIds, Transaction t) {
|
||||
@Override
|
||||
public List<Object> findIdsByParentIdList(List<Object> parentIds, Transaction t, boolean includeSoftDeletes) {
|
||||
String rawWhere = deriveWhereParentIdSql(true);
|
||||
String inClause = idBinder().idInValueExpr(false, parentIds.size());
|
||||
String expr = rawWhere + inClause;
|
||||
SpiEbeanServer server = server();
|
||||
Query<?> q = server.find(type());
|
||||
bindParentIdsIn(expr, parentIds, q);
|
||||
if (includeSoftDeletes) {
|
||||
q.setIncludeSoftDeletes();
|
||||
}
|
||||
return server.findIds(q, t);
|
||||
}
|
||||
|
||||
|
||||
@@ -11,13 +11,14 @@ import io.ebeaninternal.server.persist.DeleteMode;
|
||||
import java.util.LinkedHashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Set;
|
||||
|
||||
public final class IntersectionRow {
|
||||
|
||||
private final String tableName;
|
||||
private final BeanDescriptor<?> targetDescriptor;
|
||||
private final LinkedHashMap<String, Object> values = new LinkedHashMap<>();
|
||||
private List<Object> excludeIds;
|
||||
private Set<Object> excludeIds;
|
||||
private BeanDescriptor<?> excludeDescriptor;
|
||||
|
||||
IntersectionRow(String tableName, BeanDescriptor<?> targetDescriptor) {
|
||||
@@ -33,7 +34,7 @@ public final class IntersectionRow {
|
||||
/**
|
||||
* Set Id's to exclude. This is for deleting non-attached detail Id's.
|
||||
*/
|
||||
void setExcludeIds(List<Object> excludeIds, BeanDescriptor<?> excludeDescriptor) {
|
||||
void setExcludeIds(Set<Object> excludeIds, BeanDescriptor<?> excludeDescriptor) {
|
||||
this.excludeIds = excludeIds;
|
||||
this.excludeDescriptor = excludeDescriptor;
|
||||
}
|
||||
|
||||
@@ -605,7 +605,7 @@ public final class DefaultPersister implements Persister {
|
||||
// convert to appropriate type if required
|
||||
idList.add(descriptor.convertId(id));
|
||||
}
|
||||
return delete(descriptor, null, idList, transaction, deleteMode);
|
||||
return delete(descriptor, idList, transaction, deleteMode);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -642,146 +642,222 @@ public final class DefaultPersister implements Persister {
|
||||
|
||||
id = descriptor.convertId(id);
|
||||
DeleteMode deleteMode = (permanent || !descriptor.isSoftDelete()) ? DeleteMode.HARD : DeleteMode.SOFT;
|
||||
return delete(descriptor, id, null, transaction, deleteMode);
|
||||
return delete(descriptor, id, transaction, deleteMode);
|
||||
}
|
||||
|
||||
@Override
|
||||
public int deleteByIds(BeanDescriptor<?> descriptor, List<Object> idList, Transaction transaction, boolean permanent) {
|
||||
DeleteMode deleteMode = (permanent || !descriptor.isSoftDelete()) ? DeleteMode.HARD : DeleteMode.SOFT;
|
||||
return delete(descriptor, null, idList, transaction, deleteMode);
|
||||
return delete(descriptor, idList, transaction, deleteMode);
|
||||
}
|
||||
|
||||
private int delete(BeanDescriptor<?> descriptor, List<Object> idList, Transaction transaction, DeleteMode deleteMode) {
|
||||
if (idList == null || idList.size() <= maxDeleteBatch) {
|
||||
return new DeleteBatchHelpMultiple(descriptor, idList, transaction, deleteMode).deleteBatch();
|
||||
}
|
||||
int rows = 0;
|
||||
for (List<Object> batchOfIds : Lists.partition(idList, maxDeleteBatch)) {
|
||||
rows += new DeleteBatchHelpMultiple(descriptor, batchOfIds, transaction, deleteMode).deleteBatch();
|
||||
}
|
||||
return rows;
|
||||
}
|
||||
|
||||
/**
|
||||
* Delete by Id or a List of Id's.
|
||||
*/
|
||||
private int delete(BeanDescriptor<?> descriptor, Object id, List<Object> idList, Transaction transaction, DeleteMode deleteMode) {
|
||||
if (idList == null || idList.size() <= maxDeleteBatch) {
|
||||
return deleteBatch(descriptor, id, idList, transaction, deleteMode);
|
||||
}
|
||||
int rows = 0;
|
||||
for (List<Object> batchOfIds : Lists.partition(idList, maxDeleteBatch)) {
|
||||
rows += deleteBatch(descriptor, id, batchOfIds, transaction, deleteMode);
|
||||
}
|
||||
return rows;
|
||||
private int delete(BeanDescriptor<?> descriptor, Object id, Transaction transaction, DeleteMode deleteMode) {
|
||||
return new DeleteBatchHelpSingle(descriptor, id, transaction, deleteMode).deleteBatch();
|
||||
}
|
||||
|
||||
private int deleteBatch(BeanDescriptor<?> descriptor, Object id, List<Object> idList, Transaction transaction, DeleteMode deleteMode) {
|
||||
SpiTransaction t = (SpiTransaction) transaction;
|
||||
if (t.isPersistCascade()) {
|
||||
BeanPropertyAssocOne<?>[] propImportDelete = descriptor.propertiesOneImportedDelete();
|
||||
if (propImportDelete.length > 0) {
|
||||
// We actually need to execute a query to get the foreign key values
|
||||
// as they are required for the delete cascade. Query back just the
|
||||
// Id and the appropriate foreign key values
|
||||
Query<?> q = deleteRequiresQuery(descriptor, propImportDelete, deleteMode);
|
||||
if (idList != null) {
|
||||
q.where().idIn(idList);
|
||||
if (t.isLogSummary()) {
|
||||
t.logSummary("-- DeleteById of {0} ids[{1}] requires fetch of foreign key values", descriptor.name(), idList);
|
||||
}
|
||||
List<?> beanList = server.findList(q, t);
|
||||
deleteCascade(beanList, t, deleteMode, false);
|
||||
return beanList.size();
|
||||
private abstract class DeleteBatchHelp {
|
||||
final BeanDescriptor<?> descriptor;
|
||||
final SpiTransaction transaction;
|
||||
final DeleteMode deleteMode;
|
||||
|
||||
} else {
|
||||
q.where().idEq(id);
|
||||
if (t.isLogSummary()) {
|
||||
t.logSummary("-- DeleteById of {0} id[{1}] requires fetch of foreign key values", descriptor.name(), id);
|
||||
}
|
||||
EntityBean bean = (EntityBean) server.findOne(q, t);
|
||||
if (bean == null) {
|
||||
return 0;
|
||||
} else {
|
||||
return deleteRecurse(bean, t, deleteMode);
|
||||
}
|
||||
}
|
||||
}
|
||||
public DeleteBatchHelp(BeanDescriptor<?> descriptor, Transaction transaction, DeleteMode deleteMode) {
|
||||
this.descriptor = descriptor;
|
||||
this.transaction = (SpiTransaction) transaction;
|
||||
this.deleteMode = deleteMode;
|
||||
}
|
||||
|
||||
if (t.isPersistCascade()) {
|
||||
// OneToOne exported side with delete cascade
|
||||
BeanPropertyAssocOne<?>[] expOnes = descriptor.propertiesOneExportedDelete();
|
||||
for (BeanPropertyAssocOne<?> expOne : expOnes) {
|
||||
BeanDescriptor<?> targetDesc = expOne.targetDescriptor();
|
||||
// only cascade soft deletes when supported by target
|
||||
if (deleteMode.isHard() || targetDesc.isSoftDelete()) {
|
||||
if (deleteMode.isHard() && targetDesc.isDeleteByStatement()) {
|
||||
SqlUpdate sqlDelete = expOne.deleteByParentId(id, idList);
|
||||
executeSqlUpdate(sqlDelete, t);
|
||||
} else {
|
||||
List<Object> childIds = expOne.findIdsByParentId(id, idList, t);
|
||||
if (childIds != null && !childIds.isEmpty()) {
|
||||
deleteChildrenById(t, targetDesc, childIds, deleteMode);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
int deleteBatch() {
|
||||
|
||||
// OneToMany's with delete cascade
|
||||
BeanPropertyAssocMany<?>[] manys = descriptor.propertiesManyDelete();
|
||||
for (BeanPropertyAssocMany<?> many : manys) {
|
||||
if (!many.isManyToMany()) {
|
||||
BeanDescriptor<?> targetDesc = many.targetDescriptor();
|
||||
if (transaction.isPersistCascade()) {
|
||||
BeanPropertyAssocOne<?>[] propImportDelete = descriptor.propertiesOneImportedDelete();
|
||||
if (propImportDelete.length > 0) {
|
||||
return deleteImported(propImportDelete);
|
||||
}
|
||||
|
||||
// OneToOne exported side with delete cascade
|
||||
BeanPropertyAssocOne<?>[] expOnes = descriptor.propertiesOneExportedDelete();
|
||||
for (BeanPropertyAssocOne<?> expOne : expOnes) {
|
||||
BeanDescriptor<?> targetDesc = expOne.targetDescriptor();
|
||||
// only cascade soft deletes when supported by target
|
||||
if (deleteMode.isHard() || targetDesc.isSoftDelete()) {
|
||||
if (deleteMode.isHard() && targetDesc.isDeleteByStatement()) {
|
||||
// we can just delete children with a single statement
|
||||
SqlUpdate sqlDelete = many.deleteByParentId(id, idList);
|
||||
executeSqlUpdate(sqlDelete, t);
|
||||
executeSqlUpdate(sqlDeleteChildren(expOne), transaction);
|
||||
} else {
|
||||
// we need to fetch the Id's to delete (recurse or notify L2 cache)
|
||||
List<Object> childIds = many.findIdsByParentId(id, idList, t, null, deleteMode.isHard());
|
||||
if (!childIds.isEmpty()) {
|
||||
delete(targetDesc, null, childIds, t, deleteMode);
|
||||
List<Object> childIds = findChildIds(expOne, deleteMode.isHard());
|
||||
if (childIds != null && !childIds.isEmpty()) {
|
||||
deleteChildrenById(transaction, targetDesc, childIds, deleteMode);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// OneToMany's with delete cascade
|
||||
BeanPropertyAssocMany<?>[] manys = descriptor.propertiesManyDelete();
|
||||
for (BeanPropertyAssocMany<?> many : manys) {
|
||||
if (!many.isManyToMany()) {
|
||||
BeanDescriptor<?> targetDesc = many.targetDescriptor();
|
||||
// only cascade soft deletes when supported by target
|
||||
if (deleteMode.isHard() || targetDesc.isSoftDelete()) {
|
||||
if (deleteMode.isHard() && targetDesc.isDeleteByStatement()) {
|
||||
// we can just delete children with a single statement
|
||||
executeSqlUpdate(sqlDeleteChildren(many), transaction);
|
||||
} else {
|
||||
// we need to fetch the Id's to delete (recurse or notify L2 cache)
|
||||
List<Object> childIds = findChildIds(many, deleteMode.isHard());
|
||||
if (!childIds.isEmpty()) {
|
||||
delete(targetDesc, childIds, transaction, deleteMode);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (deleteMode.isHard()) {
|
||||
// ManyToMany's ... delete from intersection table
|
||||
BeanPropertyAssocMany<?>[] manys = descriptor.propertiesManyToMany();
|
||||
for (BeanPropertyAssocMany<?> many : manys) {
|
||||
SqlUpdate sqlDelete = many.deleteByParentId(id, idList);
|
||||
if (t.isLogSummary()) {
|
||||
t.logSummary("-- Deleting intersection table entries: {0}", many.fullName());
|
||||
if (deleteMode.isHard()) {
|
||||
// ManyToMany's ... delete from intersection table
|
||||
BeanPropertyAssocMany<?>[] manys = descriptor.propertiesManyToMany();
|
||||
for (BeanPropertyAssocMany<?> many : manys) {
|
||||
if (transaction.isLogSummary()) {
|
||||
transaction.logSummary("-- Deleting intersection table entries: {0}", many.fullName());
|
||||
}
|
||||
executeSqlUpdate(sqlDeleteChildren(many), transaction);
|
||||
}
|
||||
executeSqlUpdate(sqlDelete, t);
|
||||
}
|
||||
|
||||
// delete the bean(s)
|
||||
return deleteBeans();
|
||||
}
|
||||
|
||||
// delete the bean(s)
|
||||
SqlUpdate deleteById = descriptor.deleteById(id, idList, deleteMode);
|
||||
if (t.isLogSummary()) {
|
||||
if (idList != null) {
|
||||
t.logSummary("-- Deleting {0} Ids: {1}", descriptor.name(), idList);
|
||||
abstract int deleteImported(BeanPropertyAssocOne<?>[] propImportDelete);
|
||||
|
||||
abstract SqlUpdate sqlDeleteChildren(BeanPropertyAssoc<?> prop);
|
||||
|
||||
abstract List<Object> findChildIds(BeanPropertyAssoc<?> prop, boolean includeSoftDeletes);
|
||||
|
||||
abstract int deleteBeans();
|
||||
}
|
||||
|
||||
private class DeleteBatchHelpSingle extends DeleteBatchHelp {
|
||||
private final Object id;
|
||||
|
||||
public DeleteBatchHelpSingle(BeanDescriptor<?> descriptor, Object id, Transaction transaction, DeleteMode deleteMode) {
|
||||
super(descriptor, transaction, deleteMode);
|
||||
this.id = id;
|
||||
}
|
||||
|
||||
int deleteImported(BeanPropertyAssocOne<?>[] propImportDelete) {
|
||||
// We actually need to execute a query to get the foreign key values
|
||||
// as they are required for the delete cascade. Query back just the
|
||||
// Id and the appropriate foreign key values
|
||||
Query<?> q = deleteRequiresQuery(descriptor, propImportDelete, deleteMode);
|
||||
q.where().idEq(id);
|
||||
if (transaction.isLogSummary()) {
|
||||
transaction.logSummary("-- DeleteById of {0} id[{1}] requires fetch of foreign key values", descriptor.name(), id);
|
||||
}
|
||||
EntityBean bean = (EntityBean) server.findOne(q, transaction);
|
||||
if (bean == null) {
|
||||
return 0;
|
||||
} else {
|
||||
t.logSummary("-- Deleting {0} Id: {1}", descriptor.name(), id);
|
||||
return deleteRecurse(bean, transaction, deleteMode);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
// use Id's to update L2 cache rather than Bulk table event
|
||||
notifyDeleteById(descriptor, id, idList, transaction);
|
||||
deleteById.setAutoTableMod(false);
|
||||
if (idList != null) {
|
||||
t.event().addDeleteByIdList(descriptor, idList);
|
||||
} else {
|
||||
t.event().addDeleteById(descriptor, id);
|
||||
@Override
|
||||
SqlUpdate sqlDeleteChildren(BeanPropertyAssoc<?> prop) {
|
||||
return prop.deleteByParentId(id);
|
||||
}
|
||||
int rows = executeSqlUpdate(deleteById, t);
|
||||
|
||||
// Delete from the persistence context so that it can't be fetched again later
|
||||
PersistenceContext persistenceContext = t.persistenceContext();
|
||||
if (idList != null) {
|
||||
@Override
|
||||
List<Object> findChildIds(BeanPropertyAssoc<?> prop, boolean includeSoftDeletes) {
|
||||
return prop.findIdsByParentId(id, transaction, includeSoftDeletes);
|
||||
}
|
||||
|
||||
int deleteBeans() {
|
||||
SqlUpdate deleteById = descriptor.deleteById(id, null, deleteMode);
|
||||
if (transaction.isLogSummary()) {
|
||||
transaction.logSummary("-- Deleting {0} Id: {1}", descriptor.name(), id);
|
||||
}
|
||||
|
||||
// use Id's to update L2 cache rather than Bulk table event
|
||||
notifyDeleteById(descriptor, id, null, transaction);
|
||||
deleteById.setAutoTableMod(false);
|
||||
transaction.event().addDeleteById(descriptor, id);
|
||||
|
||||
int rows = executeSqlUpdate(deleteById, transaction);
|
||||
|
||||
// Delete from the persistence context so that it can't be fetched again later
|
||||
PersistenceContext persistenceContext = transaction.persistenceContext();
|
||||
descriptor.contextDeleted(persistenceContext, id);
|
||||
return rows;
|
||||
}
|
||||
}
|
||||
|
||||
private class DeleteBatchHelpMultiple extends DeleteBatchHelp {
|
||||
private final List<Object> idList;
|
||||
|
||||
public DeleteBatchHelpMultiple(BeanDescriptor<?> descriptor, List<Object> idList, Transaction transaction, DeleteMode deleteMode) {
|
||||
super(descriptor, transaction, deleteMode);
|
||||
this.idList = idList;
|
||||
}
|
||||
|
||||
int deleteImported(BeanPropertyAssocOne<?>[] propImportDelete) {
|
||||
// We actually need to execute a query to get the foreign key values
|
||||
// as they are required for the delete cascade. Query back just the
|
||||
// Id and the appropriate foreign key values
|
||||
Query<?> q = deleteRequiresQuery(descriptor, propImportDelete, deleteMode);
|
||||
q.where().idIn(idList);
|
||||
if (transaction.isLogSummary()) {
|
||||
transaction.logSummary("-- DeleteById of {0} ids[{1}] requires fetch of foreign key values", descriptor.name(), idList);
|
||||
}
|
||||
List<?> beanList = server.findList(q, transaction);
|
||||
deleteCascade(beanList, transaction, deleteMode, false);
|
||||
return beanList.size();
|
||||
}
|
||||
|
||||
SqlUpdate sqlDeleteChildren(BeanPropertyAssoc<?> prop) {
|
||||
return prop.deleteByParentIdList(idList);
|
||||
}
|
||||
|
||||
@Override
|
||||
List<Object> findChildIds(BeanPropertyAssoc<?> prop, boolean includeSoftDeletes) {
|
||||
return prop.findIdsByParentIdList(idList, transaction, includeSoftDeletes);
|
||||
}
|
||||
|
||||
int deleteBeans() {
|
||||
SqlUpdate deleteById = descriptor.deleteById(null, idList, deleteMode);
|
||||
if (transaction.isLogSummary()) {
|
||||
transaction.logSummary("-- Deleting {0} Ids: {1}", descriptor.name(), idList);
|
||||
}
|
||||
|
||||
// use Id's to update L2 cache rather than Bulk table event
|
||||
notifyDeleteById(descriptor, null, idList, transaction);
|
||||
deleteById.setAutoTableMod(false);
|
||||
transaction.event().addDeleteByIdList(descriptor, idList);
|
||||
|
||||
int rows = executeSqlUpdate(deleteById, transaction);
|
||||
|
||||
// Delete from the persistence context so that it can't be fetched again later
|
||||
PersistenceContext persistenceContext = transaction.persistenceContext();
|
||||
for (Object idValue : idList) {
|
||||
descriptor.contextDeleted(persistenceContext, idValue);
|
||||
}
|
||||
} else {
|
||||
descriptor.contextDeleted(persistenceContext, id);
|
||||
return rows;
|
||||
}
|
||||
return rows;
|
||||
}
|
||||
|
||||
private void notifyDeleteById(BeanDescriptor<?> descriptor, Object id, List<Object> idList, Transaction transaction) {
|
||||
@@ -1006,12 +1082,13 @@ public final class DefaultPersister implements Persister {
|
||||
* </p>
|
||||
*/
|
||||
void deleteManyDetails(SpiTransaction t, BeanDescriptor<?> desc, EntityBean parentBean,
|
||||
BeanPropertyAssocMany<?> many, List<Object> excludeDetailIds, DeleteMode deleteMode) {
|
||||
BeanPropertyAssocMany<?> many, Set<Object> excludeDetailIds, DeleteMode deleteMode) {
|
||||
if (many.cascadeInfo().isDelete()) {
|
||||
// cascade delete the beans in the collection
|
||||
BeanDescriptor<?> targetDesc = many.targetDescriptor();
|
||||
if (deleteMode.isHard() || targetDesc.isSoftDelete()) {
|
||||
if (targetDesc.isDeleteByStatement()) {
|
||||
if (targetDesc.isDeleteByStatement()
|
||||
&& (excludeDetailIds == null || excludeDetailIds.size() <= maxDeleteBatch)) { // TODO wait for #3176
|
||||
// Just delete all the children with one statement
|
||||
IntersectionRow intRow = many.buildManyDeleteChildren(parentBean, excludeDetailIds);
|
||||
SqlUpdate sqlDelete = intRow.createDelete(server, deleteMode);
|
||||
@@ -1022,7 +1099,16 @@ public final class DefaultPersister implements Persister {
|
||||
// ... and only using findIdsByParentId() when the many property isn't loaded
|
||||
// Delete recurse using the Id values of the children
|
||||
Object parentId = desc.getId(parentBean);
|
||||
List<Object> idsByParentId = many.findIdsByParentId(parentId, null, t, excludeDetailIds, deleteMode.isHard());
|
||||
List<Object> idsByParentId;
|
||||
if (excludeDetailIds == null || excludeDetailIds.size() <= maxDeleteBatch) { // TODO: Wait for #3176
|
||||
idsByParentId = many.findIdsByParentId(parentId, t, deleteMode.isHard(), excludeDetailIds);
|
||||
} else {
|
||||
// if we hit the parameter limit, we must filter that on the java side.
|
||||
// There is no easy way to batch "not in" queries.
|
||||
// checkme: We could pass the first 1000-2000 params to the DB and filter the rest
|
||||
idsByParentId = many.findIdsByParentId(parentId, t, deleteMode.isHard(), null);
|
||||
idsByParentId.removeIf(id -> excludeDetailIds.contains(id));
|
||||
}
|
||||
if (!idsByParentId.isEmpty()) {
|
||||
deleteChildrenById(t, targetDesc, idsByParentId, deleteMode);
|
||||
}
|
||||
@@ -1046,7 +1132,7 @@ public final class DefaultPersister implements Persister {
|
||||
deleteCascade(refList, t, deleteMode, true);
|
||||
} else {
|
||||
// perform delete by statement if possible
|
||||
delete(targetDesc, null, childIds, t, deleteMode);
|
||||
delete(targetDesc, childIds, t, deleteMode);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -48,7 +48,7 @@ abstract class SaveManyBase implements SaveMany {
|
||||
final void preElementCollectionUpdate() {
|
||||
if (!insertedParent) {
|
||||
request.preElementCollectionUpdate();
|
||||
persister.addToFlushQueue(many.deleteByParentId(request.beanId(), null), transaction, BatchControl.DELETE_QUEUE);
|
||||
persister.addToFlushQueue(many.deleteByParentId(request.beanId()), transaction, BatchControl.DELETE_QUEUE);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -9,7 +9,10 @@ import io.ebeaninternal.server.core.PersistRequestBean;
|
||||
import io.ebeaninternal.server.deploy.*;
|
||||
|
||||
import javax.persistence.PersistenceException;
|
||||
import java.util.*;
|
||||
import java.util.Collection;
|
||||
import java.util.HashSet;
|
||||
import java.util.Map;
|
||||
import java.util.Set;
|
||||
|
||||
import static io.ebeaninternal.server.persist.DmlUtil.isNullOrZero;
|
||||
import static java.lang.System.Logger.Level.WARNING;
|
||||
@@ -205,9 +208,10 @@ final class SaveManyBeans extends SaveManyBase {
|
||||
|
||||
/**
|
||||
* Return the Id values of beans we know are being updated (any others are orphans)
|
||||
* If there are no IDs, null is returned.
|
||||
*/
|
||||
private List<Object> detailIds() {
|
||||
final var detailIds = new ArrayList<>();
|
||||
private Set<Object> detailIds() {
|
||||
final var detailIds = new HashSet<>();
|
||||
for (Object detailBean : collection) {
|
||||
if (isMap) {
|
||||
detailBean = ((Map.Entry<?, ?>) detailBean).getValue();
|
||||
@@ -222,7 +226,7 @@ final class SaveManyBeans extends SaveManyBase {
|
||||
}
|
||||
}
|
||||
}
|
||||
return detailIds;
|
||||
return detailIds.isEmpty() ? null : detailIds;
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -7,56 +7,147 @@ import org.junit.jupiter.api.Test;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.function.Predicate;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
import static java.util.Objects.requireNonNull;
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||
|
||||
class TestOrphanCollectionReplacement extends BaseTestCase {
|
||||
|
||||
@Test
|
||||
void replaceCollection_whenOrphan_expect_forcedInsert() {
|
||||
long parentId;
|
||||
{ // setup
|
||||
List<COOneMany> children = new ArrayList<>();
|
||||
children.add(new COOneMany("c0"));
|
||||
children.add(new COOneMany("c1"));
|
||||
|
||||
COOne parent = new COOne("p0");
|
||||
parent.setChildren(children);
|
||||
|
||||
DB.save(parent);
|
||||
parentId = parent.getId();
|
||||
void replaceCollection_whenOrphan_expect_forcedInsertWithStatement() {
|
||||
long parentId = setup(1000); // can be handled by statement for SqlServer
|
||||
List<String> sql = doUpdate(parentId, name -> !"c0".equals(name));
|
||||
if (isH2() || isPostgresCompatible()) { // using deleted=true vs deleted=1
|
||||
assertThat(sql).hasSize(4);
|
||||
assertThat(sql.get(0)).contains("update coone_many set deleted=true where coone_id = ? and not ( id ");
|
||||
assertThat(sql.get(1)).contains(" -- bind(");
|
||||
assertThat(sql.get(2)).contains("insert into coone_many (coone_id, name, deleted) values (?,?,?)");
|
||||
assertThat(sql.get(3)).contains(" -- bind(");
|
||||
}
|
||||
|
||||
{ // act
|
||||
COOne fetchedParent = DB.find(COOne.class, parentId);
|
||||
assert fetchedParent != null;
|
||||
if (isSqlServer()) {
|
||||
// statement mode
|
||||
assertThat(sql).hasSize(4);
|
||||
assertThat(sql.get(0)).contains("update coone_many set deleted=1 where coone_id = ? and not ( id ");
|
||||
assertThat(sql.get(1)).contains(" -- bind(");
|
||||
assertThat(sql.get(2)).contains("insert into coone_many (id, coone_id, name, deleted) values (?,?,?,?)");
|
||||
assertThat(sql.get(3)).contains(" -- bind(");
|
||||
}
|
||||
COOne fetchedUser2 = DB.find(COOne.class, parentId);
|
||||
requireNonNull(fetchedUser2);
|
||||
assertThat(fetchedUser2.getChildren())
|
||||
.hasSize(1000)
|
||||
.extracting(COOneMany::getName)
|
||||
.doesNotContain("c0")// filtered
|
||||
.contains("c1")
|
||||
.contains("cTest"); // added
|
||||
}
|
||||
|
||||
COOneMany role = new COOneMany("c2");
|
||||
@Test
|
||||
void replaceCollection_whenOrphan_expect_forcedInsertWithFilter() {
|
||||
long parentId = setup(2500); // we cannot make a "not in" query for so many params
|
||||
List<String> sql = doUpdate(parentId, name -> !"c0".equals(name));
|
||||
if (isH2() || isPostgresCompatible()) { // using deleted=true vs deleted=1
|
||||
// CHECKME: H2 would not require the batch mode here and could theoretically do it in fewer statements
|
||||
assertThat(sql).hasSize(5);
|
||||
assertThat(sql.get(0)).contains("select t0.id from coone_many t0 where coone_id=? and t0.deleted = false and t0.deleted = false; --bind");
|
||||
assertThat(sql.get(1)).contains("update coone_many set deleted=true where id in (?)");
|
||||
assertThat(sql.get(2)).contains(" -- bind(");
|
||||
assertThat(sql.get(3)).contains("insert into coone_many (coone_id, name, deleted) values (?,?,?)");
|
||||
assertThat(sql.get(4)).contains(" -- bind(");
|
||||
}
|
||||
|
||||
List<COOneMany> filtered = fetchedParent.getChildren().stream().filter(r -> "c0".equals(r.getName())).collect(Collectors.toList());
|
||||
if (isSqlServer()) {
|
||||
// filter mode
|
||||
assertThat(sql).hasSize(5);
|
||||
assertThat(sql.get(0)).contains("select t0.id from coone_many t0 where coone_id=? and t0.deleted = 0 and t0.deleted = 0; --bind");
|
||||
assertThat(sql.get(1)).contains("update coone_many set deleted=1 where id in (?)");
|
||||
assertThat(sql.get(2)).contains(" -- bind(");
|
||||
assertThat(sql.get(3)).contains("insert into coone_many (id, coone_id, name, deleted) values (?,?,?,?)");
|
||||
assertThat(sql.get(4)).contains(" -- bind(");
|
||||
}
|
||||
COOne fetchedUser2 = DB.find(COOne.class, parentId);
|
||||
requireNonNull(fetchedUser2);
|
||||
assertThat(fetchedUser2.getChildren())
|
||||
.hasSize(2500)
|
||||
.extracting(COOneMany::getName)
|
||||
.doesNotContain("c0")// filtered
|
||||
.contains("c1")
|
||||
.contains("cTest"); // added
|
||||
}
|
||||
|
||||
List<COOneMany> updatedRoles = new ArrayList<>();
|
||||
updatedRoles.addAll(filtered);
|
||||
updatedRoles.addAll(List.of(role));
|
||||
fetchedParent.setChildren(updatedRoles);
|
||||
@Test
|
||||
void replaceCollection_whenOrphan_expect_forcedInsertWithManyReplacement() {
|
||||
long parentId = setup(5000); // we will replace 2500 beans in this step
|
||||
List<String> sql = doUpdate(parentId, name -> Integer.parseInt(name.substring(1)) >= 2500);
|
||||
if (isH2() || isPostgresCompatible()) { // using deleted=true vs deleted=1
|
||||
// CHECKME: H2 would not require the batch mode here and could theoretically do it in fewer statements
|
||||
assertThat(sql).hasSize(8);
|
||||
assertThat(sql.get(0)).contains("select t0.id from coone_many t0 where coone_id=? and t0.deleted = false and t0.deleted = false; --bind"); // find all Ids
|
||||
assertThat(sql.get(1)).contains("update coone_many set deleted=true where id in (?,?,?");
|
||||
assertThat(sql.get(2)).contains(" -- bind(Array[1000]="); // update first 1000
|
||||
assertThat(sql.get(3)).contains(" -- bind(Array[1000]="); // update second 1000
|
||||
assertThat(sql.get(4)).contains("update coone_many set deleted=true where id in (?,?,?");
|
||||
assertThat(sql.get(5)).contains(" -- bind(Array[500]="); // update last 500
|
||||
assertThat(sql.get(6)).contains("insert into coone_many (coone_id, name, deleted) values (?,?,?)");
|
||||
assertThat(sql.get(7)).contains(" -- bind(");
|
||||
}
|
||||
|
||||
LoggedSql.start();
|
||||
DB.save(fetchedParent);
|
||||
var sql = LoggedSql.stop();
|
||||
if (isH2() || isPostgresCompatible()) { // using deleted=true vs deleted=1
|
||||
assertThat(sql).hasSize(4);
|
||||
assertThat(sql.get(0)).contains("update coone_many set deleted=true where coone_id = ? and not ( id ");
|
||||
assertThat(sql.get(1)).contains(" -- bind(");
|
||||
assertThat(sql.get(2)).contains("insert into coone_many (coone_id, name, deleted) values (?,?,?)");
|
||||
assertThat(sql.get(3)).contains(" -- bind(");
|
||||
}
|
||||
if (isSqlServer()) {
|
||||
assertThat(sql).hasSize(7);
|
||||
assertThat(sql.get(0)).contains("select t0.id from coone_many t0 where coone_id=? and t0.deleted = 0 and t0.deleted = 0; --bind"); // find all Ids
|
||||
assertThat(sql.get(1)).contains("update coone_many set deleted=1 where id in (?,?,?");
|
||||
assertThat(sql.get(2)).contains(" -- bind(Array[2000]="); // update first 2000
|
||||
assertThat(sql.get(3)).contains("update coone_many set deleted=1 where id in (?,?,?");
|
||||
assertThat(sql.get(4)).contains(" -- bind(Array[500]="); // update next 500
|
||||
assertThat(sql.get(5)).contains("insert into coone_many (id, coone_id, name, deleted) values (?,?,?,?)");
|
||||
assertThat(sql.get(6)).contains(" -- bind(");
|
||||
}
|
||||
|
||||
COOne fetchedUser2 = DB.find(COOne.class, parentId);
|
||||
requireNonNull(fetchedUser2);
|
||||
assertEquals(2, fetchedUser2.getChildren().size());
|
||||
assertThat(fetchedUser2.getChildren())
|
||||
.hasSize(2501)
|
||||
.extracting(COOneMany::getName)
|
||||
.doesNotContain("c0")// filtered
|
||||
.contains("c2500")
|
||||
.contains("cTest"); // added
|
||||
}
|
||||
|
||||
|
||||
private static List<String> doUpdate(long parentId, Predicate<String> filter) {
|
||||
COOne fetchedParent = DB.find(COOne.class, parentId);
|
||||
assert fetchedParent != null;
|
||||
|
||||
|
||||
List<COOneMany> filtered = fetchedParent.getChildren().stream().filter(r -> filter.test(r.getName())).collect(Collectors.toList());
|
||||
|
||||
List<COOneMany> updatedRoles = new ArrayList<>();
|
||||
updatedRoles.addAll(filtered);
|
||||
updatedRoles.add(new COOneMany("cTest"));
|
||||
fetchedParent.setChildren(updatedRoles);
|
||||
|
||||
LoggedSql.start();
|
||||
DB.save(fetchedParent);
|
||||
return LoggedSql.stop();
|
||||
}
|
||||
|
||||
private static long setup(int count) {
|
||||
long parentId;
|
||||
// setup
|
||||
List<COOneMany> children = new ArrayList<>();
|
||||
for (int i = 0; i < count; i++) {
|
||||
children.add(new COOneMany("c" + i));
|
||||
|
||||
}
|
||||
|
||||
COOne parent = new COOne("p0");
|
||||
parent.setChildren(children);
|
||||
|
||||
DB.save(parent);
|
||||
parentId = parent.getId();
|
||||
return parentId;
|
||||
}
|
||||
}
|
||||
|
||||
+11
-11
@@ -1,10 +1,10 @@
|
||||
package org.tests.compositekeys;
|
||||
|
||||
import io.ebean.xtest.BaseTestCase;
|
||||
import io.ebean.CountDistinctOrder;
|
||||
import io.ebean.DB;
|
||||
import io.ebean.Query;
|
||||
import io.ebean.annotation.Identity;
|
||||
import io.ebean.xtest.BaseTestCase;
|
||||
import io.ebeaninternal.api.SpiEbeanServer;
|
||||
import io.ebeaninternal.server.deploy.BeanDescriptor;
|
||||
import io.ebeaninternal.server.deploy.BeanPropertyAssocMany;
|
||||
@@ -82,8 +82,8 @@ public class TestOnCascadeDeleteChildrenWithCompositeKeys extends BaseTestCase {
|
||||
ids.add(1L);
|
||||
ids.add(2L);
|
||||
|
||||
beanProperty.findIdsByParentId(null, ids, null, null, true);
|
||||
beanProperty.findIdsByParentId(1L, null, null, null, true);
|
||||
beanProperty.findIdsByParentIdList(ids, null, true);
|
||||
beanProperty.findIdsByParentId(1L, null, true);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -107,18 +107,18 @@ public class TestOnCascadeDeleteChildrenWithCompositeKeys extends BaseTestCase {
|
||||
|
||||
if (isH2() || isMariaDB() || isPostgresCompatible()) {
|
||||
assertThat(query1.getGeneratedSql()).contains("select distinct r1.attribute_, count(*) from "
|
||||
+ "(select distinct t0.user_id, t0.role_id, t1.name as attribute_ "
|
||||
+ "from em_user_role t0 join em_user t1 on t1.id = t0.user_id) r1 "
|
||||
+ "group by r1.attribute_ order by count(*) desc, r1.attribute_ limit 20");
|
||||
+ "(select distinct t0.user_id, t0.role_id, t1.name as attribute_ "
|
||||
+ "from em_user_role t0 join em_user t1 on t1.id = t0.user_id) r1 "
|
||||
+ "group by r1.attribute_ order by count(*) desc, r1.attribute_ limit 20");
|
||||
} else if (isDb2()) {
|
||||
assertThat(query1.getGeneratedSql()).contains("select distinct r1.attribute_, count(*) from "
|
||||
+ "(select distinct t0.user_id, t0.role_id, t1.name as attribute_ "
|
||||
+ "from em_user_role t0 join em_user t1 on t1.id = t0.user_id) r1 "
|
||||
+ "group by r1.attribute_ order by count(*) desc, r1.attribute_ fetch next 20 rows only");
|
||||
+ "(select distinct t0.user_id, t0.role_id, t1.name as attribute_ "
|
||||
+ "from em_user_role t0 join em_user t1 on t1.id = t0.user_id) r1 "
|
||||
+ "group by r1.attribute_ order by count(*) desc, r1.attribute_ fetch next 20 rows only");
|
||||
} else if (isSqlServer()) {
|
||||
assertThat(query1.getGeneratedSql()).contains("select distinct top 20 r1.attribute_, count(*) "
|
||||
+ "from (select distinct t0.user_id, t0.role_id, t1.name as attribute_ from em_user_role t0 "
|
||||
+ "join em_user t1 on t1.id = t0.user_id) r1 group by r1.attribute_ order by count(*) desc, r1.attribute_");
|
||||
+ "from (select distinct t0.user_id, t0.role_id, t1.name as attribute_ from em_user_role t0 "
|
||||
+ "join em_user t1 on t1.id = t0.user_id) r1 group by r1.attribute_ order by count(*) desc, r1.attribute_");
|
||||
} else {
|
||||
// no Oracle test yet
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user