Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,9 @@ public class ConditionGenerationContext {
protected String joinAlias;
protected String joinProperty;
protected MetaClass joinMetaClass;
protected String collectionPath;
protected String collectionAlias;
protected String collectionFrom;
protected List<String> valueProperties;
protected List<String> selectedExpressions;
protected boolean elementCollection;
Expand Down Expand Up @@ -122,6 +125,46 @@ public void setJoinMetaClass(@Nullable MetaClass joinMetaClass) {
this.joinMetaClass = joinMetaClass;
}

/**
* @return path to the to-many collection property whose condition is generated as an 'exists' subquery
* (e.g. {@code e.tags}), or null if the condition is not generated as a subquery
*/
@Nullable
public String getCollectionPath() {
return collectionPath;
}

public void setCollectionPath(@Nullable String collectionPath) {
this.collectionPath = collectionPath;
}

/**
* @return identification variable of the 'exists' subquery correlated with the collection property path
* by a 'member of' condition, or null if the condition is not generated as a subquery
*/
@Nullable
public String getCollectionAlias() {
return collectionAlias;
}

public void setCollectionAlias(@Nullable String collectionAlias) {
this.collectionAlias = collectionAlias;
}

/**
* @return content of the 'from' clause of the 'exists' subquery generated for a condition on a to-many
* collection property path (e.g. {@code test_Tag cje_0 left join cje_0.category cje_1}), or null if
* the condition is not generated as a subquery
*/
@Nullable
public String getCollectionFrom() {
return collectionFrom;
}

public void setCollectionFrom(@Nullable String collectionFrom) {
this.collectionFrom = collectionFrom;
}

@Nullable
public List<String> getValueProperties() {
return valueProperties;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -87,25 +87,48 @@ public String generateJoin(ConditionGenerationContext context) {
StringBuilder joinPropertyBuilder = new StringBuilder(context.entityAlias);
MetaClass metaClass = metadata.getClass(context.getEntityName());

// A join to a to-many collection multiplies rows of the main query, which requires 'select distinct'
// failing on Oracle if the entity contains a LOB attribute. Such a condition is generated as
// a self-contained 'exists' subquery instead: joins starting from the first to-many property go into
// the subquery 'from' clause and the condition itself is wrapped in 'exists' in generateWhere().
boolean collectionAsSubquery = !useInnerJoinInCondition && isGeneratedAsSubquery(metaClass, propertyName);
StringBuilder subqueryFromBuilder = null;

while (propertyName.contains(".")) {
String baseProperty = StringUtils.substringBefore(propertyName, ".");
String childProperty = StringUtils.substringAfter(propertyName, ".");

MetaProperty metaProperty = metaClass.getProperty(baseProperty);

if ((useInnerJoinInCondition && metaProperty.getRange().getCardinality().isMany()) ||
if (collectionAsSubquery && subqueryFromBuilder == null
&& (metaProperty.getType() == ASSOCIATION || metaProperty.getType() == COMPOSITION)
&& metaProperty.getRange().getCardinality().isMany()) {
MetaClass collectionMetaClass = metaProperty.getRange().asClass();
String joinAlias = joinAliasPrefix + context.generateNextJoinIndex();

context.setJoinAlias(joinAlias);
context.setJoinProperty(childProperty);
context.setJoinMetaClass(collectionMetaClass);
context.setCollectionPath(joinPropertyBuilder + "." + baseProperty);
context.setCollectionAlias(joinAlias);
// the subquery declares an entity variable correlated with the collection path
// by a 'member of' condition, so joins to it can be added inside the subquery
subqueryFromBuilder = new StringBuilder(collectionMetaClass.getName()).append(" ").append(joinAlias);
joinPropertyBuilder = new StringBuilder(joinAlias);
} else if ((useInnerJoinInCondition && metaProperty.getRange().getCardinality().isMany()) ||
(metaProperty.getType() == ASSOCIATION || metaProperty.getType() == COMPOSITION)) {
String joinAlias = joinAliasPrefix + context.generateNextJoinIndex();

context.setJoinAlias(joinAlias);
context.setJoinProperty(childProperty);
context.setJoinMetaClass(metaProperty.getRange().asClass());
StringBuilder targetBuilder = subqueryFromBuilder != null ? subqueryFromBuilder : joinBuilder;
if (useInnerJoinInCondition) {
joinBuilder.append(" join ");
targetBuilder.append(" join ");
} else {
joinBuilder.append(" left join ");
targetBuilder.append(" left join ");
}
joinBuilder.append(joinPropertyBuilder + "." + baseProperty + " " + joinAlias);
targetBuilder.append(joinPropertyBuilder + "." + baseProperty + " " + joinAlias);
joinPropertyBuilder = new StringBuilder(joinAlias);
} else {
joinPropertyBuilder.append(".").append(baseProperty);
Expand All @@ -119,6 +142,10 @@ public String generateJoin(ConditionGenerationContext context) {
propertyName = childProperty;
}

if (subqueryFromBuilder != null) {
context.setCollectionFrom(subqueryFromBuilder.toString());
}

// the property may not exist in the metaClass in case of a dynamic attributes
MetaProperty metaProperty = metaClass.findProperty(propertyName);
if (metaProperty != null && metadataTools.isElementCollection(metaProperty)) {
Expand Down Expand Up @@ -146,12 +173,26 @@ public String generateWhere(ConditionGenerationContext context) {

if (context.getJoinAlias() != null && context.getJoinProperty() != null) {
MetaClass joinMetaClass = context.getJoinMetaClass();
String where;
if (joinMetaClass != null) {
String property = getProperty(context.getJoinProperty(), joinMetaClass.getName());
return generateWhere(propertyCondition, context.getJoinAlias(), property, context.isElementCollection());
where = generateWhere(propertyCondition, context.getJoinAlias(), property, context.isElementCollection());
} else { // case of ElementCollection
return generateWhere(propertyCondition, context.getJoinAlias(), null, context.isElementCollection());
where = generateWhere(propertyCondition, context.getJoinAlias(), null, context.isElementCollection());
}
if (context.getCollectionPath() != null) {
where = String.format("exists (select 1 from %s where %s member of %s and %s)",
context.getCollectionFrom(),
context.getCollectionAlias(),
context.getCollectionPath(),
where);
if (isMatchingEmptyCollection(propertyCondition)) {
// with join-based generation an entity with an empty collection produces an all-null
// left-joined row satisfying these operations, so it must match the subquery form too
where = String.format("(%s is empty or %s)", context.getCollectionPath(), where);
}
}
return where;
} else {
String entityAlias = context.getEntityAlias();
String property = getProperty(propertyCondition.getProperty(), context.getEntityName());
Expand Down Expand Up @@ -302,6 +343,48 @@ protected String generateWhere(PropertyCondition propertyCondition, String entit
}
}

/**
* Returns true if the condition on the given property path is generated as a self-contained
* 'exists' subquery instead of a top-level join. This is the case for a path crossing a to-many
* association or composition (e.g. {@code tags.name}): a top-level join would multiply rows of
* the main query. Paths ending with an element collection keep join-based generation, as well as
* paths not resolvable in the static metadata (e.g. containing a dynamic attribute).
*/
protected boolean isGeneratedAsSubquery(MetaClass metaClass, String property) {
if (!property.contains(".")) {
return false;
}
MetaPropertyPath propertyPath = metaClass.getPropertyPath(property);
if (propertyPath == null
|| metadataTools.isElementCollection(propertyPath.getMetaProperty())) {
return false;
}
MetaProperty[] metaProperties = propertyPath.getMetaProperties();
for (int i = 0; i < metaProperties.length - 1; i++) {
MetaProperty metaProperty = metaProperties[i];
if ((metaProperty.getType() == ASSOCIATION || metaProperty.getType() == COMPOSITION)
&& metaProperty.getRange().getCardinality().isMany()) {
return true;
}
}
return false;
}

/**
* Returns true if the condition must match entities whose to-many collection is empty. With join-based
* generation such entities produce an all-null left-joined row that satisfies these operations.
*/
protected boolean isMatchingEmptyCollection(PropertyCondition propertyCondition) {
String operation = propertyCondition.getOperation();
if (PropertyCondition.Operation.IS_SET.equals(operation)) {
return !Boolean.TRUE.equals(propertyCondition.getParameterValue());
}
if (PropertyCondition.Operation.IS_COLLECTION_EMPTY.equals(operation)) {
return Boolean.TRUE.equals(propertyCondition.getParameterValue());
}
return PropertyCondition.Operation.NOT_MEMBER_OF_COLLECTION.equals(operation);
}

/**
* Returns a trailing {@code ESCAPE} clause for {@code LIKE}-based operations so that
* {@code _} and {@code %} produced by {@link QueryUtils#escapeForLike(String)} are
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -671,18 +671,25 @@ class DataManagerPropertyConditionTest extends DataSpec {
propertyConditionGenerator.generateWhere(context).contains("cje_0.name =")
}

// A condition on a path crossing a to-many association is generated as a self-contained 'exists'
// subquery instead of a top-level join, so the main query does not require 'select distinct'
// which fails on Oracle if the entity contains a LOB attribute (ORA-00932).

def "PropertyCondition generator join to many test"() {
when:

def property = PropertyCondition.equal("items.name", "Test").skipNullOrEmpty()
def context = new ConditionGenerationContext(property)
context.entityName = "test_TestAppEntity"
context.entityAlias = "e"
def joinClause = propertyConditionGenerator.generateJoin(context)
def whereClause = propertyConditionGenerator.generateWhere(context)

then:

propertyConditionGenerator.generateJoin(context).contains("join e.items ")
propertyConditionGenerator.generateWhere(context).contains(context.joinAlias + ".name =")
joinClause == ""
whereClause.startsWith("exists (select 1 from test_TestAppEntityItem cje_0 where cje_0 member of e.items and ")
whereClause.contains("cje_0.name =")
}

def "PropertyCondition generator join to one and many test"() {
Expand All @@ -692,11 +699,14 @@ class DataManagerPropertyConditionTest extends DataSpec {
def context = new ConditionGenerationContext(property)
context.entityName = "test_TestAppEntityItem"
context.entityAlias = "e"
def joinClause = propertyConditionGenerator.generateJoin(context)
def whereClause = propertyConditionGenerator.generateWhere(context)

then:
then: "the to-one prefix is joined at the top level, the collection goes into the subquery"

propertyConditionGenerator.generateJoin(context).contains(" left join e.appEntity cje_0 left join cje_0.items cje_1")
propertyConditionGenerator.generateWhere(context).contains(context.joinAlias + ".name =")
joinClause == " left join e.appEntity cje_0"
whereClause.startsWith("exists (select 1 from test_TestAppEntityItem cje_1 where cje_1 member of cje_0.items and ")
whereClause.contains("cje_1.name =")
}


Expand All @@ -707,11 +717,16 @@ class DataManagerPropertyConditionTest extends DataSpec {
def context = new ConditionGenerationContext(property)
context.entityName = "test_TestAppEntity"
context.entityAlias = "e"
def joinClause = propertyConditionGenerator.generateJoin(context)
def whereClause = propertyConditionGenerator.generateWhere(context)

then:
then: "all joins following the first to-many property are inside the subquery"

propertyConditionGenerator.generateJoin(context).count("join ") == 3
propertyConditionGenerator.generateWhere(context).contains(context.joinAlias + ".name =")
joinClause == ""
whereClause.startsWith("exists (select 1 from test_TestAppEntityItem cje_0" +
" left join cje_0.appEntity cje_1 left join cje_1.items cje_2" +
" where cje_0 member of e.items and ")
whereClause.contains("cje_2.name =")
}

def "PropertyCondition generator multiple join to one and many test"() {
Expand All @@ -726,9 +741,11 @@ class DataManagerPropertyConditionTest extends DataSpec {

then:

joinClause.count("join ") == 4
joinClause.count("left join cje_") == 3
whereClause.contains(context.joinAlias + ".name =")
joinClause == " left join e.appEntity cje_0"
whereClause.startsWith("exists (select 1 from test_TestAppEntityItem cje_1" +
" left join cje_1.appEntity cje_2 left join cje_2.items cje_3" +
" where cje_1 member of cje_0.items and ")
whereClause.contains("cje_3.name =")
}

def "basic outer join generation test"() {
Expand Down
Loading