T mergeBeans(T bean, T existing, BeanMergeOptions options);
+
/**
* Insert the bean.
*
@@ -1520,4 +1535,8 @@ static DatabaseBuilder builder() {
*/
void truncate(Class>... beanTypes);
+ /**
+ * RunDdl manually. This can be used if 'db.ddl.run=false' is set and you plan to run DDL manually.
+ */
+ void runDdl();
}
diff --git a/ebean-api/src/main/java/io/ebean/DatabaseBuilder.java b/ebean-api/src/main/java/io/ebean/DatabaseBuilder.java
index 985ba1760e..174550ab32 100644
--- a/ebean-api/src/main/java/io/ebean/DatabaseBuilder.java
+++ b/ebean-api/src/main/java/io/ebean/DatabaseBuilder.java
@@ -15,6 +15,7 @@
import io.ebean.event.changelog.ChangeLogRegister;
import io.ebean.event.readaudit.ReadAuditLogger;
import io.ebean.event.readaudit.ReadAuditPrepare;
+import io.ebean.plugin.CustomDeployParser;
import jakarta.persistence.EnumType;
import javax.sql.DataSource;
@@ -896,6 +897,12 @@ default DatabaseBuilder backgroundExecutorWrapper(BackgroundExecutorWrapper back
@Deprecated
DatabaseBuilder setBackgroundExecutorWrapper(BackgroundExecutorWrapper backgroundExecutorWrapper);
+ /**
+ * Sets the tenant partitioning mode for caches. This means, caches are created on demand,
+ * but they may not get invalidated across tenant boundaries *
+ */
+ void setTenantPartitionedCache(boolean tenantPartitionedCache);
+
/**
* Set the L2 cache default max size.
*/
@@ -1827,6 +1834,11 @@ default DatabaseBuilder resourceDirectory(String resourceDirectory) {
*/
DatabaseBuilder addServerConfigStartup(ServerConfigStartup configStartupListener);
+ /**
+ * Add a CustomDeployParser.
+ */
+ DatabaseConfig addCustomDeployParser(CustomDeployParser customDeployParser);
+
/**
* Register all the BeanPersistListener instances.
*
@@ -2079,6 +2091,11 @@ default DatabaseBuilder queryPlanEnable(boolean queryPlanEnable) {
@Deprecated
DatabaseBuilder setQueryPlanEnable(boolean queryPlanEnable);
+ /**
+ * Set platform specific query plan options.
+ */
+ DatabaseBuilder queryPlanOptions(String queryPlanOptions);
+
/**
* Set the query plan collection threshold in microseconds.
*
@@ -2563,6 +2580,11 @@ interface Settings extends DatabaseBuilder {
*/
boolean isAutoPersistUpdates();
+ /**
+ * Returns, if the caches are partitioned by tenant.
+ */
+ boolean isTenantPartitionedCache();
+
/**
* Return the L2 cache default max size.
*/
@@ -2975,6 +2997,11 @@ interface Settings extends DatabaseBuilder {
*/
List getServerConfigStartupListeners();
+ /**
+ * Returns the registered CustomDeployParsers.
+ */
+ List getCustomDeployParsers();
+
/**
* Return the default PersistenceContextScope to be used if one is not explicitly set on a query.
*
@@ -3061,6 +3088,11 @@ interface Settings extends DatabaseBuilder {
*/
boolean isQueryPlanEnable();
+ /**
+ * Returns platform specific query plan options.
+ */
+ String getQueryPlanOptions();
+
/**
* Return the query plan collection threshold in microseconds.
*/
diff --git a/ebean-api/src/main/java/io/ebean/EbeanVersion.java b/ebean-api/src/main/java/io/ebean/EbeanVersion.java
index aeb855efce..8c543e3fe4 100644
--- a/ebean-api/src/main/java/io/ebean/EbeanVersion.java
+++ b/ebean-api/src/main/java/io/ebean/EbeanVersion.java
@@ -22,8 +22,8 @@ public final class EbeanVersion {
/**
* Maintain the minimum ebean-agent version manually based on required ebean-agent bug fixes.
*/
- private static final int MIN_AGENT_MAJOR_VERSION = 12;
- private static final int MIN_AGENT_MINOR_VERSION = 12;
+ private static final int MIN_AGENT_MAJOR_VERSION = 13;
+ private static final int MIN_AGENT_MINOR_VERSION = 10;
private static String version = "unknown";
static {
diff --git a/ebean-api/src/main/java/io/ebean/LengthCheckException.java b/ebean-api/src/main/java/io/ebean/LengthCheckException.java
new file mode 100644
index 0000000000..b66bfb51c7
--- /dev/null
+++ b/ebean-api/src/main/java/io/ebean/LengthCheckException.java
@@ -0,0 +1,15 @@
+package io.ebean;
+
+/**
+ * Exception when the length check has an error.
+ */
+public class LengthCheckException extends DataIntegrityException {
+ private static final long serialVersionUID = -4771932723285724817L;
+
+ /**
+ * Create with a message.
+ */
+ public LengthCheckException(String message) {
+ super(message);
+ }
+}
diff --git a/ebean-api/src/main/java/io/ebean/Transaction.java b/ebean-api/src/main/java/io/ebean/Transaction.java
index 4f7c148189..3347b250c4 100644
--- a/ebean-api/src/main/java/io/ebean/Transaction.java
+++ b/ebean-api/src/main/java/io/ebean/Transaction.java
@@ -260,6 +260,12 @@ static Transaction current() {
*/
void setUpdateAllLoadedProperties(boolean updateAllLoadedProperties);
+ /**
+ * If set to false (default is true) generated propertes are only set, if it is the version property or have a null value.
+ * This may be useful in backup & restore scenarios, if you want set WhenCreated/WhenModified.
+ */
+ void setOverwriteGeneratedProperties(boolean overwriteGeneratedProperties);
+
/**
* Set if the L2 cache should be skipped for "find by id" and "find by natural key" queries.
*
@@ -555,4 +561,12 @@ static Transaction current() {
* Get an object added with {@link #putUserObject(String, Object)}.
*/
Object getUserObject(String name);
+
+ /**
+ * In case of nested transaction, this returns the root transaction.
+ * @return
+ */
+ default Transaction root() {
+ return this;
+ }
}
diff --git a/ebean-api/src/main/java/io/ebean/bean/BeanCollection.java b/ebean-api/src/main/java/io/ebean/bean/BeanCollection.java
index 1161476ddd..dcb002266f 100644
--- a/ebean-api/src/main/java/io/ebean/bean/BeanCollection.java
+++ b/ebean-api/src/main/java/io/ebean/bean/BeanCollection.java
@@ -162,7 +162,17 @@ enum ModifyListenMode {
*
* For maps this returns the entrySet as we need the keys of the map.
*/
- Collection> actualEntries();
+ Collection> actualEntries(boolean load);
+
+ default Collection> actualEntries() {
+ return actualEntries(false);
+ }
+ /**
+ * Returns entries, that were lazily added at the end of the list. Might be null.
+ */
+ default Collection getLazyAddedEntries(boolean reset) {
+ return null;
+ }
/**
* return true if there are real rows held. Return false is this is using
@@ -240,4 +250,9 @@ enum ModifyListenMode {
* Return a shallow copy of this collection that is modifiable.
*/
BeanCollection shallowCopy();
+
+ /**
+ * Clears the underlying collection.
+ */
+ void clear();
}
diff --git a/ebean-api/src/main/java/io/ebean/bean/EntityBean.java b/ebean-api/src/main/java/io/ebean/bean/EntityBean.java
index 53bfc9ba49..1a70a68571 100644
--- a/ebean-api/src/main/java/io/ebean/bean/EntityBean.java
+++ b/ebean-api/src/main/java/io/ebean/bean/EntityBean.java
@@ -41,6 +41,13 @@ default Object _ebean_newInstanceReadOnly() {
throw new NotEnhancedException();
}
+ /**
+ * Creates a new instance and uses the provided intercept. (For EntityExtension)
+ */
+ default Object _ebean_newExtendedInstance(int offset, EntityBean base) {
+ throw new NotEnhancedException();
+ }
+
/**
* Generated method that sets the loaded state on all the embedded beans on
* this entity bean by using EntityBeanIntercept.setEmbeddedLoaded(Object o);
@@ -120,4 +127,19 @@ default Object _ebean_getFieldIntercept(int fieldIndex) {
default void toString(ToStringBuilder builder) {
throw new NotEnhancedException();
}
+
+ /**
+ * Returns the ExtensionAccessors, this is always NONE
for non extendable beans.
+ */
+ default ExtensionAccessors _ebean_getExtensionAccessors() {
+ return ExtensionAccessors.NONE;
+ }
+
+ /**
+ * Returns the extension bean for an accessor. This will throw NotEnhancedException for non extendable beans.
+ * (It is not intended to call this method here)
+ */
+ default EntityBean _ebean_getExtension(ExtensionAccessor accessor) {
+ throw new NotEnhancedException(); // not an extendableBean
+ }
}
diff --git a/ebean-api/src/main/java/io/ebean/bean/EntityBeanIntercept.java b/ebean-api/src/main/java/io/ebean/bean/EntityBeanIntercept.java
index 1127c02b25..120068ad34 100644
--- a/ebean-api/src/main/java/io/ebean/bean/EntityBeanIntercept.java
+++ b/ebean-api/src/main/java/io/ebean/bean/EntityBeanIntercept.java
@@ -552,4 +552,24 @@ public interface EntityBeanIntercept extends Serializable {
* Update the 'next' mutable info returning the content that was obtained via dirty detection.
*/
String mutableNext(int propertyIndex);
+
+ /**
+ * Returns the value of the property. Can also return virtual properties.
+ */
+ Object value(int propertyIndex);
+
+ /**
+ * Returns the value of the property with intercept access. Can also return virtual properties.
+ */
+ Object valueIntercept(int propertyIndex);
+
+ /**
+ * Writes the value to the property. Can also write virtual properties.
+ */
+ void setValue(int propertyIndex, Object value);
+
+ /**
+ * Writes the value to the property with intercept access. Can also write virtual properties.
+ */
+ void setValueIntercept(int propertyIndex, Object value);
}
diff --git a/ebean-api/src/main/java/io/ebean/bean/EntityExtensionIntercept.java b/ebean-api/src/main/java/io/ebean/bean/EntityExtensionIntercept.java
new file mode 100644
index 0000000000..9efdd8ee07
--- /dev/null
+++ b/ebean-api/src/main/java/io/ebean/bean/EntityExtensionIntercept.java
@@ -0,0 +1,562 @@
+package io.ebean.bean;
+
+import io.ebean.ValuePair;
+
+import java.util.Map;
+import java.util.Set;
+
+/**
+ * Intercept for classes annotated with @EntityExtension. The intercept will delegate all calls to the base intercept of the
+ * ExtendableBean and adds given offset to all field operations.
+ *
+ * @author Roland Praml, FOCONIS AG
+ */
+public class EntityExtensionIntercept implements EntityBeanIntercept {
+ private final EntityBeanIntercept base;
+ private final int offset;
+
+ public EntityExtensionIntercept(Object ownerBean, int offset, EntityBean base) {
+ this.base = base._ebean_getIntercept();
+ this.offset = offset;
+ }
+
+ @Override
+ public EntityBean owner() {
+ return base.owner();
+ }
+
+ @Override
+ public PersistenceContext persistenceContext() {
+ return base.persistenceContext();
+ }
+
+ @Override
+ public void setPersistenceContext(PersistenceContext persistenceContext) {
+ base.setPersistenceContext(persistenceContext);
+ }
+
+ @Override
+ public void setNodeUsageCollector(NodeUsageCollector usageCollector) {
+ base.setNodeUsageCollector(usageCollector);
+ }
+
+ @Override
+ public Object ownerId() {
+ return base.ownerId();
+ }
+
+ @Override
+ public void setOwnerId(Object ownerId) {
+ base.setOwnerId(ownerId);
+ }
+
+ @Override
+ public Object embeddedOwner() {
+ return base.embeddedOwner();
+ }
+
+ @Override
+ public int embeddedOwnerIndex() {
+ return base.embeddedOwnerIndex();
+ }
+
+ @Override
+ public void clearGetterCallback() {
+ base.clearGetterCallback();
+ }
+
+ @Override
+ public void registerGetterCallback(PreGetterCallback getterCallback) {
+ base.registerGetterCallback(getterCallback);
+ }
+
+ @Override
+ public void setEmbeddedOwner(EntityBean parentBean, int embeddedOwnerIndex) {
+ base.setEmbeddedOwner(parentBean, embeddedOwnerIndex);
+ }
+
+ @Override
+ public void setBeanLoader(BeanLoader beanLoader, PersistenceContext ctx) {
+ base.setBeanLoader(beanLoader, ctx);
+ }
+
+ @Override
+ public void setBeanLoader(BeanLoader beanLoader) {
+ base.setBeanLoader(beanLoader);
+ }
+
+ @Override
+ public boolean isFullyLoadedBean() {
+ return base.isFullyLoadedBean();
+ }
+
+ @Override
+ public void setFullyLoadedBean(boolean fullyLoadedBean) {
+ base.setFullyLoadedBean(fullyLoadedBean);
+ }
+
+ @Override
+ public boolean isPartial() {
+ return base.isPartial();
+ }
+
+ @Override
+ public boolean isDirty() {
+ return base.isDirty();
+ }
+
+ @Override
+ public void setEmbeddedDirty(int embeddedProperty) {
+ base.setEmbeddedDirty(embeddedProperty + offset);
+ }
+
+ @Override
+ public void setDirty(boolean dirty) {
+ base.setDirty(dirty);
+ }
+
+ @Override
+ public boolean isNew() {
+ return base.isNew();
+ }
+
+ @Override
+ public boolean isNewOrDirty() {
+ return base.isNewOrDirty();
+ }
+
+ @Override
+ public boolean hasIdOnly(int idIndex) {
+ return base.hasIdOnly(idIndex + offset);
+ }
+
+ @Override
+ public boolean isReference() {
+ return base.isReference();
+ }
+
+ @Override
+ public void setReference(int idPos) {
+ base.setReference(idPos + offset);
+ }
+
+ @Override
+ public void setLoadedFromCache(boolean loadedFromCache) {
+ base.setLoadedFromCache(loadedFromCache);
+ }
+
+ @Override
+ public boolean isLoadedFromCache() {
+ return base.isLoadedFromCache();
+ }
+
+ @Override
+ public boolean isReadOnly() {
+ return base.isReadOnly();
+ }
+
+ @Override
+ public void setReadOnly(boolean readOnly) {
+ base.setReadOnly(readOnly);
+ }
+
+ @Override
+ public void setForceUpdate(boolean forceUpdate) {
+ base.setForceUpdate(forceUpdate);
+ }
+
+ @Override
+ public boolean isUpdate() {
+ return base.isUpdate();
+ }
+
+ @Override
+ public boolean isLoaded() {
+ return base.isLoaded();
+ }
+
+ @Override
+ public void setNew() {
+ base.setNew();
+ }
+
+ @Override
+ public void setLoaded() {
+ base.setLoaded();
+ }
+
+ @Override
+ public void setLoadedLazy() {
+ base.setLoadedLazy();
+ }
+
+ @Override
+ public void setLazyLoadFailure(Object ownerId) {
+ base.setLazyLoadFailure(ownerId);
+ }
+
+ @Override
+ public boolean isLazyLoadFailure() {
+ return base.isLazyLoadFailure();
+ }
+
+ @Override
+ public boolean isDisableLazyLoad() {
+ return base.isDisableLazyLoad();
+ }
+
+ @Override
+ public void setDisableLazyLoad(boolean disableLazyLoad) {
+ base.setDisableLazyLoad(disableLazyLoad);
+ }
+
+ @Override
+ public void setEmbeddedLoaded(Object embeddedBean) {
+ base.setEmbeddedLoaded(embeddedBean);
+ }
+
+ @Override
+ public boolean isEmbeddedNewOrDirty(Object embeddedBean) {
+ return base.isEmbeddedNewOrDirty(embeddedBean);
+ }
+
+ @Override
+ public Object origValue(int propertyIndex) {
+ return base.origValue(propertyIndex + offset);
+ }
+
+ @Override
+ public int findProperty(String propertyName) {
+ return base.findProperty(propertyName);
+ }
+
+ @Override
+ public String property(int propertyIndex) {
+ return base.property(propertyIndex + offset);
+ }
+
+ @Override
+ public int propertyLength() {
+ return base.propertyLength();
+ }
+
+ @Override
+ public void setPropertyLoaded(String propertyName, boolean loaded) {
+ base.setPropertyLoaded(propertyName, loaded);
+ }
+
+ @Override
+ public void setPropertyUnloaded(int propertyIndex) {
+ base.setPropertyUnloaded(propertyIndex + offset);
+ }
+
+ @Override
+ public void setLoadedProperty(int propertyIndex) {
+ base.setLoadedProperty(propertyIndex + offset);
+ }
+
+ @Override
+ public void setLoadedPropertyAll() {
+ base.setLoadedPropertyAll();
+ }
+
+ @Override
+ public boolean isLoadedProperty(int propertyIndex) {
+ return base.isLoadedProperty(propertyIndex + offset);
+ }
+
+ @Override
+ public boolean isChangedProperty(int propertyIndex) {
+ return base.isChangedProperty(propertyIndex + offset);
+ }
+
+ @Override
+ public boolean isDirtyProperty(int propertyIndex) {
+ return base.isDirtyProperty(propertyIndex + offset);
+ }
+
+ @Override
+ public void markPropertyAsChanged(int propertyIndex) {
+ base.markPropertyAsChanged(propertyIndex + offset);
+ }
+
+ @Override
+ public void setChangedProperty(int propertyIndex) {
+ base.setChangedProperty(propertyIndex + offset);
+ }
+
+ @Override
+ public void setChangeLoaded(int propertyIndex) {
+ base.setChangeLoaded(propertyIndex + offset);
+ }
+
+ @Override
+ public void setEmbeddedPropertyDirty(int propertyIndex) {
+ base.setEmbeddedPropertyDirty(propertyIndex + offset);
+ }
+
+ @Override
+ public void setOriginalValue(int propertyIndex, Object value) {
+ base.setOriginalValue(propertyIndex + offset, value);
+ }
+
+ @Override
+ public void setOriginalValueForce(int propertyIndex, Object value) {
+ base.setOriginalValueForce(propertyIndex + offset, value);
+ }
+
+ @Override
+ public void setNewBeanForUpdate() {
+ base.setNewBeanForUpdate();
+ }
+
+ @Override
+ public Set loadedPropertyNames() {
+ return base.loadedPropertyNames();
+ }
+
+ @Override
+ public boolean[] dirtyProperties() {
+ return base.dirtyProperties();
+ }
+
+ @Override
+ public Set dirtyPropertyNames() {
+ return base.dirtyPropertyNames();
+ }
+
+ @Override
+ public void addDirtyPropertyNames(Set props, String prefix) {
+ base.addDirtyPropertyNames(props, prefix);
+ }
+
+ @Override
+ public boolean hasDirtyProperty(Set propertyNames) {
+ return base.hasDirtyProperty(propertyNames);
+ }
+
+ @Override
+ public Map dirtyValues() {
+ return base.dirtyValues();
+ }
+
+ @Override
+ public void addDirtyPropertyValues(Map dirtyValues, String prefix) {
+ base.addDirtyPropertyValues(dirtyValues, prefix);
+ }
+
+ @Override
+ public void addDirtyPropertyValues(BeanDiffVisitor visitor) {
+ base.addDirtyPropertyValues(visitor);
+ }
+
+ @Override
+ public StringBuilder dirtyPropertyKey() {
+ return base.dirtyPropertyKey();
+ }
+
+ @Override
+ public void addDirtyPropertyKey(StringBuilder sb) {
+ base.addDirtyPropertyKey(sb);
+ }
+
+ @Override
+ public StringBuilder loadedPropertyKey() {
+ return base.loadedPropertyKey();
+ }
+
+ @Override
+ public boolean[] loaded() {
+ return base.loaded();
+ }
+
+ @Override
+ public int lazyLoadPropertyIndex() {
+ return base.lazyLoadPropertyIndex() - offset;
+ }
+
+ @Override
+ public String lazyLoadProperty() {
+ return base.lazyLoadProperty();
+ }
+
+ @Override
+ public void loadBean(int loadProperty) {
+ base.loadBean(loadProperty);
+ }
+
+ @Override
+ public void loadBeanInternal(int loadProperty, BeanLoader loader) {
+ base.loadBeanInternal(loadProperty + offset, loader);
+ }
+
+ @Override
+ public void initialisedMany(int propertyIndex) {
+ base.initialisedMany(propertyIndex + offset);
+ }
+
+ @Override
+ public void preGetterCallback(int propertyIndex) {
+ base.preGetterCallback(propertyIndex + offset);
+ }
+
+ @Override
+ public void preGetId() {
+ base.preGetId();
+ }
+
+ @Override
+ public void preGetter(int propertyIndex) {
+ base.preGetter(propertyIndex + offset);
+ }
+
+ @Override
+ public void preSetterMany(boolean interceptField, int propertyIndex, Object oldValue, Object newValue) {
+ base.preSetterMany(interceptField, propertyIndex + offset, oldValue, newValue);
+ }
+
+ @Override
+ public void setChangedPropertyValue(int propertyIndex, boolean setDirtyState, Object origValue) {
+ base.setChangedPropertyValue(propertyIndex + offset, setDirtyState, origValue);
+ }
+
+ @Override
+ public void setDirtyStatus() {
+ base.setDirtyStatus();
+ }
+
+ @Override
+ public void preSetter(boolean intercept, int propertyIndex, Object oldValue, Object newValue) {
+ base.preSetter(intercept, propertyIndex + offset, oldValue, newValue);
+ }
+
+ @Override
+ public void preSetter(boolean intercept, int propertyIndex, boolean oldValue, boolean newValue) {
+ base.preSetter(intercept, propertyIndex + offset, oldValue, newValue);
+ }
+
+ @Override
+ public void preSetter(boolean intercept, int propertyIndex, int oldValue, int newValue) {
+ base.preSetter(intercept, propertyIndex + offset, oldValue, newValue);
+ }
+
+ @Override
+ public void preSetter(boolean intercept, int propertyIndex, long oldValue, long newValue) {
+ base.preSetter(intercept, propertyIndex + offset, oldValue, newValue);
+ }
+
+ @Override
+ public void preSetter(boolean intercept, int propertyIndex, double oldValue, double newValue) {
+ base.preSetter(intercept, propertyIndex + offset, oldValue, newValue);
+ }
+
+ @Override
+ public void preSetter(boolean intercept, int propertyIndex, float oldValue, float newValue) {
+ base.preSetter(intercept, propertyIndex + offset, oldValue, newValue);
+ }
+
+ @Override
+ public void preSetter(boolean intercept, int propertyIndex, short oldValue, short newValue) {
+ base.preSetter(intercept, propertyIndex + offset, oldValue, newValue);
+ }
+
+ @Override
+ public void preSetter(boolean intercept, int propertyIndex, char oldValue, char newValue) {
+ base.preSetter(intercept, propertyIndex + offset, oldValue, newValue);
+ }
+
+ @Override
+ public void preSetter(boolean intercept, int propertyIndex, byte oldValue, byte newValue) {
+ base.preSetter(intercept, propertyIndex + offset, oldValue, newValue);
+ }
+
+ @Override
+ public void preSetter(boolean intercept, int propertyIndex, char[] oldValue, char[] newValue) {
+ base.preSetter(intercept, propertyIndex + offset, oldValue, newValue);
+ }
+
+ @Override
+ public void preSetter(boolean intercept, int propertyIndex, byte[] oldValue, byte[] newValue) {
+ base.preSetter(intercept, propertyIndex + offset, oldValue, newValue);
+ }
+
+ @Override
+ public void setOldValue(int propertyIndex, Object oldValue) {
+ base.setOldValue(propertyIndex + offset, oldValue);
+ }
+
+ @Override
+ public int sortOrder() {
+ return base.sortOrder();
+ }
+
+ @Override
+ public void setSortOrder(int sortOrder) {
+ base.setSortOrder(sortOrder);
+ }
+
+ @Override
+ public void setDeletedFromCollection(boolean deletedFromCollection) {
+ base.setDeletedFromCollection(deletedFromCollection);
+ }
+
+ @Override
+ public boolean isOrphanDelete() {
+ return base.isOrphanDelete();
+ }
+
+ @Override
+ public void setLoadError(int propertyIndex, Exception t) {
+ base.setLoadError(propertyIndex + offset, t);
+ }
+
+ @Override
+ public Map loadErrors() {
+ return base.loadErrors();
+ }
+
+ @Override
+ public boolean isChangedProp(int propertyIndex) {
+ return base.isChangedProp(propertyIndex + offset);
+ }
+
+ @Override
+ public MutableValueInfo mutableInfo(int propertyIndex) {
+ return base.mutableInfo(propertyIndex + offset);
+ }
+
+ @Override
+ public void mutableInfo(int propertyIndex, MutableValueInfo info) {
+ base.mutableInfo(propertyIndex + offset, info);
+ }
+
+ @Override
+ public void mutableNext(int propertyIndex, MutableValueNext next) {
+ base.mutableNext(propertyIndex + offset, next);
+ }
+
+ @Override
+ public String mutableNext(int propertyIndex) {
+ return base.mutableNext(propertyIndex + offset);
+ }
+
+ @Override
+ public Object value(int propertyIndex) {
+ return base.value(propertyIndex + offset);
+ }
+
+ @Override
+ public Object valueIntercept(int propertyIndex) {
+ return base.valueIntercept(propertyIndex + offset);
+ }
+
+ @Override
+ public void setValue(int propertyIndex, Object value) {
+ base.setValue(propertyIndex + offset, value);
+ }
+
+ @Override
+ public void setValueIntercept(int propertyIndex, Object value) {
+ base.setValueIntercept(propertyIndex + offset, value);
+ }
+}
diff --git a/ebean-api/src/main/java/io/ebean/bean/ExtensionAccessor.java b/ebean-api/src/main/java/io/ebean/bean/ExtensionAccessor.java
new file mode 100644
index 0000000000..cfea5db354
--- /dev/null
+++ b/ebean-api/src/main/java/io/ebean/bean/ExtensionAccessor.java
@@ -0,0 +1,34 @@
+package io.ebean.bean;
+
+import io.ebean.bean.extend.ExtendableBean;
+
+/**
+ * Provides access to the EntityExtensions. Each ExtendableBean may have multiple Extension-Accessors stored in the static
+ * {@link ExtensionAccessors} per class level.
+ *
+ * This interface is internally used by the enhancer.
+ *
+ * @author Roland Praml, FOCONIS AG
+ */
+public interface ExtensionAccessor {
+
+ /*
+ * Returns the extension for a given bean.
+ */
+ EntityBean getExtension(ExtendableBean bean);
+
+ /**
+ * Returns the index of this extension.
+ */
+ int getIndex();
+
+ /**
+ * Return the type of this extension.
+ */
+ Class> getType();
+
+ /**
+ * Returns the additional properties of this extension.
+ */
+ String[] getProperties();
+}
diff --git a/ebean-api/src/main/java/io/ebean/bean/ExtensionAccessors.java b/ebean-api/src/main/java/io/ebean/bean/ExtensionAccessors.java
new file mode 100644
index 0000000000..341f3b4c31
--- /dev/null
+++ b/ebean-api/src/main/java/io/ebean/bean/ExtensionAccessors.java
@@ -0,0 +1,264 @@
+package io.ebean.bean;
+
+import io.ebean.bean.extend.EntityExtension;
+import io.ebean.bean.extend.ExtendableBean;
+
+import java.util.*;
+import java.util.concurrent.locks.Lock;
+import java.util.concurrent.locks.ReentrantLock;
+
+/**
+ * Each ExtendableBean has one static member defined as
+ *
+ * private static ExtensionAccessors _ebean_extension_accessors =
+ * new ExtensionAccessors(thisClass._ebeanProps, superClass._ebean_extension_accessors | null)
+ *
+ * The ExtensionAccessors class is used to compute the additional space, that has to be reserved
+ * in the descriptor and the virtual properties, that will be added to the bean descriptor.
+ * The following class structure:
+ *
+ * @Entity
+ * class Base extends ExtendableBean {
+ * String prop0;
+ * String prop1;
+ * String prop2;
+ * }
+ * @EntityExtends(Base.class)
+ * class Ext1 {
+ * String prop3;
+ * String prop4;
+ * }
+ * @EntityExtends(Base.class)
+ * class Ext2 {
+ * String prop5;
+ * String prop6;
+ * }
+ *
+ * will create an EntityBeanIntercept for "Base" holding up to 7 fields. Writing to fields 0..2 with ebi.setValue will modify
+ * the fields in Base, the r/w accesses to fields 3..4 are routed to Ext1 and 5..6 to Ext2.
+ *
+ * Note about offset and index:
+ *
+ *
+ * When you have subclasses (class Child extends Base
) the extensions have all the same index in the parent and in
+ * the subclass, but may have different offsets, as the Child-class will provide additional fields.
+ *
+ *
+ * @author Roland Praml, FOCONIS AG
+ */
+public class ExtensionAccessors implements Iterable {
+
+ /**
+ * Default extension info for beans, that have no extension.
+ */
+ public static ExtensionAccessors NONE = new ExtensionAccessors();
+
+ /**
+ * The start offset specifies the offset where the first extension will start
+ */
+ private final int startOffset;
+
+ /**
+ * The entries.
+ */
+ private List accessors = new ArrayList<>();
+
+ /**
+ * If we inherit from a class that has extensions, we have to inherit also all extensions from here
+ */
+ private final ExtensionAccessors parent;
+
+ /**
+ * The total property length of all extensions. This will be initialized once and cannot be changed any more
+ */
+ private volatile int propertyLength = -1;
+
+ /**
+ * The offsets where the extensions will start for effective binary search.
+ */
+ private int[] offsets;
+
+ /**
+ * Lock for synchronizing the initialization.
+ */
+ private static final Lock lock = new ReentrantLock();
+
+ /**
+ * Constructor for ExtensionInfo.NONE
.
+ */
+ private ExtensionAccessors() {
+ this.startOffset = Integer.MAX_VALUE;
+ this.propertyLength = 0;
+ this.parent = null;
+ }
+
+ /**
+ * Called from enhancer. Each entity has a static field initialized with
+ * _ebean_extensions = new ExtensonInfo(thisClass._ebeanProps, superClass._ebean_extensions | null)
+ */
+ public ExtensionAccessors(String[] props, ExtensionAccessors parent) {
+ this.startOffset = props.length;
+ this.parent = parent;
+ }
+
+ /**
+ * Called from enhancer. Each class annotated with {@link EntityExtension} will show up here.
+ *
+ * @param prototype instance of the class that is annotated with {@link EntityExtension}
+ */
+ public ExtensionAccessor add(EntityBean prototype) {
+ if (propertyLength != -1) {
+ throw new UnsupportedOperationException("The extension is already in use and cannot be extended anymore");
+ }
+ Entry entry = new Entry(prototype);
+ lock.lock();
+ try {
+ accessors.add(entry);
+ } finally {
+ lock.unlock();
+ }
+ return entry;
+ }
+
+ /**
+ * returns how many extensions are registered.
+ */
+ public int size() {
+ init();
+ return accessors.size();
+ }
+
+ /**
+ * Returns the additional properties, that have been added by extensions.
+ */
+ public int getPropertyLength() {
+ init();
+ return propertyLength;
+ }
+
+ /**
+ * Copies parent extensions and initializes the offsets. This will be done once only.
+ */
+ private void init() {
+ if (propertyLength != -1) {
+ return;
+ }
+ lock.lock();
+ try {
+ if (propertyLength != -1) {
+ return;
+ }
+ // sort the accessors, so they are "stable"
+ if (parent != null) {
+ parent.init();
+ accessors.sort(Comparator.comparing(e -> e.getType().getName()));
+ if (accessors.isEmpty()) {
+ accessors = parent.accessors;
+ } else {
+ accessors.addAll(0, parent.accessors);
+ }
+ }
+ int length = 0;
+ offsets = new int[accessors.size()];
+ for (int i = 0; i < accessors.size(); i++) {
+ Entry entry = (Entry) accessors.get(i);
+ entry.index = i;
+ offsets[i] = startOffset + length;
+ length += entry.getProperties().length;
+ }
+ propertyLength = length;
+ } finally {
+ lock.unlock();
+ }
+ }
+
+ /**
+ * Returns the offset of this extension accessor.
+ * Note: The offset may vary on subclasses
+ */
+ int getOffset(ExtensionAccessor accessor) {
+ return offsets[accessor.getIndex()];
+ }
+
+ /**
+ * Finds the accessor for a given property. If the propertyIndex is lower than startOffset, no accessor will be returned,
+ * as this means that we try to access a property in the base-entity.
+ */
+ ExtensionAccessor findAccessor(int propertyIndex) {
+ init();
+ if (propertyIndex < startOffset) {
+ return null;
+ }
+ int pos = Arrays.binarySearch(offsets, propertyIndex);
+ if (pos == -1) {
+ return null;
+ }
+ if (pos < 0) {
+ pos = -2 - pos;
+ }
+ return accessors.get(pos);
+ }
+
+ @Override
+ public Iterator iterator() {
+ init();
+ return accessors.iterator();
+ }
+
+ /**
+ * Invoked by enhancer.
+ */
+ public EntityBean createInstance(ExtensionAccessor accessor, EntityBean base) {
+ int offset = getOffset(accessor);
+ return ((Entry) accessor).createInstance(offset, base);
+ }
+
+ static class Entry implements ExtensionAccessor {
+ private int index;
+ private final EntityBean prototype;
+
+ private Entry(EntityBean prototype) {
+ this.prototype = prototype;
+ }
+
+ @Override
+ public String[] getProperties() {
+ return prototype._ebean_getPropertyNames();
+ }
+
+ @Override
+ public int getIndex() {
+ return index;
+ }
+
+ @Override
+ public Class> getType() {
+ return prototype.getClass();
+ }
+
+ EntityBean createInstance(int offset, EntityBean base) {
+ return (EntityBean) prototype._ebean_newExtendedInstance(offset, base);
+ }
+
+ @Override
+ public EntityBean getExtension(ExtendableBean bean) {
+ EntityBean eb = (EntityBean) bean;
+ return eb._ebean_getExtension(Entry.this);
+ }
+ }
+
+ /**
+ * Reads the extension accessors for a given class. If the provided type is not an ExtenadableBean, the
+ * ExtensionAccessors.NONE
is returned.
+ */
+ public static ExtensionAccessors read(Class> type) {
+ if (ExtendableBean.class.isAssignableFrom(type)) {
+ try {
+ return (ExtensionAccessors) type.getField("_ebean_extension_accessors").get(null);
+ } catch (ReflectiveOperationException e) {
+ throw new RuntimeException("Could not read extension info from " + type, e);
+ }
+ }
+ return ExtensionAccessors.NONE;
+ }
+}
diff --git a/ebean-api/src/main/java/io/ebean/bean/InterceptBase.java b/ebean-api/src/main/java/io/ebean/bean/InterceptBase.java
new file mode 100644
index 0000000000..a38655100b
--- /dev/null
+++ b/ebean-api/src/main/java/io/ebean/bean/InterceptBase.java
@@ -0,0 +1,116 @@
+package io.ebean.bean;
+
+/**
+ * Base class for InterceptReadOnly / InterceptReadWrite. This class should contain only the essential member variables to keep
+ * the memory footprint low.
+ *
+ * @author Roland Praml, FOCONIS AG
+ */
+public abstract class InterceptBase implements EntityBeanIntercept {
+
+ /**
+ * The actual entity bean that 'owns' this intercept.
+ */
+ protected final EntityBean owner;
+
+ protected InterceptBase(EntityBean owner) {
+ this.owner = owner;
+ }
+
+ protected ExtensionAccessor findAccessor(int index) {
+ return owner._ebean_getExtensionAccessors().findAccessor(index);
+ }
+
+ private int getOffset(ExtensionAccessor accessor) {
+ return owner._ebean_getExtensionAccessors().getOffset(accessor);
+ }
+
+ protected EntityBean getExtensionBean(ExtensionAccessor accessor) {
+ return owner._ebean_getExtension(accessor);
+ }
+
+ @Override
+ public int findProperty(String propertyName) {
+ String[] names = owner._ebean_getPropertyNames();
+ int i;
+ for (i = 0; i < names.length; i++) {
+ if (names[i].equals(propertyName)) {
+ return i;
+ }
+ }
+ for (ExtensionAccessor acc : owner._ebean_getExtensionAccessors()) {
+ names = acc.getProperties();
+ for (int j = 0; j < names.length; j++) {
+ if (names[j].equals(propertyName)) {
+ return i;
+ }
+ i++;
+ }
+ }
+ return -1;
+ }
+
+ @Override
+ public String property(int propertyIndex) {
+ if (propertyIndex == -1) {
+ return null;
+ }
+ ExtensionAccessor accessor = findAccessor(propertyIndex);
+ if (accessor == null) {
+ return owner._ebean_getPropertyName(propertyIndex);
+ } else {
+ int offset = getOffset(accessor);
+ return getExtensionBean(accessor)._ebean_getPropertyName(propertyIndex - offset);
+ }
+ }
+
+ @Override
+ public int propertyLength() {
+ return owner._ebean_getPropertyNames().length
+ + owner._ebean_getExtensionAccessors().getPropertyLength();
+ }
+
+ @Override
+ public Object value(int index) {
+ ExtensionAccessor accessor = findAccessor(index);
+ if (accessor == null) {
+ return owner._ebean_getField(index);
+ } else {
+ int offset = getOffset(accessor);
+ return getExtensionBean(accessor)._ebean_getField(index - offset);
+ }
+ }
+
+ @Override
+ public Object valueIntercept(int index) {
+ ExtensionAccessor accessor = findAccessor(index);
+ if (accessor == null) {
+ return owner._ebean_getFieldIntercept(index);
+ } else {
+ int offset = getOffset(accessor);
+ return getExtensionBean(accessor)._ebean_getFieldIntercept(index - offset);
+ }
+ }
+
+ @Override
+ public void setValue(int index, Object value) {
+ ExtensionAccessor accessor = findAccessor(index);
+ if (accessor == null) {
+ owner._ebean_setField(index, value);
+ } else {
+ int offset = getOffset(accessor);
+ getExtensionBean(accessor)._ebean_setField(index - offset, value);
+ }
+ }
+
+ @Override
+ public void setValueIntercept(int index, Object value) {
+ ExtensionAccessor accessor = findAccessor(index);
+ if (accessor == null) {
+ owner._ebean_setFieldIntercept(index, value);
+ } else {
+ int offset = getOffset(accessor);
+ getExtensionBean(accessor)._ebean_setFieldIntercept(index - offset, value);
+ }
+ }
+}
diff --git a/ebean-api/src/main/java/io/ebean/bean/InterceptReadOnly.java b/ebean-api/src/main/java/io/ebean/bean/InterceptReadOnly.java
index 26f5faf8d5..da37254e2c 100644
--- a/ebean-api/src/main/java/io/ebean/bean/InterceptReadOnly.java
+++ b/ebean-api/src/main/java/io/ebean/bean/InterceptReadOnly.java
@@ -13,15 +13,14 @@
* required for updates such as per property changed, loaded, dirty state, original values
* bean state etc.
*/
-public class InterceptReadOnly implements EntityBeanIntercept {
+public class InterceptReadOnly extends InterceptBase implements EntityBeanIntercept {
- private final EntityBean owner;
/**
* Create with a given entity.
*/
public InterceptReadOnly(Object ownerBean) {
- this.owner = (EntityBean) ownerBean;
+ super((EntityBean) ownerBean);
}
@Override
@@ -234,21 +233,6 @@ public Object origValue(int propertyIndex) {
return null;
}
- @Override
- public int findProperty(String propertyName) {
- return 0;
- }
-
- @Override
- public String property(int propertyIndex) {
- return null;
- }
-
- @Override
- public int propertyLength() {
- return 0;
- }
-
@Override
public void setPropertyLoaded(String propertyName, boolean loaded) {
diff --git a/ebean-api/src/main/java/io/ebean/bean/InterceptReadWrite.java b/ebean-api/src/main/java/io/ebean/bean/InterceptReadWrite.java
index 3649e70298..e392ef0580 100644
--- a/ebean-api/src/main/java/io/ebean/bean/InterceptReadWrite.java
+++ b/ebean-api/src/main/java/io/ebean/bean/InterceptReadWrite.java
@@ -1,11 +1,12 @@
package io.ebean.bean;
+import io.avaje.applog.AppLog;
import io.ebean.DB;
import io.ebean.Database;
import io.ebean.ValuePair;
-
import jakarta.persistence.EntityNotFoundException;
import jakarta.persistence.PersistenceException;
+
import java.io.File;
import java.io.FileInputStream;
import java.io.IOException;
@@ -16,15 +17,18 @@
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
+import static java.lang.System.Logger.Level.WARNING;
+
/**
* This is the object added to every entity bean using byte code enhancement.
*
* This provides the mechanisms to support deferred fetching of reference beans
* and oldValues generation for concurrency checking.
*/
-public final class InterceptReadWrite implements EntityBeanIntercept {
+public final class InterceptReadWrite extends InterceptBase implements EntityBeanIntercept {
+ private static final long serialVersionUID = 1834735632647183821L;
- private static final long serialVersionUID = -3664031775464862649L;
+ public static final System.Logger log = AppLog.getLogger("io.ebean.bean");
private static final int STATE_NEW = 0;
private static final int STATE_REFERENCE = 1;
@@ -47,6 +51,11 @@ public final class InterceptReadWrite implements EntityBeanIntercept {
*/
private static final byte FLAG_MUTABLE_HASH_SET = 16;
+ /**
+ * Flag indicates that warning was logged.
+ */
+ private static final byte FLAG_MUTABLE_WARN_LOGGED = 32;
+
private final ReentrantLock lock = new ReentrantLock();
private transient NodeUsageCollector nodeUsageCollector;
private transient PersistenceContext persistenceContext;
@@ -55,11 +64,6 @@ public final class InterceptReadWrite implements EntityBeanIntercept {
private String ebeanServerName;
private boolean deletedFromCollection;
-
- /**
- * The actual entity bean that 'owns' this intercept.
- */
- private final EntityBean owner;
private EntityBean embeddedOwner;
private int embeddedOwnerIndex;
/**
@@ -101,15 +105,15 @@ public final class InterceptReadWrite implements EntityBeanIntercept {
* Create with a given entity.
*/
public InterceptReadWrite(Object ownerBean) {
- this.owner = (EntityBean) ownerBean;
- this.flags = new byte[owner._ebean_getPropertyNames().length];
+ super((EntityBean) ownerBean);
+ this.flags = new byte[super.propertyLength()];
}
/**
* EXPERIMENTAL - Constructor only for use by serialization frameworks.
*/
public InterceptReadWrite() {
- this.owner = null;
+ super(null);
this.flags = null;
}
@@ -227,8 +231,17 @@ public boolean isDirty() {
}
if (mutableInfo != null) {
for (int i = 0; i < mutableInfo.length; i++) {
- if (mutableInfo[i] != null && !mutableInfo[i].isEqualToObject(owner._ebean_getField(i))) {
- dirty = true;
+ if ((flags[i] & FLAG_MUTABLE_WARN_LOGGED) == FLAG_MUTABLE_WARN_LOGGED) {
+ break; // do not check again and do NOT mark as dirty
+ }
+ if (mutableInfo[i] != null && !mutableInfo[i].isEqualToObject(value(i))) {
+ if (readOnly) {
+ log.log(WARNING, "Mutable object in {0}.{1} ({2}) changed. Not setting bean dirty, because it is readonly",
+ owner.getClass().getName(), property(i), owner);
+ flags[i] |= FLAG_MUTABLE_WARN_LOGGED;
+ } else {
+ dirty = true;
+ }
break;
}
}
@@ -238,12 +251,16 @@ public boolean isDirty() {
@Override
public void setEmbeddedDirty(int embeddedProperty) {
+ checkReadonly();
this.dirty = true;
setEmbeddedPropertyDirty(embeddedProperty);
}
@Override
public void setDirty(boolean dirty) {
+ if (dirty) {
+ checkReadonly();
+ }
this.dirty = dirty;
}
@@ -411,25 +428,6 @@ public Object origValue(int propertyIndex) {
return origValues[propertyIndex];
}
- @Override
- public int findProperty(String propertyName) {
- final String[] names = owner._ebean_getPropertyNames();
- for (int i = 0; i < names.length; i++) {
- if (names[i].equals(propertyName)) {
- return i;
- }
- }
- return -1;
- }
-
- @Override
- public String property(int propertyIndex) {
- if (propertyIndex == -1) {
- return null;
- }
- return owner._ebean_getPropertyName(propertyIndex);
- }
-
@Override
public int propertyLength() {
return flags.length;
@@ -571,7 +569,7 @@ public void addDirtyPropertyNames(Set props, String prefix) {
props.add((prefix == null ? property(i) : prefix + property(i)));
} else if ((flags[i] & FLAG_EMBEDDED_DIRTY) != 0) {
// an embedded property has been changed - recurse
- final EntityBean embeddedBean = (EntityBean) owner._ebean_getField(i);
+ final EntityBean embeddedBean = (EntityBean) value(i);
embeddedBean._ebean_getIntercept().addDirtyPropertyNames(props, property(i) + ".");
}
}
@@ -579,9 +577,10 @@ public void addDirtyPropertyNames(Set props, String prefix) {
@Override
public boolean hasDirtyProperty(Set propertyNames) {
- final String[] names = owner._ebean_getPropertyNames();
+ String[] names = owner._ebean_getPropertyNames();
final int len = propertyLength();
- for (int i = 0; i < len; i++) {
+ int i;
+ for (i = 0; i < len; i++) {
if (isChangedProp(i)) {
if (propertyNames.contains(names[i])) {
return true;
@@ -592,6 +591,22 @@ public boolean hasDirtyProperty(Set propertyNames) {
}
}
}
+ for (ExtensionAccessor acc : owner._ebean_getExtensionAccessors()) {
+ names = acc.getProperties();
+ for (int j = 0; j < names.length; j++) {
+ if (isChangedProp(i)) {
+ if (propertyNames.contains(names[j])) {
+ return true;
+ }
+ } else if ((flags[i] & FLAG_EMBEDDED_DIRTY) != 0) {
+ if (propertyNames.contains(names[j])) {
+ return true;
+ }
+ }
+ i++;
+ }
+ }
+
return false;
}
@@ -609,7 +624,7 @@ public void addDirtyPropertyValues(Map dirtyValues, String pr
if (isChangedProp(i)) {
// the property has been changed on this bean
final String propName = (prefix == null ? property(i) : prefix + property(i));
- final Object newVal = owner._ebean_getField(i);
+ final Object newVal = value(i);
final Object oldVal = origValue(i);
if (notEqual(oldVal, newVal)) {
dirtyValues.put(propName, new ValuePair(newVal, oldVal));
@@ -628,7 +643,7 @@ public void addDirtyPropertyValues(BeanDiffVisitor visitor) {
for (int i = 0; i < len; i++) {
if (isChangedProp(i)) {
// the property has been changed on this bean
- final Object newVal = owner._ebean_getField(i);
+ final Object newVal = value(i);
final Object oldVal = origValue(i);
if (notEqual(oldVal, newVal)) {
visitor.visit(i, newVal, oldVal);
@@ -845,18 +860,14 @@ public void preSetterMany(boolean interceptField, int propertyIndex, Object oldV
if (state == STATE_NEW) {
setLoadedProperty(propertyIndex);
} else {
- if (readOnly) {
- throw new IllegalStateException("This bean is readOnly");
- }
+ checkReadonly();
setChangeLoaded(propertyIndex);
}
}
@Override
public void setChangedPropertyValue(int propertyIndex, boolean setDirtyState, Object origValue) {
- if (readOnly) {
- throw new IllegalStateException("This bean is readOnly");
- }
+ checkReadonly();
setChangedProperty(propertyIndex);
if (setDirtyState) {
setOriginalValue(propertyIndex, origValue);
@@ -867,6 +878,7 @@ public void setChangedPropertyValue(int propertyIndex, boolean setDirtyState, Ob
@Override
public void setDirtyStatus() {
if (!dirty) {
+ checkReadonly();
dirty = true;
if (embeddedOwner != null) {
// Cascade dirty state from Embedded bean to parent bean
@@ -878,6 +890,12 @@ public void setDirtyStatus() {
}
}
+ private void checkReadonly() {
+ if (readOnly) {
+ throw new IllegalStateException("This bean is readOnly");
+ }
+ }
+
@Override
public void preSetter(boolean intercept, int propertyIndex, Object oldValue, Object newValue) {
if (state == STATE_NEW) {
@@ -1036,9 +1054,10 @@ public Map loadErrors() {
public boolean isChangedProp(int i) {
if ((flags[i] & FLAG_CHANGED_PROP) != 0) {
return true;
- } else if (mutableInfo == null || mutableInfo[i] == null || mutableInfo[i].isEqualToObject(owner._ebean_getField(i))) {
+ } else if (mutableInfo == null || mutableInfo[i] == null || mutableInfo[i].isEqualToObject(value(i))) {
return false;
} else {
+ checkReadonly();
// mark for change
flags[i] |= FLAG_CHANGED_PROP;
dirty = true; // this makes the bean automatically dirty!
diff --git a/ebean-api/src/main/java/io/ebean/bean/extend/EntityExtension.java b/ebean-api/src/main/java/io/ebean/bean/extend/EntityExtension.java
new file mode 100644
index 0000000000..c62d099a4e
--- /dev/null
+++ b/ebean-api/src/main/java/io/ebean/bean/extend/EntityExtension.java
@@ -0,0 +1,70 @@
+package io.ebean.bean.extend;
+
+import java.lang.annotation.Documented;
+import java.lang.annotation.Retention;
+import java.lang.annotation.Target;
+
+import static java.lang.annotation.ElementType.TYPE;
+import static java.lang.annotation.RetentionPolicy.RUNTIME;
+
+/**
+ * Annotation for a class to extend an existing class.
+ *
+ * Normally, you would have annotated like the following example
+ *
+ *
+ * package basePkg;
+ * import extPkg.MyExtEntity;
+ * class MyBaseEntity {
+ * // this line is mandatory, to allow deletion of MyBaseEntity
+ * @OneToOne(optional = true, cascade = Cascade.ALL)
+ * private MyExtEntity
+ * }
+ *
+ * package extPkg;
+ * import basePkg.myBaseEntity;
+ * class MyExtEntity {
+ * @OneToOne(optional = false)
+ * private MyBaseEntity
+ * }
+ *
+ *
+ * If you spread your code over different packages or (especially in different maven modules), you'll get problems, because you'll get cyclic depencencies.
+ *
+ * To break up these dependencies, you can annotate 'MyExtEntity'
+ *
+ *
+ * package extPkg;
+ * import basePkg.myBaseEntity;
+ * @EntityExtension(MyBaseEntity.class)
+ * class MyExtEntity {
+ * @OneToOne(optional = false)
+ * private MyBaseEntity
+ *
+ * private String someField;
+ * }
+ *
+ * This will create a virtual property in the MyBaseEntity without adding a dependency to MyExtEntity.
+ *
+ * You may add a
+ *
+ * public static MyExtEntity get(MyBaseEntity base) {
+ * throw new NotEnhancedException();
+ * }
+ *
+ * This getter will be replaced by the enhancer, so that you can easily get it with
+ * MyExtEntiy.get(base).getSomeField()
.
+ *
+ * Technically, the instance of MyExtEntiy is stored in the _ebean_extension_storage
array of
+ * MyBaseEntity
.
+ *
+ * If you save the MyBaseEntity
, it will also save the data stored in MyExtEntity
.
+ *
+ * @author Alexander Wagner, FOCONIS AG
+ */
+@Documented
+@Target(TYPE)
+@Retention(RUNTIME)
+public @interface EntityExtension {
+ Class extends ExtendableBean>[] value();
+}
diff --git a/ebean-api/src/main/java/io/ebean/bean/extend/EntityExtensionSuperclass.java b/ebean-api/src/main/java/io/ebean/bean/extend/EntityExtensionSuperclass.java
new file mode 100644
index 0000000000..cb491c3508
--- /dev/null
+++ b/ebean-api/src/main/java/io/ebean/bean/extend/EntityExtensionSuperclass.java
@@ -0,0 +1,10 @@
+package io.ebean.bean.extend;
+
+/**
+ * Marker for EntityExtension superclass.
+ *
+ * @author Noemi Praml, FOCONIS AG
+ */
+public @interface EntityExtensionSuperclass {
+
+}
diff --git a/ebean-api/src/main/java/io/ebean/bean/extend/ExtendableBean.java b/ebean-api/src/main/java/io/ebean/bean/extend/ExtendableBean.java
new file mode 100644
index 0000000000..1f4f4b7de1
--- /dev/null
+++ b/ebean-api/src/main/java/io/ebean/bean/extend/ExtendableBean.java
@@ -0,0 +1,20 @@
+package io.ebean.bean.extend;
+
+import io.ebean.bean.EntityBean;
+import io.ebean.bean.NotEnhancedException;
+
+/**
+ * Marker interface for beans that can be extended with @EntityExtension.
+ *
+ * @author Roland Praml, FOCONIS AG
+ */
+public interface ExtendableBean {
+
+ /**
+ * Returns an array of registered extensions. This may be useful for bean validation.
+ * NOTE: The passed array should NOT be modified.
+ */
+ default EntityBean[] _ebean_getExtensions() {
+ throw new NotEnhancedException();
+ }
+}
diff --git a/ebean-api/src/main/java/io/ebean/common/BeanList.java b/ebean-api/src/main/java/io/ebean/common/BeanList.java
index 12d0770d62..8fa03659fb 100644
--- a/ebean-api/src/main/java/io/ebean/common/BeanList.java
+++ b/ebean-api/src/main/java/io/ebean/common/BeanList.java
@@ -8,14 +8,14 @@
/**
* List capable of lazy loading and modification awareness.
*/
-public final class BeanList extends AbstractBeanCollection implements List, BeanCollectionAdd {
+public class BeanList extends AbstractBeanCollection implements List, BeanCollectionAdd {
private static final long serialVersionUID = 1L;
/**
* The underlying List implementation.
*/
- private List list;
+ List list;
/**
* Specify the underlying List implementation.
@@ -111,30 +111,30 @@ public boolean checkEmptyLazyLoad() {
}
}
+ protected void initList(boolean skipLoad, boolean onlyIds) {
+ if (skipLoad) {
+ list = new ArrayList<>();
+ } else {
+ lazyLoadCollection(onlyIds);
+ }
+ }
+
private void initClear() {
lock.lock();
try {
if (list == null) {
- if (!disableLazyLoad && modifyListening) {
- lazyLoadCollection(true);
- } else {
- list = new ArrayList<>();
- }
+ initList(disableLazyLoad || !modifyListening, true);
}
} finally {
lock.unlock();
}
}
- private void init() {
+ void init() {
lock.lock();
try {
if (list == null) {
- if (disableLazyLoad) {
- list = new ArrayList<>();
- } else {
- lazyLoadCollection(false);
- }
+ initList(disableLazyLoad, false);
}
} finally {
lock.unlock();
@@ -164,7 +164,10 @@ public Collection actualDetails() {
}
@Override
- public Collection> actualEntries() {
+ public Collection> actualEntries(boolean load) {
+ if (load) {
+ init();
+ }
return list;
}
diff --git a/ebean-api/src/main/java/io/ebean/common/BeanListLazyAdd.java b/ebean-api/src/main/java/io/ebean/common/BeanListLazyAdd.java
new file mode 100644
index 0000000000..8ffa4b47df
--- /dev/null
+++ b/ebean-api/src/main/java/io/ebean/common/BeanListLazyAdd.java
@@ -0,0 +1,133 @@
+package io.ebean.common;
+
+import io.ebean.bean.BeanCollection;
+import io.ebean.bean.BeanCollectionLoader;
+import io.ebean.bean.EntityBean;
+
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.List;
+
+/**
+ * This bean list can perform additions without populating the list.
+ * This might be useful, if you just want to add entries to an existing collection.
+ * Works only for lists and only if there is no order column
+ */
+public class BeanListLazyAdd extends BeanList {
+
+ public BeanListLazyAdd(BeanCollectionLoader loader, EntityBean ownerBean, String propertyName) {
+ super(loader, ownerBean, propertyName);
+ }
+
+ private List lazyAddedEntries;
+
+ @Override
+ public boolean add(E bean) {
+ checkReadOnly();
+
+ lock.lock();
+ try {
+ if (list == null) {
+ // list is not yet initialized, so we may add elements to a spare list
+ if (lazyAddedEntries == null) {
+ lazyAddedEntries = new ArrayList<>();
+ }
+ lazyAddedEntries.add(bean);
+ } else {
+ list.add(bean);
+ }
+ } finally {
+ lock.unlock();
+ }
+
+ if (modifyListening) {
+ modifyAddition(bean);
+ }
+ return true;
+ }
+
+ @Override
+ public boolean addAll(Collection extends E> beans) {
+ checkReadOnly();
+
+ lock.lock();
+ try {
+ if (list == null) {
+ // list is not yet initialized, so we may add elements to a spare list
+ if (lazyAddedEntries == null) {
+ lazyAddedEntries = new ArrayList<>();
+ }
+ lazyAddedEntries.addAll(beans);
+ } else {
+ list.addAll(beans);
+ }
+ } finally {
+ lock.unlock();
+ }
+
+ if (modifyListening) {
+ getModifyHolder().modifyAdditionAll(beans);
+ }
+
+ return true;
+ }
+
+
+ @Override
+ public void loadFrom(BeanCollection> other) {
+ super.loadFrom(other);
+ if (lazyAddedEntries != null) {
+ list.addAll(lazyAddedEntries);
+ lazyAddedEntries = null;
+ }
+ }
+
+ /**
+ * on init, this happens on all accessor methods except on 'add' and addAll,
+ * we add the lazy added entries at the end of the list
+ */
+ @Override
+ protected void initList(boolean skipLoad, boolean onlyIds) {
+ if (skipLoad) {
+ if (lazyAddedEntries != null) {
+ list = lazyAddedEntries;
+ lazyAddedEntries = null;
+ } else {
+ list = new ArrayList<>();
+ }
+ } else {
+ lazyLoadCollection(onlyIds);
+ if (lazyAddedEntries != null) {
+ list.addAll(lazyAddedEntries);
+ lazyAddedEntries = null;
+ }
+ }
+ }
+
+ @Override
+ public List getLazyAddedEntries(boolean reset) {
+ List ret = lazyAddedEntries;
+ if (reset) {
+ lazyAddedEntries = null;
+ }
+ return ret;
+ }
+
+ @Override
+ public boolean isSkipSave() {
+ return lazyAddedEntries == null && super.isSkipSave();
+ }
+
+ public boolean checkEmptyLazyLoad() {
+ if (list != null) {
+ return false;
+ } else if (lazyAddedEntries == null) {
+ list = new ArrayList<>();
+ return true;
+ } else {
+ list = lazyAddedEntries;
+ lazyAddedEntries = null;
+ return false;
+ }
+ }
+}
diff --git a/ebean-api/src/main/java/io/ebean/common/BeanMap.java b/ebean-api/src/main/java/io/ebean/common/BeanMap.java
index fe959c22c0..d1c5801e38 100644
--- a/ebean-api/src/main/java/io/ebean/common/BeanMap.java
+++ b/ebean-api/src/main/java/io/ebean/common/BeanMap.java
@@ -187,7 +187,10 @@ public Collection actualDetails() {
* Returns the map entrySet.
*/
@Override
- public Collection> actualEntries() {
+ public Collection> actualEntries(boolean load) {
+ if (load) {
+ init();
+ }
return map.entrySet();
}
diff --git a/ebean-api/src/main/java/io/ebean/common/BeanSet.java b/ebean-api/src/main/java/io/ebean/common/BeanSet.java
index dda2b24ee9..d0d890f8b6 100644
--- a/ebean-api/src/main/java/io/ebean/common/BeanSet.java
+++ b/ebean-api/src/main/java/io/ebean/common/BeanSet.java
@@ -165,7 +165,10 @@ public Collection actualDetails() {
}
@Override
- public Collection> actualEntries() {
+ public Collection> actualEntries(boolean load) {
+ if (load) {
+ init();
+ }
return set;
}
diff --git a/ebean-api/src/main/java/io/ebean/config/DatabaseConfig.java b/ebean-api/src/main/java/io/ebean/config/DatabaseConfig.java
index 3c216852f8..510f18716a 100644
--- a/ebean-api/src/main/java/io/ebean/config/DatabaseConfig.java
+++ b/ebean-api/src/main/java/io/ebean/config/DatabaseConfig.java
@@ -19,6 +19,7 @@
import io.ebean.event.readaudit.ReadAuditLogger;
import io.ebean.event.readaudit.ReadAuditPrepare;
import io.ebean.meta.MetricNamingMatch;
+import io.ebean.plugin.CustomDeployParser;
import io.ebean.util.StringHelper;
import jakarta.persistence.EnumType;
@@ -417,6 +418,7 @@ public class DatabaseConfig implements DatabaseBuilder.Settings {
private List queryAdapters = new ArrayList<>();
private final List bulkTableEventListeners = new ArrayList<>();
private final List configStartupListeners = new ArrayList<>();
+ private final List customDeployParsers = new ArrayList<>();
/**
* By default inserts are included in the change log.
@@ -452,6 +454,8 @@ public class DatabaseConfig implements DatabaseBuilder.Settings {
private int backgroundExecutorShutdownSecs = 30;
private BackgroundExecutorWrapper backgroundExecutorWrapper = new MdcBackgroundExecutorWrapper();
+ private boolean tenantPartitionedCache;
+
// defaults for the L2 bean caching
private int cacheMaxSize = 10000;
@@ -511,6 +515,11 @@ public class DatabaseConfig implements DatabaseBuilder.Settings {
*/
private boolean queryPlanEnable;
+ /**
+ * Additional platform specific options for query-plan generation.
+ */
+ private String queryPlanOptions;
+
/**
* The default threshold in micros for collecting query plans.
*/
@@ -1200,6 +1209,26 @@ public DatabaseConfig setBackgroundExecutorWrapper(BackgroundExecutorWrapper bac
return this;
}
+ /**
+ * Returns, if the caches are partitioned by tenant.
+ */
+ @Override
+ public boolean isTenantPartitionedCache() {
+ return tenantPartitionedCache;
+ }
+
+ /**
+ * Sets the tenant partitioning mode for caches. This means, caches are created on demand,
+ * but they may not get invalidated across tenant boundaries *
+ */
+ @Override
+ public void setTenantPartitionedCache(boolean tenantPartitionedCache) {
+ this.tenantPartitionedCache = tenantPartitionedCache;
+ }
+
+ /**
+ * Return the L2 cache default max size.
+ */
@Override
public int getCacheMaxSize() {
return cacheMaxSize;
@@ -2007,6 +2036,23 @@ public List getServerConfigStartupListeners() {
return configStartupListeners;
}
+ @Override
+ public DatabaseConfig addCustomDeployParser(CustomDeployParser customDeployParser) {
+ customDeployParsers.add(customDeployParser);
+ return this;
+ }
+
+ @Override
+ public List getCustomDeployParsers() {
+ return customDeployParsers;
+ }
+
+ /**
+ * Register all the BeanPersistListener instances.
+ *
+ * Note alternatively you can use {@link #add(BeanPersistListener)} to add
+ * BeanPersistListener instances one at a time.
+ */
@Override
public DatabaseConfig setPersistListeners(List persistListeners) {
this.persistListeners = persistListeners;
@@ -2146,6 +2192,7 @@ protected void loadSettings(PropertiesWrapper p) {
queryPlanTTLSeconds = p.getInt("queryPlanTTLSeconds", queryPlanTTLSeconds);
slowQueryMillis = p.getLong("slowQueryMillis", slowQueryMillis);
queryPlanEnable = p.getBoolean("queryPlan.enable", queryPlanEnable);
+ queryPlanOptions = p.get("queryPlan.options", queryPlanOptions);
queryPlanThresholdMicros = p.getLong("queryPlan.thresholdMicros", queryPlanThresholdMicros);
queryPlanCapture = p.getBoolean("queryPlan.capture", queryPlanCapture);
queryPlanCapturePeriodSecs = p.getLong("queryPlan.capturePeriodSecs", queryPlanCapturePeriodSecs);
@@ -2235,6 +2282,15 @@ protected void loadSettings(PropertiesWrapper p) {
ddlPlaceholders = p.get("ddl.placeholders", ddlPlaceholders);
ddlHeader = p.get("ddl.header", ddlHeader);
+ tenantPartitionedCache = p.getBoolean("tenantPartitionedCache", tenantPartitionedCache);
+
+ cacheMaxIdleTime = p.getInt("cacheMaxIdleTime", cacheMaxIdleTime);
+ cacheMaxSize = p.getInt("cacheMaxSize", cacheMaxSize);
+ cacheMaxTimeToLive = p.getInt("cacheMaxTimeToLive", cacheMaxTimeToLive);
+ queryCacheMaxIdleTime = p.getInt("queryCacheMaxIdleTime", queryCacheMaxIdleTime);
+ queryCacheMaxSize = p.getInt("queryCacheMaxSize", queryCacheMaxSize);
+ queryCacheMaxTimeToLive = p.getInt("queryCacheMaxTimeToLive", queryCacheMaxTimeToLive);
+
// read tenant-configuration from config:
// tenant.mode = NONE | DB | SCHEMA | CATALOG | PARTITION
String mode = p.get("tenant.mode");
@@ -2461,6 +2517,17 @@ public DatabaseConfig setQueryPlanEnable(boolean queryPlanEnable) {
return this;
}
+ @Override
+ public String getQueryPlanOptions() {
+ return queryPlanOptions;
+ }
+
+ @Override
+ public DatabaseConfig queryPlanOptions(String queryPlanOptions) {
+ this.queryPlanOptions = queryPlanOptions;
+ return this;
+ }
+
@Override
public long getQueryPlanThresholdMicros() {
return queryPlanThresholdMicros;
diff --git a/ebean-api/src/main/java/io/ebean/meta/MetaQueryPlan.java b/ebean-api/src/main/java/io/ebean/meta/MetaQueryPlan.java
index 7c431022c5..4258b7c9ca 100644
--- a/ebean-api/src/main/java/io/ebean/meta/MetaQueryPlan.java
+++ b/ebean-api/src/main/java/io/ebean/meta/MetaQueryPlan.java
@@ -44,6 +44,11 @@ public interface MetaQueryPlan {
*/
String plan();
+ /**
+ * The tenant ID of the plan.
+ */
+ Object tenantId();
+
/**
* Return the query execution time associated with the bind values capture.
*/
diff --git a/ebean-api/src/main/java/io/ebean/metric/CountMetric.java b/ebean-api/src/main/java/io/ebean/metric/CountMetric.java
index 3ce785c1ea..d125134552 100644
--- a/ebean-api/src/main/java/io/ebean/metric/CountMetric.java
+++ b/ebean-api/src/main/java/io/ebean/metric/CountMetric.java
@@ -5,7 +5,7 @@
/**
* Metric for timed events like transaction execution times.
*/
-public interface CountMetric {
+public interface CountMetric extends Metric {
/**
* Add to the counter.
@@ -32,8 +32,4 @@ public interface CountMetric {
*/
void reset();
- /**
- * Visit non empty metrics.
- */
- void visit(MetricVisitor visitor);
}
diff --git a/ebean-api/src/main/java/io/ebean/metric/Metric.java b/ebean-api/src/main/java/io/ebean/metric/Metric.java
new file mode 100644
index 0000000000..374b2134e2
--- /dev/null
+++ b/ebean-api/src/main/java/io/ebean/metric/Metric.java
@@ -0,0 +1,13 @@
+package io.ebean.metric;
+
+import io.ebean.meta.MetricVisitor;
+
+/**
+ * Base for all metrics.
+ */
+public interface Metric {
+ /**
+ * Visit the underlying metric.
+ */
+ void visit(MetricVisitor visitor);
+}
diff --git a/ebean-api/src/main/java/io/ebean/metric/MetricFactory.java b/ebean-api/src/main/java/io/ebean/metric/MetricFactory.java
index b3dcc51606..b0d07ef2fb 100644
--- a/ebean-api/src/main/java/io/ebean/metric/MetricFactory.java
+++ b/ebean-api/src/main/java/io/ebean/metric/MetricFactory.java
@@ -4,6 +4,9 @@
import io.ebean.XBootstrapService;
import io.ebean.service.BootstrapService;
+import java.util.function.IntSupplier;
+import java.util.function.LongSupplier;
+
/**
* Factory to create timed metric counters.
*/
@@ -31,6 +34,16 @@ static MetricFactory get() {
*/
CountMetric createCountMetric(String name);
+ /**
+ * Create a metric, that gets the value from a supplier.
+ */
+ Metric createMetric(String name, LongSupplier supplier);
+
+ /**
+ * Create a metric, that gets the value from a supplier.
+ */
+ Metric createMetric(String name, IntSupplier supplier);
+
/**
* Create a Timed metric.
*/
diff --git a/ebean-api/src/main/java/io/ebean/metric/MetricRegistry.java b/ebean-api/src/main/java/io/ebean/metric/MetricRegistry.java
new file mode 100644
index 0000000000..d881320247
--- /dev/null
+++ b/ebean-api/src/main/java/io/ebean/metric/MetricRegistry.java
@@ -0,0 +1,31 @@
+package io.ebean.metric;
+
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.List;
+
+/**
+ * Global registry of custom metrics instances created.
+ */
+public final class MetricRegistry {
+ public interface RegistryEntry {
+ void remove();
+ }
+
+ private static final List list = Collections.synchronizedList(new ArrayList<>());
+
+ /**
+ * Register the metric instance.
+ */
+ public static RegistryEntry register(Metric location) {
+ list.add(location);
+ return () -> list.remove(location);
+ }
+
+ /**
+ * Return all the registered extra metrics.
+ */
+ public static List registered() {
+ return list;
+ }
+}
diff --git a/ebean-api/src/main/java/io/ebean/metric/QueryPlanMetric.java b/ebean-api/src/main/java/io/ebean/metric/QueryPlanMetric.java
index 24999ce0ed..c1f24eea45 100644
--- a/ebean-api/src/main/java/io/ebean/metric/QueryPlanMetric.java
+++ b/ebean-api/src/main/java/io/ebean/metric/QueryPlanMetric.java
@@ -5,15 +5,11 @@
/**
* Internal Query plan metric holder.
*/
-public interface QueryPlanMetric {
+public interface QueryPlanMetric extends Metric {
/**
* Return the underlying timed metric.
*/
TimedMetric metric();
- /**
- * Visit the underlying metric.
- */
- void visit(MetricVisitor visitor);
}
diff --git a/ebean-api/src/main/java/io/ebean/metric/TimedMetric.java b/ebean-api/src/main/java/io/ebean/metric/TimedMetric.java
index 17c3defb95..4ec95581a4 100644
--- a/ebean-api/src/main/java/io/ebean/metric/TimedMetric.java
+++ b/ebean-api/src/main/java/io/ebean/metric/TimedMetric.java
@@ -5,7 +5,7 @@
/**
* Metric for timed events like transaction execution times.
*/
-public interface TimedMetric {
+public interface TimedMetric extends Metric {
/**
* Add a time event (usually in microseconds).
@@ -37,8 +37,4 @@ public interface TimedMetric {
*/
TimedMetricStats collect(boolean reset);
- /**
- * Visit non empty metrics.
- */
- void visit(MetricVisitor visitor);
}
diff --git a/ebean-api/src/main/java/io/ebean/metric/TimedMetricMap.java b/ebean-api/src/main/java/io/ebean/metric/TimedMetricMap.java
index 5d10397ba2..aeff635e82 100644
--- a/ebean-api/src/main/java/io/ebean/metric/TimedMetricMap.java
+++ b/ebean-api/src/main/java/io/ebean/metric/TimedMetricMap.java
@@ -5,7 +5,7 @@
/**
* A map of timed metrics keyed by a string.
*/
-public interface TimedMetricMap {
+public interface TimedMetricMap extends Metric {
/**
* Add a time event given the start nanos.
@@ -17,8 +17,4 @@ public interface TimedMetricMap {
*/
void add(String key, long exeMicros);
- /**
- * Visit the metric.
- */
- void visit(MetricVisitor visitor);
}
diff --git a/ebean-api/src/main/java/io/ebean/plugin/CustomDeployParser.java b/ebean-api/src/main/java/io/ebean/plugin/CustomDeployParser.java
new file mode 100644
index 0000000000..4c854f2f1e
--- /dev/null
+++ b/ebean-api/src/main/java/io/ebean/plugin/CustomDeployParser.java
@@ -0,0 +1,15 @@
+package io.ebean.plugin;
+
+import io.ebean.config.dbplatform.DatabasePlatform;
+
+/**
+ * Fired after all beans are parsed. You may implement own parsers to handle custom annotations.
+ * (See test case for example)
+ *
+ * @author Roland Praml, FOCONIS AG
+ */
+@FunctionalInterface
+public interface CustomDeployParser {
+
+ void parse(DeployBeanDescriptorMeta descriptor, DatabasePlatform databasePlatform);
+}
diff --git a/ebean-api/src/main/java/io/ebean/plugin/DeployBeanDescriptorMeta.java b/ebean-api/src/main/java/io/ebean/plugin/DeployBeanDescriptorMeta.java
new file mode 100644
index 0000000000..4347a5057c
--- /dev/null
+++ b/ebean-api/src/main/java/io/ebean/plugin/DeployBeanDescriptorMeta.java
@@ -0,0 +1,36 @@
+package io.ebean.plugin;
+
+import java.util.Collection;
+
+/**
+ * General deployment information. This is used in {@link CustomDeployParser}.
+ *
+ * @author Roland Praml, FOCONIS AG
+ */
+public interface DeployBeanDescriptorMeta {
+
+ /**
+ * Return a collection of all BeanProperty deployment information.
+ */
+ public Collection extends DeployBeanPropertyMeta> propertiesAll();
+
+ /**
+ * Get a BeanProperty by its name.
+ */
+ public DeployBeanPropertyMeta getBeanProperty(String secondaryBeanName);
+
+ /**
+ * Return the DeployBeanDescriptorMeta for the given bean class.
+ */
+ public DeployBeanDescriptorMeta getDeployBeanDescriptorMeta(Class> propertyType);
+
+ /**
+ * Returns the discriminator column, if any.
+ * @return
+ */
+ public String getDiscriminatorColumn();
+
+ public String getBaseTable();
+
+ DeployBeanPropertyMeta idProperty();
+}
diff --git a/ebean-api/src/main/java/io/ebean/plugin/DeployBeanPropertyAssocMeta.java b/ebean-api/src/main/java/io/ebean/plugin/DeployBeanPropertyAssocMeta.java
new file mode 100644
index 0000000000..d7d1d637a3
--- /dev/null
+++ b/ebean-api/src/main/java/io/ebean/plugin/DeployBeanPropertyAssocMeta.java
@@ -0,0 +1,23 @@
+package io.ebean.plugin;
+
+public interface DeployBeanPropertyAssocMeta extends DeployBeanPropertyMeta {
+
+ /**
+ * Return the mappedBy deployment attribute.
+ *
+ * This is the name of the property in the 'detail' bean that maps back to
+ * this 'master' bean.
+ *
+ */
+ String getMappedBy();
+
+ /**
+ * Return the base table for this association.
+ *
+ * This has the table name which is used to determine the relationship for
+ * this association.
+ *
+ */
+ String getBaseTable();
+
+}
diff --git a/ebean-api/src/main/java/io/ebean/plugin/DeployBeanPropertyMeta.java b/ebean-api/src/main/java/io/ebean/plugin/DeployBeanPropertyMeta.java
new file mode 100644
index 0000000000..4ba2d3e2a1
--- /dev/null
+++ b/ebean-api/src/main/java/io/ebean/plugin/DeployBeanPropertyMeta.java
@@ -0,0 +1,37 @@
+package io.ebean.plugin;
+
+import java.lang.reflect.Field;
+
+public interface DeployBeanPropertyMeta {
+
+ /**
+ * Return the name of the property.
+ */
+ String getName();
+
+ /**
+ * The database column name this is mapped to.
+ */
+ String getDbColumn();
+
+ /**
+ * Return the bean Field associated with this property.
+ */
+ Field getField();
+
+ /**
+ * The property is based on a formula.
+ */
+ void setSqlFormula(String sqlSelect, String sqlJoin);
+
+ /**
+ * Return the bean type.
+ */
+ Class> getOwningType();
+
+ /**
+ * Return the property type.
+ */
+ Class> getPropertyType();
+
+}
diff --git a/ebean-api/src/main/java/io/ebean/text/json/JsonBeanReader.java b/ebean-api/src/main/java/io/ebean/text/json/JsonBeanReader.java
index 935b788600..7acdb976f1 100644
--- a/ebean-api/src/main/java/io/ebean/text/json/JsonBeanReader.java
+++ b/ebean-api/src/main/java/io/ebean/text/json/JsonBeanReader.java
@@ -12,18 +12,11 @@
*/
public interface JsonBeanReader {
- /**
- * Read the JSON into given bean. Will update existing properties.
- */
- T read(T target);
-
/**
* Read the JSON returning a bean.
*/
- default T read() {
- return read(null);
- }
-
+ T read();
+
/**
* Create a new reader taking the context from the existing one but using a new JsonParser.
*/
diff --git a/ebean-api/src/main/java/io/ebean/text/json/JsonContext.java b/ebean-api/src/main/java/io/ebean/text/json/JsonContext.java
index d2c0df822b..3c31f11667 100644
--- a/ebean-api/src/main/java/io/ebean/text/json/JsonContext.java
+++ b/ebean-api/src/main/java/io/ebean/text/json/JsonContext.java
@@ -2,6 +2,7 @@
import com.fasterxml.jackson.core.JsonGenerator;
import com.fasterxml.jackson.core.JsonParser;
+import io.ebean.BeanMergeOptions;
import io.ebean.FetchPath;
import io.ebean.plugin.BeanType;
@@ -64,7 +65,9 @@ public interface JsonContext {
* instances, so the object identity will not be preserved here.
*
* @throws JsonIOException When IOException occurs
+ * @deprecated use {@link #toBean(Class, JsonParser)} and {@link io.ebean.Database#mergeBeans(Object, Object, BeanMergeOptions)}
*/
+ @Deprecated
void toBean(T target, JsonParser parser) throws JsonIOException;
/**
@@ -72,7 +75,9 @@ public interface JsonContext {
* See {@link #toBean(Class, JsonParser)} for details modified.
*
* @throws JsonIOException When IOException occurs
+ * @deprecated use {@link #toBean(Class, JsonParser, JsonReadOptions)} and {@link io.ebean.Database#mergeBeans(Object, Object, BeanMergeOptions)}
*/
+ @Deprecated
void toBean(T target, JsonParser parser, JsonReadOptions options) throws JsonIOException;
/**
@@ -80,7 +85,9 @@ public interface JsonContext {
* See {@link #toBean(Class, JsonParser)} for details
*
* @throws JsonIOException When IOException occurs
+ * @deprecated use {@link #toBean(Class, Reader)} and {@link io.ebean.Database#mergeBeans(Object, Object, BeanMergeOptions)}
*/
+ @Deprecated
void toBean(T target, Reader json) throws JsonIOException;
/**
@@ -88,7 +95,9 @@ public interface JsonContext {
* See {@link #toBean(Class, JsonParser)} for details modified.
*
* @throws JsonIOException When IOException occurs
+ * @deprecated use {@link #toBean(Class, Reader, JsonReadOptions)} and {@link io.ebean.Database#mergeBeans(Object, Object, BeanMergeOptions)}
*/
+ @Deprecated
void toBean(T target, Reader json, JsonReadOptions options) throws JsonIOException;
/**
@@ -96,7 +105,9 @@ public interface JsonContext {
* See {@link #toBean(Class, JsonParser)} for details
*
* @throws JsonIOException When IOException occurs
+ * @deprecated use {@link #toBean(Class, String)} and {@link io.ebean.Database#mergeBeans(Object, Object, BeanMergeOptions)}
*/
+ @Deprecated
void toBean(T target, String json) throws JsonIOException;
/**
@@ -104,7 +115,9 @@ public interface JsonContext {
* See {@link #toBean(Class, JsonParser)} for details
*
* @throws JsonIOException When IOException occurs
+ * @deprecated use {@link #toBean(Class, String, JsonReadOptions)} and {@link io.ebean.Database#mergeBeans(Object, Object, BeanMergeOptions)}
*/
+ @Deprecated
void toBean(T target, String json, JsonReadOptions options) throws JsonIOException;
/**
diff --git a/ebean-api/src/main/java/module-info.java b/ebean-api/src/main/java/module-info.java
index 27548d1ee6..3d6f4da50d 100644
--- a/ebean-api/src/main/java/module-info.java
+++ b/ebean-api/src/main/java/module-info.java
@@ -21,6 +21,7 @@
exports io.ebean;
exports io.ebean.bean;
+ exports io.ebean.bean.extend;
exports io.ebean.cache;
exports io.ebean.meta;
exports io.ebean.config;
diff --git a/ebean-api/src/test/java/io/ebean/EbeanVersionTest.java b/ebean-api/src/test/java/io/ebean/EbeanVersionTest.java
index 5eb17bbe87..5b78dbbe15 100644
--- a/ebean-api/src/test/java/io/ebean/EbeanVersionTest.java
+++ b/ebean-api/src/test/java/io/ebean/EbeanVersionTest.java
@@ -9,9 +9,9 @@ class EbeanVersionTest {
@Test
void checkMinAgentVersion_ok() {
- assertFalse(EbeanVersion.checkMinAgentVersion("12.12.0"));
- assertFalse(EbeanVersion.checkMinAgentVersion("12.12.99"));
- assertFalse(EbeanVersion.checkMinAgentVersion("13.1.0"));
+ assertFalse(EbeanVersion.checkMinAgentVersion("13.10.0"));
+ assertFalse(EbeanVersion.checkMinAgentVersion("13.10.99"));
+ assertFalse(EbeanVersion.checkMinAgentVersion("14.1.0"));
}
@Test
diff --git a/ebean-bom/pom.xml b/ebean-bom/pom.xml
index a28002ad41..62aa65f575 100644
--- a/ebean-bom/pom.xml
+++ b/ebean-bom/pom.xml
@@ -4,7 +4,7 @@
ebean-parent
io.ebean
- 14.7.0
+ 14.7.0-FOC2-SNAPSHOT
ebean bom
@@ -89,25 +89,25 @@
io.ebean
ebean
- 14.7.0
+ 14.7.0-FOC2-SNAPSHOT
io.ebean
ebean-api
- 14.7.0
+ 14.7.0-FOC2-SNAPSHOT
io.ebean
ebean-core
- 14.7.0
+ 14.7.0-FOC2-SNAPSHOT
io.ebean
ebean-core-type
- 14.7.0
+ 14.7.0-FOC2-SNAPSHOT
@@ -125,13 +125,13 @@
io.ebean
ebean-jackson-mapper
- 14.7.0
+ 14.7.0-FOC2-SNAPSHOT
io.ebean
ebean-ddl-generator
- 14.7.0
+ 14.7.0-FOC2-SNAPSHOT
@@ -155,37 +155,37 @@
io.ebean
ebean-querybean
- 14.7.0
+ 14.7.0-FOC2-SNAPSHOT
io.ebean
querybean-generator
- 14.7.0
+ 14.7.0-FOC2-SNAPSHOT
io.ebean
kotlin-querybean-generator
- 14.7.0
+ 14.7.0-FOC2-SNAPSHOT
io.ebean
ebean-test
- 14.7.0
+ 14.7.0-FOC2-SNAPSHOT
io.ebean
ebean-redis
- 14.7.0
+ 14.7.0-FOC2-SNAPSHOT
io.ebean
ebean-spring-txn
- 14.7.0
+ 14.7.0-FOC2-SNAPSHOT
@@ -193,79 +193,79 @@
io.ebean
ebean-clickhouse
- 14.7.0
+ 14.7.0-FOC2-SNAPSHOT
io.ebean
ebean-db2
- 14.7.0
+ 14.7.0-FOC2-SNAPSHOT
io.ebean
ebean-h2
- 14.7.0
+ 14.7.0-FOC2-SNAPSHOT
io.ebean
ebean-hana
- 14.7.0
+ 14.7.0-FOC2-SNAPSHOT
io.ebean
ebean-mariadb
- 14.7.0
+ 14.7.0-FOC2-SNAPSHOT
io.ebean
ebean-mysql
- 14.7.0
+ 14.7.0-FOC2-SNAPSHOT
io.ebean
ebean-nuodb
- 14.7.0
+ 14.7.0-FOC2-SNAPSHOT
io.ebean
ebean-oracle
- 14.7.0
+ 14.7.0-FOC2-SNAPSHOT
io.ebean
ebean-postgres
- 14.7.0
+ 14.7.0-FOC2-SNAPSHOT
io.ebean
ebean-postgis
- 14.7.0
+ 14.7.0-FOC2-SNAPSHOT
io.ebean
ebean-postgis-types
- 14.7.0
+ 14.7.0-FOC2-SNAPSHOT
io.ebean
ebean-sqlite
- 14.7.0
+ 14.7.0-FOC2-SNAPSHOT
io.ebean
ebean-sqlserver
- 14.7.0
+ 14.7.0-FOC2-SNAPSHOT
diff --git a/ebean-core-type/pom.xml b/ebean-core-type/pom.xml
index 6fa566dee7..9e8b72f82c 100644
--- a/ebean-core-type/pom.xml
+++ b/ebean-core-type/pom.xml
@@ -4,7 +4,7 @@
ebean-parent
io.ebean
- 14.7.0
+ 14.7.0-FOC2-SNAPSHOT
ebean-core-type
@@ -16,7 +16,7 @@
io.ebean
ebean-api
- 14.7.0
+ 14.7.0-FOC2-SNAPSHOT
diff --git a/ebean-core/pom.xml b/ebean-core/pom.xml
index 81b6fafa9f..6b9289db3b 100644
--- a/ebean-core/pom.xml
+++ b/ebean-core/pom.xml
@@ -3,7 +3,7 @@
ebean-parent
io.ebean
- 14.7.0
+ 14.7.0-FOC2-SNAPSHOT
ebean-core
@@ -22,7 +22,7 @@
io.ebean
ebean-api
- 14.7.0
+ 14.7.0-FOC2-SNAPSHOT
@@ -46,7 +46,7 @@
io.ebean
ebean-core-type
- 14.7.0
+ 14.7.0-FOC2-SNAPSHOT
@@ -159,21 +159,21 @@
io.ebean
ebean-platform-h2
- 14.7.0
+ 14.7.0-FOC2-SNAPSHOT
test
io.ebean
ebean-platform-postgres
- 14.7.0
+ 14.7.0-FOC2-SNAPSHOT
test
io.ebean
ebean-platform-sqlserver
- 14.7.0
+ 14.7.0-FOC2-SNAPSHOT
test
diff --git a/ebean-core/src/main/java/io/ebeaninternal/api/HashQuery.java b/ebean-core/src/main/java/io/ebeaninternal/api/HashQuery.java
index a207f3c573..9f77331016 100644
--- a/ebean-core/src/main/java/io/ebeaninternal/api/HashQuery.java
+++ b/ebean-core/src/main/java/io/ebeaninternal/api/HashQuery.java
@@ -7,13 +7,15 @@ public final class HashQuery {
private final CQueryPlanKey planHash;
private final BindValuesKey bindValuesKey;
+ private final Class> dtoType;
/**
* Create the HashQuery.
*/
- public HashQuery(CQueryPlanKey planHash, BindValuesKey bindValuesKey) {
+ public HashQuery(CQueryPlanKey planHash, BindValuesKey bindValuesKey, Class> dtoType) {
this.planHash = planHash;
this.bindValuesKey = bindValuesKey;
+ this.dtoType = dtoType;
}
@Override
@@ -25,6 +27,7 @@ public String toString() {
public int hashCode() {
int hc = 92821 * planHash.hashCode();
hc = 92821 * hc + bindValuesKey.hashCode();
+ hc = 92821 * hc + dtoType.hashCode();
return hc;
}
@@ -37,6 +40,6 @@ public boolean equals(Object obj) {
return false;
}
HashQuery e = (HashQuery) obj;
- return e.bindValuesKey.equals(bindValuesKey) && e.planHash.equals(planHash);
+ return e.bindValuesKey.equals(bindValuesKey) && e.planHash.equals(planHash) && e.dtoType.equals(dtoType);
}
}
diff --git a/ebean-core/src/main/java/io/ebeaninternal/api/SpiDbQueryPlan.java b/ebean-core/src/main/java/io/ebeaninternal/api/SpiDbQueryPlan.java
index 535232d76a..c8a4210055 100644
--- a/ebean-core/src/main/java/io/ebeaninternal/api/SpiDbQueryPlan.java
+++ b/ebean-core/src/main/java/io/ebeaninternal/api/SpiDbQueryPlan.java
@@ -12,6 +12,6 @@ public interface SpiDbQueryPlan extends MetaQueryPlan {
/**
* Extend with queryTimeMicros, captureCount, captureMicros and when the bind values were captured.
*/
- SpiDbQueryPlan with(long queryTimeMicros, long captureCount, long captureMicros, Instant whenCaptured);
+ SpiDbQueryPlan with(long queryTimeMicros, long captureCount, long captureMicros, Instant whenCaptured, Object tenantId);
}
diff --git a/ebean-core/src/main/java/io/ebeaninternal/api/SpiDdlGenerator.java b/ebean-core/src/main/java/io/ebeaninternal/api/SpiDdlGenerator.java
index b3c4858139..d821e75b84 100644
--- a/ebean-core/src/main/java/io/ebeaninternal/api/SpiDdlGenerator.java
+++ b/ebean-core/src/main/java/io/ebeaninternal/api/SpiDdlGenerator.java
@@ -12,4 +12,9 @@ public interface SpiDdlGenerator {
*/
void execute(boolean online);
+ /**
+ * Run DDL manually. This can be used to initialize multi tenant environments or if you plan not to run
+ * DDL on startup
+ */
+ void runDdl();
}
diff --git a/ebean-core/src/main/java/io/ebeaninternal/api/SpiExpression.java b/ebean-core/src/main/java/io/ebeaninternal/api/SpiExpression.java
index f5153464f6..c3b2901131 100644
--- a/ebean-core/src/main/java/io/ebeaninternal/api/SpiExpression.java
+++ b/ebean-core/src/main/java/io/ebeaninternal/api/SpiExpression.java
@@ -19,7 +19,9 @@ public interface SpiExpression extends Expression {
/**
* Simplify nested expressions if possible.
*/
- void simplify();
+ default SpiExpression simplify(BeanDescriptor> descriptor) {
+ return this;
+ }
/**
* Write the expression as an elastic search expression.
diff --git a/ebean-core/src/main/java/io/ebeaninternal/api/SpiQuery.java b/ebean-core/src/main/java/io/ebeaninternal/api/SpiQuery.java
index 3d1e460865..09a69dc89a 100644
--- a/ebean-core/src/main/java/io/ebeaninternal/api/SpiQuery.java
+++ b/ebean-core/src/main/java/io/ebeaninternal/api/SpiQuery.java
@@ -877,6 +877,11 @@ public static TemporalMode of(SpiQuery> query) {
*/
void setManualId();
+ /**
+ * Set the DTO type, that should be part of the queryHash.
+ */
+ void setDtoType(Class> dtoType);
+
/**
* Set default select clauses where none have been explicitly defined.
*/
diff --git a/ebean-core/src/main/java/io/ebeaninternal/api/SpiTransaction.java b/ebean-core/src/main/java/io/ebeaninternal/api/SpiTransaction.java
index 0e5df22511..2f1605d8d3 100644
--- a/ebean-core/src/main/java/io/ebeaninternal/api/SpiTransaction.java
+++ b/ebean-core/src/main/java/io/ebeaninternal/api/SpiTransaction.java
@@ -103,6 +103,11 @@ public interface SpiTransaction extends Transaction {
*/
Boolean isUpdateAllLoadedProperties();
+ /**
+ * Returns true, if generated properties are overwritten (default) or are only set, if they are null.
+ */
+ boolean isOverwriteGeneratedProperties();
+
/**
* Return the batchSize specifically set for this transaction or 0.
*
diff --git a/ebean-core/src/main/java/io/ebeaninternal/api/SpiTransactionManager.java b/ebean-core/src/main/java/io/ebeaninternal/api/SpiTransactionManager.java
index 01311d1673..cc0ff69efa 100644
--- a/ebean-core/src/main/java/io/ebeaninternal/api/SpiTransactionManager.java
+++ b/ebean-core/src/main/java/io/ebeaninternal/api/SpiTransactionManager.java
@@ -60,6 +60,6 @@ public interface SpiTransactionManager {
/**
* Return a connection used for query plan collection.
*/
- Connection queryPlanConnection() throws SQLException;
+ Connection queryPlanConnection(Object tenantId) throws SQLException;
}
diff --git a/ebean-core/src/main/java/io/ebeaninternal/api/SpiTransactionProxy.java b/ebean-core/src/main/java/io/ebeaninternal/api/SpiTransactionProxy.java
index 9c2057a4a5..183ede3c22 100644
--- a/ebean-core/src/main/java/io/ebeaninternal/api/SpiTransactionProxy.java
+++ b/ebean-core/src/main/java/io/ebeaninternal/api/SpiTransactionProxy.java
@@ -1,6 +1,7 @@
package io.ebeaninternal.api;
import io.ebean.ProfileLocation;
+import io.ebean.Transaction;
import io.ebean.TransactionCallback;
import io.ebean.annotation.DocStoreMode;
import io.ebean.event.changelog.BeanChange;
@@ -248,6 +249,16 @@ public void setUpdateAllLoadedProperties(boolean updateAllLoaded) {
transaction.setUpdateAllLoadedProperties(updateAllLoaded);
}
+ @Override
+ public void setOverwriteGeneratedProperties(boolean overwriteGeneratedProperties) {
+ transaction.setOverwriteGeneratedProperties(overwriteGeneratedProperties);
+ }
+
+ @Override
+ public boolean isOverwriteGeneratedProperties() {
+ return transaction.isOverwriteGeneratedProperties();
+ }
+
@Override
public Boolean isUpdateAllLoadedProperties() {
return transaction.isUpdateAllLoadedProperties();
@@ -452,4 +463,9 @@ public void postRollback(Throwable cause) {
public void deactivateExternal() {
transaction.deactivateExternal();
}
+
+ @Override
+ public Transaction root() {
+ return transaction.root();
+ }
}
diff --git a/ebean-core/src/main/java/io/ebeaninternal/api/json/SpiJsonReader.java b/ebean-core/src/main/java/io/ebeaninternal/api/json/SpiJsonReader.java
index 613b105cc6..4ed612defc 100644
--- a/ebean-core/src/main/java/io/ebeaninternal/api/json/SpiJsonReader.java
+++ b/ebean-core/src/main/java/io/ebeaninternal/api/json/SpiJsonReader.java
@@ -34,5 +34,4 @@ public interface SpiJsonReader {
Object readValueUsingObjectMapper(Class> propertyType) throws IOException;
- boolean update();
}
diff --git a/ebean-core/src/main/java/io/ebeaninternal/server/cache/CacheManagerOptions.java b/ebean-core/src/main/java/io/ebeaninternal/server/cache/CacheManagerOptions.java
index deda681e29..b61b6bbc1c 100644
--- a/ebean-core/src/main/java/io/ebeaninternal/server/cache/CacheManagerOptions.java
+++ b/ebean-core/src/main/java/io/ebeaninternal/server/cache/CacheManagerOptions.java
@@ -13,18 +13,20 @@
public final class CacheManagerOptions {
private final ClusterManager clusterManager;
- private final DatabaseBuilder.Settings databaseBuilder;
+ private final String serverName;
private final boolean localL2Caching;
private CurrentTenantProvider currentTenantProvider;
private QueryCacheEntryValidate queryCacheEntryValidate;
private ServerCacheFactory cacheFactory = new DefaultServerCacheFactory();
private ServerCacheOptions beanDefault = new ServerCacheOptions();
private ServerCacheOptions queryDefault = new ServerCacheOptions();
+ private final boolean tenantPartitionedCache;
CacheManagerOptions() {
this.localL2Caching = true;
this.clusterManager = null;
- this.databaseBuilder = null;
+ this.serverName = "db";
+ this.tenantPartitionedCache = false;
this.cacheFactory = new DefaultServerCacheFactory();
this.beanDefault = new ServerCacheOptions();
this.queryDefault = new ServerCacheOptions();
@@ -32,9 +34,10 @@ public final class CacheManagerOptions {
public CacheManagerOptions(ClusterManager clusterManager, DatabaseBuilder.Settings config, boolean localL2Caching) {
this.clusterManager = clusterManager;
- this.databaseBuilder = config;
+ this.serverName = config.getName();
this.localL2Caching = localL2Caching;
this.currentTenantProvider = config.getCurrentTenantProvider();
+ this.tenantPartitionedCache = config.isTenantPartitionedCache();
}
public CacheManagerOptions with(ServerCacheOptions beanDefault, ServerCacheOptions queryDefault) {
@@ -55,7 +58,7 @@ public CacheManagerOptions with(CurrentTenantProvider currentTenantProvider) {
}
public String getServerName() {
- return (databaseBuilder == null) ? "db" : databaseBuilder.getName();
+ return serverName;
}
public boolean isLocalL2Caching() {
@@ -85,4 +88,6 @@ public ClusterManager getClusterManager() {
public QueryCacheEntryValidate getQueryCacheEntryValidate() {
return queryCacheEntryValidate;
}
+
+ public boolean isTenantPartitionedCache() { return tenantPartitionedCache; }
}
diff --git a/ebean-core/src/main/java/io/ebeaninternal/server/cache/DefaultCacheHolder.java b/ebean-core/src/main/java/io/ebeaninternal/server/cache/DefaultCacheHolder.java
index aba5ab3638..06d322ecd1 100644
--- a/ebean-core/src/main/java/io/ebeaninternal/server/cache/DefaultCacheHolder.java
+++ b/ebean-core/src/main/java/io/ebeaninternal/server/cache/DefaultCacheHolder.java
@@ -32,6 +32,7 @@ final class DefaultCacheHolder {
private final ServerCacheOptions queryDefault;
private final CurrentTenantProvider tenantProvider;
private final QueryCacheEntryValidate queryCacheEntryValidate;
+ private final boolean tenantPartitionedCache;
DefaultCacheHolder(CacheManagerOptions builder) {
this.cacheFactory = builder.getCacheFactory();
@@ -39,6 +40,7 @@ final class DefaultCacheHolder {
this.queryDefault = builder.getQueryDefault();
this.tenantProvider = builder.getCurrentTenantProvider();
this.queryCacheEntryValidate = builder.getQueryCacheEntryValidate();
+ this.tenantPartitionedCache = builder.isTenantPartitionedCache();
}
void visitMetrics(MetricVisitor visitor) {
@@ -56,16 +58,42 @@ ServerCache getCache(Class> beanType, String collectionProperty) {
return getCacheInternal(beanType, ServerCacheType.COLLECTION_IDS, collectionProperty);
}
+ private String key(String beanName) {
+ if (tenantPartitionedCache) {
+ StringBuilder sb = new StringBuilder(beanName.length() + 64);
+ sb.append(beanName);
+ sb.append('.');
+ sb.append(tenantProvider.currentId());
+ return sb.toString();
+ } else {
+ return beanName;
+ }
+ }
+
private String key(String beanName, ServerCacheType type) {
- return beanName + type.code();
+ StringBuilder sb = new StringBuilder(beanName.length() + 64);
+ sb.append(beanName);
+ if (tenantPartitionedCache) {
+ sb.append('.');
+ sb.append(tenantProvider.currentId());
+ }
+ sb.append(type.code());
+ return sb.toString();
}
private String key(String beanName, String collectionProperty, ServerCacheType type) {
+ StringBuilder sb = new StringBuilder(beanName.length() + 64);
+ sb.append(beanName);
+ if (tenantPartitionedCache) {
+ sb.append('.');
+ sb.append(tenantProvider.currentId());
+ }
if (collectionProperty != null) {
- return beanName + "." + collectionProperty + type.code();
- } else {
- return beanName + type.code();
+ sb.append('.');
+ sb.append(collectionProperty);
}
+ sb.append(type.code());
+ return sb.toString();
}
/**
@@ -82,12 +110,17 @@ private ServerCache createCache(Class> beanType, ServerCacheType type, String
if (type == ServerCacheType.COLLECTION_IDS) {
lock.lock();
try {
- collectIdCaches.computeIfAbsent(beanType.getName(), s -> new ConcurrentSkipListSet<>()).add(key);
+ collectIdCaches.computeIfAbsent(key(beanType.getName()), s -> new ConcurrentSkipListSet<>()).add(key);
} finally {
lock.unlock();
}
}
- return cacheFactory.createCache(new ServerCacheConfig(type, key, shortName, options, tenantProvider, queryCacheEntryValidate));
+ if (tenantPartitionedCache) {
+ return cacheFactory.createCache(new ServerCacheConfig(type, key, shortName, options, null, queryCacheEntryValidate));
+ } else {
+ return cacheFactory.createCache(new ServerCacheConfig(type, key, shortName, options, tenantProvider, queryCacheEntryValidate));
+ }
+
}
void clearAll() {
@@ -103,7 +136,7 @@ public void clear(String name) {
clearIfExists(key(name, ServerCacheType.QUERY));
clearIfExists(key(name, ServerCacheType.BEAN));
clearIfExists(key(name, ServerCacheType.NATURAL_KEY));
- Set keys = collectIdCaches.get(name);
+ Set keys = collectIdCaches.get(key(name));
if (keys != null) {
for (String collectionIdKey : keys) {
clearIfExists(collectionIdKey);
@@ -147,4 +180,7 @@ private ServerCacheOptions getBeanOptions(Class> cls) {
return beanDefault.copy(nearCache);
}
+ boolean isTenantPartitionedCache() {
+ return tenantPartitionedCache;
+ }
}
diff --git a/ebean-core/src/main/java/io/ebeaninternal/server/cache/DefaultServerCache.java b/ebean-core/src/main/java/io/ebeaninternal/server/cache/DefaultServerCache.java
index 2590265716..a0eea215b2 100644
--- a/ebean-core/src/main/java/io/ebeaninternal/server/cache/DefaultServerCache.java
+++ b/ebean-core/src/main/java/io/ebeaninternal/server/cache/DefaultServerCache.java
@@ -6,6 +6,7 @@
import io.ebean.cache.ServerCacheStatistics;
import io.ebean.meta.MetricVisitor;
import io.ebean.metric.CountMetric;
+import io.ebean.metric.Metric;
import io.ebean.metric.MetricFactory;
import java.io.Serializable;
@@ -48,6 +49,7 @@ public class DefaultServerCache implements ServerCache {
protected final CountMetric idleCount;
protected final CountMetric ttlCount;
protected final CountMetric lruCount;
+ protected final Metric sizeCount;
protected final String name;
protected final String shortName;
protected final int maxSize;
@@ -81,6 +83,7 @@ public DefaultServerCache(DefaultServerCacheConfig config) {
this.idleCount = factory.createCountMetric(prefix + shortName + ".idle");
this.ttlCount = factory.createCountMetric(prefix + shortName + ".ttl");
this.lruCount = factory.createCountMetric(prefix + shortName + ".lru");
+ this.sizeCount = factory.createMetric(prefix + shortName + ".size", map::size);
}
@@ -103,6 +106,7 @@ public void visit(MetricVisitor visitor) {
idleCount.visit(visitor);
ttlCount.visit(visitor);
lruCount.visit(visitor);
+ sizeCount.visit(visitor);
}
@Override
diff --git a/ebean-core/src/main/java/io/ebeaninternal/server/cache/DefaultServerCacheManager.java b/ebean-core/src/main/java/io/ebeaninternal/server/cache/DefaultServerCacheManager.java
index 0b7264ff90..39558d19d8 100644
--- a/ebean-core/src/main/java/io/ebeaninternal/server/cache/DefaultServerCacheManager.java
+++ b/ebean-core/src/main/java/io/ebeaninternal/server/cache/DefaultServerCacheManager.java
@@ -154,4 +154,8 @@ public ServerCache getBeanCache(Class> beanType) {
return cacheHolder.getCache(beanType, ServerCacheType.BEAN);
}
+ @Override
+ public boolean isTenantPartitionedCache() {
+ return cacheHolder.isTenantPartitionedCache();
+ }
}
diff --git a/ebean-core/src/main/java/io/ebeaninternal/server/cache/SpiCacheManager.java b/ebean-core/src/main/java/io/ebeaninternal/server/cache/SpiCacheManager.java
index bf6d4d5a21..ebeec00f68 100644
--- a/ebean-core/src/main/java/io/ebeaninternal/server/cache/SpiCacheManager.java
+++ b/ebean-core/src/main/java/io/ebeaninternal/server/cache/SpiCacheManager.java
@@ -88,4 +88,10 @@ public interface SpiCacheManager {
*/
void clearLocal(Class> beanType);
+ /**
+ * returns true, if this chacheManager runs in tenant partitioned mode
+ * @return
+ */
+ boolean isTenantPartitionedCache();
+
}
diff --git a/ebean-core/src/main/java/io/ebeaninternal/server/core/AbstractSqlQueryRequest.java b/ebean-core/src/main/java/io/ebeaninternal/server/core/AbstractSqlQueryRequest.java
index 4acd78ee93..073a21b4f0 100644
--- a/ebean-core/src/main/java/io/ebeaninternal/server/core/AbstractSqlQueryRequest.java
+++ b/ebean-core/src/main/java/io/ebeaninternal/server/core/AbstractSqlQueryRequest.java
@@ -126,7 +126,7 @@ private String limitOffset(String sql) {
/**
* Prepare and execute the SQL using the Binder.
*/
- public void executeSql(Binder binder, SpiQuery.Type type) throws SQLException {
+ public void executeSql(Binder binder) throws SQLException {
startNano = System.nanoTime();
executeAsSql(binder);
}
diff --git a/ebean-core/src/main/java/io/ebeaninternal/server/core/DefaultContainer.java b/ebean-core/src/main/java/io/ebeaninternal/server/core/DefaultContainer.java
index c0f7174489..91896abcfb 100644
--- a/ebean-core/src/main/java/io/ebeaninternal/server/core/DefaultContainer.java
+++ b/ebean-core/src/main/java/io/ebeaninternal/server/core/DefaultContainer.java
@@ -181,6 +181,7 @@ private BootupClasses bootupClasses(DatabaseBuilder.Settings config) {
bootup.addFindControllers(config.getFindControllers());
bootup.addPersistListeners(config.getPersistListeners());
bootup.addQueryAdapters(config.getQueryAdapters());
+ bootup.addCustomDeployParser(config.getCustomDeployParsers());
bootup.addChangeLogInstances(config);
return bootup;
}
diff --git a/ebean-core/src/main/java/io/ebeaninternal/server/core/DefaultServer.java b/ebean-core/src/main/java/io/ebeaninternal/server/core/DefaultServer.java
index 3303027877..c3161bea96 100644
--- a/ebean-core/src/main/java/io/ebeaninternal/server/core/DefaultServer.java
+++ b/ebean-core/src/main/java/io/ebeaninternal/server/core/DefaultServer.java
@@ -16,6 +16,8 @@
import io.ebean.event.readaudit.ReadAuditLogger;
import io.ebean.event.readaudit.ReadAuditPrepare;
import io.ebean.meta.*;
+import io.ebean.metric.Metric;
+import io.ebean.metric.MetricRegistry;
import io.ebean.migration.auto.AutoMigrationRunner;
import io.ebean.plugin.BeanType;
import io.ebean.plugin.Plugin;
@@ -815,6 +817,12 @@ public void merge(Object bean, MergeOptions options, @Nullable Transaction trans
executeInTrans((txn) -> persister.merge(desc, checkEntityBean(bean), options, txn), transaction);
}
+ @Override
+ public T mergeBeans(T bean, T existing, BeanMergeOptions options) {
+ BeanDescriptor> desc = desc(bean.getClass());
+ return (T) desc.mergeBeans(checkEntityBean(bean), (EntityBean) existing, options);
+ }
+
@Override
public void lock(Object bean) {
BeanDescriptor> desc = desc(bean.getClass());
@@ -890,6 +898,7 @@ public DtoQuery createNamedDtoQuery(Class dtoType, String namedQuery)
@Override
public DtoQuery findDto(Class dtoType, SpiQuery> ormQuery) {
DtoBeanDescriptor descriptor = dtoBeanManager.descriptor(dtoType);
+ ormQuery.setDtoType(dtoType);
return new DefaultDtoQuery<>(this, descriptor, ormQuery);
}
@@ -932,6 +941,18 @@ public T find(Class beanType, Object id, @Nullable Transaction transactio
return findId(query);
}
+ DtoQueryRequest createDtoQueryRequest(Type type, SpiDtoQuery query) {
+ SpiQuery> ormQuery = query.ormQuery();
+ if (ormQuery != null) {
+ ormQuery.setType(type);
+ ormQuery.setManualId();
+ SpiOrmQueryRequest> ormRequest = createQueryRequest(type, ormQuery);
+ return new DtoQueryRequest<>(this, dtoQueryEngine, query, ormRequest);
+ } else {
+ return new DtoQueryRequest<>(this, dtoQueryEngine, query, null);
+ }
+ }
+
SpiOrmQueryRequest createQueryRequest(Type type, SpiQuery query) {
SpiOrmQueryRequest request = buildQueryRequest(type, query);
request.prepareQuery();
@@ -950,6 +971,7 @@ private SpiOrmQueryRequest buildQueryRequest(SpiQuery query) {
transaction = currentServerTransaction();
}
if (!query.isRawSql()) {
+ query.simplifyExpressions();
query.setDefaultRawSqlIfRequired();
if (!query.isAutoTunable() || !autoTuneService.tuneQuery(query)) {
// use deployment FetchType.LAZY/EAGER annotations
@@ -969,40 +991,6 @@ private SpiOrmQueryRequest buildQueryRequest(SpiQuery query) {
return new OrmQueryRequest<>(this, queryEngine, query, transaction);
}
- /**
- * Try to get the object out of the persistence context.
- */
- @Nullable
- @SuppressWarnings("unchecked")
- private T findIdCheckPersistenceContextAndCache(SpiQuery query, Object id) {
- SpiTransaction t = query.transaction();
- if (t == null) {
- t = currentServerTransaction();
- }
- BeanDescriptor desc = query.descriptor();
- id = desc.convertId(id);
- PersistenceContext pc = null;
- if (t != null && useTransactionPersistenceContext(query)) {
- // first look in the transaction scoped persistence context
- pc = t.persistenceContext();
- if (pc != null) {
- WithOption o = desc.contextGetWithOption(pc, id);
- if (o != null) {
- if (o.isDeleted()) {
- // Bean was previously deleted in the same transaction / persistence context
- return null;
- }
- return (T) o.getBean();
- }
- }
- }
- if (!query.isBeanCacheGet() || (t != null && t.isSkipCache())) {
- return null;
- }
- // Hit the L2 bean cache
- return desc.cacheBeanGet(id, query.isReadOnly(), pc);
- }
-
/**
* Return true if transactions PersistenceContext should be used.
*/
@@ -1023,15 +1011,59 @@ public PersistenceContextScope persistenceContextScope(SpiQuery> query) {
@SuppressWarnings("unchecked")
private T findId(SpiQuery query) {
query.setType(Type.BEAN);
+ SpiOrmQueryRequest request = null;
if (SpiQuery.Mode.NORMAL == query.mode() && !query.isForceHitDatabase()) {
// See if we can skip doing the fetch completely by getting the bean from the
// persistence context or the bean cache
- T bean = findIdCheckPersistenceContextAndCache(query, query.getId());
- if (bean != null) {
- return bean;
+ SpiTransaction t = (SpiTransaction) query.transaction();
+ if (t == null) {
+ t = currentServerTransaction();
+ }
+ BeanDescriptor desc = query.descriptor();
+ Object id = desc.convertId(query.getId());
+ PersistenceContext pc = null;
+ if (t != null && useTransactionPersistenceContext(query)) {
+ // first look in the transaction scoped persistence context
+ pc = t.persistenceContext();
+ if (pc != null) {
+ WithOption o = desc.contextGetWithOption(pc, id);
+ if (o != null) {
+ // We have found a hit. This could be also one with o.deleted() == true
+ // if bean was previously deleted in the same transaction / persistence context
+ return (T) o.getBean();
+ }
+ }
+ }
+ if (t == null || !t.isSkipCache()) {
+ if (query.queryCacheMode() != CacheMode.OFF) {
+ request = buildQueryRequest(query);
+ if (request.isQueryCacheActive()) {
+ // Hit the query cache
+ request.prepareQuery();
+ T bean = request.getFromQueryCache();
+ if (bean != null) {
+ return bean;
+ }
+ }
+ }
+ if (query.isBeanCacheGet()) {
+ // Hit the L2 bean cache
+ T bean = desc.cacheBeanGet(id, query.isReadOnly(), pc);
+ if (bean != null) {
+ if (request != null && request.isQueryCachePut()) {
+ // copy bean from the L2 cache to the faster query cache, if caching is enabled
+ request.prepareQuery();
+ request.putToQueryCache(bean);
+ }
+ return bean;
+ }
+ }
}
}
- SpiOrmQueryRequest request = buildQueryRequest(query);
+
+ if (request == null) {
+ request = buildQueryRequest(query);
+ }
request.prepareQuery();
if (request.isUseDocStore()) {
return docStore().find(request);
@@ -1077,15 +1109,18 @@ private T extractUnique(List list) {
public Set findSet(SpiQuery query) {
SpiOrmQueryRequest request = buildQueryRequest(Type.SET, query);
request.resetBeanCacheAutoMode(false);
+ if (request.isQueryCacheActive()) {
+ request.prepareQuery();
+ Object result = request.getFromQueryCache();
+ if (result != null) {
+ return (Set) result;
+ }
+ }
if (request.isGetAllFromBeanCache()) {
// hit bean cache and got all results from cache
return request.beanCacheHitsAsSet();
}
request.prepareQuery();
- Object result = request.getFromQueryCache();
- if (result != null) {
- return (Set) result;
- }
try {
request.initTransIfRequired();
return request.findSet();
@@ -1098,16 +1133,19 @@ public Set findSet(SpiQuery query) {
@SuppressWarnings({"unchecked", "rawtypes"})
public Map findMap(SpiQuery query) {
SpiOrmQueryRequest request = buildQueryRequest(Type.MAP, query);
+ if (request.isQueryCacheActive()) {
+ request.prepareQuery();
+ Object result = request.getFromQueryCache();
+ if (result != null) {
+ return (Map) result;
+ }
+ }
request.resetBeanCacheAutoMode(false);
if (request.isGetAllFromBeanCache()) {
// hit bean cache and got all results from cache
return request.beanCacheHitsAsMap();
}
request.prepareQuery();
- Object result = request.getFromQueryCache();
- if (result != null) {
- return (Map) result;
- }
try {
request.initTransIfRequired();
return request.findMap();
@@ -1413,15 +1451,18 @@ public List findList(SpiQuery query) {
private List findList(SpiQuery query, boolean findOne) {
SpiOrmQueryRequest request = buildQueryRequest(Type.LIST, query);
request.resetBeanCacheAutoMode(findOne);
+ if (request.isQueryCacheActive()) {
+ request.prepareQuery();
+ Object result = request.getFromQueryCache();
+ if (result != null) {
+ return (List) result;
+ }
+ }
if (request.isGetAllFromBeanCache()) {
// hit bean cache and got all results from cache
return request.beanCacheHits();
}
request.prepareQuery();
- Object result = request.getFromQueryCache();
- if (result != null) {
- return (List) result;
- }
if (request.isUseDocStore()) {
return docStore().findList(request);
}
@@ -1517,7 +1558,7 @@ public T findSingleAttribute(SpiSqlQuery query, Class cls) {
@Override
public void findDtoEach(SpiDtoQuery query, Consumer consumer) {
- DtoQueryRequest request = new DtoQueryRequest<>(this, dtoQueryEngine, query);
+ DtoQueryRequest request = createDtoQueryRequest(Type.ITERATE, query);
try {
request.initTransIfRequired();
request.findEach(consumer);
@@ -1528,7 +1569,7 @@ public void findDtoEach(SpiDtoQuery query, Consumer consumer) {
@Override
public void findDtoEach(SpiDtoQuery query, int batch, Consumer> consumer) {
- DtoQueryRequest request = new DtoQueryRequest<>(this, dtoQueryEngine, query);
+ DtoQueryRequest request = createDtoQueryRequest(Type.ITERATE, query);
try {
request.initTransIfRequired();
request.findEach(batch, consumer);
@@ -1539,7 +1580,7 @@ public void findDtoEach(SpiDtoQuery query, int batch, Consumer> c
@Override
public void findDtoEachWhile(SpiDtoQuery query, Predicate consumer) {
- DtoQueryRequest request = new DtoQueryRequest<>(this, dtoQueryEngine, query);
+ DtoQueryRequest request = createDtoQueryRequest(Type.ITERATE, query);
try {
request.initTransIfRequired();
request.findEachWhile(consumer);
@@ -1550,7 +1591,7 @@ public void findDtoEachWhile(SpiDtoQuery query, Predicate consumer) {
@Override
public QueryIterator findDtoIterate(SpiDtoQuery query) {
- DtoQueryRequest request = new DtoQueryRequest<>(this, dtoQueryEngine, query);
+ DtoQueryRequest request = createDtoQueryRequest(Type.ITERATE, query);
try {
request.initTransIfRequired();
return request.findIterate();
@@ -1567,7 +1608,11 @@ public Stream findDtoStream(SpiDtoQuery query) {
@Override
public List findDtoList(SpiDtoQuery query) {
- DtoQueryRequest request = new DtoQueryRequest<>(this, dtoQueryEngine, query);
+ DtoQueryRequest request = createDtoQueryRequest(Type.LIST, query);
+ List ret = request.getFromQueryCache();
+ if (ret != null) {
+ return ret;
+ }
try {
request.initTransIfRequired();
return request.findList();
@@ -1579,13 +1624,7 @@ public List findDtoList(SpiDtoQuery query) {
@Nullable
@Override
public T findDtoOne(SpiDtoQuery query) {
- DtoQueryRequest request = new DtoQueryRequest<>(this, dtoQueryEngine, query);
- try {
- request.initTransIfRequired();
- return extractUnique(request.findList());
- } finally {
- request.endTransIfRequired();
- }
+ return extractUnique(findDtoList(query));
}
/**
@@ -2192,12 +2231,7 @@ public void slowQueryCheck(long timeMicros, int rowCount, SpiQuery> query) {
}
@Override
- public Set checkUniqueness(Object bean) {
- return checkUniqueness(bean, null);
- }
-
- @Override
- public Set checkUniqueness(Object bean, @Nullable Transaction transaction) {
+ public Set checkUniqueness(Object bean, @Nullable Transaction transaction, boolean useQueryCache, boolean skipClean) {
EntityBean entityBean = checkEntityBean(bean);
BeanDescriptor> beanDesc = descriptor(entityBean.getClass());
BeanProperty idProperty = beanDesc.idProperty();
@@ -2209,14 +2243,15 @@ public Set checkUniqueness(Object bean, @Nullable Transaction transact
if (entityBean._ebean_getIntercept().isNew() && id != null) {
// Primary Key is changeable only on new models - so skip check if we are not new
SpiQuery> query = new DefaultOrmQuery<>(beanDesc, this, expressionFactory);
+ query.setUseQueryCache(useQueryCache);
query.usingTransaction(transaction);
query.setId(id);
- if (findCount(query) > 0) {
+ if (exists(query)) {
return Collections.singleton(idProperty);
}
}
for (BeanProperty[] props : beanDesc.uniqueProps()) {
- Set ret = checkUniqueness(entityBean, beanDesc, props, transaction);
+ Set ret = checkUniqueness(entityBean, beanDesc, props, transaction, useQueryCache, skipClean);
if (ret != null) {
return ret;
}
@@ -2224,13 +2259,34 @@ public Set checkUniqueness(Object bean, @Nullable Transaction transact
return Collections.emptySet();
}
+ /**
+ * Checks, if any property is dirty.
+ */
+ private boolean isAnyPropertyDirty(EntityBean entityBean, BeanProperty[] props) {
+ if (entityBean._ebean_getIntercept().isNew()) {
+ return true;
+ }
+ for (BeanProperty prop : props) {
+ if (entityBean._ebean_getIntercept().isDirtyProperty(prop.propertyIndex())) {
+ return true;
+ }
+ }
+ return false;
+ }
+
/**
* Returns a set of properties if saving the bean will violate the unique constraints (defined by given properties).
*/
@Nullable
- private Set checkUniqueness(EntityBean entityBean, BeanDescriptor> beanDesc, BeanProperty[] props, @Nullable Transaction transaction) {
+ private Set checkUniqueness(EntityBean entityBean, BeanDescriptor> beanDesc, BeanProperty[] props, @Nullable Transaction transaction,
+ boolean useQueryCache, boolean skipClean) {
+ if (skipClean && !isAnyPropertyDirty(entityBean, props)) {
+ return null;
+ }
+
BeanProperty idProperty = beanDesc.idProperty();
SpiQuery> query = new DefaultOrmQuery<>(beanDesc, this, expressionFactory);
+ query.setUseQueryCache(useQueryCache);
query.usingTransaction(transaction);
ExpressionList> exprList = query.where();
if (!entityBean._ebean_getIntercept().isNew()) {
@@ -2244,7 +2300,7 @@ private Set checkUniqueness(EntityBean entityBean, BeanDescriptor> b
}
exprList.eq(prop.name(), value);
}
- if (findCount(query) > 0) {
+ if (exists(query)) {
Set ret = new LinkedHashSet<>();
Collections.addAll(ret, props);
return ret;
@@ -2268,6 +2324,9 @@ public void visitMetrics(MetricVisitor visitor) {
persister.visitMetrics(visitor);
}
extraMetrics.visitMetrics(visitor);
+ for (Metric metric : MetricRegistry.registered()) {
+ metric.visit(visitor);
+ }
visitor.visitEnd();
}
@@ -2286,4 +2345,9 @@ List queryPlanInit(QueryPlanInit initRequest) {
List queryPlanCollectNow(QueryPlanRequest request) {
return queryPlanManager.collect(request);
}
+
+ @Override
+ public void runDdl() {
+ ddlGenerator.runDdl();
+ }
}
diff --git a/ebean-core/src/main/java/io/ebeaninternal/server/core/DtoQueryRequest.java b/ebean-core/src/main/java/io/ebeaninternal/server/core/DtoQueryRequest.java
index 3cb56ec27c..90dafe1d16 100644
--- a/ebean-core/src/main/java/io/ebeaninternal/server/core/DtoQueryRequest.java
+++ b/ebean-core/src/main/java/io/ebeaninternal/server/core/DtoQueryRequest.java
@@ -4,7 +4,6 @@
import io.ebean.core.type.DataReader;
import io.ebeaninternal.api.SpiDtoQuery;
import io.ebeaninternal.api.SpiEbeanServer;
-import io.ebeaninternal.api.SpiQuery;
import io.ebeaninternal.server.dto.DtoColumn;
import io.ebeaninternal.server.dto.DtoMappingRequest;
import io.ebeaninternal.server.dto.DtoQueryPlan;
@@ -31,11 +30,13 @@ public final class DtoQueryRequest extends AbstractSqlQueryRequest {
private final DtoQueryEngine queryEngine;
private DtoQueryPlan plan;
private DataReader dataReader;
+ private SpiOrmQueryRequest> ormRequest;
- DtoQueryRequest(SpiEbeanServer server, DtoQueryEngine engine, SpiDtoQuery query) {
+ DtoQueryRequest(SpiEbeanServer server, DtoQueryEngine engine, SpiDtoQuery query, SpiOrmQueryRequest> ormRequest) {
super(server, query);
this.queryEngine = engine;
this.query = query;
+ this.ormRequest = ormRequest;
query.obtainLocation();
}
@@ -43,21 +44,16 @@ public final class DtoQueryRequest extends AbstractSqlQueryRequest {
* Prepare and execute the SQL using the Binder.
*/
@Override
- public void executeSql(Binder binder, SpiQuery.Type type) throws SQLException {
+ public void executeSql(Binder binder) throws SQLException {
startNano = System.nanoTime();
- SpiQuery> ormQuery = query.ormQuery();
- if (ormQuery != null) {
- ormQuery.setType(type);
- ormQuery.setManualId();
-
- query.setCancelableQuery(ormQuery);
+ if (ormRequest != null) {
// execute the underlying ORM query returning the ResultSet
- ormQuery.usingTransaction(transaction);
- SpiResultSet result = server.findResultSet(ormQuery);
+ query.setCancelableQuery(query.ormQuery());
+ ormRequest.transaction(transaction);
+ SpiResultSet result = ormRequest.findResultSet();
this.pstmt = result.statement();
- this.sql = ormQuery.getGeneratedSql();
- setResultSet(result.resultSet(), ormQuery.queryPlanKey());
-
+ this.sql = ormRequest.query().getGeneratedSql();
+ setResultSet(result.resultSet(), ormRequest.query().queryPlanKey());
} else {
// native SQL query execution
executeAsSql(binder);
@@ -156,4 +152,18 @@ static String parseColumn(String columnLabel) {
return columnLabel;
}
+ public List getFromQueryCache() {
+ if (ormRequest != null) {
+ return ormRequest.getFromQueryCache();
+ } else {
+ return null;
+ }
+ }
+
+ public void putToQueryCache(List result) {
+ if (ormRequest != null && ormRequest.isQueryCachePut()) {
+ ormRequest.putToQueryCache(result);
+ }
+ }
+
}
diff --git a/ebean-core/src/main/java/io/ebeaninternal/server/core/InternalConfiguration.java b/ebean-core/src/main/java/io/ebeaninternal/server/core/InternalConfiguration.java
index c6b6717417..57d4f840b9 100644
--- a/ebean-core/src/main/java/io/ebeaninternal/server/core/InternalConfiguration.java
+++ b/ebean-core/src/main/java/io/ebeaninternal/server/core/InternalConfiguration.java
@@ -583,7 +583,8 @@ public QueryPlanManager initQueryPlanManager(TransactionManager transactionManag
return QueryPlanManager.NOOP;
}
long threshold = config.getQueryPlanThresholdMicros();
- return new CQueryPlanManager(transactionManager, threshold, queryPlanLogger(databasePlatform.platform()), extraMetrics);
+ return new CQueryPlanManager(transactionManager, config.getCurrentTenantProvider(),
+ threshold, queryPlanLogger(databasePlatform.platform()), extraMetrics);
}
/**
@@ -595,6 +596,8 @@ QueryPlanLogger queryPlanLogger(Platform platform) {
return new QueryPlanLoggerSqlServer();
case ORACLE:
return new QueryPlanLoggerOracle();
+ case DB2:
+ return new QueryPlanLoggerDb2(config.getQueryPlanOptions());
case POSTGRES:
return new QueryPlanLoggerExplain("explain (analyze, buffers) ");
case YUGABYTE:
@@ -619,6 +622,11 @@ private static class NoopDdl implements SpiDdlGenerator {
this.ddlRun = ddlRun;
}
+ @Override
+ public void runDdl() {
+ CoreLog.log.log(ERROR, "Manual DDL run not possible");
+ }
+
@Override
public void execute(boolean online) {
if (online && ddlRun) {
diff --git a/ebean-core/src/main/java/io/ebeaninternal/server/core/OrmQueryRequest.java b/ebean-core/src/main/java/io/ebeaninternal/server/core/OrmQueryRequest.java
index e804c1a171..08c0967f7c 100644
--- a/ebean-core/src/main/java/io/ebeaninternal/server/core/OrmQueryRequest.java
+++ b/ebean-core/src/main/java/io/ebeaninternal/server/core/OrmQueryRequest.java
@@ -39,10 +39,12 @@ public final class OrmQueryRequest extends BeanRequest implements SpiOrmQuery
private PersistenceContext persistenceContext;
private HashQuery cacheKey;
private CQueryPlanKey queryPlanKey;
+ // The queryPlan during the request.
+ private CQueryPlan queryPlan;
private SpiQuerySecondary secondaryQueries;
private List cacheBeans;
private boolean inlineCountDistinct;
- private Set dependentTables;
+ private boolean prepared;
private SpiQueryManyJoin manyJoin;
public OrmQueryRequest(SpiEbeanServer server, OrmQueryEngine queryEngine, SpiQuery query, SpiTransaction t) {
@@ -71,6 +73,7 @@ public boolean isDeleteByStatement() {
} else {
// delete by ids due to cascading delete needs
queryPlanKey = query.setDeleteByIdsPlan();
+ queryPlan = null;
return false;
}
}
@@ -168,11 +171,24 @@ private void adapterPreQuery() {
*/
@Override
public void prepareQuery() {
- manyJoin = query.convertJoins();
- secondaryQueries = query.secondaryQuery();
- beanDescriptor.prepareQuery(query);
- adapterPreQuery();
- queryPlanKey = query.prepare(this);
+ if (!prepared) {
+ manyJoin = query.convertJoins();
+ secondaryQueries = query.secondaryQuery();
+ beanDescriptor.prepareQuery(query);
+ adapterPreQuery();
+ queryPlanKey = query.prepare(this);
+ prepared = true;
+ }
+ }
+
+ /**
+ * The queryPlanKey has to be updated, if elements are removed from an already prepared query.
+ */
+ private void updateQueryPlanKey() {
+ if (prepared) {
+ queryPlanKey = query.prepare(this);
+ queryPlan = null;
+ }
}
public boolean isNativeSql() {
@@ -467,7 +483,10 @@ public boolean includeManyJoin() {
* query plan for this query exists.
*/
public CQueryPlan queryPlan() {
- return beanDescriptor.queryPlan(queryPlanKey);
+ if (queryPlan == null) {
+ queryPlan = beanDescriptor.queryPlan(queryPlanKey);
+ }
+ return queryPlan;
}
/**
@@ -485,6 +504,7 @@ public CQueryPlanKey queryPlanKey() {
* Put the QueryPlan into the cache.
*/
public void putQueryPlan(CQueryPlan queryPlan) {
+ this.queryPlan = queryPlan;
beanDescriptor.queryPlan(queryPlanKey, queryPlan);
}
@@ -493,8 +513,16 @@ public void resetBeanCacheAutoMode(boolean findOne) {
query.resetBeanCacheAutoMode(findOne);
}
+ @Override
+ public boolean isQueryCacheActive() {
+ return query.queryCacheMode() != CacheMode.OFF
+ && (transaction == null || !transaction.isSkipCache())
+ && !server.isDisableL2Cache();
+ }
+
+ @Override
public boolean isQueryCachePut() {
- return cacheKey != null && query.queryCacheMode().isPut();
+ return cacheKey != null && queryPlan != null && query.queryCacheMode().isPut();
}
public boolean isBeanCachePutMany() {
@@ -603,7 +631,14 @@ public boolean getFromBeanCache() {
BeanCacheResult cacheResult = beanDescriptor.cacheIdLookup(persistenceContext, idLookup.idValues());
// adjust the query (IN clause) based on the cache hits
this.cacheBeans = idLookup.removeHits(cacheResult);
- return idLookup.allHits();
+ if (idLookup.allHits()) {
+ return true;
+ } else {
+ if (!this.cacheBeans.isEmpty()) {
+ updateQueryPlanKey();
+ }
+ return false;
+ }
}
if (!beanDescriptor.isNaturalKeyCaching()) {
return false;
@@ -616,7 +651,14 @@ public boolean getFromBeanCache() {
BeanCacheResult cacheResult = beanDescriptor.naturalKeyLookup(persistenceContext, naturalKeySet.keys());
// adjust the query (IN clause) based on the cache hits
this.cacheBeans = data.removeHits(cacheResult);
- return data.allHits();
+ if (data.allHits()) {
+ return true;
+ } else {
+ if (!this.cacheBeans.isEmpty()) {
+ updateQueryPlanKey();
+ }
+ return false;
+ }
}
}
return false;
@@ -628,9 +670,7 @@ public boolean getFromBeanCache() {
@Override
@SuppressWarnings("unchecked")
public Object getFromQueryCache() {
- if (query.queryCacheMode() == CacheMode.OFF
- || (transaction != null && transaction.isSkipCache())
- || server.isDisableL2Cache()) {
+ if (!isQueryCacheActive()) {
return null;
} else {
cacheKey = query.queryHash();
@@ -684,8 +724,9 @@ private boolean readAuditQueryType() {
}
}
+ @Override
public void putToQueryCache(Object result) {
- beanDescriptor.queryCachePut(cacheKey, new QueryCacheEntry(result, dependentTables, transaction.startNanoTime()));
+ beanDescriptor.queryCachePut(cacheKey, new QueryCacheEntry(result, queryPlan.dependentTables(), transaction.startNanoTime()));
}
/**
@@ -755,15 +796,6 @@ public boolean isInlineCountDistinct() {
return inlineCountDistinct;
}
- public void addDependentTables(Set tables) {
- if (tables != null && !tables.isEmpty()) {
- if (dependentTables == null) {
- dependentTables = new LinkedHashSet<>();
- }
- dependentTables.addAll(tables);
- }
- }
-
/**
* Return true if no MaxRows or use LIMIT in SQL update.
*/
diff --git a/ebean-core/src/main/java/io/ebeaninternal/server/core/PersistRequestBean.java b/ebean-core/src/main/java/io/ebeaninternal/server/core/PersistRequestBean.java
index 93358d0c6d..6286da2e0f 100644
--- a/ebean-core/src/main/java/io/ebeaninternal/server/core/PersistRequestBean.java
+++ b/ebean-core/src/main/java/io/ebeaninternal/server/core/PersistRequestBean.java
@@ -275,24 +275,30 @@ private void onUpdateGeneratedProperties() {
} else {
// @WhenModified set without invoking interception
Object oldVal = prop.getValue(entityBean);
- Object value = generatedProperty.getUpdateValue(prop, entityBean, now());
- prop.setValueChanged(entityBean, value);
- intercept.setOldValue(prop.propertyIndex(), oldVal);
+ if (transaction == null || transaction.isOverwriteGeneratedProperties() || oldVal == null) { // version handled above
+ Object value = generatedProperty.getUpdateValue(prop, entityBean, now());
+ prop.setValueChanged(entityBean, value);
+ intercept.setOldValue(prop.propertyIndex(), oldVal);
+ }
}
}
}
private void onFailedUpdateUndoGeneratedProperties() {
for (BeanProperty prop : beanDescriptor.propertiesGenUpdate()) {
- Object oldVal = intercept.origValue(prop.propertyIndex());
- prop.setValue(entityBean, oldVal);
+ if (transaction == null || transaction.isOverwriteGeneratedProperties() || prop.isVersion()) {
+ Object oldVal = intercept.origValue(prop.propertyIndex());
+ prop.setValue(entityBean, oldVal);
+ }
}
}
private void onInsertGeneratedProperties() {
for (BeanProperty prop : beanDescriptor.propertiesGenInsert()) {
- Object value = prop.generatedProperty().getInsertValue(prop, entityBean, now());
- prop.setValueChanged(entityBean, value);
+ if (transaction == null || transaction.isOverwriteGeneratedProperties() || prop.isVersion() || prop.getValue(entityBean) == null) {
+ Object value = prop.generatedProperty().getInsertValue(prop, entityBean, now());
+ prop.setValueChanged(entityBean, value);
+ }
}
}
diff --git a/ebean-core/src/main/java/io/ebeaninternal/server/core/SpiOrmQueryRequest.java b/ebean-core/src/main/java/io/ebeaninternal/server/core/SpiOrmQueryRequest.java
index e81da64f54..3361b87414 100644
--- a/ebean-core/src/main/java/io/ebeaninternal/server/core/SpiOrmQueryRequest.java
+++ b/ebean-core/src/main/java/io/ebeaninternal/server/core/SpiOrmQueryRequest.java
@@ -135,6 +135,21 @@ public interface SpiOrmQueryRequest extends BeanQueryRequest, DocQueryRequ
*/
A getFromQueryCache();
+ /**
+ * Return if query cache is active.
+ */
+ boolean isQueryCacheActive();
+
+ /**
+ * Return if results should be put to query cache.
+ */
+ boolean isQueryCachePut();
+
+ /**
+ * Put the result to the query cache.
+ */
+ void putToQueryCache(Object result);
+
/**
* Maybe hit the bean cache returning true if everything was obtained from the
* cache (that there were no misses).
diff --git a/ebean-core/src/main/java/io/ebeaninternal/server/core/bootup/BootupClasses.java b/ebean-core/src/main/java/io/ebeaninternal/server/core/bootup/BootupClasses.java
index 726411dd2f..4b89778d0d 100644
--- a/ebean-core/src/main/java/io/ebeaninternal/server/core/bootup/BootupClasses.java
+++ b/ebean-core/src/main/java/io/ebeaninternal/server/core/bootup/BootupClasses.java
@@ -2,6 +2,7 @@
import io.ebean.annotation.DocStore;
import io.ebean.DatabaseBuilder;
+import io.ebean.bean.extend.EntityExtension;
import io.ebean.config.IdGenerator;
import io.ebean.config.ScalarTypeConverter;
import io.ebean.core.type.ScalarType;
@@ -11,6 +12,7 @@
import io.ebean.event.changelog.ChangeLogRegister;
import io.ebean.event.readaudit.ReadAuditLogger;
import io.ebean.event.readaudit.ReadAuditPrepare;
+import io.ebean.plugin.CustomDeployParser;
import io.ebean.util.AnnotationUtil;
import io.ebeaninternal.api.CoreLog;
@@ -38,6 +40,7 @@ public class BootupClasses implements Predicate> {
private final List> embeddableList = new ArrayList<>();
private final List> entityList = new ArrayList<>();
+ private final List> entityExtensionList = new ArrayList<>();
private final List>> scalarTypeList = new ArrayList<>();
private final List>> scalarConverterList = new ArrayList<>();
private final List>> attributeConverterList = new ArrayList<>();
@@ -54,6 +57,7 @@ public class BootupClasses implements Predicate> {
private final List> beanPersistListenerCandidates = new ArrayList<>();
private final List> beanQueryAdapterCandidates = new ArrayList<>();
private final List> serverConfigStartupCandidates = new ArrayList<>();
+ private final List> customDeployParserCandidates = new ArrayList<>();
private final List idGeneratorInstances = new ArrayList<>();
private final List beanPersistControllerInstances = new ArrayList<>();
@@ -63,6 +67,7 @@ public class BootupClasses implements Predicate> {
private final List beanPersistListenerInstances = new ArrayList<>();
private final List beanQueryAdapterInstances = new ArrayList<>();
private final List serverConfigStartupInstances = new ArrayList<>();
+ private final List customDeployParserInstances = new ArrayList<>();
// single objects
private Class extends ChangeLogPrepare> changeLogPrepareClass;
@@ -172,6 +177,10 @@ public void addServerConfigStartup(List startupInstances) {
add(startupInstances, serverConfigStartupInstances, serverConfigStartupCandidates);
}
+ public void addCustomDeployParser(List customDeployParser) {
+ add(customDeployParser, customDeployParserInstances, customDeployParserCandidates);
+ }
+
public void addChangeLogInstances(DatabaseBuilder.Settings config) {
readAuditPrepare = config.getReadAuditPrepare();
readAuditLogger = config.getReadAuditLogger();
@@ -288,6 +297,10 @@ public List getBeanQueryAdapters() {
return createAdd(beanQueryAdapterInstances, beanQueryAdapterCandidates);
}
+ public List getCustomDeployParsers() {
+ return createAdd(customDeployParserInstances, customDeployParserCandidates);
+ }
+
/**
* Return the list of Embeddable classes.
*/
@@ -302,6 +315,13 @@ public List> getEntities() {
return entityList;
}
+ /**
+ * Return the list of entity extension classes.
+ */
+ public List> getEntityExtensionList() {
+ return entityExtensionList;
+ }
+
/**
* Return the list of ScalarTypes found.
*/
@@ -329,6 +349,8 @@ public boolean test(Class> cls) {
embeddableList.add(cls);
} else if (isEntity(cls)) {
entityList.add(cls);
+ } else if (isEntityExtension(cls)) {
+ entityExtensionList.add(cls);
} else {
return isInterestingInterface(cls);
}
@@ -408,6 +430,11 @@ private boolean isInterestingInterface(Class> cls) {
interesting = true;
}
+ if (CustomDeployParser.class.isAssignableFrom(cls)) {
+ customDeployParserCandidates.add((Class extends CustomDeployParser>) cls);
+ interesting = true;
+ }
+
// single instances, last assigned wins
if (ChangeLogListener.class.isAssignableFrom(cls)) {
changeLogListenerClass = (Class extends ChangeLogListener>) cls;
@@ -440,6 +467,10 @@ private boolean isEntity(Class> cls) {
return has(cls, Entity.class) || has(cls, Table.class) || has(cls, DocStore.class);
}
+ private boolean isEntityExtension(Class> cls) {
+ return has(cls, EntityExtension.class);
+ }
+
private boolean isEmbeddable(Class> cls) {
return has(cls, Embeddable.class);
}
diff --git a/ebean-core/src/main/java/io/ebeaninternal/server/deploy/BaseCollectionHelp.java b/ebean-core/src/main/java/io/ebeaninternal/server/deploy/BaseCollectionHelp.java
index b6cb6314ba..f161f18230 100644
--- a/ebean-core/src/main/java/io/ebeaninternal/server/deploy/BaseCollectionHelp.java
+++ b/ebean-core/src/main/java/io/ebeaninternal/server/deploy/BaseCollectionHelp.java
@@ -11,7 +11,7 @@
abstract class BaseCollectionHelp implements BeanCollectionHelp {
final BeanPropertyAssocMany