mirror of
https://github.com/ebean-orm/ebean.git
synced 2024-04-21 10:51:47 +00:00
#115 - Mapping - Add support for @ElementCollection enhancement
Add simple Map support with Json
This commit is contained in:
@@ -28,7 +28,7 @@ public class BeanCollectionHelpFactory {
|
||||
case SET:
|
||||
return elementCollection ? new BeanSetHelpElement<>(many) : new BeanSetHelp<>(many);
|
||||
case MAP:
|
||||
return new BeanMapHelp<>(many);
|
||||
return elementCollection ? new BeanMapHelpElement<>(many) :new BeanMapHelp<>(many);
|
||||
default:
|
||||
throw new RuntimeException("Invalid type " + manyType);
|
||||
}
|
||||
|
||||
@@ -3330,6 +3330,10 @@ public class BeanDescriptor<T> implements BeanType<T>, STreeType {
|
||||
jsonHelp.jsonWriteDirtyProperties(writeJson, bean, dirtyProps);
|
||||
}
|
||||
|
||||
public void jsonWriteMapEntry(SpiJsonWriter ctx, Map.Entry<?, ?> entry) throws IOException {
|
||||
throw new IllegalStateException("Unexpected - expect Element override");
|
||||
}
|
||||
|
||||
public void jsonWriteElement(SpiJsonWriter ctx, Object element) {
|
||||
throw new IllegalStateException("Unexpected - expect Element override");
|
||||
}
|
||||
|
||||
@@ -31,6 +31,7 @@ class BeanDescriptorElement<T> extends BeanDescriptor<T> {
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isElementType() {
|
||||
return true;
|
||||
}
|
||||
@@ -40,6 +41,7 @@ class BeanDescriptorElement<T> extends BeanDescriptor<T> {
|
||||
return new ElementEntityBean(properties);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void jsonWriteElement(SpiJsonWriter ctx, Object element) {
|
||||
try {
|
||||
scalarType.jsonWrite(ctx.gen(), element);
|
||||
@@ -48,6 +50,7 @@ class BeanDescriptorElement<T> extends BeanDescriptor<T> {
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public Object jsonReadCollection(ReadJson readJson, EntityBean parentBean) throws IOException {
|
||||
|
||||
JsonParser parser = readJson.getParser();
|
||||
|
||||
@@ -0,0 +1,97 @@
|
||||
package io.ebeaninternal.server.deploy;
|
||||
|
||||
import com.fasterxml.jackson.core.JsonParser;
|
||||
import com.fasterxml.jackson.core.JsonToken;
|
||||
import io.ebean.bean.EntityBean;
|
||||
import io.ebeaninternal.server.deploy.meta.DeployBeanDescriptor;
|
||||
import io.ebeaninternal.server.text.json.ReadJson;
|
||||
import io.ebeaninternal.server.text.json.SpiJsonWriter;
|
||||
import io.ebeaninternal.server.type.ScalarType;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.util.Map;
|
||||
|
||||
/**
|
||||
* Bean descriptor used with ElementCollection (where we don't have a mapped type/class).
|
||||
*/
|
||||
class BeanDescriptorElementMap<T> extends BeanDescriptor<T> {
|
||||
|
||||
private final ScalarType[] scalarTypes;
|
||||
private final ElementHelp elementHelp;
|
||||
private final boolean stringKey;
|
||||
|
||||
BeanDescriptorElementMap(BeanDescriptorMap owner, DeployBeanDescriptor<T> deploy, ElementHelp elementHelp) {
|
||||
super(owner, deploy);
|
||||
this.elementHelp = elementHelp;
|
||||
BeanProperty[] props = propertiesNonTransient();
|
||||
this.scalarTypes = new ScalarType[props.length];
|
||||
for (int i = 0; i < props.length; i++) {
|
||||
scalarTypes[i] = props[i].getScalarType();
|
||||
}
|
||||
this.stringKey = String.class.equals(scalarTypes[0].getType());
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isElementType() {
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected EntityBean createPrototypeEntityBean(Class<T> beanType) {
|
||||
return new ElementEntityBean(properties);
|
||||
}
|
||||
|
||||
@Override
|
||||
@SuppressWarnings("unchecked")
|
||||
public void jsonWriteMapEntry(SpiJsonWriter ctx, Map.Entry<?, ?> entry) throws IOException {
|
||||
ctx.writeStartObject();
|
||||
if (stringKey) {
|
||||
Object key = entry.getKey();
|
||||
String keyName = (key == null) ? "null" : key.toString();
|
||||
ctx.writeFieldName(keyName);
|
||||
scalarTypes[1].jsonWrite(ctx.gen(), entry.getValue());
|
||||
} else {
|
||||
ctx.writeFieldName("key");
|
||||
scalarTypes[0].jsonWrite(ctx.gen(), entry.getKey());
|
||||
ctx.writeFieldName("value");
|
||||
scalarTypes[1].jsonWrite(ctx.gen(), entry.getValue());
|
||||
}
|
||||
ctx.writeEndObject();
|
||||
}
|
||||
|
||||
@Override
|
||||
public Object jsonReadCollection(ReadJson readJson, EntityBean parentBean) throws IOException {
|
||||
|
||||
JsonParser parser = readJson.getParser();
|
||||
ElementCollector add = elementHelp.createCollector();
|
||||
do {
|
||||
JsonToken token = parser.nextToken();
|
||||
if (token != JsonToken.START_OBJECT) {
|
||||
break;
|
||||
}
|
||||
if (stringKey) {
|
||||
String key = parser.nextFieldName();
|
||||
parser.nextToken();
|
||||
Object val = scalarTypes[0].jsonRead(parser);
|
||||
add.addKeyValue(key, val);
|
||||
|
||||
} else {
|
||||
parser.nextFieldName();
|
||||
Object key= scalarTypes[0].jsonRead(parser);
|
||||
|
||||
parser.nextFieldName();
|
||||
Object val = scalarTypes[0].jsonRead(parser);
|
||||
add.addKeyValue(key, val);
|
||||
}
|
||||
|
||||
token = parser.nextToken();
|
||||
if (token != JsonToken.END_OBJECT) {
|
||||
break;
|
||||
}
|
||||
|
||||
} while (true);
|
||||
|
||||
return add.collection();
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1660,7 +1660,11 @@ public class BeanDescriptorManager implements BeanDescriptorMap {
|
||||
*/
|
||||
public <A> BeanDescriptor<A> createElementDescriptor(DeployBeanDescriptor<A> elementDescriptor, ManyType manyType) {
|
||||
|
||||
return new BeanDescriptorElement<>(this, elementDescriptor, elementHelper(manyType));
|
||||
ElementHelp elementHelp = elementHelper(manyType);
|
||||
if (manyType.isMap()) {
|
||||
return new BeanDescriptorElementMap<>(this, elementDescriptor, elementHelp);
|
||||
}
|
||||
return new BeanDescriptorElement<>(this, elementDescriptor, elementHelp);
|
||||
}
|
||||
|
||||
private ElementHelp elementHelper(ManyType manyType) {
|
||||
|
||||
@@ -17,7 +17,7 @@ import java.util.Map.Entry;
|
||||
/**
|
||||
* Helper specifically for dealing with Maps.
|
||||
*/
|
||||
public final class BeanMapHelp<T> extends BaseCollectionHelp<T> {
|
||||
public class BeanMapHelp<T> extends BaseCollectionHelp<T> {
|
||||
|
||||
private final BeanPropertyAssocMany<T> many;
|
||||
private final BeanDescriptor<T> targetDescriptor;
|
||||
@@ -175,8 +175,7 @@ public final class BeanMapHelp<T> extends BaseCollectionHelp<T> {
|
||||
if (!map.isEmpty() || ctx.isIncludeEmpty()) {
|
||||
ctx.beginAssocMany(name);
|
||||
for (Entry<?, ?> entry : map.entrySet()) {
|
||||
//FIXME: json write map key ...
|
||||
targetDescriptor.jsonWrite(ctx, (EntityBean) entry.getValue());
|
||||
many.jsonWriteMapEntry(ctx, entry);
|
||||
}
|
||||
ctx.endAssocMany();
|
||||
}
|
||||
|
||||
@@ -0,0 +1,31 @@
|
||||
package io.ebeaninternal.server.deploy;
|
||||
|
||||
import io.ebean.bean.BeanCollection;
|
||||
import io.ebean.bean.EntityBean;
|
||||
import io.ebean.common.BeanMap;
|
||||
import io.ebeaninternal.server.text.json.SpiJsonWriter;
|
||||
|
||||
public class BeanMapHelpElement<T> extends BeanMapHelp<T> {
|
||||
|
||||
BeanMapHelpElement(BeanPropertyAssocMany<T> many) {
|
||||
super(many);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void add(BeanCollection<?> collection, EntityBean bean, boolean withCheck) {
|
||||
Object key = bean._ebean_getField(0);
|
||||
Object val = bean._ebean_getField(1);
|
||||
|
||||
BeanMap<?, ?> map = ((BeanMap<?, ?>) collection);
|
||||
if (withCheck) {
|
||||
map.internalPutWithCheck(key, val);
|
||||
} else {
|
||||
map.internalPut(key, val);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
void jsonWriteElement(SpiJsonWriter ctx, Object element) {
|
||||
throw new IllegalStateException("not called");
|
||||
}
|
||||
}
|
||||
@@ -882,8 +882,13 @@ public class BeanPropertyAssocMany<T> extends BeanPropertyAssoc<T> implements ST
|
||||
return help;
|
||||
}
|
||||
|
||||
public void jsonWriteMapEntry(SpiJsonWriter ctx, Map.Entry<?, ?> entry) throws IOException {
|
||||
// Writing as json array rather than object ...
|
||||
targetDescriptor.jsonWrite(ctx, (EntityBean) entry.getValue());
|
||||
}
|
||||
|
||||
public void jsonWriteElementValue(SpiJsonWriter ctx, Object element) {
|
||||
throw new IllegalStateException("Never Expected");
|
||||
throw new IllegalStateException("Unexpected - expect Element override");
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -1,12 +1,12 @@
|
||||
package io.ebeaninternal.server.deploy;
|
||||
|
||||
import io.ebean.PersistenceIOException;
|
||||
import io.ebean.bean.EntityBean;
|
||||
import io.ebeaninternal.server.deploy.meta.DeployBeanPropertySimpleCollection;
|
||||
import io.ebeaninternal.server.text.json.ReadJson;
|
||||
import io.ebeaninternal.server.text.json.SpiJsonWriter;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.util.Map;
|
||||
|
||||
public class BeanPropertySimpleCollection<T> extends BeanPropertyAssocMany<T> {
|
||||
|
||||
@@ -34,18 +34,12 @@ public class BeanPropertySimpleCollection<T> extends BeanPropertyAssocMany<T> {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Json write scalar value.
|
||||
*/
|
||||
@Override
|
||||
public void jsonWriteValue(SpiJsonWriter writeJson, Object value) {
|
||||
try {
|
||||
scalarType.jsonWrite(writeJson.gen(), value);
|
||||
} catch (IOException e) {
|
||||
throw new PersistenceIOException(e);
|
||||
}
|
||||
public void jsonWriteMapEntry(SpiJsonWriter ctx, Map.Entry<?, ?> entry) throws IOException {
|
||||
elementDescriptor.jsonWriteMapEntry(ctx, entry);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void jsonWriteElementValue(SpiJsonWriter ctx, Object element) {
|
||||
elementDescriptor.jsonWriteElement(ctx, element);
|
||||
}
|
||||
|
||||
@@ -10,6 +10,11 @@ public interface ElementCollector {
|
||||
*/
|
||||
void addElement(Object element);
|
||||
|
||||
/**
|
||||
* Add an element.
|
||||
*/
|
||||
void addKeyValue(Object key, Object element);
|
||||
|
||||
/**
|
||||
* Return the populated collection/map.
|
||||
*/
|
||||
|
||||
@@ -19,6 +19,11 @@ class ElementHelpList implements ElementHelp {
|
||||
list.add(element);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void addKeyValue(Object key, Object element) {
|
||||
throw new IllegalStateException("never called");
|
||||
}
|
||||
|
||||
@Override
|
||||
public Object collection() {
|
||||
return list;
|
||||
|
||||
@@ -16,8 +16,12 @@ class ElementHelpMap implements ElementHelp {
|
||||
|
||||
@Override
|
||||
public void addElement(Object element) {
|
||||
throw new RuntimeException("asd");
|
||||
//map.put()
|
||||
throw new IllegalStateException("never called");
|
||||
}
|
||||
|
||||
@Override
|
||||
public void addKeyValue(Object key, Object element) {
|
||||
map.put(key, element);
|
||||
}
|
||||
|
||||
@Override
|
||||
|
||||
@@ -19,6 +19,11 @@ class ElementHelpSet implements ElementHelp {
|
||||
set.add(element);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void addKeyValue(Object key, Object element) {
|
||||
throw new IllegalStateException("never called");
|
||||
}
|
||||
|
||||
@Override
|
||||
public Object collection() {
|
||||
return set;
|
||||
|
||||
@@ -29,6 +29,7 @@ import javax.persistence.JoinColumn;
|
||||
import javax.persistence.JoinTable;
|
||||
import javax.persistence.ManyToMany;
|
||||
import javax.persistence.MapKey;
|
||||
import javax.persistence.MapKeyColumn;
|
||||
import javax.persistence.OneToMany;
|
||||
import javax.persistence.OrderBy;
|
||||
import javax.persistence.OrderColumn;
|
||||
@@ -212,24 +213,50 @@ class AnnotationAssocManys extends AnnotationParser {
|
||||
elementDescriptor.setBaseTable(new TableName(fullTableName), readConfig.getAsOfViewSuffix(), readConfig.getVersionsBetweenSuffix());
|
||||
|
||||
ScalarType<?> scalarType = util.getTypeManager().getScalarType(elementType);
|
||||
DeployBeanProperty elementProp = new DeployBeanProperty(elementDescriptor, elementType, scalarType, null);
|
||||
|
||||
elementProp.setName("value");
|
||||
elementProp.setDbColumn(prop.getDbColumn());
|
||||
int sortOrder = 0;
|
||||
if (!prop.getManyType().isMap()) {
|
||||
elementDescriptor.setProperties(new String[]{"value"});
|
||||
} else {
|
||||
elementDescriptor.setProperties(new String[]{"key", "value"});
|
||||
String dbKeyColumn = "key";
|
||||
MapKeyColumn mapKeyColumn = get(prop, MapKeyColumn.class);
|
||||
if (mapKeyColumn != null) {
|
||||
dbKeyColumn = mapKeyColumn.name();
|
||||
}
|
||||
|
||||
DeployBeanProperty keyProp = new DeployBeanProperty(elementDescriptor, elementType, scalarType, null);
|
||||
setElementProperty(keyProp, "key", dbKeyColumn, sortOrder++);
|
||||
elementDescriptor.addBeanProperty(keyProp);
|
||||
if (mapKeyColumn != null) {
|
||||
keyProp.setDbLength(mapKeyColumn.length());
|
||||
keyProp.setDbScale(mapKeyColumn.scale());
|
||||
}
|
||||
}
|
||||
|
||||
DeployBeanProperty valueProp = new DeployBeanProperty(elementDescriptor, elementType, scalarType, null);
|
||||
setElementProperty(valueProp, "value", prop.getDbColumn(), sortOrder++);
|
||||
if (column != null) {
|
||||
valueProp.setDbLength(column.length());
|
||||
valueProp.setDbScale(column.scale());
|
||||
}
|
||||
|
||||
elementDescriptor.addBeanProperty(valueProp);
|
||||
elementDescriptor.setName(prop.getFullBeanName());
|
||||
|
||||
factory.createUnidirectional(elementDescriptor, prop.getOwningType(), beanTable, prop.getTableJoin());
|
||||
prop.setElementDescriptor(factory.createElementDescriptor(elementDescriptor, prop.getManyType()));
|
||||
}
|
||||
|
||||
private void setElementProperty(DeployBeanProperty elementProp, String name, String dbColumn, int sortOrder) {
|
||||
elementProp.setName(name);
|
||||
elementProp.setDbColumn(dbColumn);
|
||||
elementProp.setNullable(false);
|
||||
elementProp.setDbInsertable(true);
|
||||
elementProp.setDbUpdateable(true);
|
||||
elementProp.setDbRead(true);
|
||||
elementProp.setSortOrder(sortOrder);
|
||||
elementProp.setElementProperty();
|
||||
|
||||
elementDescriptor.addBeanProperty(elementProp);
|
||||
elementDescriptor.setProperties(new String[]{"value"});
|
||||
elementDescriptor.setName(prop.getFullBeanName());
|
||||
|
||||
Class<?> owningType = prop.getOwningType();
|
||||
|
||||
factory.createUnidirectional(elementDescriptor, owningType, beanTable, prop.getTableJoin());
|
||||
prop.setElementDescriptor(factory.createElementDescriptor(elementDescriptor, prop.getManyType()));
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -213,7 +213,6 @@ public class DeployCreateProperties {
|
||||
} catch (NullPointerException e) {
|
||||
logger.debug("expected non-scalar type {}", e.getMessage());
|
||||
}
|
||||
// TODO: Handle Collection of CompoundType and Embedded Type
|
||||
return new DeployBeanPropertyAssocMany(desc, targetType, manyType);
|
||||
}
|
||||
|
||||
|
||||
@@ -29,7 +29,6 @@ import io.ebeaninternal.server.deploy.BeanManager;
|
||||
import io.ebeaninternal.server.deploy.BeanProperty;
|
||||
import io.ebeaninternal.server.deploy.BeanPropertyAssocMany;
|
||||
import io.ebeaninternal.server.deploy.BeanPropertyAssocOne;
|
||||
import io.ebeaninternal.server.deploy.BeanPropertySimpleCollection;
|
||||
import io.ebeaninternal.server.deploy.IntersectionRow;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
@@ -554,7 +553,7 @@ public final class DefaultPersister implements Persister {
|
||||
/**
|
||||
* Execute the delete request returning true if a delete occurred.
|
||||
*/
|
||||
private int deleteRequest(PersistRequestBean<?> req) {
|
||||
int deleteRequest(PersistRequestBean<?> req) {
|
||||
return deleteRequest(req, null);
|
||||
}
|
||||
|
||||
@@ -890,7 +889,7 @@ public final class DefaultPersister implements Persister {
|
||||
for (BeanPropertyAssocMany<?> many : desc.propertiesManySave()) {
|
||||
// check that property is loaded and collection should be cascaded to
|
||||
if (request.isLoadedProperty(many) && !many.isSkipSaveBeanCollection(parentBean, insertedParent)) {
|
||||
saveMany2(insertedParent, many, parentBean, request);
|
||||
saveMany(insertedParent, many, parentBean, request);
|
||||
if (!insertedParent) {
|
||||
request.addUpdatedManyProperty(many);
|
||||
}
|
||||
@@ -898,201 +897,23 @@ public final class DefaultPersister implements Persister {
|
||||
}
|
||||
}
|
||||
|
||||
private void saveMany2(boolean insertedParent, BeanPropertyAssocMany<?> many, EntityBean parentBean, PersistRequestBean<?> request) {
|
||||
saveMany(saveManyRequest(insertedParent, many, parentBean, request));
|
||||
private void saveMany(boolean insertedParent, BeanPropertyAssocMany<?> many, EntityBean parentBean, PersistRequestBean<?> request) {
|
||||
saveManyRequest(insertedParent, many, parentBean, request).save();
|
||||
}
|
||||
|
||||
private SaveManyPropRequest saveManyRequest(boolean insertedParent, BeanPropertyAssocMany<?> many, EntityBean parentBean, PersistRequestBean<?> request) {
|
||||
if (many instanceof BeanPropertySimpleCollection) {
|
||||
private SaveManyBase saveManyRequest(boolean insertedParent, BeanPropertyAssocMany<?> many, EntityBean parentBean, PersistRequestBean<?> request) {
|
||||
if (!many.isElementCollection()) {
|
||||
return new SaveManyBeans(insertedParent, many, parentBean, request, this);
|
||||
|
||||
} else if (many.getManyType().isMap()) {
|
||||
return new SaveManySimpleMap(insertedParent, many, parentBean, request);
|
||||
|
||||
} else {
|
||||
return new SaveManySimpleCollection(insertedParent, many, parentBean, request);
|
||||
} else {
|
||||
return new SaveManyPropRequest(insertedParent, many, parentBean, request);
|
||||
}
|
||||
}
|
||||
|
||||
private void saveMany(SaveManyPropRequest saveMany) {
|
||||
|
||||
if (saveMany.getMany().hasJoinTable()) {
|
||||
|
||||
// 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);
|
||||
}
|
||||
// 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());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
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()) {
|
||||
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, saveMany.getTransaction(), PersistRequest.Type.DELETE, saveMany.getFlags()));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Save the details from a OneToMany collection.
|
||||
*/
|
||||
private void saveAssocManyDetails(SaveManyPropRequest saveMany, boolean deleteMissingChildren) {
|
||||
|
||||
saveMany.saveDetails(this, deleteMissingChildren);
|
||||
}
|
||||
|
||||
/**
|
||||
* 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(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<?> manyValue = (BeanCollection<?>) value;
|
||||
setListenMode(manyValue, prop);
|
||||
manyValue.modifyReset();
|
||||
}
|
||||
} else {
|
||||
// BeanCollection so get the additions/deletions
|
||||
BeanCollection<?> manyValue = (BeanCollection<?>) value;
|
||||
if (setListenMode(manyValue, prop)) {
|
||||
additions = manyValue.getActualDetails();
|
||||
} else {
|
||||
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);
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if we need to set the listen mode (on new collections persisted for the first time).
|
||||
*/
|
||||
private boolean setListenMode(BeanCollection<?> manyValue, BeanPropertyAssocMany<?> prop) {
|
||||
ModifyListenMode mode = manyValue.getModifyListening();
|
||||
if (mode == null) {
|
||||
// new collection persisted for the first time
|
||||
manyValue.setModifyListening(prop.getModifyListenMode());
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
private void deleteAssocManyIntersection(EntityBean bean, BeanPropertyAssocMany<?> many, Transaction t, boolean publish) {
|
||||
void deleteAssocManyIntersection(EntityBean bean, BeanPropertyAssocMany<?> many, Transaction t, boolean publish) {
|
||||
|
||||
// delete all intersection rows for this bean
|
||||
IntersectionRow intRow = many.buildManyToManyDeleteChildren(bean, publish);
|
||||
@@ -1241,9 +1062,7 @@ public final class DefaultPersister implements Persister {
|
||||
BeanDescriptor<?> desc = request.getBeanDescriptor();
|
||||
|
||||
// imported ones with save cascade
|
||||
BeanPropertyAssocOne<?>[] ones = desc.propertiesOneImportedSave();
|
||||
|
||||
for (BeanPropertyAssocOne<?> prop : ones) {
|
||||
for (BeanPropertyAssocOne<?> prop : desc.propertiesOneImportedSave()) {
|
||||
// check for partial objects
|
||||
if (request.isLoadedProperty(prop)) {
|
||||
EntityBean detailBean = prop.getValueAsEntityBean(request.getEntityBean());
|
||||
@@ -1277,8 +1096,7 @@ public final class DefaultPersister implements Persister {
|
||||
|
||||
DeleteUnloadedForeignKeys fkeys = null;
|
||||
|
||||
BeanPropertyAssocOne<?>[] ones = request.getBeanDescriptor().propertiesOneImportedDelete();
|
||||
for (BeanPropertyAssocOne<?> one : ones) {
|
||||
for (BeanPropertyAssocOne<?> one : request.getBeanDescriptor().propertiesOneImportedDelete()) {
|
||||
if (!request.isLoadedProperty(one)) {
|
||||
// we have cascade Delete on a partially populated bean and
|
||||
// this property was not loaded (so we are going to have to fetch it)
|
||||
@@ -1299,8 +1117,7 @@ public final class DefaultPersister implements Persister {
|
||||
|
||||
boolean softDelete = request.isSoftDelete();
|
||||
|
||||
BeanPropertyAssocOne<?>[] ones = request.getBeanDescriptor().propertiesOneImportedDelete();
|
||||
for (BeanPropertyAssocOne<?> prop : ones) {
|
||||
for (BeanPropertyAssocOne<?> prop : request.getBeanDescriptor().propertiesOneImportedDelete()) {
|
||||
if (!softDelete || prop.isTargetSoftDelete()) {
|
||||
if (request.isLoadedProperty(prop)) {
|
||||
Object detailBean = prop.getValue(request.getEntityBean());
|
||||
@@ -1355,7 +1172,7 @@ public final class DefaultPersister implements Persister {
|
||||
/**
|
||||
* Create the Persist Request Object additionally specifying the publish status.
|
||||
*/
|
||||
private <T> PersistRequestBean<T> createPublishRequest(T bean, Transaction t, PersistRequest.Type type, int flags) {
|
||||
<T> PersistRequestBean<T> createPublishRequest(T bean, Transaction t, PersistRequest.Type type, int flags) {
|
||||
return createRequestInternal(bean, t, type, Flags.unsetRecuse(flags));
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,37 @@
|
||||
package io.ebeaninternal.server.persist;
|
||||
|
||||
import io.ebean.bean.EntityBean;
|
||||
import io.ebeaninternal.api.SpiEbeanServer;
|
||||
import io.ebeaninternal.api.SpiTransaction;
|
||||
import io.ebeaninternal.server.core.PersistRequestBean;
|
||||
import io.ebeaninternal.server.deploy.BeanPropertyAssocMany;
|
||||
|
||||
/**
|
||||
* Base for saving entity bean collections and element collections.
|
||||
*/
|
||||
abstract class SaveManyBase {
|
||||
|
||||
final PersistRequestBean<?> request;
|
||||
final SpiEbeanServer server;
|
||||
final boolean insertedParent;
|
||||
final BeanPropertyAssocMany<?> many;
|
||||
final SpiTransaction transaction;
|
||||
final EntityBean parentBean;
|
||||
final Object value;
|
||||
|
||||
SaveManyBase(boolean insertedParent, BeanPropertyAssocMany<?> many, EntityBean parentBean, PersistRequestBean<?> request) {
|
||||
this.request = request;
|
||||
this.server = request.getServer();
|
||||
this.insertedParent = insertedParent;
|
||||
this.many = many;
|
||||
this.parentBean = parentBean;
|
||||
this.transaction = request.getTransaction();
|
||||
this.value = many.getValue(parentBean);
|
||||
}
|
||||
|
||||
/**
|
||||
* Save the collection.
|
||||
*/
|
||||
abstract void save();
|
||||
|
||||
}
|
||||
@@ -0,0 +1,381 @@
|
||||
package io.ebeaninternal.server.persist;
|
||||
|
||||
import io.ebean.SqlUpdate;
|
||||
import io.ebean.bean.BeanCollection;
|
||||
import io.ebean.bean.EntityBean;
|
||||
import io.ebean.bean.EntityBeanIntercept;
|
||||
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).
|
||||
*/
|
||||
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 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();
|
||||
}
|
||||
|
||||
@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() {
|
||||
return transaction.isSaveAssocManyIntersection(many.getIntersectionTableJoin().getTable(), many.getBeanDescriptor().getName());
|
||||
}
|
||||
|
||||
private boolean isModifyListenMode() {
|
||||
return BeanCollection.ModifyListenMode.REMOVALS == many.getModifyListenMode();
|
||||
}
|
||||
|
||||
private void modifyListenReset(BeanCollection<?> c) {
|
||||
if (insertedParent) {
|
||||
// after insert set the modify listening mode for private owned etc
|
||||
c.setModifyListening(many.getModifyListenMode());
|
||||
}
|
||||
c.modifyReset();
|
||||
}
|
||||
|
||||
private void resetModifyState() {
|
||||
if (value instanceof BeanCollection<?>) {
|
||||
modifyListenReset((BeanCollection<?>) value);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 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, false);
|
||||
}
|
||||
|
||||
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.
|
||||
*/
|
||||
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.
|
||||
*/
|
||||
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.
|
||||
* <p>
|
||||
* This is done via MapBeans.
|
||||
* </p>
|
||||
*/
|
||||
private void saveAssocManyIntersection(boolean deleteMissingChildren) {
|
||||
|
||||
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
|
||||
persister.deleteAssocManyIntersection(parentBean, many, transaction, publish);
|
||||
}
|
||||
|
||||
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()) {
|
||||
// ensure any cascade batch has been flushed prior
|
||||
// to inserting into the intersection table
|
||||
transaction.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 (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);
|
||||
SqlUpdate sqlInsert = intRow.createInsert(server);
|
||||
persister.executeSqlUpdate(sqlInsert, transaction);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
if (deletions != null && !deletions.isEmpty()) {
|
||||
// ensure any cascade batch has been flushed prior
|
||||
// to inserting into the intersection table
|
||||
transaction.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 = many.buildManyToManyMapBean(parentBean, otherDelete, publish);
|
||||
SqlUpdate sqlDelete = intRow.createDelete(server, false);
|
||||
persister.executeSqlUpdate(sqlDelete, transaction);
|
||||
}
|
||||
}
|
||||
|
||||
// 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().isLoaded()) {
|
||||
// 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).
|
||||
*/
|
||||
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;
|
||||
}
|
||||
}
|
||||
@@ -1,251 +0,0 @@
|
||||
package io.ebeaninternal.server.persist;
|
||||
|
||||
import io.ebean.bean.BeanCollection;
|
||||
import io.ebean.bean.EntityBean;
|
||||
import io.ebean.bean.EntityBeanIntercept;
|
||||
import io.ebeaninternal.api.SpiTransaction;
|
||||
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.ManyType;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collection;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
/**
|
||||
* Helper to wrap the details when saving a OneToMany or ManyToMany relationship.
|
||||
*/
|
||||
class SaveManyPropRequest {
|
||||
|
||||
final PersistRequestBean<?> request;
|
||||
private final boolean insertedParent;
|
||||
final BeanPropertyAssocMany<?> many;
|
||||
final EntityBean parentBean;
|
||||
final SpiTransaction transaction;
|
||||
private final boolean cascade;
|
||||
private final boolean deleteMissingChildren;
|
||||
private final boolean publish;
|
||||
|
||||
private final Object value;
|
||||
private final BeanDescriptor<?> targetDescriptor;
|
||||
private final boolean isMap;
|
||||
private final boolean saveRecurseSkippable;
|
||||
|
||||
Collection<?> collection;
|
||||
DefaultPersister persister;
|
||||
private boolean deleteMissing;
|
||||
private int sortOrder;
|
||||
|
||||
SaveManyPropRequest(boolean insertedParent, BeanPropertyAssocMany<?> many, EntityBean parentBean, PersistRequestBean<?> request) {
|
||||
this.request = 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();
|
||||
this.value = many.getValue(parentBean);
|
||||
this.targetDescriptor = many.getTargetDescriptor();
|
||||
this.isMap = ManyType.MAP == many.getManyType();
|
||||
this.saveRecurseSkippable = many.isSaveRecurseSkippable();
|
||||
}
|
||||
|
||||
public boolean isSaveIntersection() {
|
||||
return transaction.isSaveAssocManyIntersection(many.getIntersectionTableJoin().getTable(), many.getBeanDescriptor().getName());
|
||||
}
|
||||
|
||||
Object getValue() {
|
||||
return value;
|
||||
}
|
||||
|
||||
boolean isModifyListenMode() {
|
||||
return BeanCollection.ModifyListenMode.REMOVALS == many.getModifyListenMode();
|
||||
}
|
||||
|
||||
boolean isDeleteMissingChildren() {
|
||||
return deleteMissingChildren;
|
||||
}
|
||||
|
||||
public int getFlags() {
|
||||
return request.getFlags();
|
||||
}
|
||||
|
||||
boolean isInsertedParent() {
|
||||
return insertedParent;
|
||||
}
|
||||
|
||||
BeanPropertyAssocMany<?> getMany() {
|
||||
return many;
|
||||
}
|
||||
|
||||
EntityBean getParentBean() {
|
||||
return parentBean;
|
||||
}
|
||||
|
||||
SpiTransaction getTransaction() {
|
||||
return transaction;
|
||||
}
|
||||
|
||||
boolean isCascade() {
|
||||
return cascade;
|
||||
}
|
||||
|
||||
boolean isPublish() {
|
||||
return publish;
|
||||
}
|
||||
|
||||
void modifyListenReset(BeanCollection<?> c) {
|
||||
if (insertedParent) {
|
||||
// after insert set the modify listening mode for private owned etc
|
||||
c.setModifyListening(many.getModifyListenMode());
|
||||
}
|
||||
c.modifyReset();
|
||||
}
|
||||
|
||||
void resetModifyState() {
|
||||
if (value instanceof BeanCollection<?>) {
|
||||
modifyListenReset((BeanCollection<?>) value);
|
||||
}
|
||||
}
|
||||
|
||||
void saveDetails(DefaultPersister persister, boolean deleteMissing) {
|
||||
|
||||
this.persister = persister;
|
||||
this.deleteMissing = deleteMissing;
|
||||
|
||||
// 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 = BeanCollectionUtil.getActualEntries(value);
|
||||
if (collection != null) {
|
||||
processDetails();
|
||||
}
|
||||
}
|
||||
|
||||
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, false);
|
||||
}
|
||||
|
||||
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.
|
||||
*/
|
||||
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.
|
||||
*/
|
||||
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;
|
||||
}
|
||||
}
|
||||
@@ -4,16 +4,27 @@ import io.ebean.SqlUpdate;
|
||||
import io.ebean.bean.EntityBean;
|
||||
import io.ebeaninternal.api.SpiEbeanServer;
|
||||
import io.ebeaninternal.server.core.PersistRequestBean;
|
||||
import io.ebeaninternal.server.deploy.BeanCollectionUtil;
|
||||
import io.ebeaninternal.server.deploy.BeanPropertyAssocMany;
|
||||
|
||||
class SaveManySimpleCollection extends SaveManyPropRequest {
|
||||
import java.util.Collection;
|
||||
|
||||
/**
|
||||
* Save details for a simple scalar element collection.
|
||||
*/
|
||||
class SaveManySimpleCollection extends SaveManyBase {
|
||||
|
||||
SaveManySimpleCollection(boolean insertedParent, BeanPropertyAssocMany<?> many, EntityBean parentBean, PersistRequestBean<?> request) {
|
||||
super(insertedParent, many, parentBean, request);
|
||||
}
|
||||
|
||||
@Override
|
||||
void processDetails() {
|
||||
void save() {
|
||||
|
||||
Collection<?> collection = BeanCollectionUtil.getActualEntries(value);
|
||||
if (collection == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
Object parentId = request.getBeanId();
|
||||
|
||||
|
||||
@@ -0,0 +1,52 @@
|
||||
package io.ebeaninternal.server.persist;
|
||||
|
||||
import io.ebean.SqlUpdate;
|
||||
import io.ebean.bean.EntityBean;
|
||||
import io.ebeaninternal.api.SpiEbeanServer;
|
||||
import io.ebeaninternal.server.core.PersistRequestBean;
|
||||
import io.ebeaninternal.server.deploy.BeanCollectionUtil;
|
||||
import io.ebeaninternal.server.deploy.BeanPropertyAssocMany;
|
||||
|
||||
import java.util.Map;
|
||||
import java.util.Set;
|
||||
|
||||
/**
|
||||
* Save details for a simple scalar map element collection.
|
||||
*/
|
||||
class SaveManySimpleMap extends SaveManyBase {
|
||||
|
||||
SaveManySimpleMap(boolean insertedParent, BeanPropertyAssocMany<?> many, EntityBean parentBean, PersistRequestBean<?> request) {
|
||||
super(insertedParent, many, parentBean, request);
|
||||
}
|
||||
|
||||
@SuppressWarnings("unchecked")
|
||||
@Override
|
||||
void save() {
|
||||
|
||||
Set<Map.Entry<?, ?>> entries = (Set<Map.Entry<?, ?>>)BeanCollectionUtil.getActualEntries(value);
|
||||
if (entries == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
Object parentId = request.getBeanId();
|
||||
|
||||
SqlUpdate sqlDelete = many.deleteByParentId(parentId, null);
|
||||
|
||||
SpiEbeanServer server = request.getServer();
|
||||
server.execute(sqlDelete, transaction);
|
||||
|
||||
transaction.depth(+1);
|
||||
|
||||
String insert = many.insertElementCollection();
|
||||
SqlUpdate sqlInsert = server.createSqlUpdate(insert);
|
||||
|
||||
for (Map.Entry<?, ?> entry : entries) {
|
||||
sqlInsert.setParameter(1, parentId);
|
||||
sqlInsert.setParameter(2, entry.getKey());
|
||||
sqlInsert.setParameter(3, entry.getValue());
|
||||
server.execute(sqlInsert, transaction);
|
||||
}
|
||||
|
||||
transaction.depth(-1);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user