Please wait. This can take some minutes ...
Many resources are needed to download a project. Please understand that we have to compensate our server costs. Thank you in advance.
Project price only 1 $
You can buy this project and download/modify it how often you want.
com.avaje.ebeaninternal.server.persist.DefaultPersister Maven / Gradle / Ivy
package com.avaje.ebeaninternal.server.persist;
import com.avaje.ebean.CallableSql;
import com.avaje.ebean.Query;
import com.avaje.ebean.SqlUpdate;
import com.avaje.ebean.Transaction;
import com.avaje.ebean.Update;
import com.avaje.ebean.bean.BeanCollection;
import com.avaje.ebean.bean.BeanCollection.ModifyListenMode;
import com.avaje.ebean.bean.EntityBean;
import com.avaje.ebean.bean.EntityBeanIntercept;
import com.avaje.ebean.bean.PersistenceContext;
import com.avaje.ebeaninternal.api.SpiEbeanServer;
import com.avaje.ebeaninternal.api.SpiTransaction;
import com.avaje.ebeaninternal.api.SpiUpdate;
import com.avaje.ebeaninternal.server.core.Message;
import com.avaje.ebeaninternal.server.core.PersistRequest;
import com.avaje.ebeaninternal.server.core.PersistRequest.Type;
import com.avaje.ebeaninternal.server.core.PersistRequestBean;
import com.avaje.ebeaninternal.server.core.PersistRequestCallableSql;
import com.avaje.ebeaninternal.server.core.PersistRequestOrmUpdate;
import com.avaje.ebeaninternal.server.core.PersistRequestUpdateSql;
import com.avaje.ebeaninternal.server.core.Persister;
import com.avaje.ebeaninternal.server.deploy.BeanCollectionUtil;
import com.avaje.ebeaninternal.server.deploy.BeanDescriptor;
import com.avaje.ebeaninternal.server.deploy.BeanDescriptorManager;
import com.avaje.ebeaninternal.server.deploy.BeanManager;
import com.avaje.ebeaninternal.server.deploy.BeanProperty;
import com.avaje.ebeaninternal.server.deploy.BeanPropertyAssocMany;
import com.avaje.ebeaninternal.server.deploy.BeanPropertyAssocOne;
import com.avaje.ebeaninternal.server.deploy.IntersectionRow;
import com.avaje.ebeaninternal.server.deploy.ManyType;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import javax.persistence.PersistenceException;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.List;
import java.util.Map;
import java.util.Set;
/**
* Persister implementation using DML.
*
* This object uses DmlPersistExecute to perform the actual persist execution.
*
*
* This object:
*
* Determines insert or update for saved beans
* Determines the concurrency mode
* Handles cascading of save and delete
* Handles the batching and queueing
*
*
* @see com.avaje.ebeaninternal.server.persist.DefaultPersistExecute
*/
public final class DefaultPersister implements Persister {
private static final Logger PUB = LoggerFactory.getLogger("org.avaje.ebean.PUB");
private static final Logger logger = LoggerFactory.getLogger(DefaultPersister.class);
/**
* Actually does the persisting work.
*/
private final PersistExecute persistExecute;
private final SpiEbeanServer server;
private final BeanDescriptorManager beanDescriptorManager;
private final boolean updatesDeleteMissingChildren;
public DefaultPersister(SpiEbeanServer server, Binder binder, BeanDescriptorManager descMgr) {
this.server = server;
this.updatesDeleteMissingChildren = server.getServerConfig().isUpdatesDeleteMissingChildren();
this.beanDescriptorManager = descMgr;
this.persistExecute = new DefaultPersistExecute(binder, server.getServerConfig().getPersistBatchSize());
}
/**
* Execute the CallableSql.
*/
public int executeCallable(CallableSql callSql, Transaction t) {
return executeOrQueue(new PersistRequestCallableSql(server, callSql, (SpiTransaction) t, persistExecute));
}
/**
* Execute the orm update.
*/
public int executeOrmUpdate(Update> update, Transaction t) {
SpiUpdate> ormUpdate = (SpiUpdate>) update;
BeanManager> mgr = beanDescriptorManager.getBeanManager(ormUpdate.getBeanType());
if (mgr == null) {
String msg = "No BeanManager found for type [" + ormUpdate.getBeanType() + "]. Is it an entity?";
throw new PersistenceException(msg);
}
return executeOrQueue(new PersistRequestOrmUpdate(server, mgr, ormUpdate, (SpiTransaction) t, persistExecute));
}
private int executeOrQueue(PersistRequest request) {
try {
request.initTransIfRequired();
int rc = request.executeOrQueue();
request.commitTransIfRequired();
return rc;
} catch (RuntimeException e) {
request.rollbackTransIfRequired();
throw e;
}
}
/**
* Execute the updateSql.
*/
public int executeSqlUpdate(SqlUpdate updSql, Transaction t) {
return executeOrQueue(new PersistRequestUpdateSql(server, updSql, (SpiTransaction) t, persistExecute));
}
/**
* Restore draft beans to match live beans given the query.
*/
@Override
public List draftRestore(Query query, Transaction transaction) {
Class beanType = query.getBeanType();
BeanDescriptor desc = server.getBeanDescriptor(beanType);
DraftHandler draftHandler = new DraftHandler(desc, transaction);
List liveBeans = draftHandler.fetchSourceBeans(query, false);
PUB.debug("draftRestore [{}] count[{}]", desc.getName(), liveBeans.size());
if (liveBeans.isEmpty()) {
return Collections.emptyList();
}
draftHandler.fetchDestinationBeans(liveBeans, true);
BeanManager mgr = beanDescriptorManager.getBeanManager(beanType);
for (T liveBean: liveBeans) {
T draftBean = draftHandler.publishToDestinationBean(liveBean);
// reset @DraftDirty and @DraftReset properties
draftHandler.resetDraft(draftBean);
PUB.trace("draftRestore bean [{}] id[{}]", desc.getName(), draftHandler.getId());
update(createRequest(draftBean, transaction, null, mgr, Type.UPDATE, true, false));
}
PUB.debug("draftRestore - complete for [{}]", desc.getName());
return draftHandler.getDrafts();
}
/**
* Helper method to return the list of Id values for the list of beans.
*/
private List getBeanIds(BeanDescriptor desc, List beans) {
List idList = new ArrayList();
for (T liveBean: beans) {
idList.add(desc.getBeanId(liveBean));
}
return idList;
}
/**
* Publish from draft to live given the query.
*/
@Override
public List publish(Query query, Transaction transaction) {
Class beanType = query.getBeanType();
BeanDescriptor desc = server.getBeanDescriptor(beanType);
DraftHandler draftHandler = new DraftHandler(desc, transaction);
List draftBeans = draftHandler.fetchSourceBeans(query, true);
PUB.debug("publish [{}] count[{}]", desc.getName(), draftBeans.size());
if (draftBeans.isEmpty()) {
return Collections.emptyList();
}
draftHandler.fetchDestinationBeans(draftBeans, false);
BeanManager mgr = beanDescriptorManager.getBeanManager(beanType);
List livePublish = new ArrayList(draftBeans.size());
for (T draftBean: draftBeans) {
T liveBean = draftHandler.publishToDestinationBean(draftBean);
livePublish.add(liveBean);
// reset @DraftDirty and @DraftReset properties
draftHandler.resetDraft(draftBean);
Type persistType = draftHandler.isInsert() ? Type.INSERT : Type.UPDATE;
PUB.trace("publish bean [{}] id[{}] type[{}]", desc.getName(), draftHandler.getId(), persistType);
PersistRequestBean request = createRequest(liveBean, transaction, null, mgr, persistType, true, true);
if (persistType == Type.INSERT) {
insert(request);
} else {
update(request);
}
}
draftHandler.updateDrafts(transaction, mgr);
PUB.debug("publish - complete for [{}]", desc.getName());
return livePublish;
}
/**
* Helper to handle draft beans (properties reset etc).
*/
class DraftHandler {
final BeanDescriptor desc;
final Transaction transaction;
final BeanProperty draftDirty;
final List draftUpdates = new ArrayList();
/**
* Id value of the last published bean.
*/
Object id;
/**
* True if the last published bean is new/insert.
*/
boolean insert;
/**
* The destination beans to publish/restore to mapped by id.
*/
Map, T> destBeans;
DraftHandler(BeanDescriptor desc, Transaction transaction) {
this.desc = desc;
this.transaction = transaction;
this.draftDirty = desc.getDraftDirty();
}
/**
* Return the list of draft beans with changes (to be persisted).
*/
List getDrafts() {
return draftUpdates;
}
/**
* Set the draft dirty state to false and reset any dirtyReset properties.
*/
void resetDraft(T draftBean) {
if (desc.draftReset(draftBean)) {
// draft bean is dirty so collect it for persisting later
draftUpdates.add(draftBean);
}
}
/**
* Save all the draft beans (with various properties reset etc).
*/
void updateDrafts(Transaction transaction, BeanManager mgr) {
if (!draftUpdates.isEmpty()) {
// update the dirty status on the drafts that have been published
PUB.debug("publish - update dirty status on [{}] drafts", draftUpdates.size());
for (T draftUpdate : draftUpdates) {
update(createRequest(draftUpdate, transaction, null, mgr, Type.UPDATE, false, false));
}
}
}
/**
* Fetch the source beans based on the query.
*/
List fetchSourceBeans(Query query, boolean asDraft) {
desc.draftQueryOptimise(query);
if (asDraft) {
query.asDraft();
}
return server.findList(query, transaction);
}
/**
* Fetch the destination beans that will be published to.
*/
void fetchDestinationBeans(List sourceBeans, boolean asDraft) {
List ids = getBeanIds(desc, sourceBeans);
Query destQuery = server.find(desc.getBeanType()).where().idIn(ids).query();
if (asDraft) {
destQuery.asDraft();
}
desc.draftQueryOptimise(destQuery);
this.destBeans = server.findMap(destQuery, transaction);
}
/**
* Publish/restore the values from the sourceBean to the matching destination bean.
*/
T publishToDestinationBean(T sourceBean) {
id = desc.getBeanId(sourceBean);
T destBean = destBeans.get(id);
insert = (destBean == null);
// apply changes from liveBean to draftBean
return desc.publish(sourceBean, destBean);
}
/**
* Return true if the last publish resulted in an new bean to insert.
*/
boolean isInsert() {
return insert;
}
/**
* Return the Id value of the last published/restored bean.
*/
Object getId() {
return id;
}
}
/**
* Recursively delete the bean. This calls back to the EbeanServer.
*/
private void deleteRecurse(EntityBean detailBean, Transaction t, boolean softDelete) {
Type deleteType = softDelete ? Type.SOFT_DELETE : Type.DELETE_PERMANENT;
deleteRequest(createRequest(detailBean, t, deleteType));
}
/**
* Update the bean.
*/
public void update(EntityBean entityBean, Transaction t) {
update(entityBean, t, updatesDeleteMissingChildren);
}
/**
* Update the bean specifying deleteMissingChildren.
*/
public void update(EntityBean entityBean, Transaction t, boolean deleteMissingChildren) {
PersistRequestBean> req = createRequest(entityBean, t, PersistRequest.Type.UPDATE);
req.setDeleteMissingChildren(deleteMissingChildren);
req.checkDraft();
try {
req.initTransIfRequiredWithBatchCascade();
if (req.isReference()) {
// its a reference so see if there are manys to save...
if (req.isPersistCascade()) {
saveAssocMany(false, req, false);
}
req.checkUpdatedManysOnly();
} else {
update(req);
}
req.commitTransIfRequired();
req.flushBatchOnCascade();
} catch (RuntimeException ex) {
req.rollbackTransIfRequired();
throw ex;
}
}
/**
* Insert or update the bean.
*/
public void save(EntityBean bean, Transaction t) {
if (bean._ebean_getIntercept().isLoaded()) {
// deleteMissingChildren is false when using 'save' on 'loaded' beans
update(bean, t, false);
} else {
insert(bean, t);
}
}
/**
* Insert this bean.
*/
public void insert(EntityBean bean, Transaction t) {
PersistRequestBean> req = createRequest(bean, t, PersistRequest.Type.INSERT);
try {
req.initTransIfRequiredWithBatchCascade();
insert(req);
req.commitTransIfRequired();
req.flushBatchOnCascade();
} catch (RuntimeException ex) {
req.rollbackTransIfRequired();
throw ex;
}
}
private void saveRecurse(EntityBean bean, Transaction t, Object parentBean, boolean insertMode, boolean publish) {
// determine insert or update taking into account stateless updates
PersistRequestBean> request = createRequestRecurse(bean, t, parentBean, insertMode, publish);
if (request.isReference()) {
// its a reference...
if (request.isPersistCascade()) {
// save any associated List held beans
saveAssocMany(false, request, insertMode);
}
request.checkUpdatedManysOnly();
} else {
if (request.isInsert()) {
insert(request);
} else {
update(request);
}
}
}
/**
* Insert the bean.
*/
private void insert(PersistRequestBean> request) {
if (request.isRegisteredBean()) {
// skip as already inserted/updated in this request (recursive cascading)
return;
}
try {
if (request.isPersistCascade()) {
// save associated One beans recursively first
saveAssocOne(request, true);
}
// set the IDGenerated value if required
setIdGenValue(request);
request.executeOrQueue();
if (request.isPersistCascade()) {
// save any associated List held beans
saveAssocMany(true, request, true);
}
} finally {
request.unRegisterBean();
}
}
/**
* Update the bean.
*/
private void update(PersistRequestBean> request) {
if (request.isRegisteredBean()) {
// skip as already inserted/updated in this request (recursive cascading)
return;
}
try {
if (request.isPersistCascade()) {
// save associated One beans recursively first
saveAssocOne(request, false);
}
if (request.isDirty()) {
request.executeOrQueue();
} else {
// skip validation on unchanged bean
if (logger.isDebugEnabled()) {
logger.debug(Message.msg("persist.update.skipped", request.getBean()));
}
}
if (request.isPersistCascade()) {
// save all the beans in assocMany's after
saveAssocMany(false, request, false);
}
request.checkUpdatedManysOnly();
} finally {
request.unRegisterBean();
}
}
/**
* Delete the bean with the explicit transaction.
* Return false if the delete is executed without OCC and 0 rows were deleted.
*/
public boolean delete(EntityBean bean, Transaction t, boolean permanent) {
Type deleteType = permanent ? Type.DELETE_PERMANENT : Type.DELETE;
PersistRequestBean originalRequest = createRequest(bean, t, deleteType);
if (originalRequest.isHardDeleteDraft()) {
// a hard delete of a draftable bean so first we need to delete the associated 'live' bean
// due to FK constraint and then after that execute the original delete of the draft bean
return deleteRequest(createPublishRequest(originalRequest.createReference(), t, Type.DELETE_PERMANENT, true), originalRequest);
} else {
// normal delete or soft delete
return deleteRequest(originalRequest);
}
}
/**
* Execute the delete request returning true if a delete occurred.
*/
private boolean deleteRequest(PersistRequestBean> req) {
return deleteRequest(req, null);
}
/**
* Execute the delete request support a second delete request for live and draft permanent delete.
* A common transaction is used across both requests.
*/
private boolean deleteRequest(PersistRequestBean> req, PersistRequestBean> draftReq) {
if (req.isRegisteredForDeleteBean()) {
// skip deleting bean. Used where cascade is on
// both sides of a relationship
if (logger.isDebugEnabled()) {
logger.debug("skipping delete on alreadyRegistered " + req.getBean());
}
return false;
}
try {
req.initTransIfRequiredWithBatchCascade();
boolean deleted = delete(req);
if (draftReq != null) {
// delete the 'draft' bean ('live' bean deleted first)
draftReq.setTrans(req.getTransaction());
deleted = delete(draftReq);
}
req.commitTransIfRequired();
req.flushBatchOnCascade();
return deleted;
} catch (RuntimeException ex) {
req.rollbackTransIfRequired();
throw ex;
}
}
private void deleteList(List> beanList, Transaction t, boolean softDelete) {
for (int i = 0; i < beanList.size(); i++) {
deleteRecurse((EntityBean) beanList.get(i), t, softDelete);
}
}
/**
* Delete by a List of Id's.
*/
public int deleteMany(Class> beanType, Collection> ids, Transaction transaction, boolean permanent) {
if (ids == null || ids.isEmpty()) {
return 0;
}
BeanDescriptor> descriptor = beanDescriptorManager.getBeanDescriptor(beanType);
ArrayList idList = new ArrayList(ids.size());
for (Object id : ids) {
// convert to appropriate type if required
idList.add(descriptor.convertId(id));
}
boolean softDelete = !permanent && descriptor.isSoftDelete();
return delete(descriptor, null, idList, transaction, softDelete);
}
/**
* Delete by Id.
*/
public int delete(Class> beanType, Object id, Transaction transaction, boolean permanent) {
BeanDescriptor> descriptor = beanDescriptorManager.getBeanDescriptor(beanType);
id = descriptor.convertId(id);
boolean softDelete = !permanent && descriptor.isSoftDelete();
return delete(descriptor, id, null, transaction, softDelete);
}
/**
* Delete by Id or a List of Id's.
*/
private int delete(BeanDescriptor> descriptor, Object id, List idList, Transaction transaction, boolean softDelete) {
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, softDelete);
if (idList != null) {
q.where().idIn(idList);
if (t.isLogSummary()) {
t.logSummary("-- DeleteById of " + descriptor.getName() + " ids[" + idList + "] requires fetch of foreign key values");
}
List> beanList = server.findList(q, t);
deleteList(beanList, t, softDelete);
return beanList.size();
} else {
q.where().idEq(id);
if (t.isLogSummary()) {
t.logSummary("-- DeleteById of " + descriptor.getName() + " id[" + id + "] requires fetch of foreign key values");
}
EntityBean bean = (EntityBean) server.findUnique(q, t);
if (bean == null) {
return 0;
} else {
deleteRecurse(bean, t, softDelete);
return 1;
}
}
}
}
if (t.isPersistCascade()) {
// OneToOne exported side with delete cascade
BeanPropertyAssocOne>[] expOnes = descriptor.propertiesOneExportedDelete();
for (int i = 0; i < expOnes.length; i++) {
BeanDescriptor> targetDesc = expOnes[i].getTargetDescriptor();
// only cascade soft deletes when supported by target
if (!softDelete || targetDesc.isSoftDelete()) {
if (!softDelete && targetDesc.isDeleteByStatement()) {
SqlUpdate sqlDelete = expOnes[i].deleteByParentId(id, idList);
executeSqlUpdate(sqlDelete, t);
} else {
List childIds = expOnes[i].findIdsByParentId(id, idList, t);
deleteChildrenById(t, targetDesc, childIds, softDelete);
}
}
}
// OneToMany's with delete cascade
BeanPropertyAssocMany>[] manys = descriptor.propertiesManyDelete();
for (int i = 0; i < manys.length; i++) {
BeanDescriptor> targetDesc = manys[i].getTargetDescriptor();
// only cascade soft deletes when supported by target
if (!softDelete || targetDesc.isSoftDelete()) {
if (!softDelete && targetDesc.isDeleteByStatement()) {
// we can just delete children with a single statement
SqlUpdate sqlDelete = manys[i].deleteByParentId(id, idList);
executeSqlUpdate(sqlDelete, t);
} else {
// we need to fetch the Id's to delete (recurse or notify L2 cache)
List childIds = manys[i].findIdsByParentId(id, idList, t, null);
if (!childIds.isEmpty()) {
delete(targetDesc, null, childIds, t, softDelete);
}
}
}
}
}
if (!softDelete) {
// ManyToMany's ... delete from intersection table
BeanPropertyAssocMany>[] manys = descriptor.propertiesManyToMany();
for (int i = 0; i < manys.length; i++) {
SqlUpdate sqlDelete = manys[i].deleteByParentId(id, idList);
if (t.isLogSummary()) {
t.logSummary("-- Deleting intersection table entries: " + manys[i].getFullBeanName());
}
executeSqlUpdate(sqlDelete, t);
}
}
// delete the bean(s)
SqlUpdate deleteById = descriptor.deleteById(id, idList, softDelete);
if (t.isLogSummary()) {
if (idList != null) {
t.logSummary("-- Deleting " + descriptor.getName() + " Ids: " + idList);
} else {
t.logSummary("-- Deleting " + descriptor.getName() + " Id: " + id);
}
}
// use Id's to update L2 cache rather than Bulk table event
deleteById.setAutoTableMod(false);
if (idList != null) {
t.getEvent().addDeleteByIdList(descriptor, idList);
} else {
t.getEvent().addDeleteById(descriptor, id);
}
int rows = executeSqlUpdate(deleteById, t);
// Delete from the persistence context so that it can't be fetched again later
PersistenceContext persistenceContext = t.getPersistenceContext();
if (idList != null) {
for (Object idValue : idList) {
descriptor.contextDeleted(persistenceContext, idValue);
}
} else {
descriptor.contextDeleted(persistenceContext, id);
}
return rows;
}
/**
* We need to create and execute a query to get the foreign key values as
* the delete cascades to them (foreign keys).
*/
private Query> deleteRequiresQuery(BeanDescriptor> desc, BeanPropertyAssocOne>[] propImportDelete, boolean softDelete) {
Query> q = server.createQuery(desc.getBeanType());
StringBuilder sb = new StringBuilder(30);
for (int i = 0; i < propImportDelete.length; i++) {
sb.append(propImportDelete[i].getName()).append(",");
}
q.setAutoTune(false);
q.select(sb.toString());
if (!softDelete) {
// hard delete so we want this query to include logically deleted rows (if any)
q.setIncludeSoftDeletes();
}
return q;
}
/**
* Delete the bean.
*
* Note that preDelete fires before the deletion of children.
*
*/
private boolean delete(PersistRequestBean> request) {
DeleteUnloadedForeignKeys unloadedForeignKeys = null;
if (request.isPersistCascade()) {
// delete children first ... register the
// bean to handle bi-directional cascading
request.registerDeleteBean();
deleteAssocMany(request);
request.unregisterDeleteBean();
unloadedForeignKeys = getDeleteUnloadedForeignKeys(request);
if (unloadedForeignKeys != null) {
// there are foreign keys that we don't have on this partially
// populated bean so we actually need to query them (to cascade delete)
unloadedForeignKeys.queryForeignKeys();
}
}
int count = request.executeOrQueue();
if (request.isPersistCascade()) {
deleteAssocOne(request);
if (unloadedForeignKeys != null) {
unloadedForeignKeys.deleteCascade();
}
}
// return true if using JDBC batch (as we can't tell until the batch is flushed)
return count != 0;
}
/**
* Save the associated child beans contained in a List.
*
* This will automatically copy over any join properties from the parent
* bean to the child beans.
*
*/
private void saveAssocMany(boolean insertedParent, PersistRequestBean> request, boolean insertMode) {
EntityBean parentBean = request.getEntityBean();
BeanDescriptor> desc = request.getBeanDescriptor();
SpiTransaction t = request.getTransaction();
// exported ones with cascade save
BeanPropertyAssocOne>[] expOnes = desc.propertiesOneExportedSave();
for (int i = 0; i < expOnes.length; i++) {
BeanPropertyAssocOne> prop = expOnes[i];
// check for partial beans
if (request.isLoadedProperty(prop)) {
EntityBean detailBean = prop.getValueAsEntityBean(parentBean);
if (detailBean != null) {
if (!prop.isSaveRecurseSkippable(detailBean)) {
t.depth(+1);
prop.setParentBeanToChild(parentBean, detailBean);
saveRecurse(detailBean, t, parentBean, insertMode, request.isPublish());
t.depth(-1);
}
}
}
}
// many's with cascade save
BeanPropertyAssocMany>[] manys = desc.propertiesManySave();
for (int i = 0; i < manys.length; i++) {
// check that property is loaded and collection should be cascaded to
if (request.isLoadedProperty(manys[i]) && !manys[i].isSkipSaveBeanCollection(parentBean, insertedParent)) {
saveMany(new SaveManyPropRequest(insertedParent, manys[i], parentBean, request), insertMode);
if (!insertedParent) {
request.addUpdatedManyProperty(manys[i]);
}
}
}
}
/**
* Helper to wrap the details when saving a OneToMany or ManyToMany
* relationship.
*/
private static class SaveManyPropRequest {
private final boolean insertedParent;
private final BeanPropertyAssocMany> many;
private final EntityBean parentBean;
private final SpiTransaction transaction;
private final boolean cascade;
private final boolean deleteMissingChildren;
private final boolean publish;
private SaveManyPropRequest(boolean insertedParent, BeanPropertyAssocMany> many, EntityBean parentBean, PersistRequestBean> request) {
this.insertedParent = insertedParent;
this.many = many;
this.cascade = many.getCascadeInfo().isSave();
this.parentBean = parentBean;
this.transaction = request.getTransaction();
this.deleteMissingChildren = request.isDeleteMissingChildren();
this.publish = request.isPublish();
}
private SaveManyPropRequest(BeanPropertyAssocMany> many, EntityBean parentBean, SpiTransaction t) {
this.insertedParent = false;
this.many = many;
this.parentBean = parentBean;
this.transaction = t;
this.cascade = true;
this.deleteMissingChildren = false;
this.publish = false;
}
public boolean isSaveIntersection() {
return transaction.isSaveAssocManyIntersection(many.getIntersectionTableJoin().getTable(), many.getBeanDescriptor().getName());
}
private Object getValue() {
return many.getValue(parentBean);
}
private boolean isModifyListenMode() {
return ModifyListenMode.REMOVALS.equals(many.getModifyListenMode());
}
private boolean isDeleteMissingChildren() {
return deleteMissingChildren;
}
private boolean isInsertedParent() {
return insertedParent;
}
private BeanPropertyAssocMany> getMany() {
return many;
}
private EntityBean getParentBean() {
return parentBean;
}
private SpiTransaction getTransaction() {
return transaction;
}
private boolean isCascade() {
return cascade;
}
public void modifyListenReset(BeanCollection> c) {
if (insertedParent) {
// after insert set the modify listening mode
// for private owned etc
c.setModifyListening(many.getModifyListenMode());
}
c.modifyReset();
}
public boolean isPublish() {
return publish;
}
private void resetModifyState() {
Object details = getValue();
if (details instanceof BeanCollection>) {
modifyListenReset((BeanCollection>)details);
}
}
}
private void saveMany(SaveManyPropRequest saveMany, boolean insertMode) {
if (saveMany.getMany().isManyToMany()) {
// check if we can save the m2m intersection in this direction
// we only allow one direction based on first traversed basis
boolean saveIntersectionFromThisDirection = saveMany.isSaveIntersection();
if (saveMany.isCascade()) {
saveAssocManyDetails(saveMany, false, insertMode);
}
// for ManyToMany save the 'relationship' via inserts/deletes
// into/from the intersection table
if (saveIntersectionFromThisDirection) {
// only allowed on one direction of a m2m based on beanName
saveAssocManyIntersection(saveMany, saveMany.isDeleteMissingChildren());
} else {
saveMany.resetModifyState();
}
} else {
if (saveMany.isModifyListenMode()) {
// delete any removed beans via private owned. Needs to occur before
// a 'deleteMissingChildren' statement occurs
removeAssocManyPrivateOwned(saveMany);
}
if (saveMany.isCascade()) {
// potentially deletes 'missing children' for 'stateless update'
saveAssocManyDetails(saveMany, saveMany.isDeleteMissingChildren(), insertMode);
}
}
}
private void removeAssocManyPrivateOwned(SaveManyPropRequest saveMany) {
Object details = saveMany.getValue();
// check that the list is not null and if it is a BeanCollection
// check that is has been populated (don't trigger lazy loading)
if (details instanceof BeanCollection>) {
BeanCollection> c = (BeanCollection>) details;
Set> modifyRemovals = c.getModifyRemovals();
saveMany.modifyListenReset(c);
if (modifyRemovals != null && !modifyRemovals.isEmpty()) {
SpiTransaction t = saveMany.getTransaction();
// increase depth for batching order
t.depth(+1);
for (Object removedBean : modifyRemovals) {
if (removedBean instanceof EntityBean) {
EntityBean eb = (EntityBean) removedBean;
if (eb._ebean_getIntercept().isLoaded()) {
// only delete if the bean was loaded meaning that it is known to exist in the DB
deleteRequest(createPublishRequest(removedBean, t, PersistRequest.Type.DELETE, saveMany.isPublish()));
}
}
}
t.depth(-1);
}
}
}
/**
* Save the details from a OneToMany collection.
*/
private void saveAssocManyDetails(SaveManyPropRequest saveMany, boolean deleteMissingChildren, boolean insertMode) {
BeanPropertyAssocMany> prop = saveMany.getMany();
Object details = saveMany.getValue();
// check that the list is not null and if it is a BeanCollection
// check that is has been populated (don't trigger lazy loading)
// For a Map this is a collection of Map.Entry objects and not beans
Collection> collection = BeanCollectionUtil.getActualEntries(details);
if (collection == null) {
// nothing to do here
return;
}
BeanDescriptor> targetDescriptor = prop.getTargetDescriptor();
if (saveMany.isInsertedParent()) {
// performance optimisation for large collections
targetDescriptor.preAllocateIds(collection.size());
}
ArrayList detailIds = null;
if (deleteMissingChildren) {
// collect the Id's (to exclude from deleteManyDetails)
detailIds = new ArrayList();
}
// increase depth for batching order
SpiTransaction t = saveMany.getTransaction();
t.depth(+1);
// if a map, then we get the key value and
// set it to the appropriate property on the
// detail bean before we save it
boolean isMap = ManyType.MAP.equals(prop.getManyType());
EntityBean parentBean = saveMany.getParentBean();
Object mapKeyValue = null;
boolean saveSkippable = prop.isSaveRecurseSkippable();
boolean skipSavingThisBean;
for (Object detailBean : collection) {
if (isMap) {
// its a map so need the key and value
Map.Entry, ?> entry = (Map.Entry, ?>) detailBean;
mapKeyValue = entry.getKey();
detailBean = entry.getValue();
}
if (detailBean instanceof EntityBean) {
EntityBean detail = (EntityBean) detailBean;
EntityBeanIntercept ebi = detail._ebean_getIntercept();
if (prop.isManyToMany()) {
skipSavingThisBean = targetDescriptor.isReference(ebi);
} else {
if (targetDescriptor.isReference(ebi)) {
// we can skip this one
skipSavingThisBean = true;
} else if (ebi.isNewOrDirty()) {
skipSavingThisBean = false;
// set the parent bean to detailBean
prop.setJoinValuesToChild(parentBean, detail, mapKeyValue);
} else {
// unmodified so skip depending on prop.isSaveRecurseSkippable();
skipSavingThisBean = saveSkippable;
}
}
if (!skipSavingThisBean) {
saveRecurse(detail, t, parentBean, insertMode, saveMany.isPublish());
}
}
}
if (detailIds != null) {
// stateless update with deleteMissingChildren so first
// flushBatch to ensure that we have Id values from any inserts
t.flushBatch();
// now collect the Id values to determine the 'missing children'
for (Object detailBean : collection) {
if (isMap) {
detailBean = ((Map.Entry, ?>) detailBean).getValue();
}
if (detailBean instanceof EntityBean) {
Object id = targetDescriptor.getId((EntityBean) detailBean);
if (!DmlUtil.isNullOrZero(id)) {
// remember the Id (other details not in the collection) will be removed
detailIds.add(id);
}
}
}
// deleting missing children - children not in our collected detailIds
deleteManyDetails(t, prop.getBeanDescriptor(), parentBean, prop, detailIds, false);
}
t.depth(-1);
}
/**
* Save the additions and removals from a ManyToMany collection as inserts
* and deletes from the intersection table.
*
* This is done via MapBeans.
*
*/
private void saveAssocManyIntersection(SaveManyPropRequest saveManyPropRequest, boolean deleteMissingChildren) {
BeanPropertyAssocMany> prop = saveManyPropRequest.getMany();
Object value = prop.getValue(saveManyPropRequest.getParentBean());
if (value == null) {
return;
}
SpiTransaction t = saveManyPropRequest.getTransaction();
boolean vanillaCollection = !(value instanceof BeanCollection>);
if (vanillaCollection || deleteMissingChildren) {
// delete all intersection rows and then treat all
// beans in the collection as additions
deleteAssocManyIntersection(saveManyPropRequest.getParentBean(), prop, t, saveManyPropRequest.isPublish());
}
Collection> deletions = null;
Collection> additions;
if (saveManyPropRequest.isInsertedParent() || vanillaCollection || deleteMissingChildren) {
// treat everything in the list/set/map as an intersection addition
if (value instanceof Map, ?>) {
additions = ((Map, ?>) value).values();
} else if (value instanceof Collection>) {
additions = (Collection>) value;
} else {
String msg = "Unhandled ManyToMany type " + value.getClass().getName() + " for " + prop.getFullBeanName();
throw new PersistenceException(msg);
}
if (!vanillaCollection) {
((BeanCollection>) value).modifyReset();
}
} else {
// BeanCollection so get the additions/deletions
BeanCollection> manyValue = (BeanCollection>) value;
additions = manyValue.getModifyAdditions();
deletions = manyValue.getModifyRemovals();
// reset so the changes are only processed once
manyValue.modifyReset();
}
t.depth(+1);
if (additions != null && !additions.isEmpty()) {
// ensure any cascade batch has been flushed prior
// to inserting into the intersection table
t.flushBatch();
for (Object other : additions) {
EntityBean otherBean = (EntityBean) other;
// the object from the 'other' side of the ManyToMany
if (deletions != null && deletions.remove(otherBean)) {
String m = "Inserting and Deleting same object? " + otherBean;
if (t.isLogSummary()) {
t.logSummary(m);
}
logger.warn(m);
} else {
if (!prop.hasImportedId(otherBean)) {
String msg = "ManyToMany bean " + otherBean + " does not have an Id value.";
throw new PersistenceException(msg);
} else {
// build a intersection row for 'insert'
IntersectionRow intRow = prop.buildManyToManyMapBean(saveManyPropRequest.getParentBean(), otherBean, saveManyPropRequest.isPublish());
SqlUpdate sqlInsert = intRow.createInsert(server);
executeSqlUpdate(sqlInsert, t);
}
}
}
}
if (deletions != null && !deletions.isEmpty()) {
// ensure any cascade batch has been flushed prior
// to inserting into the intersection table
t.flushBatch();
for (Object other : deletions) {
EntityBean otherDelete = (EntityBean) other;
// the object from the 'other' side of the ManyToMany
// build a intersection row for 'delete'
IntersectionRow intRow = prop.buildManyToManyMapBean(saveManyPropRequest.getParentBean(), otherDelete, saveManyPropRequest.isPublish());
SqlUpdate sqlDelete = intRow.createDelete(server, false);
executeSqlUpdate(sqlDelete, t);
}
}
// decrease the depth back to what it was
t.depth(-1);
}
private int deleteAssocManyIntersection(EntityBean bean, BeanPropertyAssocMany> many, Transaction t, boolean publish) {
// delete all intersection rows for this bean
IntersectionRow intRow = many.buildManyToManyDeleteChildren(bean, publish);
SqlUpdate sqlDelete = intRow.createDeleteChildren(server);
return executeSqlUpdate(sqlDelete, t);
}
/**
* Delete beans in any associated many.
*
* This is called prior to deleting the parent bean.
*
*/
private void deleteAssocMany(PersistRequestBean> request) {
SpiTransaction t = request.getTransaction();
t.depth(-1);
BeanDescriptor> desc = request.getBeanDescriptor();
EntityBean parentBean = request.getEntityBean();
boolean softDelete = request.isSoftDelete();
BeanPropertyAssocOne>[] expOnes = desc.propertiesOneExportedDelete();
if (expOnes.length > 0) {
DeleteUnloadedForeignKeys unloaded = null;
for (int i = 0; i < expOnes.length; i++) {
BeanPropertyAssocOne> prop = expOnes[i];
// for soft delete check cascade type also supports soft delete
if (!softDelete || prop.getTargetDescriptor().isSoftDelete()) {
if (request.isLoadedProperty(prop)) {
Object detailBean = prop.getValue(parentBean);
if (detailBean != null) {
deleteRecurse((EntityBean)detailBean, t, softDelete);
}
} else {
if (unloaded == null) {
unloaded = new DeleteUnloadedForeignKeys(server, request);
}
unloaded.add(prop);
}
}
}
if (unloaded != null) {
unloaded.queryForeignKeys();
unloaded.deleteCascade();
}
}
// Many's with delete cascade
BeanPropertyAssocMany>[] manys = desc.propertiesManyDelete();
for (int i = 0; i < manys.length; i++) {
if (manys[i].isManyToMany()) {
if (!softDelete) {
// delete associated rows from intersection table (but not during soft delete)
deleteAssocManyIntersection(parentBean, manys[i], t, request.isPublish());
}
} else {
if (ModifyListenMode.REMOVALS.equals(manys[i].getModifyListenMode())) {
// PrivateOwned ...
// if soft delete then check target also supports soft delete
if (!softDelete || manys[i].getTargetDescriptor().isSoftDelete()) {
Object details = manys[i].getValue(parentBean);
if (details instanceof BeanCollection>) {
Set> modifyRemovals = ((BeanCollection>) details).getModifyRemovals();
if (modifyRemovals != null && !modifyRemovals.isEmpty()) {
// delete the orphans that have been removed from the collection
for (Object detail : modifyRemovals) {
EntityBean detailBean = (EntityBean) detail;
if (manys[i].hasId(detailBean)) {
deleteRecurse(detailBean, t, softDelete);
}
}
}
}
}
}
deleteManyDetails(t, desc, parentBean, manys[i], null, softDelete);
}
}
// restore the depth
t.depth(+1);
}
/**
* Delete the 'many' detail beans for a given parent bean.
*
* For stateless updates this deletes details beans that are no longer in
* the many - the excludeDetailIds holds the detail beans that are in the
* collection (and should not be deleted).
*
*/
private void deleteManyDetails(SpiTransaction t, BeanDescriptor> desc, EntityBean parentBean,
BeanPropertyAssocMany> many, ArrayList excludeDetailIds, boolean softDelete) {
if (many.getCascadeInfo().isDelete()) {
// cascade delete the beans in the collection
BeanDescriptor> targetDesc = many.getTargetDescriptor();
if (!softDelete || targetDesc.isSoftDelete()) {
if (targetDesc.isDeleteRecurseSkippable() && !targetDesc.isBeanCaching()) {
// Just delete all the children with one statement
IntersectionRow intRow = many.buildManyDeleteChildren(parentBean, excludeDetailIds);
SqlUpdate sqlDelete = intRow.createDelete(server, softDelete);
executeSqlUpdate(sqlDelete, t);
} else {
// Delete recurse using the Id values of the children
Object parentId = desc.getId(parentBean);
List idsByParentId = many.findIdsByParentId(parentId, null, t, excludeDetailIds);
if (!idsByParentId.isEmpty()) {
deleteChildrenById(t, targetDesc, idsByParentId, softDelete);
}
}
}
}
}
/**
* Cascade delete child entities by Id.
*
* Will use delete by object if the child entity has manyToMany relationships.
*/
private void deleteChildrenById(SpiTransaction t, BeanDescriptor> targetDesc, List childIds, boolean softDelete) {
if (targetDesc.propertiesManyToMany().length > 0) {
// convert into a list of reference objects and perform delete by object
List refList = new ArrayList(childIds.size());
for (Object id : childIds) {
refList.add(targetDesc.createReference(null, id, null));
}
deleteList(refList, t, softDelete);
} else {
// perform delete by statement if possible
delete(targetDesc, null, childIds, t, softDelete);
}
}
/**
* Save any associated one beans.
*/
private void saveAssocOne(PersistRequestBean> request, boolean insertMode) {
BeanDescriptor> desc = request.getBeanDescriptor();
// imported ones with save cascade
BeanPropertyAssocOne>[] ones = desc.propertiesOneImportedSave();
for (int i = 0; i < ones.length; i++) {
BeanPropertyAssocOne> prop = ones[i];
// check for partial objects
if (request.isLoadedProperty(prop)) {
EntityBean detailBean = prop.getValueAsEntityBean(request.getEntityBean());
if (detailBean != null
&& !prop.isSaveRecurseSkippable(detailBean)
&& !prop.isReference(detailBean)
&& !request.isParent(detailBean)) {
SpiTransaction t = request.getTransaction();
t.depth(-1);
saveRecurse(detailBean, t, null, insertMode, request.isPublish());
t.depth(+1);
}
}
}
}
/**
* Support for loading any Imported Associated One properties that are not
* loaded but required for Delete cascade.
*/
private DeleteUnloadedForeignKeys getDeleteUnloadedForeignKeys(PersistRequestBean> request) {
DeleteUnloadedForeignKeys fkeys = null;
BeanPropertyAssocOne>[] ones = request.getBeanDescriptor().propertiesOneImportedDelete();
for (int i = 0; i < ones.length; i++) {
if (!request.isLoadedProperty(ones[i])) {
// we have cascade Delete on a partially populated bean and
// this property was not loaded (so we are going to have to fetch it)
if (fkeys == null) {
fkeys = new DeleteUnloadedForeignKeys(server, request);
}
fkeys.add(ones[i]);
}
}
return fkeys;
}
/**
* Delete any associated one beans.
*/
private void deleteAssocOne(PersistRequestBean> request) {
BeanPropertyAssocOne>[] ones = request.getBeanDescriptor().propertiesOneImportedDelete();
for (int i = 0; i < ones.length; i++) {
BeanPropertyAssocOne> prop = ones[i];
if (request.isLoadedProperty(prop)) {
Object detailBean = prop.getValue(request.getEntityBean());
if (detailBean != null) {
EntityBean detail = (EntityBean) detailBean;
if (prop.hasId(detail)) {
deleteRecurse(detail, request.getTransaction(), request.isSoftDelete());
}
}
}
}
}
/**
* Set Id Generated value for insert.
*/
private void setIdGenValue(PersistRequestBean> request) {
BeanDescriptor> desc = request.getBeanDescriptor();
if (!desc.isUseIdGenerator()) {
return;
}
BeanProperty idProp = desc.getIdProperty();
if (idProp == null || idProp.isEmbedded()) {
// not supporting IdGeneration for concatenated or Embedded
return;
}
EntityBean bean = request.getEntityBean();
Object uid = idProp.getValue(bean);
if (DmlUtil.isNullOrZero(uid)) {
// generate the nextId and set it to the property
Object nextId = desc.nextId(request.getTransaction());
// cast the data type if required and set it
desc.convertSetId(nextId, bean);
}
}
/**
* Create the Persist Request Object that wraps all the objects used to
* perform an insert, update or delete.
*/
private PersistRequestBean createRequest(T bean, Transaction t, PersistRequest.Type type) {
return createRequestInternal(bean, t, type, false, false);
}
/**
* Create the Persist Request Object additionally specifying the publish status.
*/
private PersistRequestBean createPublishRequest(T bean, Transaction t, PersistRequest.Type type, boolean publish) {
return createRequestInternal(bean, t, type, false, publish);
}
/**
* Create the Persist Request Object additionally specifying the publish status.
*/
private PersistRequestBean createRequestInternal(T bean, Transaction t, PersistRequest.Type type, boolean saveRecurse, boolean publish) {
BeanManager mgr = getBeanManager(bean);
if (mgr == null) {
throw new PersistenceException(errNotRegistered(bean.getClass()));
}
return createRequest(bean, t, null, mgr, type, saveRecurse, publish);
}
/**
* Create an Insert or Update PersistRequestBean when cascading.
*
* This call determines the PersistRequest.Type based on bean state and the insert flag (root persist type).
*/
private PersistRequestBean createRequestRecurse(T bean, Transaction t, Object parentBean, boolean insertMode, boolean publish) {
BeanManager mgr = getBeanManager(bean);
if (mgr == null) {
throw new PersistenceException(errNotRegistered(bean.getClass()));
}
BeanDescriptor desc = mgr.getBeanDescriptor();
EntityBean entityBean = (EntityBean) bean;
PersistRequest.Type type;
if (publish) {
// insert if it is a new bean (as publish created it)
type = entityBean._ebean_getIntercept().isNew() ? Type.INSERT : Type.UPDATE;
} else {
// determine Insert or Update based on bean state and insert flag
type = desc.isInsertMode(entityBean._ebean_getIntercept(), insertMode) ? Type.INSERT : Type.UPDATE;
}
return createRequest(bean, t, parentBean, mgr, type, true, publish);
}
/**
* Create the Persist Request Object that wraps all the objects used to
* perform an insert, update or delete.
*/
@SuppressWarnings({"unchecked", "rawtypes"})
private PersistRequestBean createRequest(T bean, Transaction t, Object parentBean, BeanManager> mgr,
PersistRequest.Type type, boolean saveRecurse, boolean publish) {
if (type == Type.DELETE_PERMANENT) {
type = Type.DELETE;
} else if (type == Type.DELETE && mgr.getBeanDescriptor().isSoftDelete()) {
// automatically convert to soft delete for types that support it
type = Type.SOFT_DELETE;
}
return new PersistRequestBean(server, bean, parentBean, mgr, (SpiTransaction) t, persistExecute, type, saveRecurse, publish);
}
private String errNotRegistered(Class> beanClass) {
String msg = "The type [" + beanClass + "] is not a registered entity?";
msg += " If you don't explicitly list the entity classes to use Ebean will search for them in the classpath.";
msg += " If the entity is in a Jar check the ebean.search.jars property in ebean.properties file or check ServerConfig.addJar().";
return msg;
}
/**
* Return the BeanDescriptor for a bean that is being persisted.
*
* Note that this checks to see if the bean is a MapBean with a tableName.
* If so it will return the table based BeanDescriptor.
*
*/
@SuppressWarnings("unchecked")
private BeanManager getBeanManager(T bean) {
return (BeanManager) beanDescriptorManager.getBeanManager(bean.getClass());
}
}