io.ebeaninternal.server.querydefn.OrmQueryDetail Maven / Gradle / Ivy
Go to download
Show more of this group Show more artifacts with this name
Show all versions of ebean Show documentation
Show all versions of ebean Show documentation
composite of common runtime dependencies for all platforms
package io.ebeaninternal.server.querydefn;
import io.ebean.FetchConfig;
import io.ebean.util.SplitName;
import io.ebeaninternal.server.deploy.BeanDescriptor;
import io.ebeaninternal.server.deploy.BeanPropertyAssoc;
import io.ebeaninternal.server.el.ElPropertyDeploy;
import io.ebeaninternal.server.el.ElPropertyValue;
import javax.persistence.PersistenceException;
import java.io.Serializable;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Iterator;
import java.util.LinkedHashMap;
import java.util.LinkedHashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
/**
* Represents the internal structure of an Object Relational query.
*
* Holds the select() and join() details of a ORM query.
*
*
* It is worth noting that for AutoTune a "tuned fetch info" builds an instance of OrmQueryDetail.
* Tuning a query is a matter of replacing an instance of this class with one that has been tuned
* with select() and join() set.
*
*/
public class OrmQueryDetail implements Serializable {
private static final long serialVersionUID = -2510486880141461807L;
/**
* Root level properties.
*/
private OrmQueryProperties baseProps = new OrmQueryProperties();
/**
* Contains the fetch/lazy/query joins and their properties.
*/
private LinkedHashMap fetchPaths = new LinkedHashMap<>();
/**
* Return a deep copy of the OrmQueryDetail.
*/
public OrmQueryDetail copy() {
OrmQueryDetail copy = new OrmQueryDetail();
copy.baseProps = baseProps.copy();
for (Map.Entry entry : fetchPaths.entrySet()) {
copy.fetchPaths.put(entry.getKey(), entry.getValue().copy());
}
return copy;
}
/**
* Add a nested OrmQueryDetail to this detail.
*/
public void addNested(String path, OrmQueryDetail other, FetchConfig config) {
fetch(path, other.baseProps.getProperties(), config);
for (Map.Entry entry : other.fetchPaths.entrySet()) {
fetch(path + "." + entry.getKey(), entry.getValue().getProperties(), entry.getValue().getFetchConfig());
}
}
/**
* Calculate the hash for the query plan.
*/
public void queryPlanHash(StringBuilder builder) {
baseProps.queryPlanHash(builder);
if (fetchPaths != null) {
for (OrmQueryProperties p : fetchPaths.values()) {
p.queryPlanHash(builder);
}
}
}
/**
* Return true if equal in terms of autoTune (select and fetch without property ordering).
*/
public boolean isAutoTuneEqual(OrmQueryDetail otherDetail) {
if (!isSameByAutoTune(baseProps, otherDetail.baseProps)) {
return false;
}
if (fetchPaths == null) {
return otherDetail.fetchPaths == null;
}
if (fetchPaths.size() != otherDetail.fetchPaths.size()) {
return false;
}
// check without regard to ordering
for (Map.Entry entry : fetchPaths.entrySet()) {
OrmQueryProperties chunk = otherDetail.getChunk(entry.getKey(), false);
if (!isSameByAutoTune(entry.getValue(), chunk)) {
return false;
}
}
return true;
}
private boolean isSameByAutoTune(OrmQueryProperties p1, OrmQueryProperties p2) {
return p1 == null ? p2 == null : p1.isSameByAutoTune(p2);
}
@Override
public String toString() {
return asString();
}
/**
* Return the detail in string form.
*/
public String asString() {
StringBuilder sb = new StringBuilder();
if (!baseProps.isEmpty()) {
baseProps.append("select ", sb);
}
if (fetchPaths != null) {
for (OrmQueryProperties join : fetchPaths.values()) {
if (sb.length() > 0) {
sb.append(" ");
}
join.append("fetch ", sb);
}
}
return sb.toString();
}
@Override
public int hashCode() {
throw new RuntimeException("should not use");
}
/**
* set the properties to include on the base / root entity.
*/
public void select(String columns) {
baseProps = new OrmQueryProperties(null, columns, null);
}
boolean containsProperty(String property) {
return baseProps.isIncluded(property);
}
/**
* Set the base query properties to be empty.
*/
public void setEmptyBase() {
this.baseProps = new OrmQueryProperties(null, new LinkedHashSet<>());
}
/**
* Set the base / root query properties.
*/
public void setBase(OrmQueryProperties baseProps) {
this.baseProps = baseProps;
}
List removeSecondaryQueries() {
return removeSecondaryQueries(false);
}
List removeSecondaryLazyQueries() {
return removeSecondaryQueries(true);
}
private List removeSecondaryQueries(boolean lazyQuery) {
ArrayList matchingPaths = new ArrayList<>(2);
for (OrmQueryProperties chunk : fetchPaths.values()) {
boolean match = lazyQuery ? chunk.isLazyFetch() : chunk.isQueryFetch();
if (match) {
matchingPaths.add(chunk.getPath());
}
}
if (matchingPaths.isEmpty()) {
return null;
}
// sort into depth order to remove
Collections.sort(matchingPaths);
// the list of secondary queries
ArrayList props = new ArrayList<>();
for (String path : matchingPaths) {
OrmQueryProperties secQuery = fetchPaths.remove(path);
if (secQuery != null) {
props.add(secQuery);
// remove any child properties for this path
Iterator pass2It = fetchPaths.values().iterator();
while (pass2It.hasNext()) {
OrmQueryProperties pass2Prop = pass2It.next();
if (secQuery.isChild(pass2Prop)) {
// remove join to secondary query from the main query
// and add to this secondary query
pass2It.remove();
secQuery.add(pass2Prop);
}
}
}
}
// Add the secondary queries as select properties
// to the parent chunk to ensure the foreign keys
// are included in the query
for (OrmQueryProperties prop : props) {
String path = prop.getPath();
// split into parent and property
String[] split = SplitName.split(path);
// add property to parent chunk
OrmQueryProperties chunk = getChunk(split[0], true);
chunk.addSecondaryQueryJoin(split[1]);
}
return props;
}
boolean tuneFetchProperties(OrmQueryDetail tunedDetail) {
boolean tuned = false;
OrmQueryProperties tunedRoot = tunedDetail.getChunk(null, false);
if (tunedRoot != null) {
tuned = true;
baseProps = tunedRoot;
for (OrmQueryProperties tunedChunk : tunedDetail.fetchPaths.values()) {
fetch(tunedChunk.copy());
}
}
return tuned;
}
/**
* Add or replace the fetch detail.
*/
protected void fetch(OrmQueryProperties chunk) {
String path = chunk.getPath();
if (path == null) {
baseProps = chunk;
} else {
fetchPaths.put(path, chunk);
}
}
/**
* Remove all joins and properties.
*
* Typically for the row count query.
*
*/
public void clear() {
fetchPaths.clear();
}
/**
* Set the fetch properties and configuration for a given path.
*
* @param path the property to join
* @param partialProps the properties on the join property to include
*/
public void fetch(String path, String partialProps, FetchConfig fetchConfig) {
fetch(new OrmQueryProperties(path, partialProps, fetchConfig));
}
/**
* Add for raw sql etc when the properties are already parsed into a set.
*/
public void fetch(String path, LinkedHashSet properties) {
fetch(new OrmQueryProperties(path, properties));
}
/**
* Sort the fetch paths into depth order adding any missing parent paths if necessary.
*/
public void sortFetchPaths(BeanDescriptor> d) {
sortFetchPaths(d, true);
}
private void sortFetchPaths(BeanDescriptor> d, boolean addIds) {
if (!fetchPaths.isEmpty()) {
LinkedHashMap sorted = new LinkedHashMap<>();
for (OrmQueryProperties p : fetchPaths.values()) {
sortFetchPaths(d, p, sorted, addIds);
}
fetchPaths = sorted;
}
}
private void sortFetchPaths(BeanDescriptor> d, OrmQueryProperties p, LinkedHashMap sorted, boolean addId) {
String path = p.getPath();
if (!sorted.containsKey(path)) {
String parentPath = p.getParentPath();
if (parentPath == null || sorted.containsKey(parentPath)) {
// off root path or parent already ahead in fetch order
sorted.put(path, p);
} else {
OrmQueryProperties parentProp = fetchPaths.get(parentPath);
if (parentProp == null) {
ElPropertyValue el = d.getElGetValue(parentPath);
if (el == null) {
throw new PersistenceException("Path [" + parentPath + "] not valid from " + d.getFullName());
}
// add a missing parent path just fetching the Id property
BeanPropertyAssoc> assocOne = (BeanPropertyAssoc>) el.getBeanProperty();
if (addId) {
parentProp = new OrmQueryProperties(parentPath, assocOne.getTargetIdProperty());
} else {
parentProp = new OrmQueryProperties(parentPath, new LinkedHashSet<>());
}
}
sortFetchPaths(d, parentProp, sorted, addId);
sorted.put(path, p);
}
}
}
/**
* Mark 'fetch joins' to 'many' properties over to 'query joins' where needed.
*/
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;
// flag that is set once the many fetch property is chosen
boolean fetchJoinFirstMany = allowOne;
sortFetchPaths(beanDescriptor, addIds);
List pairs = sortByFetchPreference(beanDescriptor);
for (FetchEntry pair : pairs) {
ElPropertyDeploy elProp = pair.getElProp();
if (elProp.containsManySince(manyFetchProperty)) {
// this is a join to a *ToMany
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 = pair.getPath();
} else {
// convert this one over to a 'query join'
chunk.markForQueryJoin();
}
}
}
}
}
/**
* Sort the fetch entries taking into account fetchPreference on the path.
*/
private List sortByFetchPreference(BeanDescriptor> desc) {
List entries = new ArrayList<>(fetchPaths.size());
int idx = 0;
for (Map.Entry 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.
*/
private boolean isQueryJoinCandidate(String lazyLoadManyPath, OrmQueryProperties chunk) {
return chunk.isFetchJoin()
&& !isLazyLoadManyRoot(lazyLoadManyPath, chunk)
&& !hasParentSecJoin(lazyLoadManyPath, chunk);
}
/**
* Return true if this is actually the root level of a +query/+lazy loading query.
*/
private boolean isLazyLoadManyRoot(String lazyLoadManyPath, OrmQueryProperties chunk) {
return lazyLoadManyPath != null && lazyLoadManyPath.equals(chunk.getPath());
}
/**
* If the chunk has a parent that is a query or lazy join. In this case it does not need to be
* converted.
*/
private boolean hasParentSecJoin(String lazyLoadManyPath, OrmQueryProperties chunk) {
OrmQueryProperties parent = getParent(chunk);
if (parent == null) {
return false;
} else {
if (lazyLoadManyPath != null && lazyLoadManyPath.equals(parent.getPath())) {
return false;
} else {
return !parent.isFetchJoin() || hasParentSecJoin(lazyLoadManyPath, parent);
}
}
}
/**
* Return the parent chunk.
*/
private OrmQueryProperties getParent(OrmQueryProperties chunk) {
String parentPath = chunk.getParentPath();
return parentPath == null ? null : fetchPaths.get(parentPath);
}
/**
* Set any default select clauses for the main bean and any joins that have not explicitly defined
* a select clause.
*
* That is this will use FetchType.LAZY to exclude some properties by default.
*
*/
public void setDefaultSelectClause(BeanDescriptor> desc) {
if (desc.hasDefaultSelectClause() && !hasSelectClause()) {
baseProps = new OrmQueryProperties(null, desc.getDefaultSelectClause());
}
for (OrmQueryProperties joinProps : fetchPaths.values()) {
if (!joinProps.hasSelectClause()) {
BeanDescriptor> assocDesc = desc.getBeanDescriptor(joinProps.getPath());
if (assocDesc != null && assocDesc.hasDefaultSelectClause()) {
fetch(joinProps.getPath(), assocDesc.getDefaultSelectClause(), joinProps.getFetchConfig());
}
}
}
}
public boolean hasSelectClause() {
return baseProps.hasSelectClause();
}
/**
* Return true if the query detail has neither select properties specified or any joins defined.
*/
public boolean isEmpty() {
return fetchPaths.isEmpty() && baseProps.allProperties();
}
/**
* Return true if there are no joins.
*/
public boolean isJoinsEmpty() {
return fetchPaths.isEmpty();
}
/**
* Add the explicit bean join.
*
* This is also used to Exclude the matching property from the parent select (aka remove the
* foreign key) because it is now included in it's on node in the SqlTree.
*
*/
public void includeBeanJoin(String parentPath, String propertyName) {
OrmQueryProperties parentChunk = getChunk(parentPath, true);
parentChunk.includeBeanJoin(propertyName);
}
public OrmQueryProperties getChunk(String path, boolean create) {
if (path == null) {
return baseProps;
}
OrmQueryProperties props = fetchPaths.get(path);
if (create && props == null) {
props = new OrmQueryProperties(path);
fetch(props);
return props;
} else {
return props;
}
}
/**
* Return true if the fetch path is included.
*/
public boolean includesPath(String path) {
OrmQueryProperties chunk = fetchPaths.get(path);
// may not have fetch properties if just +cache etc
return chunk != null && !chunk.isCache();
}
/**
* Return the fetch paths for this detail.
*/
public Set getFetchPaths() {
return fetchPaths.keySet();
}
/**
* Return the underlying fetch path entries.
*/
public Set> entries() {
return fetchPaths.entrySet();
}
private static class FetchEntry implements Comparable {
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;
}
}
}
}
© 2015 - 2025 Weber Informatics LLC | Privacy Policy