package io.ebeaninternal.server.persist;
import io.ebean.bean.BeanCollection;
import io.ebean.bean.EntityBean;
import io.ebean.bean.EntityBeanIntercept;
import io.ebeaninternal.api.SpiSqlUpdate;
import io.ebeaninternal.server.core.PersistRequest;
import io.ebeaninternal.server.core.PersistRequestBean;
import io.ebeaninternal.server.deploy.BeanCollectionUtil;
import io.ebeaninternal.server.deploy.BeanDescriptor;
import io.ebeaninternal.server.deploy.BeanProperty;
import io.ebeaninternal.server.deploy.BeanPropertyAssocMany;
import io.ebeaninternal.server.deploy.IntersectionRow;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import javax.persistence.PersistenceException;
import java.util.ArrayList;
import java.util.Collection;
import java.util.List;
import java.util.Map;
import java.util.Set;
Saves the details for a OneToMany or ManyToMany relationship (entity beans).
/**
* Saves the details for a OneToMany or ManyToMany relationship (entity beans).
*/
public class SaveManyBeans extends SaveManyBase {
private static final Logger log = LoggerFactory.getLogger(SaveManyBeans.class);
private final boolean cascade;
private final boolean publish;
private final BeanDescriptor<?> targetDescriptor;
private final boolean isMap;
private final boolean saveRecurseSkippable;
private final DeleteMode deleteMode;
private Collection<?> collection;
private DefaultPersister persister;
private boolean deleteMissing;
private int sortOrder;
SaveManyBeans(boolean insertedParent, BeanPropertyAssocMany<?> many, EntityBean parentBean, PersistRequestBean<?> request, DefaultPersister persister) {
super(insertedParent, many, parentBean, request);
this.persister = persister;
this.cascade = many.getCascadeInfo().isSave();
this.publish = request.isPublish();
this.targetDescriptor = many.getTargetDescriptor();
this.isMap = many.getManyType().isMap();
this.saveRecurseSkippable = many.isSaveRecurseSkippable();
this.deleteMode = targetDescriptor.isSoftDelete() ? DeleteMode.SOFT : DeleteMode.HARD;
}
@Override
void save() {
if (many.hasJoinTable()) {
// check if we can save the m2m intersection in this direction
// we only allow one direction based on first traversed basis
boolean saveIntersectionFromThisDirection = isSaveIntersection();
if (cascade) {
saveAssocManyDetails(false);
}
// 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(request.isDeleteMissingChildren());
} else {
resetModifyState();
}
} else {
if (isModifyListenMode()) {
// delete any removed beans via private owned. Needs to occur before
// a 'deleteMissingChildren' statement occurs
removeAssocManyPrivateOwned();
}
if (cascade) {
// potentially deletes 'missing children' for 'stateless update'
saveAssocManyDetails(request.isDeleteMissingChildren());
}
}
}
private boolean isSaveIntersection() {
if (!many.isManyToMany()) {
return true;
}
return transaction.isSaveAssocManyIntersection(many.getIntersectionTableJoin().getTable(), many.getBeanDescriptor().rootName());
}
private boolean isModifyListenMode() {
return BeanCollection.ModifyListenMode.REMOVALS == many.getModifyListenMode();
}
Save the details from a OneToMany collection.
/**
* Save the details from a OneToMany collection.
*/
private void saveAssocManyDetails(boolean deleteMissingChildren) {
this.deleteMissing = deleteMissingChildren;
// check that the list is not null and if it is a BeanCollection
// check that is has been populated (don't trigger lazy loading)
collection = BeanCollectionUtil.getActualEntries(value);
if (collection != null) {
processDetails();
}
}
private void processDetails() {
BeanProperty orderColumn = null;
boolean hasOrderColumn = many.hasOrderColumn();
if (hasOrderColumn) {
if (!insertedParent && canSkipForOrderColumn()) {
return;
}
orderColumn = targetDescriptor.getOrderColumn();
}
if (insertedParent) {
// performance optimisation for large collections
targetDescriptor.preAllocateIds(collection.size());
}
if (deleteMissing) {
// collect the Id's (to exclude from deleteManyDetails)
List<Object> detailIds = collectIds(collection, targetDescriptor, isMap);
// deleting missing children - children not in our collected detailIds
persister.deleteManyDetails(transaction, many.getBeanDescriptor(), parentBean, many, detailIds, deleteMode);
}
transaction.depth(+1);
saveAllBeans(orderColumn);
if (hasOrderColumn) {
resetModifyState();
}
transaction.depth(-1);
}
private void saveAllBeans(BeanProperty orderColumn) {
// if a map, then we get the key value and
// set it to the appropriate property on the
// detail bean before we save it
Object mapKeyValue = null;
boolean skipSavingThisBean;
for (Object detailBean : collection) {
sortOrder++;
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 (many.hasJoinTable()) {
skipSavingThisBean = targetDescriptor.isReference(ebi);
} else {
if (orderColumn != null) {
orderColumn.setValue(detail, sortOrder);
ebi.setDirty(true);
}
if (targetDescriptor.isReference(ebi)) {
// we can skip this one
skipSavingThisBean = true;
} else if (ebi.isNewOrDirty()) {
skipSavingThisBean = false;
// set the parent bean to detailBean
many.setJoinValuesToChild(parentBean, detail, mapKeyValue);
} else {
// unmodified so skip depending on prop.isSaveRecurseSkippable();
skipSavingThisBean = saveRecurseSkippable;
}
}
if (!skipSavingThisBean) {
persister.saveRecurse(detail, transaction, parentBean, request.getFlags());
}
}
}
}
Return true if we can skip based on .. no modifications to the collection and no beans are dirty.
/**
* Return true if we can skip based on .. no modifications to the collection and no beans are dirty.
*/
private boolean canSkipForOrderColumn() {
return value instanceof BeanCollection
&& !((BeanCollection<?>) value).wasTouched()
&& noDirtyBeans();
}
private boolean noDirtyBeans() {
for (Object bean : collection) {
if (bean instanceof EntityBean && ((EntityBean) bean)._ebean_getIntercept().isDirty()) {
return false;
}
}
return true;
}
Collect the Id values of the details to remove 'missing children' for stateless updates.
/**
* Collect the Id values of the details to remove 'missing children' for stateless updates.
*/
private List<Object> collectIds(Collection<?> collection, BeanDescriptor<?> targetDescriptor, boolean isMap) {
List<Object> detailIds = new ArrayList<>();
// stateless update with deleteMissingChildren so first
// collect the Id values to remove 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);
}
}
}
return detailIds;
}
Save the additions and removals from a ManyToMany collection as inserts
and deletes from the intersection table.
This is done via MapBeans.
/**
* Save the additions and removals from a ManyToMany collection as inserts
* and deletes from the intersection table.
* <p>
* This is done via MapBeans.
* </p>
*/
private void saveAssocManyIntersection(boolean deleteMissingChildren) {
if (value == null) {
return;
}
if (request.isQueueManyIntersection()) {
// queue/delay until bean persist request is flushed
this.deleteMissing = deleteMissingChildren;
request.addManyIntersection(this);
} else {
saveAssocManyIntersection(deleteMissingChildren, false);
}
}
Push intersection table changes onto batch flush queue.
/**
* Push intersection table changes onto batch flush queue.
*/
public void saveIntersectionBatch() {
saveAssocManyIntersection(deleteMissing, true);
}
private void saveAssocManyIntersection(boolean deleteMissingChildren, boolean queue) {
boolean vanillaCollection = !(value instanceof BeanCollection<?>);
if (vanillaCollection || deleteMissingChildren) {
// delete all intersection rows and then treat all
// beans in the collection as additions
persister.deleteManyIntersection(parentBean, many, transaction, publish, queue);
}
Collection<?> deletions = null;
Collection<?> additions;
if (insertedParent || 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 {
throw new PersistenceException("Unhandled ManyToMany type " + value.getClass().getName() + " for " + many.getFullBeanName());
}
if (!vanillaCollection) {
BeanCollection<?> manyValue = (BeanCollection<?>) value;
setListenMode(manyValue, many);
manyValue.modifyReset();
}
} else {
// BeanCollection so get the additions/deletions
BeanCollection<?> manyValue = (BeanCollection<?>) value;
if (setListenMode(manyValue, many)) {
additions = manyValue.getActualDetails();
} else {
additions = manyValue.getModifyAdditions();
deletions = manyValue.getModifyRemovals();
}
// reset so the changes are only processed once
manyValue.modifyReset();
}
transaction.depth(+1);
if (additions != null && !additions.isEmpty()) {
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 (transaction.isLogSummary()) {
transaction.logSummary(m);
}
log.warn(m);
} else {
if (!many.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 = many.buildManyToManyMapBean(parentBean, otherBean, publish);
SpiSqlUpdate sqlInsert = intRow.createInsert(server);
persister.executeOrQueue(sqlInsert, transaction, queue);
}
}
}
}
if (deletions != null && !deletions.isEmpty()) {
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 = many.buildManyToManyMapBean(parentBean, otherDelete, publish);
SpiSqlUpdate sqlDelete = intRow.createDelete(server, DeleteMode.HARD);
persister.executeOrQueue(sqlDelete, transaction, queue);
}
}
// decrease the depth back to what it was
transaction.depth(-1);
}
private void removeAssocManyPrivateOwned() {
// 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 (value instanceof BeanCollection<?>) {
BeanCollection<?> c = (BeanCollection<?>) value;
Set<?> modifyRemovals = c.getModifyRemovals();
modifyListenReset(c);
if (modifyRemovals != null && !modifyRemovals.isEmpty()) {
for (Object removedBean : modifyRemovals) {
if (removedBean instanceof EntityBean) {
EntityBean eb = (EntityBean) removedBean;
if (!eb._ebean_getIntercept().isNew()) {
// only delete if the bean was loaded meaning that it is known to exist in the DB
persister.deleteRequest(persister.createPublishRequest(removedBean, transaction, PersistRequest.Type.DELETE, request.getFlags()));
}
}
}
}
}
}
Check if we need to set the listen mode (on new collections persisted for the first time).
/**
* Check if we need to set the listen mode (on new collections persisted for the first time).
*/
private boolean setListenMode(BeanCollection<?> manyValue, BeanPropertyAssocMany<?> prop) {
BeanCollection.ModifyListenMode mode = manyValue.getModifyListening();
if (mode == null) {
// new collection persisted for the first time
manyValue.setModifyListening(prop.getModifyListenMode());
return true;
}
return false;
}
}