package io.ebeaninternal.server.core;
import io.ebean.CacheMode;
import io.ebean.OrderBy;
import io.ebean.PersistenceContextScope;
import io.ebean.QueryIterator;
import io.ebean.Version;
import io.ebean.bean.BeanCollection;
import io.ebean.bean.EntityBean;
import io.ebean.bean.PersistenceContext;
import io.ebean.cache.QueryCacheEntry;
import io.ebean.common.BeanList;
import io.ebean.common.BeanMap;
import io.ebean.common.CopyOnFirstWriteList;
import io.ebean.event.BeanFindController;
import io.ebean.event.BeanQueryAdapter;
import io.ebean.text.json.JsonReadOptions;
import io.ebeaninternal.api.BeanCacheResult;
import io.ebeaninternal.api.CQueryPlanKey;
import io.ebeaninternal.api.CacheIdLookup;
import io.ebeaninternal.api.HashQuery;
import io.ebeaninternal.api.LoadContext;
import io.ebeaninternal.api.NaturalKeyQueryData;
import io.ebeaninternal.api.NaturalKeySet;
import io.ebeaninternal.api.SpiEbeanServer;
import io.ebeaninternal.api.SpiQuery;
import io.ebeaninternal.api.SpiQuery.Type;
import io.ebeaninternal.api.SpiQuerySecondary;
import io.ebeaninternal.api.SpiTransaction;
import io.ebeaninternal.server.deploy.BeanDescriptor;
import io.ebeaninternal.server.deploy.BeanProperty;
import io.ebeaninternal.server.deploy.BeanPropertyAssocMany;
import io.ebeaninternal.server.deploy.DeployParser;
import io.ebeaninternal.server.deploy.DeployPropertyParserMap;
import io.ebeaninternal.server.el.ElPropertyValue;
import io.ebeaninternal.server.loadcontext.DLoadContext;
import io.ebeaninternal.server.query.CQueryPlan;
import io.ebeaninternal.server.query.CancelableQuery;
import io.ebeaninternal.server.transaction.DefaultPersistenceContext;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import javax.persistence.PersistenceException;
import java.sql.SQLException;
import java.util.ArrayList;
import java.util.Collection;
import java.util.LinkedHashMap;
import java.util.LinkedHashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.function.Consumer;
import java.util.function.Predicate;
Wraps the objects involved in executing a Query.
/**
* Wraps the objects involved in executing a Query.
*/
public final class OrmQueryRequest<T> extends BeanRequest implements SpiOrmQueryRequest<T> {
private static final Logger log = LoggerFactory.getLogger(OrmQueryRequest.class);
private final BeanDescriptor<T> beanDescriptor;
private final OrmQueryEngine queryEngine;
private final SpiQuery<T> query;
private final BeanFindController finder;
private final Boolean readOnly;
private LoadContext loadContext;
private PersistenceContext persistenceContext;
private JsonReadOptions jsonRead;
private HashQuery cacheKey;
private CQueryPlanKey queryPlanKey;
private SpiQuerySecondary secondaryQueries;
private List<T> cacheBeans;
private BeanPropertyAssocMany<?> manyProperty;
private boolean inlineCountDistinct;
private Set<String> dependentTables;
Create the InternalQueryRequest.
/**
* Create the InternalQueryRequest.
*/
public OrmQueryRequest(SpiEbeanServer server, OrmQueryEngine queryEngine, SpiQuery<T> query, SpiTransaction t) {
super(server, t);
this.beanDescriptor = query.getBeanDescriptor();
this.finder = beanDescriptor.getBeanFinder();
this.queryEngine = queryEngine;
this.query = query;
this.readOnly = query.isReadOnly();
this.persistenceContext = query.getPersistenceContext();
}
public PersistenceException translate(String bindLog, String sql, SQLException e) {
return queryEngine.translate(this, bindLog, sql, e);
}
@Override
public boolean isDeleteByStatement() {
if (!transaction.isPersistCascade() || beanDescriptor.isDeleteByStatement()) {
// plain delete by query
return true;
} else {
// delete by ids due to cascading delete needs
queryPlanKey = query.setDeleteByIdsPlan();
return false;
}
}
@Override
public boolean isPadInExpression() {
return beanDescriptor.isPadInExpression();
}
@Override
public boolean isMultiValueIdSupported() {
return beanDescriptor.isMultiValueIdSupported();
}
@Override
public boolean isMultiValueSupported(Class<?> valueType) {
return queryEngine.isMultiValueSupported(valueType);
}
Mark the transaction as not being query only.
/**
* Mark the transaction as not being query only.
*/
@Override
public void markNotQueryOnly() {
transaction.markNotQueryOnly();
}
Return the database platform like clause.
/**
* Return the database platform like clause.
*/
@Override
public String getDBLikeClause(boolean rawLikeExpression) {
return ebeanServer.getDatabasePlatform().getLikeClause(rawLikeExpression);
}
Return the database platform escaped like string.
/**
* Return the database platform escaped like string.
*/
@Override
public String escapeLikeString(String value) {
return ebeanServer.getDatabasePlatform().escapeLikeString(value);
}
@Override
public void executeSecondaryQueries(boolean forEach) {
// disable lazy loading leaves loadContext null
if (loadContext != null) {
loadContext.executeSecondaryQueries(this, forEach);
}
}
For use with QueryIterator and secondary queries this returns the minimum
batch size that should be loaded before executing the secondary queries.
If -1 is returned then NO secondary queries are registered and simple
iteration is fine.
/**
* For use with QueryIterator and secondary queries this returns the minimum
* batch size that should be loaded before executing the secondary queries.
* <p>
* If -1 is returned then NO secondary queries are registered and simple
* iteration is fine.
* </p>
*/
public int getSecondaryQueriesMinBatchSize(int defaultQueryBatch) {
return loadContext.getSecondaryQueriesMinBatchSize(defaultQueryBatch);
}
Return the Normal, sharedInstance, ReadOnly state of this query.
/**
* Return the Normal, sharedInstance, ReadOnly state of this query.
*/
public Boolean isReadOnly() {
return readOnly;
}
Return the BeanDescriptor for the associated bean.
/**
* Return the BeanDescriptor for the associated bean.
*/
@Override
public BeanDescriptor<T> getBeanDescriptor() {
return beanDescriptor;
}
Return the graph context for this query.
/**
* Return the graph context for this query.
*/
public LoadContext getGraphContext() {
return loadContext;
}
@Override
public boolean isUseDocStore() {
return query.isUseDocStore();
}
Run BeanQueryAdapter preQuery() if needed.
/**
* Run BeanQueryAdapter preQuery() if needed.
*/
private void adapterPreQuery() {
BeanQueryAdapter queryAdapter = beanDescriptor.getQueryAdapter();
if (queryAdapter != null) {
queryAdapter.preQuery(this);
}
}
Prepare the query and calculate the query plan key.
/**
* Prepare the query and calculate the query plan key.
*/
@Override
public void prepareQuery() {
beanDescriptor.prepareQuery(query);
adapterPreQuery();
this.secondaryQueries = query.convertJoins();
this.queryPlanKey = query.prepare(this);
}
public boolean isNativeSql() {
return query.isNativeSql();
}
public boolean isRawSql() {
return query.isRawSql();
}
public DeployParser createDeployParser() {
if (query.isRawSql()) {
return new DeployPropertyParserMap(query.getRawSql().getColumnMapping().getMapping());
} else {
return beanDescriptor.parser();
}
}
Return the PersistenceContext used for this request.
/**
* Return the PersistenceContext used for this request.
*/
public PersistenceContext getPersistenceContext() {
return persistenceContext;
}
Add the bean to the persistence context.
/**
* Add the bean to the persistence context.
*/
public void persistenceContextAdd(EntityBean bean) {
Object id = beanDescriptor.getId(bean);
beanDescriptor.contextPut(persistenceContext, id, bean);
}
This will create a local (readOnly) transaction if no current transaction
exists.
A transaction may have been passed in explicitly or currently be active in
the thread local. If not, then a readOnly transaction is created to execute
this query.
/**
* This will create a local (readOnly) transaction if no current transaction
* exists.
* <p>
* A transaction may have been passed in explicitly or currently be active in
* the thread local. If not, then a readOnly transaction is created to execute
* this query.
* </p>
*/
@Override
public void initTransIfRequired() {
// first check if the query requires its own transaction
if (transaction == null) {
if (query.getType().isUpdate()) {
// bulk update or delete query
transaction = ebeanServer.beginServerTransaction();
} else {
// create an implicit transaction to execute this query
// potentially using read-only DataSource with autoCommit
transaction = ebeanServer.createReadOnlyTransaction(query.getTenantId());
}
createdTransaction = true;
}
persistenceContext = getPersistenceContext(query, transaction);
loadContext = new DLoadContext(this, secondaryQueries);
}
Rollback the transaction if it was created for this request.
/**
* Rollback the transaction if it was created for this request.
*/
@Override
public void rollbackTransIfRequired() {
if (createdTransaction) {
try {
transaction.end();
} catch (Exception e) {
// Just log this and carry on. A previous exception has been
// thrown and if this rollback throws exception it likely means
// that the connection is broken (and the dataSource and db will cleanup)
log.error("Error trying to rollback a transaction (after a prior exception thrown)", e);
}
}
}
Return the JsonReadOptions taking into account lazy loading and persistence context.
/**
* Return the JsonReadOptions taking into account lazy loading and persistence context.
*/
@Override
public JsonReadOptions createJsonReadOptions() {
persistenceContext = getPersistenceContext(query, transaction);
if (query.getPersistenceContext() == null) {
query.setPersistenceContext(persistenceContext);
}
jsonRead = new JsonReadOptions();
jsonRead.setPersistenceContext(persistenceContext);
if (!query.isDisableLazyLoading()) {
loadContext = new DLoadContext(this, secondaryQueries);
jsonRead.setLoadContext(loadContext);
}
return jsonRead;
}
For iterate queries reset the persistenceContext and loadContext.
/**
* For iterate queries reset the persistenceContext and loadContext.
*/
public void flushPersistenceContextOnIterate() {
if (persistenceContext.resetLimit()) {
persistenceContext = persistenceContext.forIterateReset();
loadContext.resetPersistenceContext(persistenceContext);
if (jsonRead != null) {
jsonRead.setPersistenceContext(persistenceContext);
jsonRead.setLoadContext(loadContext);
}
}
}
Get the TransactionContext either explicitly set on the query or
transaction scoped.
/**
* Get the TransactionContext either explicitly set on the query or
* transaction scoped.
*/
private PersistenceContext getPersistenceContext(SpiQuery<?> query, SpiTransaction t) {
// check if there is already a persistence context set which is the case
// when lazy loading or query joins are executed
PersistenceContext ctx = query.getPersistenceContext();
if (ctx != null) return ctx;
// determine the scope (from the query and then server)
PersistenceContextScope scope = ebeanServer.getPersistenceContextScope(query);
if (scope == PersistenceContextScope.QUERY || t == null) {
return new DefaultPersistenceContext();
}
if (Type.ITERATE == query.getType()) {
return t.getPersistenceContext().forIterate();
} else {
return t.getPersistenceContext();
}
}
Will end a locally created transaction.
It ends the query only transaction.
/**
* Will end a locally created transaction.
* <p>
* It ends the query only transaction.
*/
@Override
public void endTransIfRequired() {
if (createdTransaction && transaction.isActive()) {
transaction.commit();
if (query.getType().isUpdate()) {
// for implicit update/delete queries clear the thread local
ebeanServer.clearServerTransaction();
}
}
}
Return true if this is a find by id (rather than List Set or Map).
/**
* Return true if this is a find by id (rather than List Set or Map).
*/
public boolean isFindById() {
return query.getType() == Type.BEAN;
}
Return true if this is a findEach, findIterate type query where we expect many results.
/**
* Return true if this is a findEach, findIterate type query where we expect many results.
*/
public boolean isFindIterate() {
return query.getType() == Type.ITERATE;
}
@Override
public int delete() {
return notifyCache(queryEngine.delete(this), false);
}
@Override
public int update() {
return notifyCache(queryEngine.update(this), true);
}
private int notifyCache(int rows, boolean update) {
if (rows > 0) {
beanDescriptor.cacheUpdateQuery(update, transaction);
}
return rows;
}
@Override
public SpiResultSet findResultSet() {
return queryEngine.findResultSet(this);
}
@Override
public Object findId() {
return queryEngine.findId(this);
}
@Override
public int findCount() {
return queryEngine.findCount(this);
}
@Override
public <A> List<A> findIds() {
return queryEngine.findIds(this);
}
@Override
public void findEach(Consumer<T> consumer) {
try (QueryIterator<T> it = queryEngine.findIterate(this)) {
while (it.hasNext()) {
consumer.accept(it.next());
}
}
}
@Override
public void findEachWhile(Predicate<T> consumer) {
try (QueryIterator<T> it = queryEngine.findIterate(this)) {
while (it.hasNext()) {
if (!consumer.test(it.next())) {
break;
}
}
}
}
@Override
public QueryIterator<T> findIterate() {
return queryEngine.findIterate(this);
}
@Override
@SuppressWarnings("unchecked")
public List<T> findList() {
return (List<T>) queryEngine.findMany(this);
}
@Override
public List<Version<T>> findVersions() {
return queryEngine.findVersions(this);
}
@Override
@SuppressWarnings("unchecked")
public Set<T> findSet() {
return (Set<T>) queryEngine.findMany(this);
}
@Override
@SuppressWarnings("unchecked")
public <K> Map<K, T> findMap() {
String mapKey = query.getMapKey();
if (mapKey == null) {
BeanProperty idProp = beanDescriptor.getIdProperty();
if (idProp != null) {
query.setMapKey(idProp.getName());
} else {
throw new PersistenceException("No mapKey specified for query");
}
}
return (Map<K, T>) queryEngine.findMany(this);
}
@Override
public <A> List<A> findSingleAttributeList() {
return queryEngine.findSingleAttributeList(this);
}
Return a bean specific finder if one has been set.
/**
* Return a bean specific finder if one has been set.
*/
public BeanFindController getBeanFinder() {
return finder;
}
@Override
public SpiQuery<T> getQuery() {
return query;
}
Determine and return the ToMany property that is included in the query.
/**
* Determine and return the ToMany property that is included in the query.
*/
public BeanPropertyAssocMany<?> determineMany() {
manyProperty = beanDescriptor.getManyProperty(query);
return manyProperty;
}
Return the many property that is fetched in the query or null if there is not one.
/**
* Return the many property that is fetched in the query or null if there is not one.
*/
public BeanPropertyAssocMany<?> getManyProperty() {
return manyProperty;
}
Return a queryPlan for the current query if one exists. Returns null if no
query plan for this query exists.
/**
* Return a queryPlan for the current query if one exists. Returns null if no
* query plan for this query exists.
*/
public CQueryPlan getQueryPlan() {
return beanDescriptor.getQueryPlan(queryPlanKey);
}
Return the queryPlanHash.
This identifies the query plan for a given bean type. It effectively
matches a SQL statement with ? bind variables. A query plan can be reused
with just the bind variables changing.
/**
* Return the queryPlanHash.
* <p>
* This identifies the query plan for a given bean type. It effectively
* matches a SQL statement with ? bind variables. A query plan can be reused
* with just the bind variables changing.
* </p>
*/
public CQueryPlanKey getQueryPlanKey() {
return queryPlanKey;
}
Put the QueryPlan into the cache.
/**
* Put the QueryPlan into the cache.
*/
public void putQueryPlan(CQueryPlan queryPlan) {
beanDescriptor.putQueryPlan(queryPlanKey, queryPlan);
}
@Override
public void resetBeanCacheAutoMode(boolean findOne) {
query.resetBeanCacheAutoMode(findOne);
}
public boolean isQueryCachePut() {
return cacheKey != null && query.getUseQueryCache().isPut();
}
public boolean isBeanCachePutMany() {
return !transaction.isSkipCacheExplicit() && query.isBeanCachePut();
}
public boolean isBeanCachePut() {
return !transaction.isSkipCache() && query.isBeanCachePut();
}
Merge in prior L2 bean cache hits with the query result.
/**
* Merge in prior L2 bean cache hits with the query result.
*/
public void mergeCacheHits(BeanCollection<T> result) {
if (cacheBeans != null && !cacheBeans.isEmpty()) {
if (query.getType() == Type.MAP) {
mergeCacheHitsToMap(result);
} else {
mergeCacheHitsToList(result);
}
}
}
private void mergeCacheHitsToList(BeanCollection<T> result) {
for (T hit : cacheBeans) {
result.internalAdd(hit);
}
if (result instanceof BeanList) {
OrderBy<T> orderBy = query.getOrderBy();
if (orderBy != null) {
// in memory sort after merging the cache hits with the DB hits
beanDescriptor.sort(((BeanList<T>) result).getActualList(), orderBy.toStringFormat());
}
}
}
@SuppressWarnings({"rawtypes"})
private void mergeCacheHitsToMap(BeanCollection<T> result) {
BeanMap map = (BeanMap)result;
ElPropertyValue property = mapProperty();
for (T bean : cacheBeans) {
map.internalPut(property.pathGet(bean), bean);
}
}
@Override
public List<T> getBeanCacheHits() {
OrderBy<T> orderBy = query.getOrderBy();
if (orderBy != null) {
beanDescriptor.sort(cacheBeans, orderBy.toStringFormat());
}
return cacheBeans;
}
@Override
public <K> Map<K, T> getBeanCacheHitsAsMap() {
OrderBy<T> orderBy = query.getOrderBy();
if (orderBy != null) {
beanDescriptor.sort(cacheBeans, orderBy.toStringFormat());
}
return cacheBeansToMap();
}
@SuppressWarnings("unchecked")
private <K> Map<K, T> cacheBeansToMap() {
ElPropertyValue property = mapProperty();
Map<K,T> map = new LinkedHashMap<>();
for (T bean : cacheBeans) {
map.put((K)property.pathGet(bean), bean);
}
return map;
}
private ElPropertyValue mapProperty() {
ElPropertyValue property = beanDescriptor.getElGetValue(query.getMapKey());
if (property == null) {
throw new IllegalStateException("Unknown map key property "+query.getMapKey());
}
return property;
}
@Override
public boolean getFromBeanCache() {
if (!query.isBeanCacheGet()) {
return false;
}
// check if the query can use the bean cache
// 1. Find by Ids
// - hit beanCache with Ids
// - keep cache beans, ensure query modified to fetch misses
// - query and Load misses into bean cache
// - merge the 2 results and return
//
CacheIdLookup<T> idLookup = query.cacheIdLookup();
if (idLookup != null) {
BeanCacheResult<T> 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 (!beanDescriptor.isNaturalKeyCaching()) {
return false;
}
NaturalKeyQueryData<T> data = query.naturalKey();
if (data != null) {
NaturalKeySet naturalKeySet = data.buildKeys();
if (naturalKeySet != null) {
// use the natural keys to lookup Ids to then hit the bean cache
BeanCacheResult<T> cacheResult = beanDescriptor.naturalKeyLookup(persistenceContext, naturalKeySet.keys());
// adjust the query (IN clause) based on the cache hits
this.cacheBeans = data.removeHits(cacheResult);
return data.allHits();
}
}
return false;
}
Try to get the query result from the query cache.
/**
* Try to get the query result from the query cache.
*/
@Override
@SuppressWarnings("unchecked")
public Object getFromQueryCache() {
if (query.getUseQueryCache() == CacheMode.OFF
|| (transaction != null && transaction.isSkipCache())
|| ebeanServer.isDisableL2Cache()) {
return null;
} else {
cacheKey = query.queryHash();
}
if (!query.getUseQueryCache().isGet()) {
return null;
}
Object cached = beanDescriptor.queryCacheGet(cacheKey);
if (cached != null && isAuditReads() && readAuditQueryType()) {
if (cached instanceof BeanCollection) {
// raw sql can't use L2 cache so normal queries only in here
Collection<T> actualDetails = ((BeanCollection<T>)cached).getActualDetails();
List<Object> ids = new ArrayList<>(actualDetails.size());
for (T bean : actualDetails) {
ids.add(beanDescriptor.getIdForJson(bean));
}
beanDescriptor.readAuditMany(queryPlanKey.getPartialKey(), "l2-query-cache", ids);
}
}
if (Boolean.FALSE.equals(query.isReadOnly())) {
// return shallow copies if readonly is explicitly set to false
if (cached instanceof BeanCollection) {
cached = ((BeanCollection<?>)cached).getShallowCopy();
} else if (cached instanceof List) {
cached = new CopyOnFirstWriteList<>((List<?>)cached);
} else if (cached instanceof Set) {
cached = new LinkedHashSet<>((Set<?>)cached);
} else if (cached instanceof Map) {
cached = new LinkedHashMap<>((Map<?,?>)cached);
}
}
return cached;
}
Return true if the query type contains bean data (not just ids etc) and hence we want to include
it in read auditing. Return false for row count and find ids queries.
/**
* Return true if the query type contains bean data (not just ids etc) and hence we want to include
* it in read auditing. Return false for row count and find ids queries.
*/
private boolean readAuditQueryType() {
Type type = query.getType();
switch (type) {
case BEAN:
case ITERATE:
case LIST:
case SET:
case MAP:
return true;
default:
return false;
}
}
public void putToQueryCache(Object result) {
beanDescriptor.queryCachePut(cacheKey, new QueryCacheEntry(result, dependentTables, transaction.getStartNanoTime()));
}
Set an Query object that owns the PreparedStatement that can be cancelled.
/**
* Set an Query object that owns the PreparedStatement that can be cancelled.
*/
public void setCancelableQuery(CancelableQuery cancelableQuery) {
query.setCancelableQuery(cancelableQuery);
}
Log the SQL if the logLevel is appropriate.
/**
* Log the SQL if the logLevel is appropriate.
*/
public void logSql(String sql) {
transaction.logSql(sql);
}
Return the batch size for lazy loading on this bean query request.
/**
* Return the batch size for lazy loading on this bean query request.
*/
public int getLazyLoadBatchSize() {
int batchSize = query.getLazyLoadBatchSize();
return (batchSize > 0) ? batchSize : ebeanServer.getLazyLoadBatchSize();
}
Return true if read auditing is on for this query request.
This means that read audit is on for this bean type and that query has not explicitly disabled it.
/**
* Return true if read auditing is on for this query request.
* <p>
* This means that read audit is on for this bean type and that query has not explicitly disabled it.
* </p>
*/
public boolean isAuditReads() {
return beanDescriptor.isReadAuditing() && !query.isDisableReadAudit();
}
Return the base table alias for this query.
/**
* Return the base table alias for this query.
*/
public String getBaseTableAlias() {
return query.getAlias(beanDescriptor.getBaseTableAlias());
}
Set the JDBC buffer fetchSize hint if not set explicitly.
/**
* Set the JDBC buffer fetchSize hint if not set explicitly.
*/
public void setDefaultFetchBuffer(int fetchSize) {
query.setDefaultFetchBuffer(fetchSize);
}
Return the tenantId associated with this request.
/**
* Return the tenantId associated with this request.
*/
public Object getTenantId() {
return (transaction == null) ? null : transaction.getTenantId();
}
Check for slow query event.
/**
* Check for slow query event.
*/
public void slowQueryCheck(long executionTimeMicros, int rowCount) {
ebeanServer.slowQueryCheck(executionTimeMicros, rowCount, query);
}
public void setInlineCountDistinct() {
inlineCountDistinct = true;
}
public boolean isInlineCountDistinct() {
return inlineCountDistinct;
}
public void addDependentTables(Set<String> 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.
/**
* Return true if no MaxRows or use LIMIT in SQL update.
*/
public boolean isInlineSqlUpdateLimit() {
return query.getMaxRows() < 1 || ebeanServer.getDatabasePlatform().isInlineSqlUpdateLimit();
}
}