Compare commits

..
Author SHA1 Message Date
Rob Bygrave 4b72924529 [maven-release-plugin] prepare release ebean-11.5.1 2017-11-13 23:17:54 +13:00
Rob Bygrave a75ed676f8 Bump pom version to 11.5.1-SNAPSHOT 2017-11-13 23:16:38 +13:00
Rob Bygrave c3438b888d #995 - @OneToMany with constraints on the many side 2017-11-13 22:43:56 +13:00
Rob Bygrave 56be274acc No effective change - change internal use of ArrayList to List 2017-11-13 22:42:39 +13:00
Rob Bygrave 84bb4ff78c #1207 - Regression added in 11.4.1 with NPE on @transactional(type = TxType.SUPPORTS) 2017-11-13 21:33:39 +13:00
Rob Bygrave 4054eda1f6 #1207 - Regression added in 11.4.1 with NPE on @transactional(type = TxType.SUPPORTS) 2017-11-13 21:32:52 +13:00
Rob Bygrave a4b9354e43 #1206 - NPE when using @Encrypted with Enum property that is null 2017-11-13 19:12:57 +13:00
Rob Bygrave 06e033f23f #1181 - ENH: Add @FetchPreference support - Secondary query generation is based on order of fetch statements 2017-11-08 23:20:08 +13:00
Rob Bygrave 156f505394 #1181 - ENH: Add @FetchPreference support - Secondary query generation is based on order of fetch statements 2017-11-08 23:19:58 +13:00
Rob Bygrave 9b2b2f7787 #1205 - ENH: Add support for raw expression array binding with Postgres - e.g. raw(" foo = any(?) ", collection) 2017-11-08 00:56:13 +13:00
Rob Bygrave e68dae0baa Update ebean-annotation to 3.3 and tests to use 11.5.1 agent 2017-11-07 22:57:04 +13:00
Rob Bygrave 2cf864954f #1204 - Check presence of JAXB and javax @PostConstruct (which may not be present in minimal Java 9) 2017-11-07 20:08:10 +13:00
Rob Bygrave 2b94487573 #1202 - Automatically remove old query plans from Ebean's internal query plan cache 2017-11-06 22:59:42 +13:00
Rob Bygrave fcf730ec39 [maven-release-plugin] prepare for next development iteration 2017-11-06 14:49:08 +13:00
42 changed files with 1221 additions and 96 deletions
+5 -5
View File
@@ -9,7 +9,7 @@
<groupId>io.ebean</groupId>
<artifactId>ebean</artifactId>
<version>11.4.1</version>
<version>11.5.1</version>
<packaging>jar</packaging>
<name>ebean</name>
@@ -17,7 +17,7 @@
<scm>
<developerConnection>scm:git:git@github.com:ebean-orm/ebean.git</developerConnection>
<tag>ebean-11.4.1</tag>
<tag>ebean-11.5.1</tag>
</scm>
<profiles>
@@ -92,7 +92,7 @@
<dependency>
<groupId>io.ebean</groupId>
<artifactId>ebean-annotation</artifactId>
<version>3.2</version>
<version>3.4</version>
</dependency>
<dependency>
@@ -215,7 +215,7 @@
<dependency>
<groupId>io.ebean</groupId>
<artifactId>ebean-agent</artifactId>
<version>11.4.1</version>
<version>11.5.1</version>
<scope>test</scope>
</dependency>
@@ -288,7 +288,7 @@
<plugin>
<groupId>io.ebean</groupId>
<artifactId>ebean-maven-plugin</artifactId>
<version>11.4.1</version>
<version>11.5.1</version>
<executions>
<execution>
<id>test</id>
@@ -54,6 +54,22 @@ public class ClassLoadConfig {
return isPresent("javax.validation.constraints.NotNull");
}
/**
* Return true if javax PostConstruct annotation is present (maybe not in java9).
* If not we don't support PostConstruct lifecycle events.
*/
public boolean isJavaxPostConstructPresent() {
return isPresent("javax.annotation.PostConstruct");
}
/**
* Return true if javax JAXB is present (maybe not in java9).
* If not we don't try to parse or support 'extra ddl'.
*/
public boolean isJavaxJAXBPresent() {
return isPresent("javax.xml.bind.JAXBException");
}
/**
* Return true if Jackson annotations like JsonIgnore are present.
*/
@@ -435,6 +435,11 @@ public class ServerConfig {
private String jodaLocalTimeMode;
/**
* Time to live for query plans - defaults to 5 minutes.
*/
private int queryPlanTTLSeconds = 60 * 5;
/**
* Set to true to globally disable L2 caching (typically for performance testing).
*/
@@ -2595,6 +2600,7 @@ public class ServerConfig {
dbTypeConfig.setGeometrySRID(srid);
}
queryPlanTTLSeconds = p.getInt("queryPlanTTLSeconds", queryPlanTTLSeconds);
slowQueryMillis = p.getLong("slowQueryMillis", slowQueryMillis);
docStoreOnly = p.getBoolean("docStoreOnly", docStoreOnly);
disableL2Cache = p.getBoolean("disableL2Cache", disableL2Cache);
@@ -2817,6 +2823,20 @@ public class ServerConfig {
this.disableL2Cache = disableL2Cache;
}
/**
* Return the query plan time to live.
*/
public int getQueryPlanTTLSeconds() {
return queryPlanTTLSeconds;
}
/**
* Set the query plan time to live.
*/
public void setQueryPlanTTLSeconds(int queryPlanTTLSeconds) {
this.queryPlanTTLSeconds = queryPlanTTLSeconds;
}
/**
* Run the DB migration against the DataSource.
*/
@@ -89,6 +89,10 @@ public class ScopeTrans {
}
public String toString() {
return "ScopeTrans[" + transaction + "]";
}
/**
* Return the current/active transaction.
*/
@@ -25,6 +25,10 @@ public class ScopedTransaction extends SpiTransactionProxy {
this.manager = manager;
}
public String toString() {
return "ScopedTransaction[" + current + "]";
}
/**
* Push the scope transaction.
*/
@@ -56,6 +60,7 @@ public class ScopedTransaction extends SpiTransactionProxy {
private void pop() {
if (!stack.isEmpty()) {
current = stack.pop();
transaction = current.getTransaction();
} else {
manager.set(null);
}
@@ -36,6 +36,7 @@ public class DdlGenerator {
private final boolean generateDdl;
private final boolean runDdl;
private final boolean createOnly;
private final boolean jaxbPresent;
private CurrentModel currentModel;
private String dropAllContent;
@@ -43,6 +44,7 @@ public class DdlGenerator {
public DdlGenerator(SpiEbeanServer server, ServerConfig serverConfig) {
this.server = server;
this.jaxbPresent = serverConfig.getClassLoadConfig().isJavaxJAXBPresent();
this.generateDdl = serverConfig.isDdlGenerate();
this.createOnly = serverConfig.isDdlCreateOnly();
if (serverConfig.getTenantMode().isDynamicDataSource() && serverConfig.isDdlRun()) {
@@ -139,7 +141,7 @@ public class DdlGenerator {
runScript(false, createAllContent, getCreateFileName());
String ignoreExtraDdl = System.getProperty("ebean.ignoreExtraDdl");
if (!"true".equalsIgnoreCase(ignoreExtraDdl)) {
if (!"true".equalsIgnoreCase(ignoreExtraDdl) && jaxbPresent) {
String extraApply = ExtraDdlXmlReader.buildExtra(server.getDatabasePlatform().getName());
if (extraApply != null) {
runScript(false, extraApply, "extra-dll");
@@ -724,33 +724,31 @@ public final class DefaultServer implements SpiServer, SpiEbeanServer {
* This will also potentially throw exceptions for MANDATORY and NEVER types.
* </p>
*/
private boolean createNewTransaction(SpiTransaction t, TxScope scope) {
TxType type = scope.getType();
private boolean createNewTransaction(SpiTransaction current, TxType type) {
switch (type) {
case REQUIRED:
return t == null;
return current == null;
case REQUIRES_NEW:
return true;
case MANDATORY:
if (t == null) {
if (current == null) {
throw new PersistenceException("Transaction missing when MANDATORY");
}
return false;
case SUPPORTS:
return current == null;
case NEVER:
if (t != null) {
if (current != null) {
throw new PersistenceException("Transaction exists for Transactional NEVER");
}
return false;
case SUPPORTS:
return false;
return true; // always use NoTransaction instance
case NOT_SUPPORTED:
throw new RuntimeException("NOT_SUPPORTED should already be handled?");
return true; // always use NoTransaction instance
default:
throw new RuntimeException("Should never get here?");
@@ -811,14 +809,17 @@ public final class DefaultServer implements SpiServer, SpiEbeanServer {
SpiTransaction transaction = txnContainer.current();
boolean createTransaction;
if (txScope.getType() == TxType.NOT_SUPPORTED) {
createTransaction = false;
transaction = null;
} else {
createTransaction = createNewTransaction(transaction, txScope);
if (createTransaction) {
transaction = transactionManager.createTransaction(txScope.getProfileId(), true, txScope.getIsolationLevel());
TxType type = txScope.getType();
boolean createTransaction = createNewTransaction(transaction, type);
if (createTransaction) {
switch (type) {
case SUPPORTS:
case NOT_SUPPORTED:
case NEVER:
transaction = NoTransaction.INSTANCE;
break;
default:
transaction = transactionManager.createTransaction(txScope.getProfileId(), true, txScope.getIsolationLevel());
}
}
@@ -151,6 +151,7 @@ public class InternalConfiguration {
this.beanDescriptorManager = new BeanDescriptorManager(this);
Map<String, String> asOfTableMapping = beanDescriptorManager.deploy();
Map<String, String> draftTableMap = beanDescriptorManager.getDraftTableMap();
beanDescriptorManager.scheduleBackgroundTrim();
this.dataTimeZone = initDataTimeZone();
this.binder = getBinder(typeManager, databasePlatform, dataTimeZone);
@@ -0,0 +1,419 @@
package io.ebeaninternal.server.core;
import io.ebean.TransactionCallback;
import io.ebean.annotation.DocStoreMode;
import io.ebean.annotation.PersistBatch;
import io.ebean.bean.PersistenceContext;
import io.ebean.event.changelog.BeanChange;
import io.ebean.event.changelog.ChangeSet;
import io.ebeaninternal.api.SpiProfileTransactionEvent;
import io.ebeaninternal.api.SpiTransaction;
import io.ebeaninternal.api.TransactionEvent;
import io.ebeaninternal.server.persist.BatchControl;
import io.ebeaninternal.server.transaction.ProfileStream;
import io.ebeanservice.docstore.api.DocStoreTransaction;
import javax.persistence.PersistenceException;
import java.sql.Connection;
import java.sql.SQLException;
/**
* Place holder for use with SUPPORTS and NEVER transactional when there really isn't a transaction.
*/
class NoTransaction implements SpiTransaction {
static final NoTransaction INSTANCE = new NoTransaction();
@Override
public boolean isActive() {
// always false
return false;
}
@Override
public void commitAndContinue() {
// do nothing
}
@Override
public void commit() {
// do nothing
}
@Override
public void rollback() throws PersistenceException {
// do nothing
}
@Override
public void rollback(Throwable e) throws PersistenceException {
// do nothing
}
@Override
public void end() {
// do nothing
}
@Override
public void close() {
// do nothing
}
@Override
public String getLogPrefix() {
return null;
}
@Override
public boolean isLogSql() {
return false;
}
@Override
public boolean isLogSummary() {
return false;
}
@Override
public void logSql(String msg) {
}
@Override
public void logSummary(String msg) {
}
@Override
public void registerDeferred(PersistDeferredRelationship derived) {
}
@Override
public void registerDeleteBean(Integer hash) {
}
@Override
public void unregisterDeleteBean(Integer hash) {
}
@Override
public boolean isRegisteredDeleteBean(Integer hash) {
return false;
}
@Override
public void unregisterBean(Object bean) {
}
@Override
public boolean isRegisteredBean(Object bean) {
return false;
}
@Override
public String getId() {
return null;
}
@Override
public Boolean isUpdateAllLoadedProperties() {
return null;
}
@Override
public DocStoreMode getDocStoreMode() {
return null;
}
@Override
public int getDocStoreBatchSize() {
return 0;
}
@Override
public void register(TransactionCallback callback) {
}
@Override
public boolean isReadOnly() {
return false;
}
@Override
public void setReadOnly(boolean readOnly) {
}
@Override
public void setRollbackOnly() {
}
@Override
public boolean isRollbackOnly() {
return false;
}
@Override
public void setDocStoreMode(DocStoreMode mode) {
}
@Override
public void setDocStoreBatchSize(int batchSize) {
}
@Override
public void setPersistCascade(boolean persistCascade) {
}
@Override
public void setUpdateAllLoadedProperties(boolean updateAllLoadedProperties) {
}
@Override
public void setSkipCache(boolean skipCache) {
}
@Override
public boolean isSkipCache() {
return false;
}
@Override
public void setBatchMode(boolean useBatch) {
}
@Override
public void setBatch(PersistBatch persistBatchMode) {
}
@Override
public PersistBatch getBatch() {
return null;
}
@Override
public void setBatchOnCascade(PersistBatch batchOnCascadeMode) {
}
@Override
public PersistBatch getBatchOnCascade() {
return null;
}
@Override
public void setBatchSize(int batchSize) {
}
@Override
public int getBatchSize() {
return 0;
}
@Override
public void setBatchGetGeneratedKeys(boolean getGeneratedKeys) {
}
@Override
public void setBatchFlushOnMixed(boolean batchFlushOnMixed) {
}
@Override
public void setBatchFlushOnQuery(boolean batchFlushOnQuery) {
}
@Override
public boolean isBatchFlushOnQuery() {
return false;
}
@Override
public void flush() throws PersistenceException {
}
@Override
public void flushBatch() throws PersistenceException {
}
@Override
public Connection getConnection() {
return null;
}
@Override
public void addModification(String tableName, boolean inserts, boolean updates, boolean deletes) {
}
@Override
public void putUserObject(String name, Object value) {
}
@Override
public Object getUserObject(String name) {
return null;
}
@Override
public Boolean getBatchGetGeneratedKeys() {
return null;
}
@Override
public void depth(int diff) {
}
@Override
public int depth() {
return 0;
}
@Override
public boolean isExplicit() {
return false;
}
@Override
public TransactionEvent getEvent() {
return null;
}
@Override
public boolean isPersistCascade() {
return false;
}
@Override
public boolean isBatchThisRequest(PersistRequest.Type type) {
return false;
}
@Override
public BatchControl getBatchControl() {
return null;
}
@Override
public void setBatchControl(BatchControl control) {
}
@Override
public PersistenceContext getPersistenceContext() {
return null;
}
@Override
public void setPersistenceContext(PersistenceContext context) {
}
@Override
public Connection getInternalConnection() {
return null;
}
@Override
public boolean isSaveAssocManyIntersection(String intersectionTable, String beanName) {
return false;
}
@Override
public boolean checkBatchEscalationOnCascade(PersistRequestBean<?> request) {
return false;
}
@Override
public void flushBatchOnCascade() {
}
@Override
public void flushBatchOnRollback() {
}
@Override
public PersistenceException translate(String message, SQLException cause) {
return null;
}
@Override
public void markNotQueryOnly() {
}
@Override
public void checkBatchEscalationOnCollection() {
}
@Override
public void flushBatchOnCollection() {
}
@Override
public void addBeanChange(BeanChange beanChange) {
}
@Override
public void sendChangeLog(ChangeSet changeSet) {
}
@Override
public DocStoreTransaction getDocStoreTransaction() {
return null;
}
@Override
public void setTenantId(Object tenantId) {
}
@Override
public Object getTenantId() {
return null;
}
@Override
public long profileOffset() {
return 0;
}
@Override
public void profileEvent(SpiProfileTransactionEvent event) {
}
@Override
public ProfileStream profileStream() {
return null;
}
}
@@ -89,6 +89,7 @@ import java.sql.Types;
import java.util.ArrayList;
import java.util.Collection;
import java.util.HashMap;
import java.util.Iterator;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
@@ -1530,6 +1531,24 @@ public class BeanDescriptor<T> implements MetaBeanInfo, BeanType<T> {
}
}
/**
* Trim query plans not used since the passed in epoch time.
*/
public List<CQueryPlan> trimQueryPlans(long unusedSince) {
List<CQueryPlan> list = new ArrayList<>();
Iterator<CQueryPlan> it = queryPlanCache.values().iterator();
while (it.hasNext()) {
CQueryPlan queryPlan = it.next();
if (queryPlan.getLastQueryTime() < unusedSince) {
it.remove();
list.add(queryPlan);
}
}
return list;
}
/**
* Execute the postLoad if a BeanPostLoad exists for this bean.
*/
@@ -49,6 +49,7 @@ import io.ebeaninternal.server.persist.platform.MultiValueBind;
import io.ebeaninternal.server.properties.BeanPropertiesReader;
import io.ebeaninternal.server.properties.BeanPropertyAccess;
import io.ebeaninternal.server.properties.EnhanceBeanPropertyAccess;
import io.ebeaninternal.server.query.CQueryPlan;
import io.ebeaninternal.xmlmapping.XmlMappingReader;
import io.ebeaninternal.xmlmapping.model.XmAliasMapping;
import io.ebeaninternal.xmlmapping.model.XmColumnMapping;
@@ -80,6 +81,7 @@ import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.concurrent.TimeUnit;
import java.util.stream.Collectors;
/**
@@ -190,6 +192,8 @@ public class BeanDescriptorManager implements BeanDescriptorMap {
*/
private final Map<String, String> draftTableMap = new HashMap<>();
private final int queryPlanTTLSeconds;
/**
* Create for a given database dbConfig.
*/
@@ -207,6 +211,7 @@ public class BeanDescriptorManager implements BeanDescriptorMap {
this.multiValueBind = config.getMultiValueBind();
this.idBinderFactory = new IdBinderFactory(databasePlatform.isIdInExpandedForm(), multiValueBind);
this.eagerFetchLobs = serverConfig.isEagerFetchLobs();
this.queryPlanTTLSeconds = serverConfig.getQueryPlanTTLSeconds();
this.asOfViewSuffix = getAsOfViewSuffix(databasePlatform, serverConfig);
String versionsBetweenSuffix = getVersionsBetweenSuffix(databasePlatform, serverConfig);
@@ -222,7 +227,7 @@ public class BeanDescriptorManager implements BeanDescriptorMap {
this.updateChangesOnly = serverConfig.isUpdateChangesOnly();
this.beanLifecycleAdapterFactory = new BeanLifecycleAdapterFactory();
this.beanLifecycleAdapterFactory = new BeanLifecycleAdapterFactory(serverConfig);
this.persistControllerManager = new PersistControllerManager(bootupClasses);
this.postLoadManager = new PostLoadManager(bootupClasses);
this.postConstructManager = new PostConstructManager(bootupClasses);
@@ -236,6 +241,25 @@ public class BeanDescriptorManager implements BeanDescriptorMap {
this.changeLogRegister = config.changeLogRegister(bootupClasses.getChangeLogRegister());
}
/**
* Run periodic trim of query plans.
*/
public void scheduleBackgroundTrim() {
backgroundExecutor.executePeriodically(this::trimQueryPlans, 30L, TimeUnit.SECONDS);
}
private void trimQueryPlans() {
long lastUsed = System.currentTimeMillis() - (queryPlanTTLSeconds * 1000L);
for (BeanDescriptor<?> descriptor : immutableDescriptorList) {
if (!descriptor.isEmbedded()) {
List<CQueryPlan> trimmedPlans = descriptor.trimQueryPlans(lastUsed);
if (!trimmedPlans.isEmpty()) {
logger.trace("trimmed {} query plans for type:{}", trimmedPlans.size(), descriptor.getName());
}
}
}
}
/**
* Return the AsOfViewSuffix based on the DbHistorySupport.
*/
@@ -41,8 +41,10 @@ public final class BeanFkeyProperty implements ElPropertyValue {
return false;
}
public int getDeployOrder() {
return deployOrder;
@Override
public int getFetchPreference() {
// return some decently high value
return 1000;
}
private String calcPlaceHolder(String prefix, String dbColumn) {
@@ -2,6 +2,7 @@ package io.ebeaninternal.server.deploy;
import io.ebean.annotation.PostSoftDelete;
import io.ebean.annotation.PreSoftDelete;
import io.ebean.config.ServerConfig;
import io.ebean.event.BeanPersistAdapter;
import io.ebean.event.BeanPersistRequest;
import io.ebean.event.BeanPostConstructListener;
@@ -31,6 +32,12 @@ import java.util.List;
*/
class BeanLifecycleAdapterFactory {
private final boolean postConstructPresent;
BeanLifecycleAdapterFactory(ServerConfig serverConfig) {
this.postConstructPresent = serverConfig.getClassLoadConfig().isJavaxPostConstructPresent();
}
/**
* Register a BeanPersistController for methods annotated with lifecycle events.
*/
@@ -41,7 +48,7 @@ class BeanLifecycleAdapterFactory {
// look for annotated methods
MethodsHolder methodHolder = new MethodsHolder();
for (Method m : methods) {
methodHolder.checkMethod(m);
methodHolder.checkMethod(m, postConstructPresent);
}
if (methodHolder.hasPersistMethods()) {
@@ -86,7 +93,7 @@ class BeanLifecycleAdapterFactory {
/**
* Check the method for all the annotations we are interested in.
*/
private void checkMethod(Method method) {
private void checkMethod(Method method, boolean postConstructPresent) {
if (method.isAnnotationPresent(PrePersist.class)) {
preInserts.add(method);
hasPersistMethods = true;
@@ -125,8 +132,10 @@ class BeanLifecycleAdapterFactory {
if (method.isAnnotationPresent(PostLoad.class)) {
postLoads.add(method);
}
if (method.isAnnotationPresent(PostConstruct.class)) {
postConstructs.add(method);
if (postConstructPresent) {
if (method.isAnnotationPresent(PostConstruct.class)) {
postConstructs.add(method);
}
}
}
}
@@ -537,6 +537,12 @@ public class BeanProperty implements ElPropertyValue, Property {
return dbEncryptFunction.getDecryptSql(tableAlias + "." + this.getDbColumn());
}
@Override
public int getFetchPreference() {
// return some decently high value - override on ToMany property
return 1000;
}
/**
* Add any extra joins required to support this property. Generally a no
* operation except for a OneToOne exported.
@@ -73,6 +73,8 @@ public abstract class BeanPropertyAssoc<T> extends BeanProperty {
final String extraWhere;
final int fetchPreference;
boolean saveRecurseSkippable;
/**
@@ -88,6 +90,7 @@ public abstract class BeanPropertyAssoc<T> extends BeanProperty {
this.targetType = deploy.getTargetType();
this.cascadeInfo = deploy.getCascadeInfo();
this.fetchPreference = deploy.getFetchPreference();
}
/**
@@ -109,6 +112,11 @@ public abstract class BeanPropertyAssoc<T> extends BeanProperty {
}
}
@Override
public int getFetchPreference() {
return fetchPreference;
}
/**
* Create a ElPropertyValue for a *ToOne or *ToMany.
*/
@@ -297,7 +297,7 @@ public class BeanPropertyAssocMany<T> extends BeanPropertyAssoc<T> {
/**
* Find the Id's of detail beans given a parent Id or list of parent Id's.
*/
public List<Object> findIdsByParentId(Object parentId, List<Object> parentIdList, Transaction t, ArrayList<Object> excludeDetailIds) {
public List<Object> findIdsByParentId(Object parentId, List<Object> parentIdList, Transaction t, List<Object> excludeDetailIds) {
if (parentId != null) {
return findIdsByParentId(parentId, t, excludeDetailIds);
} else {
@@ -305,7 +305,7 @@ public class BeanPropertyAssocMany<T> extends BeanPropertyAssoc<T> {
}
}
private List<Object> findIdsByParentId(Object parentId, Transaction t, ArrayList<Object> excludeDetailIds) {
private List<Object> findIdsByParentId(Object parentId, Transaction t, List<Object> excludeDetailIds) {
String rawWhere = deriveWhereParentIdSql(false, "");
List<Object> bindValues = new ArrayList<>();
@@ -374,7 +374,7 @@ public class BeanPropertyAssocMany<T> extends BeanPropertyAssoc<T> {
}
}
private List<Object> findIdsByParentIdList(List<Object> parentIdList, Transaction t, ArrayList<Object> excludeDetailIds) {
private List<Object> findIdsByParentIdList(List<Object> parentIdList, Transaction t, List<Object> excludeDetailIds) {
String rawWhere = deriveWhereParentIdSql(true, "");
String inClause = buildInClauseBinding(parentIdList.size(), exportedPropertyBindProto);
@@ -402,19 +402,19 @@ public class BeanPropertyAssocMany<T> extends BeanPropertyAssoc<T> {
return server.findIds(q, t);
}
private SqlUpdate deleteByParentIdList(List<Object> parentIdist) {
private SqlUpdate deleteByParentIdList(List<Object> parentIdList) {
StringBuilder sb = new StringBuilder(100);
sb.append(deleteByParentIdInSql);
String inClause = buildInClauseBinding(parentIdist.size(), exportedPropertyBindProto);
String inClause = buildInClauseBinding(parentIdList.size(), exportedPropertyBindProto);
sb.append(inClause);
DefaultSqlUpdate delete = new DefaultSqlUpdate(sb.toString());
if (exportedProperties.length == 1) {
bindWhereParendId(delete, new MultiValueWrapper(parentIdist));
bindWhereParendId(delete, new MultiValueWrapper(parentIdList));
} else {
for (Object aParentIdist : parentIdist) {
for (Object aParentIdist : parentIdList) {
bindWhereParendId(delete, aParentIdist);
}
}
@@ -900,7 +900,7 @@ public class BeanPropertyAssocMany<T> extends BeanPropertyAssoc<T> {
throw new PersistenceException(from + ": Could not find mapKey property [" + mapKey + "] on [" + to + "]");
}
public IntersectionRow buildManyDeleteChildren(EntityBean parentBean, ArrayList<Object> excludeDetailIds) {
public IntersectionRow buildManyDeleteChildren(EntityBean parentBean, List<Object> excludeDetailIds) {
IntersectionRow row = new IntersectionRow(tableJoin.getTable(), targetDescriptor);
if (excludeDetailIds != null && !excludeDetailIds.isEmpty()) {
@@ -7,7 +7,6 @@ import io.ebeaninternal.server.core.DefaultSqlUpdate;
import io.ebeaninternal.server.expression.DefaultExpressionRequest;
import io.ebeaninternal.server.expression.IdInExpression;
import java.util.ArrayList;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
@@ -20,7 +19,7 @@ public class IntersectionRow {
private final LinkedHashMap<String, Object> values = new LinkedHashMap<>();
private ArrayList<Object> excludeIds;
private List<Object> excludeIds;
private BeanDescriptor<?> excludeDescriptor;
public IntersectionRow(String tableName, BeanDescriptor<?> targetDescriptor) {
@@ -36,7 +35,7 @@ public class IntersectionRow {
/**
* Set Id's to exclude. This is for deleting non-attached detail Id's.
*/
public void setExcludeIds(ArrayList<Object> excludeIds, BeanDescriptor<?> excludeDescriptor) {
public void setExcludeIds(List<Object> excludeIds, BeanDescriptor<?> excludeDescriptor) {
this.excludeIds = excludeIds;
this.excludeDescriptor = excludeDescriptor;
}
@@ -40,6 +40,8 @@ public abstract class DeployBeanPropertyAssoc<T> extends DeployBeanProperty {
private String docStoreDoc;
private int fetchPreference = 1000;
/**
* Construct the property.
*/
@@ -147,4 +149,11 @@ public abstract class DeployBeanPropertyAssoc<T> extends DeployBeanProperty {
return docStoreDoc;
}
public int getFetchPreference() {
return fetchPreference;
}
public void setFetchPreference(int fetchPreference) {
this.fetchPreference = fetchPreference;
}
}
@@ -1,5 +1,6 @@
package io.ebeaninternal.server.deploy.parse;
import io.ebean.annotation.FetchPreference;
import io.ebean.annotation.HistoryExclude;
import io.ebean.annotation.PrivateOwned;
import io.ebean.annotation.Where;
@@ -90,6 +91,11 @@ class AnnotationAssocManys extends AnnotationParser {
prop.setExtraWhere(where.clause());
}
FetchPreference fetchPreference = get(prop, FetchPreference.class);
if (fetchPreference != null) {
prop.setFetchPreference(fetchPreference.value());
}
// check for manually defined joins
BeanTable beanTable = prop.getBeanTable();
@@ -1,5 +1,6 @@
package io.ebeaninternal.server.deploy.parse;
import io.ebean.annotation.FetchPreference;
import io.ebean.annotation.Where;
import io.ebean.config.NamingConvention;
import io.ebeaninternal.server.deploy.BeanDescriptorManager;
@@ -87,6 +88,11 @@ public class AnnotationAssocOnes extends AnnotationParser {
prop.setExtraWhere(where.clause());
}
FetchPreference fetchPreference = get(prop, FetchPreference.class);
if (fetchPreference != null) {
prop.setFetchPreference(fetchPreference.value());
}
if (validationAnnotations) {
NotNull notNull = get(prop, NotNull.class);
if (notNull != null && isEbeanValidationGroups(notNull.groups())) {
@@ -84,6 +84,11 @@ public class ElPropertyChain implements ElPropertyValue {
return "expr:" + expression + " chain:" + Arrays.toString(chain);
}
@Override
public int getFetchPreference() {
return chain[0].getFetchPreference();
}
@Override
public boolean isAggregation() {
return false;
@@ -74,4 +74,10 @@ public interface ElPropertyDeploy {
* Return true if this is an aggregation property.
*/
boolean isAggregation();
/**
* Return the fetch preference. This can be used to control which ToMany relationship
* is left as a 'join' and which get converted to query join.
*/
int getFetchPreference();
}
@@ -5,8 +5,10 @@ import io.ebeaninternal.api.SpiExpression;
import io.ebeaninternal.api.SpiExpressionRequest;
import io.ebeaninternal.api.SpiExpressionValidation;
import io.ebeaninternal.server.deploy.BeanDescriptor;
import io.ebeaninternal.server.persist.MultiValueWrapper;
import java.io.IOException;
import java.util.Collection;
class RawExpression extends NonPrepareExpression {
@@ -43,7 +45,12 @@ class RawExpression extends NonPrepareExpression {
public void addBindValues(SpiExpressionRequest request) {
if (values != null) {
for (Object value : values) {
request.addBindValue(value);
if (value instanceof Collection<?>) {
// support for Postgres = any(?) type raw expression
request.addBindValue(new MultiValueWrapper((Collection<?>)value));
} else {
request.addBindValue(value);
}
}
}
}
@@ -949,20 +949,15 @@ public final class DefaultPersister implements Persister {
Set<?> modifyRemovals = c.getModifyRemovals();
saveMany.modifyListenReset(c);
if (modifyRemovals != null && !modifyRemovals.isEmpty()) {
SpiTransaction t = saveMany.getTransaction();
// increase depth for batching order
t.depth(+1);
for (Object removedBean : modifyRemovals) {
if (removedBean instanceof EntityBean) {
EntityBean eb = (EntityBean) removedBean;
if (eb._ebean_getIntercept().isLoaded()) {
// only delete if the bean was loaded meaning that it is known to exist in the DB
deleteRequest(createPublishRequest(removedBean, t, PersistRequest.Type.DELETE, saveMany.isPublish()));
deleteRequest(createPublishRequest(removedBean, saveMany.getTransaction(), PersistRequest.Type.DELETE, saveMany.isPublish()));
}
}
}
t.depth(-1);
}
}
}
@@ -992,21 +987,23 @@ public final class DefaultPersister implements Persister {
targetDescriptor.preAllocateIds(collection.size());
}
ArrayList<Object> detailIds = null;
SpiTransaction t = saveMany.getTransaction();
boolean isMap = ManyType.MAP.equals(prop.getManyType());
EntityBean parentBean = saveMany.getParentBean();
if (deleteMissingChildren) {
// collect the Id's (to exclude from deleteManyDetails)
detailIds = new ArrayList<>();
List<Object> detailIds = collectIds(collection, targetDescriptor, isMap);
// deleting missing children - children not in our collected detailIds
deleteManyDetails(t, prop.getBeanDescriptor(), parentBean, prop, detailIds, false);
}
// increase depth for batching order
SpiTransaction t = saveMany.getTransaction();
t.depth(+1);
// if a map, then we get the key value and
// set it to the appropriate property on the
// detail bean before we save it
boolean isMap = ManyType.MAP.equals(prop.getManyType());
EntityBean parentBean = saveMany.getParentBean();
Object mapKeyValue = null;
boolean saveSkippable = prop.isSaveRecurseSkippable();
@@ -1047,28 +1044,30 @@ public final class DefaultPersister implements Persister {
}
}
if (detailIds != null) {
// stateless update with deleteMissingChildren so first
// flushBatch to ensure that we have Id values from any inserts
t.flushBatch();
// now collect the Id values to determine the 'missing children'
for (Object detailBean : collection) {
if (isMap) {
detailBean = ((Map.Entry<?, ?>) detailBean).getValue();
}
if (detailBean instanceof EntityBean) {
Object id = targetDescriptor.getId((EntityBean) detailBean);
if (!DmlUtil.isNullOrZero(id)) {
// remember the Id (other details not in the collection) will be removed
detailIds.add(id);
}
t.depth(-1);
}
/**
* Collect the Id values of the details to remove 'missing children' for stateless updates.
*/
private List<Object> collectIds(Collection<?> collection, BeanDescriptor<?> targetDescriptor, boolean isMap) {
List<Object> detailIds = new ArrayList<>();
// stateless update with deleteMissingChildren so first
// collect the Id values to remove the 'missing children'
for (Object detailBean : collection) {
if (isMap) {
detailBean = ((Map.Entry<?, ?>) detailBean).getValue();
}
if (detailBean instanceof EntityBean) {
Object id = targetDescriptor.getId((EntityBean) detailBean);
if (!DmlUtil.isNullOrZero(id)) {
// remember the Id (other details not in the collection) will be removed
detailIds.add(id);
}
}
// deleting missing children - children not in our collected detailIds
deleteManyDetails(t, prop.getBeanDescriptor(), parentBean, prop, detailIds, false);
}
t.depth(-1);
return detailIds;
}
/**
@@ -1268,7 +1267,7 @@ public final class DefaultPersister implements Persister {
* </p>
*/
private void deleteManyDetails(SpiTransaction t, BeanDescriptor<?> desc, EntityBean parentBean,
BeanPropertyAssocMany<?> many, ArrayList<Object> excludeDetailIds, boolean softDelete) {
BeanPropertyAssocMany<?> many, List<Object> excludeDetailIds, boolean softDelete) {
if (many.getCascadeInfo().isDelete()) {
// cascade delete the beans in the collection
@@ -252,7 +252,7 @@ class CQueryBuilder {
// skip building the SqlTree and Sql string
predicates.prepare(false);
String sql = queryPlan.getSql();
return new CQueryRowCount(request, predicates, sql);
return new CQueryRowCount(queryPlan, request, predicates, sql);
}
predicates.prepare(true);
@@ -283,7 +283,7 @@ class CQueryBuilder {
queryPlan = new CQueryPlan(request, sql, sqlTree, false, s.isIncludesRowNumberColumn(), predicates.getLogWhereSql());
request.putQueryPlan(queryPlan);
return new CQueryRowCount(request, predicates, sql);
return new CQueryRowCount(queryPlan, request, predicates, sql);
}
/**
@@ -16,6 +16,8 @@ import java.sql.SQLException;
*/
class CQueryRowCount {
private final CQueryPlan queryPlan;
/**
* The overall find request wrapper object.
*/
@@ -54,7 +56,8 @@ class CQueryRowCount {
/**
* Create the Sql select based on the request.
*/
CQueryRowCount(OrmQueryRequest<?> request, CQueryPredicates predicates, String sql) {
CQueryRowCount(CQueryPlan queryPlan, OrmQueryRequest<?> request, CQueryPredicates predicates, String sql) {
this.queryPlan = queryPlan;
this.request = request;
this.query = request.getQuery();
this.sql = sql;
@@ -116,6 +119,7 @@ class CQueryRowCount {
rowCount = rset.getInt(1);
executionTimeMicros = (System.nanoTime() - startNano) / 1000L;
queryPlan.executionTime(rowCount, executionTimeMicros, query.getParentNode());
request.slowQueryCheck(executionTimeMicros, rowCount);
return rowCount;
@@ -318,6 +318,10 @@ public class OrmQueryDetail implements Serializable {
*/
void markQueryJoins(BeanDescriptor<?> beanDescriptor, String lazyLoadManyPath, boolean allowOne, boolean addIds) {
if (fetchPaths.isEmpty()) {
return;
}
// the name of the many fetch property if there is one
String manyFetchProperty = null;
@@ -325,22 +329,19 @@ public class OrmQueryDetail implements Serializable {
boolean fetchJoinFirstMany = allowOne;
sortFetchPaths(beanDescriptor, addIds);
List<FetchEntry> pairs = sortByFetchPreference(beanDescriptor);
for (String fetchPath : fetchPaths.keySet()) {
ElPropertyDeploy elProp = beanDescriptor.getElPropertyDeploy(fetchPath);
if (elProp == null) {
throw new PersistenceException("Invalid fetch path " + fetchPath + " from " + beanDescriptor.getFullName());
}
for (FetchEntry pair : pairs) {
ElPropertyDeploy elProp = pair.getElProp();
if (elProp.containsManySince(manyFetchProperty)) {
// this is a join to a *ToMany
OrmQueryProperties chunk = fetchPaths.get(fetchPath);
OrmQueryProperties chunk = pair.getProperties();
if (isQueryJoinCandidate(lazyLoadManyPath, chunk)) {
// this is a 'fetch join' (included in main query)
if (fetchJoinFirstMany) {
// letting the first one remain a 'fetch join'
fetchJoinFirstMany = false;
manyFetchProperty = fetchPath;
manyFetchProperty = pair.getPath();
} else {
// convert this one over to a 'query join'
chunk.markForQueryJoin();
@@ -350,6 +351,25 @@ public class OrmQueryDetail implements Serializable {
}
}
/**
* Sort the fetch entries taking into account fetchPreference on the path.
*/
private List<FetchEntry> sortByFetchPreference(BeanDescriptor<?> desc) {
List<FetchEntry> entries = new ArrayList<>(fetchPaths.size());
int idx = 0;
for (Map.Entry<String, OrmQueryProperties> entry : fetchPaths.entrySet()) {
String fetchPath = entry.getKey();
ElPropertyDeploy elProp = desc.getElPropertyDeploy(fetchPath);
if (elProp == null) {
throw new PersistenceException("Invalid fetch path " + fetchPath + " from " + desc.getFullName());
}
entries.add(new FetchEntry(idx++, fetchPath, elProp, entry.getValue()));
}
Collections.sort(entries);
return entries;
}
/**
* Return true if this path is a candidate for converting to a query join.
*/
@@ -483,4 +503,45 @@ public class OrmQueryDetail implements Serializable {
public Set<Map.Entry<String, OrmQueryProperties>> entries() {
return fetchPaths.entrySet();
}
private static class FetchEntry implements Comparable<FetchEntry> {
private final int index;
private final String path;
private final OrmQueryProperties properties;
private final ElPropertyDeploy elProp;
FetchEntry(int index, String path, ElPropertyDeploy elProp, OrmQueryProperties value) {
this.index = index;
this.path = path;
this.elProp = elProp;
this.properties = value;
}
String getPath() {
return path;
}
OrmQueryProperties getProperties() {
return properties;
}
ElPropertyDeploy getElProp() {
return elProp;
}
/**
* Sort by fetchPreference and then by index order.
*/
@Override
public int compareTo(FetchEntry other) {
int fp = elProp.getFetchPreference();
int op = other.elProp.getFetchPreference();
if (fp == op) {
return Integer.compare(index, other.index);
} else {
return (fp < op) ? -1 : 1;
}
}
}
}
@@ -39,8 +39,7 @@ public class TransactionMap {
*/
public State getStateWithCreate(String serverName) {
State state = map.computeIfAbsent(serverName, k -> new State());
return state;
return map.computeIfAbsent(serverName, k -> new State());
}
/**
@@ -65,6 +65,9 @@ public class ScalarTypeEncryptedWrapper<T> implements ScalarType<T> {
}
private byte[] encrypt(T value) {
if (value == null) {
return null;
}
String formatValue = wrapped.formatValue(value);
return dataEncryptSupport.encryptObject(formatValue);
}
@@ -4,13 +4,11 @@ import io.ebean.BaseTestCase;
import io.ebean.Ebean;
import io.ebean.SqlQuery;
import io.ebean.SqlRow;
import io.ebean.Update;
import io.ebean.annotation.ForPlatform;
import io.ebean.annotation.Platform;
import io.ebean.config.dbplatform.DbEncrypt;
import io.ebeaninternal.api.SpiEbeanServer;
import org.ebeantest.LoggedSqlCollector;
import org.junit.Assert;
import org.junit.Test;
import org.tests.model.basic.EBasicEncrypt;
@@ -18,6 +16,7 @@ import java.sql.Date;
import java.util.List;
import static org.assertj.core.api.Assertions.assertThat;
import static org.junit.Assert.assertEquals;
public class TestEncrypt extends BaseTestCase {
@@ -40,8 +39,7 @@ public class TestEncrypt extends BaseTestCase {
@ForPlatform(Platform.H2)
public void test() {
Update<EBasicEncrypt> deleteAll = Ebean.createUpdate(EBasicEncrypt.class, "delete from EBasicEncrypt");
deleteAll.execute();
Ebean.find(EBasicEncrypt.class).delete();
EBasicEncrypt e = new EBasicEncrypt();
e.setName("testname");
@@ -63,12 +61,14 @@ public class TestEncrypt extends BaseTestCase {
e1.setName("testmod");
e1.setDescription("moddesc");
e1.setStatus(EBasicEncrypt.Status.ONE);
Ebean.save(e1);
EBasicEncrypt e2 = Ebean.find(EBasicEncrypt.class, e.getId());
e2.getDescription();
assertEquals("moddesc", e2.getDescription());
assertEquals(EBasicEncrypt.Status.ONE, e2.getStatus());
SpiEbeanServer server = (SpiEbeanServer) Ebean.getServer(null);
DbEncrypt dbEncrypt = server.getDatabasePlatform().getDbEncrypt();
@@ -82,11 +82,11 @@ public class TestEncrypt extends BaseTestCase {
List<EBasicEncrypt> list = Ebean.find(EBasicEncrypt.class).where()
.eq("description", "moddesc").findList();
Assert.assertEquals(1, list.size());
assertEquals(1, list.size());
list = Ebean.find(EBasicEncrypt.class).where().startsWith("description", "modde").findList();
Assert.assertEquals(1, list.size());
assertEquals(1, list.size());
}
}
@@ -0,0 +1,30 @@
package org.tests.cascade;
import io.ebean.BaseTestCase;
import io.ebean.Ebean;
import org.junit.Test;
import org.tests.model.basic.TSDetail;
import org.tests.model.basic.TSMaster;
public class TestPrivateOwnedCascadeOrder extends BaseTestCase {
@Test
public void test() {
// setup
TSMaster master = new TSMaster();
master.getDetails().add(new TSDetail("c97", "ONE"));
master.getDetails().add(new TSDetail("c96", "TWO"));
Ebean.save(master);
// act
TSMaster master1 = Ebean.find(master.getClass(), master.getId());
// Check Ebean deletes the existing c97 first as the unique values clash
master1.getDetails().clear();
master1.getDetails().add(new TSDetail("c98", "TWO"));
Ebean.save(master1);
}
}
@@ -3,6 +3,8 @@ package org.tests.model.basic;
import io.ebean.annotation.Encrypted;
import javax.persistence.Entity;
import javax.persistence.EnumType;
import javax.persistence.Enumerated;
import javax.persistence.Id;
import javax.persistence.Table;
import java.sql.Date;
@@ -12,6 +14,11 @@ import java.sql.Timestamp;
@Table(name = "e_basicenc")
public class EBasicEncrypt {
public enum Status {
ONE,
TWO
}
@Id
Integer id;
@@ -24,6 +31,10 @@ public class EBasicEncrypt {
@Encrypted(dbLength = 20)
Date dob;
@Enumerated(EnumType.ORDINAL)
@Encrypted(dbLength = 20)
Status status;
//@Version
Timestamp lastUpdate;
@@ -51,6 +62,14 @@ public class EBasicEncrypt {
this.dob = dob;
}
public Status getStatus() {
return status;
}
public void setStatus(Status status) {
this.status = status;
}
public String getDescription() {
return description;
}
@@ -1,5 +1,7 @@
package org.tests.model.basic;
import io.ebean.annotation.Index;
import javax.persistence.Entity;
import javax.persistence.GeneratedValue;
import javax.persistence.GenerationType;
@@ -23,6 +25,9 @@ public class TSDetail {
String description;
@Index(unique = true)
String someUniqueValue;
boolean active;
@ManyToOne
@@ -32,6 +37,11 @@ public class TSDetail {
this.name = name;
}
public TSDetail(String name, String someUniqueValue) {
this.name = name;
this.someUniqueValue = someUniqueValue;
}
public TSDetail() {
}
@@ -60,6 +70,14 @@ public class TSDetail {
this.description = description;
}
public String getSomeUniqueValue() {
return someUniqueValue;
}
public void setSomeUniqueValue(String someUniqueValue) {
this.someUniqueValue = someUniqueValue;
}
public boolean isActive() {
return active;
}
@@ -1,5 +1,6 @@
package org.tests.model.converstation;
import io.ebean.annotation.FetchPreference;
import org.tests.model.BaseModel;
import javax.persistence.Column;
@@ -21,9 +22,11 @@ public class Conversation extends BaseModel {
@ManyToOne
Group group;
@FetchPreference(1)
@OneToMany(mappedBy = "conversation")
List<Participation> participants;
@FetchPreference(2)
@OneToMany(mappedBy = "conversation")
List<Message> messages;
@@ -4,12 +4,17 @@ import io.ebean.BaseTestCase;
import io.ebean.Ebean;
import io.ebean.Expr;
import io.ebean.Query;
import io.ebean.annotation.ForPlatform;
import io.ebean.annotation.Platform;
import org.tests.model.basic.Customer;
import org.tests.model.basic.Order;
import org.tests.model.basic.OrderDetail;
import org.tests.model.basic.ResetBasicData;
import org.junit.Test;
import java.sql.Timestamp;
import java.util.ArrayList;
import java.util.List;
import static org.assertj.core.api.Assertions.assertThat;
@@ -45,6 +50,25 @@ public class TestWhereRawClause extends BaseTestCase {
}
@Test
@ForPlatform(Platform.POSTGRES)
public void testRawPostgresArray() {
ResetBasicData.reset();
List<String> names = new ArrayList<>();
names.add("Rob");
names.add("Fiona");
List<Customer> list = Ebean.find(Customer.class)
.where()
.raw("name = any(?)", names)
.findList();
assertThat(list).isNotEmpty();
}
@Test
public void testRawWithBindParams() {
@@ -0,0 +1,69 @@
package org.tests.query.other;
import io.ebean.BaseTestCase;
import io.ebean.Ebean;
import io.ebean.Query;
import org.junit.Test;
import org.tests.model.converstation.Conversation;
import static org.assertj.core.api.StrictAssertions.assertThat;
public class TestFetchPreference extends BaseTestCase {
@Test
public void fetchPreference_overrideOrder() {
Query<Conversation> query = Ebean.find(Conversation.class)
// FetchPreference overrides so participants is joined and messages query joined
.fetch("messages")
.fetch("participants");
query.findList();
String sql = sqlOf(query, 1);
assertThat(sql).contains(" from c_conversation t0 left join c_participation t1 ");
}
@Test
public void fetchPreference_sr() {
Query<Conversation> query = Ebean.find(Conversation.class)
// without FetchPreference this ToMany would be our first ToMany join
// and participants would be query joined
.fetch("group.users")
.fetch("messages")
.fetch("participants");
query.findList();
String sql = sqlOf(query, 1);
// join to group (the ToOne part only) and participants (our preferred ToMany path)
assertThat(sql).contains(" from c_conversation t0 left join c_group t1 on t1.id = t0.group_id left join c_participation t2");
}
@Test
public void fetchPreference_inOrder() {
Query<Conversation> query = Ebean.find(Conversation.class)
.fetch("participants")
.fetch("messages");
query.findList();
String sql = sqlOf(query, 1);
assertThat(sql).contains(" from c_conversation t0 left join c_participation t1 ");
}
@Test
public void fetchQuery_onlyOneJoinCandidate() {
Query<Conversation> query = Ebean.find(Conversation.class)
.fetch("messages")
.fetchQuery("participants");
query.findList();
String sql = sqlOf(query, 1);
// participants is explicitly a "query join" so we can join to message
assertThat(sql).contains(" from c_conversation t0 left join c_message t1 ");
}
}
@@ -0,0 +1,47 @@
package org.tests.transaction;
import io.ebean.BaseTestCase;
import io.ebean.Ebean;
import io.ebean.Transaction;
import io.ebean.annotation.Transactional;
import io.ebean.annotation.TxType;
import org.junit.Test;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import javax.persistence.PersistenceException;
public class TestTransactionalNever extends BaseTestCase {
private static final Logger log = LoggerFactory.getLogger(TestTransactionalNever.class);
private Transaction outerTxn;
@Test
public void testWithNever() {
new SomeTransactionalWithNever().doStuff();
}
@Test(expected = PersistenceException.class)
public void testBarfOnExisting() {
doWithTransaction();
}
class SomeTransactionalWithNever {
@Transactional(type = TxType.NEVER)
void doStuff() {
outerTxn = Ebean.currentTransaction();
log.info("currentTransaction ...{}", outerTxn);
}
}
@Transactional
private void doWithTransaction() {
// always barf
new SomeTransactionalWithNever().doStuff();
}
}
@@ -0,0 +1,64 @@
package org.tests.transaction;
import io.ebean.BaseTestCase;
import io.ebean.Ebean;
import io.ebean.Transaction;
import io.ebean.annotation.Transactional;
import io.ebean.annotation.TxType;
import org.junit.Test;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import static org.junit.Assert.assertNotNull;
import static org.junit.Assert.assertNull;
import static org.junit.Assert.assertSame;
public class TestTransactionalNotSupports extends BaseTestCase {
private static final Logger log = LoggerFactory.getLogger(TestTransactionalNotSupports.class);
private Transaction outerTxn;
private Transaction currentTxn;
@Test
public void noCurrentTransaction() {
outerTxn = null;
new SomeTransactionalWithNotSupported().doStuff();
assertNull(outerTxn);
}
@Test
public void withOuterTransaction_expect_currentTransaction_null_and_originalTxnRestored() {
outerTxn = null;
Ebean.beginTransaction();
try {
currentTxn = Ebean.currentTransaction();
assertNotNull(currentTxn);
new SomeTransactionalWithNotSupported().doStuff();
// there was no current transaction inside the NOT_SUPPORTED
assertNull(outerTxn);
// the original transaction was restored
Transaction restored = Ebean.currentTransaction();
assertSame(currentTxn, restored);
} finally {
Ebean.endTransaction();
}
}
class SomeTransactionalWithNotSupported {
@Transactional(type = TxType.NOT_SUPPORTED)
void doStuff() {
outerTxn = Ebean.currentTransaction();
log.info("outer ...{}", outerTxn);
}
}
}
@@ -0,0 +1,88 @@
package org.tests.transaction;
import io.ebean.BaseTestCase;
import io.ebean.Ebean;
import io.ebean.Transaction;
import io.ebean.annotation.Transactional;
import io.ebean.annotation.TxType;
import org.junit.Test;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import static org.junit.Assert.assertNotNull;
import static org.junit.Assert.assertNull;
import static org.junit.Assert.assertSame;
public class TestTransactionalRequired extends BaseTestCase {
private static final Logger log = LoggerFactory.getLogger(TestTransactionalRequired.class);
private Transaction outerTxn;
private Transaction innerTxn;
private Transaction currentTxn;
@Test
public void noCurrentTransaction() {
outerTxn = null;
assertNull(Ebean.currentTransaction());
new OuterTransactionalWithRequired().doOuter();
assertNull(Ebean.currentTransaction());
assertNotNull(outerTxn);
assertNotNull(innerTxn);
assertSame(outerTxn, innerTxn);
}
@Test
public void withOuterBegin() {
outerTxn = null;
Ebean.beginTransaction();
try {
currentTxn = Ebean.currentTransaction();
assertNotNull(currentTxn);
new OuterTransactionalWithRequired().doOuter();
// the original transaction was restored
Transaction restored = Ebean.currentTransaction();
assertSame(currentTxn, restored);
assertSame(currentTxn, innerTxn);
assertSame(currentTxn, outerTxn);
} finally {
Ebean.endTransaction();
}
assertNull(Ebean.currentTransaction());
}
class OuterTransactionalWithRequired {
@Transactional(type = TxType.REQUIRED)
void doOuter() {
outerTxn = Ebean.currentTransaction();
log.info("outer ...{}", outerTxn);
new InTransactionalWithRequired().doInner();
Transaction current = Ebean.currentTransaction();
assertSame(outerTxn, current);
}
}
class InTransactionalWithRequired {
@Transactional(type = TxType.REQUIRED)
void doInner() {
innerTxn = Ebean.currentTransaction();
log.info("inner ...{}", innerTxn);
}
}
}
@@ -0,0 +1,70 @@
package org.tests.transaction;
import io.ebean.BaseTestCase;
import io.ebean.Ebean;
import io.ebean.Transaction;
import io.ebean.annotation.Transactional;
import io.ebean.annotation.TxType;
import org.junit.Test;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.sql.Connection;
import static org.junit.Assert.assertNotNull;
import static org.junit.Assert.assertNotSame;
import static org.junit.Assert.assertNull;
import static org.junit.Assert.assertSame;
public class TestTransactionalRequiresNew extends BaseTestCase {
private static final Logger log = LoggerFactory.getLogger(TestTransactionalRequiresNew.class);
private Connection outerConn;
private Transaction outerTxn;
@Test
public void basic() {
outerTxn = null;
assertNull(Ebean.currentTransaction());
new OuterTransactionalWithRequired().doOuter();
assertNull(Ebean.currentTransaction());
assertNotNull(outerTxn);
}
class OuterTransactionalWithRequired {
@Transactional
void doOuter() {
outerTxn = Ebean.currentTransaction();
log.info("outer before ...{}", outerTxn);
outerConn = outerTxn.getConnection();
new InTransactionalWithRequiresNew().doInner();
// restore the outerTxn
Transaction current = Ebean.currentTransaction();
log.info("outer after ...{}", current);
assertSame(outerConn, current.getConnection());
}
}
class InTransactionalWithRequiresNew {
@Transactional(type = TxType.REQUIRES_NEW)
void doInner() {
Transaction innerTxn = Ebean.currentTransaction();
log.info("inner ...{} {}", innerTxn);
Connection connection = innerTxn.getConnection();
assertNotSame(connection, outerConn);
}
}
}
@@ -0,0 +1,53 @@
package org.tests.transaction;
import io.ebean.BaseTestCase;
import io.ebean.Ebean;
import io.ebean.Transaction;
import io.ebean.annotation.Transactional;
import io.ebean.annotation.TxType;
import org.junit.Test;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import static org.junit.Assert.assertNotNull;
import static org.junit.Assert.assertNull;
public class TestTransactionalSupports extends BaseTestCase {
private static final Logger log = LoggerFactory.getLogger(TestTransactionalSupports.class);
private Transaction outerTxn;
@Test
public void noCurrentTransaction() {
outerTxn = null;
new SomeTransactionalWithSupports().doStuff();
assertNull(outerTxn);
}
@Test
public void withOuterTransaction_expect_currentTransactionAvailable() {
outerTxn = null;
Ebean.beginTransaction();
try {
new SomeTransactionalWithSupports().doStuff();
assertNotNull(outerTxn);
} finally {
Ebean.endTransaction();
}
}
class SomeTransactionalWithSupports {
@Transactional(type = TxType.SUPPORTS)
void doStuff() {
outerTxn = Ebean.currentTransaction();
log.info("outer ...{}", outerTxn);
}
}
}
+1 -1
View File
@@ -88,7 +88,7 @@
<logger name="io.ebean.cache.NATKEY" level="TRACE"/>
<logger name="io.ebean.cache.COLL" level="TRACE"/>
<!--<logger name="org.avaje.classpath" level="INFO"/>-->
<!--<logger name="io.ebeaninternal.server.deploy" level="TRACE"/>-->
<!--<logger name="io.ebeaninternal.server.autotune" level="TRACE"/>-->