org.eclipse.persistence.internal.expressions.RelationExpression Maven / Gradle / Ivy
Show all versions of eclipselink Show documentation
/*
* Copyright (c) 1998, 2024 Oracle and/or its affiliates. All rights reserved.
* Copyright (c) 2022 IBM Corporation. All rights reserved.
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Public License v. 2.0 which is available at
* http://www.eclipse.org/legal/epl-2.0,
* or the Eclipse Distribution License v. 1.0 which is available at
* http://www.eclipse.org/org/documents/edl-v10.php.
*
* SPDX-License-Identifier: EPL-2.0 OR BSD-3-Clause
*/
// Contributors:
// Oracle - initial API and implementation from Oracle TopLink
// 11/10/2011-2.4 Guy Pelletier
// - 357474: Address primaryKey option from tenant discriminator column
package org.eclipse.persistence.internal.expressions;
import org.eclipse.persistence.descriptors.ClassDescriptor;
import org.eclipse.persistence.exceptions.QueryException;
import org.eclipse.persistence.expressions.Expression;
import org.eclipse.persistence.expressions.ExpressionBuilder;
import org.eclipse.persistence.expressions.ExpressionOperator;
import org.eclipse.persistence.internal.helper.DatabaseField;
import org.eclipse.persistence.internal.sessions.AbstractRecord;
import org.eclipse.persistence.internal.sessions.AbstractSession;
import org.eclipse.persistence.mappings.DatabaseMapping;
import org.eclipse.persistence.mappings.OneToOneMapping;
import org.eclipse.persistence.queries.ReportQuery;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.Set;
/**
* Purpose:Used for all relation operators except for between.
*/
public class RelationExpression extends CompoundExpression {
/** PERF: Cache if the expression is an object comparison expression. */
protected Boolean isObjectComparisonExpression;
public RelationExpression() {
super();
}
/**
* Test that both of our children are field nodes
*/
protected boolean allChildrenAreFields() {
return (this.firstChild.getFields().size() == 1) && (this.secondChild.getFields().size() == 1);
}
/**
* INTERNAL:
* Modify this individual expression node to use outer joins wherever there are
* equality operations between two field nodes.
*/
@Override
protected void convertNodeToUseOuterJoin() {
if ((this.operator.getSelector() == ExpressionOperator.Equal) && allChildrenAreFields()) {
setOperator(getOperator(ExpressionOperator.EqualOuterJoin));
}
}
/**
* INTERNAL:
* Used for debug printing.
*/
@Override
public String descriptionOfNodeType() {
return "Relation";
}
/**
* INTERNAL:
* Check if the object conforms to the expression in memory.
* This is used for in-memory querying.
* If the expression in not able to determine if the object conform throw a not supported exception.
*/
@Override
public boolean doesConform(Object object, AbstractSession session, AbstractRecord translationRow, int valueHolderPolicy, boolean isObjectUnregistered) {
if ((this.secondChild.getBuilder().getSession() == null) || (this.firstChild.getBuilder().getSession() == null)) {
// Parallel selects are not supported in memory.
throw QueryException.cannotConformExpression();
}
// Extract the value from the right side.
//CR 3677 integration of valueHolderPolicy
Object rightValue =
this.secondChild.valueFromObject(object, session, translationRow, valueHolderPolicy, isObjectUnregistered);
// Extract the value from the object.
//CR 3677 integration of valueHolderPolicy
Object leftValue =
this.firstChild.valueFromObject(object, session, translationRow, valueHolderPolicy, isObjectUnregistered);
// The right value may be a Collection of values from an anyof, or an in.
if (rightValue instanceof Collection> collection) {
// Vector may mean anyOf, or an IN.
// CR#3240862, code for IN was incorrect, and was check for between which is a function not a relation.
// Must check for IN and NOTIN, currently object comparison is not supported.
// IN must be handled separately because right is always a vector of values, vector never means anyof.
if ((this.operator.getSelector() == ExpressionOperator.In) ||
(this.operator.getSelector() == ExpressionOperator.NotIn)) {
if (isObjectComparison(session)) {
// In object comparisons are not currently supported, in-memory or database.
throw QueryException.cannotConformExpression();
} else {
// Left may be single value or anyof vector.
if (leftValue instanceof List> leftVector) {
return doesAnyOfLeftValuesConform(leftVector, rightValue, session);
} else {
return this.operator.doesRelationConform(leftValue, rightValue);
}
}
}
// Otherwise right vector means an anyof on right, so must check each value.
for (Iterator> iterator = collection.iterator(); iterator.hasNext(); ) {
Object tempRight = iterator.next();
// Left may also be an anyof some must check each left with each right.
if (leftValue instanceof List> leftVector) {
// If anyof the left match return true, otherwise keep checking.
if (doesAnyOfLeftValuesConform(leftVector, tempRight, session)) {
return true;
}
}
if (doValuesConform(leftValue, tempRight, session)) {
return true;
}
}
// None of the value conform.
return false;
}
// Otherwise the left may also be a vector of values from an anyof.
if (leftValue instanceof List> leftVector) {
return doesAnyOfLeftValuesConform(leftVector, rightValue, session);
}
// Otherwise it is a simple value to value comparison, or simple object to object comparison.
return doValuesConform(leftValue, rightValue, session);
}
/**
* Conform in-memory the collection of left values with the right value for this expression.
* This is used for anyOf support when the left side is a collection of values.
*/
protected boolean doesAnyOfLeftValuesConform(List> leftValues, Object rightValue, AbstractSession session) {
// Check each left value with the right value.
for (int index = 0; index < leftValues.size(); index++) {
Object leftValue = leftValues.get(index);
if (doValuesConform(leftValue, rightValue, session)) {
// Return true if any value matches.
return true;
}
}
// Return false only if none of the values match.
return false;
}
/**
* Conform in-memory the two values.
*/
protected boolean doValuesConform(Object leftValue, Object rightValue, AbstractSession session) {
// Check for object comparison.
if (isObjectComparison(session)) {
return doesObjectConform(leftValue, rightValue, session);
} else {
return this.operator.doesRelationConform(leftValue, rightValue);
}
}
/**
* INTERNAL:
* Check if the object conforms to the expression in memory.
* This is used for in-memory querying across object relationships.
*/
public boolean doesObjectConform(Object leftValue, Object rightValue, AbstractSession session) {
if ((leftValue == null) && (rightValue == null)) {
return performSelector(true);
}
if ((leftValue == null) || (rightValue == null)) {
//both are not null.
return performSelector(false);
}
Class> javaClass = leftValue.getClass();
if (javaClass != rightValue.getClass()) {
return performSelector(false);
}
ClassDescriptor descriptor = session.getDescriptor(javaClass);
// Currently cannot conform aggregate comparisons in-memory.
if (descriptor.isAggregateDescriptor()) {
throw QueryException.cannotConformExpression();
}
Object leftPrimaryKey = descriptor.getObjectBuilder().extractPrimaryKeyFromObject(leftValue, session);
Object rightPrimaryKey = descriptor.getObjectBuilder().extractPrimaryKeyFromObject(rightValue, session);
return performSelector(leftPrimaryKey.equals(rightPrimaryKey));
}
/**
* INTERNAL:
* Extract the values from the expression into the row.
* Ensure that the query is querying the exact primary key.
* @param requireExactMatch refers to the primary key extracted gaurenteeing the result,
* if not exact it is a heuristic and the cache hit will be conformed to the expression after the lookup
* Return false if not on the primary key.
*/
@Override
public boolean extractValues(boolean primaryKeyOnly, boolean requireExactMatch, ClassDescriptor descriptor, AbstractRecord primaryKeyRow, AbstractRecord translationRow) {
// If an exact match is required then the operator must be equality.
if (requireExactMatch && (!(this.operator.getSelector() == ExpressionOperator.Equal))) {
return false;
}
// If not an exact match only =, <, <=, >=, >,... are allowed but not IN which has a different type
if ((!requireExactMatch) && (this.operator.getSelector() == ExpressionOperator.In)) {
return false;
}
DatabaseField field = null;
Object value = null;
if (this.secondChild.isConstantExpression()) {
value = ((ConstantExpression)this.secondChild).getValue();
} else if (this.secondChild.isParameterExpression() && (translationRow != null)) {
value = translationRow.get(((ParameterExpression)this.secondChild).getField());
} else if (this.firstChild.isConstantExpression()) {
value = ((ConstantExpression)this.firstChild).getValue();
} else if (this.firstChild.isParameterExpression() && (translationRow != null)) {
value = translationRow.get(((ParameterExpression)this.firstChild).getField());
}
if (value == null) {
return false;
}
// Descriptor to use for child query key
ClassDescriptor descriptorForChild = null;
// Ensure that the primary key is being queried on.
if (this.firstChild.isFieldExpression()) {
FieldExpression child = (FieldExpression)this.firstChild;
// Only get value for the source object.
if (!child.getBaseExpression().isExpressionBuilder()) {
return false;
}
field = child.getField();
} else if (this.firstChild.isQueryKeyExpression()) {
QueryKeyExpression child = (QueryKeyExpression)this.firstChild;
// Only get value for the source object.
if (!child.getBaseExpression().isExpressionBuilder()) {
return false;
}
descriptorForChild = ((ExpressionBuilder)child.getBaseExpression()).getDescriptor();
if (descriptorForChild == null) {
descriptorForChild = descriptor;
}
DatabaseMapping mapping = descriptorForChild.getObjectBuilder().getMappingForAttributeName(child.getName());
if (mapping != null) {
if (primaryKeyOnly && !mapping.isPrimaryKeyMapping()) {
return false;
}
// Only support referencing limited number of relationship types.
if (mapping.isObjectReferenceMapping() || mapping.isAggregateObjectMapping()) {
mapping.writeFromAttributeIntoRow(value, primaryKeyRow, getSession());
return true;
}
if (!mapping.isAbstractColumnMapping()) {
return false;
}
field = mapping.getField();
} else {
// Only get field for the source object.
field = descriptorForChild.getObjectBuilder().getFieldForQueryKeyName(child.getName());
}
} else if (this.secondChild.isFieldExpression()) {
FieldExpression child = (FieldExpression)this.secondChild;
// Only get field for the source object.
if (!child.getBaseExpression().isExpressionBuilder()) {
return false;
}
field = child.getField();
} else if (this.secondChild.isQueryKeyExpression()) {
QueryKeyExpression child = (QueryKeyExpression)this.secondChild;
// Only get value for the source object.
if (!child.getBaseExpression().isExpressionBuilder()) {
return false;
}
descriptorForChild = ((ExpressionBuilder)child.getBaseExpression()).getDescriptor();
if (descriptorForChild == null) {
descriptorForChild = descriptor;
}
DatabaseMapping mapping = descriptorForChild.getObjectBuilder().getMappingForAttributeName(child.getName());
// Only support referencing limited number of relationship types.
if (mapping != null) {
if (primaryKeyOnly && !mapping.isPrimaryKeyMapping()) {
return false;
}
if (mapping.isObjectReferenceMapping() || mapping.isAggregateObjectMapping()) {
mapping.writeFromAttributeIntoRow(value, primaryKeyRow, getSession());
return true;
}
if (!mapping.isAbstractColumnMapping()) {
return false;
}
field = mapping.getField();
} else {
field = descriptorForChild.getObjectBuilder().getFieldForQueryKeyName(child.getName());
}
} else {
return false;
}
if (field == null) {
return false;
}
// Check child descriptor's primary key fields if the passed descriptor does not contain the field
if (primaryKeyOnly && !descriptor.getPrimaryKeyFields().contains(field)) {
if (descriptorForChild != null && descriptorForChild != descriptor && descriptorForChild.getPrimaryKeyFields().contains(field)) {
// Child descriptor's pk fields contains the field, return true.
// Do not add the field from the query key's descriptor to the primaryKeyRow
return true;
} else {
return false;
}
}
// Do not replace the field in the row with the same field
if (primaryKeyRow.get(field) != null) {
return false;
}
primaryKeyRow.put(field, value);
return true;
}
/**
* INTERNAL:
* Return if the expression is not a valid primary key expression and add all primary key fields to the set.
*/
@Override
public boolean extractFields(boolean requireExactMatch, boolean primaryKey, ClassDescriptor descriptor, List searchFields, Set foundFields) {
// If an exact match is required then the operator must be equality.
if (requireExactMatch && (!(this.operator.getSelector() == ExpressionOperator.Equal))) {
return false;
}
// If not an exact match only =, <, <=, >=, >,... are allowed but not IN which has a different type
if ((!requireExactMatch) && (this.operator.getSelector() == ExpressionOperator.In)) {
return false;
}
DatabaseField field = null;
if (!(this.secondChild.isConstantExpression() || this.secondChild.isParameterExpression())
&& !(this.firstChild.isConstantExpression() || (this.firstChild.isParameterExpression()))) {
return false;
}
// Ensure that the primary key is being queried on.
if (this.firstChild.isFieldExpression()) {
FieldExpression child = (FieldExpression)this.firstChild;
// Only get value for the source object.
if (!child.getBaseExpression().isExpressionBuilder()) {
return false;
}
field = child.getField();
} else if (this.firstChild.isQueryKeyExpression()) {
QueryKeyExpression child = (QueryKeyExpression)this.firstChild;
// Only get value for the source object.
if (!child.getBaseExpression().isExpressionBuilder()) {
return false;
}
DatabaseMapping mapping = descriptor.getObjectBuilder().getMappingForAttributeName(child.getName());
if (mapping != null) {
if (primaryKey && !mapping.isPrimaryKeyMapping()) {
return false;
}
// Only support referencing limited number of relationship types.
if (mapping.isObjectReferenceMapping() || mapping.isAggregateObjectMapping()) {
for (DatabaseField mappingField : mapping.getFields()) {
if (searchFields.contains(mappingField)) {
foundFields.add(mappingField);
}
}
return true;
}
if (!mapping.isAbstractColumnMapping()) {
return false;
}
field = mapping.getField();
} else {
// Only get field for the source object.
field = descriptor.getObjectBuilder().getFieldForQueryKeyName(child.getName());
}
} else if (this.secondChild.isFieldExpression()) {
FieldExpression child = (FieldExpression)this.secondChild;
// Only get field for the source object.
if (!child.getBaseExpression().isExpressionBuilder()) {
return false;
}
field = child.getField();
} else if (this.secondChild.isQueryKeyExpression()) {
QueryKeyExpression child = (QueryKeyExpression)this.secondChild;
// Only get value for the source object.
if (!child.getBaseExpression().isExpressionBuilder()) {
return false;
}
DatabaseMapping mapping = descriptor.getObjectBuilder().getMappingForAttributeName(child.getName());
// Only support referencing limited number of relationship types.
if (mapping != null) {
if (!mapping.isPrimaryKeyMapping()) {
return false;
}
if (mapping.isObjectReferenceMapping() || mapping.isAggregateObjectMapping()) {
for (DatabaseField mappingField : mapping.getFields()) {
if (searchFields.contains(mappingField)) {
foundFields.add(mappingField);
}
}
return true;
}
if (!mapping.isAbstractColumnMapping()) {
return false;
}
field = mapping.getField();
} else {
field = descriptor.getObjectBuilder().getFieldForQueryKeyName(child.getName());
}
} else {
return false;
}
if ((field == null) || (!searchFields.contains(field))) {
return false;
}
foundFields.add(field);
return true;
}
/**
* Check if the expression is an equal null expression, these must be handle in a special way in SQL.
*/
public boolean isEqualNull(ExpressionSQLPrinter printer) {
if (isObjectComparison(printer.getSession())) {
return false;
} else if (this.operator.getSelector() != ExpressionOperator.Equal) {
return false;
} else if (this.secondChild.isConstantExpression() && (((ConstantExpression)this.secondChild).getValue() == null)) {
return true;
} else if (this.secondChild.isParameterExpression() && (printer.getTranslationRow() != null) &&
(((ParameterExpression)this.secondChild).getValue(printer.getTranslationRow(), printer.getSession()) == null)) {
return true;
} else {
return false;
}
}
/**
* Check if the expression is an equal null expression, these must be handle in a special way in SQL.
*/
public boolean isNotEqualNull(ExpressionSQLPrinter printer) {
if (isObjectComparison(printer.getSession())) {
return false;
} else if (this.operator.getSelector() != ExpressionOperator.NotEqual) {
return false;
} else if (this.secondChild.isConstantExpression() && (((ConstantExpression)this.secondChild).getValue() == null)) {
return true;
} else if (this.secondChild.isParameterExpression() && (printer.getTranslationRow() != null) &&
(((ParameterExpression)this.secondChild).getValue(printer.getTranslationRow(), printer.getSession()) == null)) {
return true;
} else {
return false;
}
}
/**
* INTERNAL:
* Return if the represents an object comparison.
*/
protected boolean isObjectComparison(AbstractSession session) {
if (this.isObjectComparisonExpression == null) {
// PERF: direct-access.
// Base must have a session set in its builder to call getMapping, isAttribute
if (this.firstChild.getBuilder().getSession() == null) {
this.firstChild.getBuilder().setSession(session.getRootSession(null));
}
if (this.secondChild.getBuilder().getSession() == null) {
this.secondChild.getBuilder().setSession(session.getRootSession(null));
}
if ((!this.firstChild.isObjectExpression()) || ((ObjectExpression)this.firstChild).isAttribute()) {
if ((this.secondChild.isObjectExpression()) && !((ObjectExpression)this.secondChild).isAttribute()) {
DatabaseMapping mapping = ((ObjectExpression)this.secondChild).getMapping();
if ((mapping != null) && (mapping.isDirectCollectionMapping()) && !(this.secondChild.isMapEntryExpression())) {
this.isObjectComparisonExpression = Boolean.FALSE;
} else {
this.isObjectComparisonExpression = this.firstChild.isObjectExpression()
|| this.firstChild.isValueExpression()
|| this.firstChild.isSubSelectExpression()
|| (this.firstChild.isFunctionExpression() && ((FunctionExpression) this.firstChild).operator.isAnyOrAll());
}
} else {
this.isObjectComparisonExpression = Boolean.FALSE;
}
} else {
DatabaseMapping mapping = ((ObjectExpression)this.firstChild).getMapping();
if ((mapping != null) && (mapping.isDirectCollectionMapping()) && !(this.firstChild.isMapEntryExpression())) {
this.isObjectComparisonExpression = Boolean.FALSE;
} else {
this.isObjectComparisonExpression = this.secondChild.isObjectExpression()
|| this.secondChild.isValueExpression()
|| this.secondChild.isSubSelectExpression()
|| (this.secondChild.isFunctionExpression() && ((FunctionExpression) this.secondChild).operator.isAnyOrAll());
}
}
}
return this.isObjectComparisonExpression;
}
/**
* INTERNAL:
*/
@Override
public boolean isRelationExpression() {
return true;
}
/**
* PERF: Optimize out unnecessary joins.
* Check for relation based on foreign keys, i.e. emp.address.id = :id, and avoid join.
* @return null if cannot be optimized, otherwise the optimized normalized expression.
*/
protected Expression checkForeignKeyJoinOptimization(Expression first, Expression second, ExpressionNormalizer normalizer) {
if (first.isQueryKeyExpression()
&& (((QueryKeyExpression)first).getBaseExpression() != null)
&& ((QueryKeyExpression)first).getBaseExpression().isQueryKeyExpression()) {
// Do not optimize for subselect if it is using parent builder, and needs to clone.
if(normalizer.getStatement().isSubSelect() && normalizer.getStatement().getParentStatement().getBuilder().equals(first.getBuilder())) {
return null;
}
QueryKeyExpression mappingExpression = (QueryKeyExpression)((QueryKeyExpression)first).getBaseExpression();
if ((mappingExpression.getBaseExpression() != null)
&& mappingExpression.getBaseExpression().isObjectExpression()
&& (!mappingExpression.shouldUseOuterJoin())) {
// Must ensure it has been normalized first.
mappingExpression.getBaseExpression().normalize(normalizer);
DatabaseMapping mapping = mappingExpression.getMapping();
if ((mapping != null) && mapping.isOneToOneMapping()
&& (!((OneToOneMapping)mapping).hasCustomSelectionQuery())
&& ((OneToOneMapping)mapping).isForeignKeyRelationship()
&& (second.isConstantExpression() || second.isParameterExpression())) {
DatabaseField targetField = ((QueryKeyExpression)first).getField();
DatabaseField sourceField = ((OneToOneMapping)mapping).getTargetToSourceKeyFields().get(targetField);
if (sourceField != null) {
Expression optimizedExpression = this.operator.expressionFor(mappingExpression.getBaseExpression().getField(sourceField), second);
// Ensure the base still applies the correct conversion.
second.setLocalBase(first);
return optimizedExpression.normalize(normalizer);
}
}
}
}
return null;
}
/**
* INTERNAL:
* Check for object comparison as this requires for the expression to be replaced by the object comparison.
*/
@Override
public Expression normalize(ExpressionNormalizer normalizer) {
// PERF: Optimize out unnecessary joins.
Expression optimizedExpression = checkForeignKeyJoinOptimization(this.firstChild, this.secondChild, normalizer);
if (optimizedExpression == null) {
optimizedExpression = checkForeignKeyJoinOptimization(this.secondChild, this.firstChild, normalizer);
}
if (optimizedExpression != null) {
return optimizedExpression;
}
if (!isObjectComparison(normalizer.getSession())) {
return super.normalize(normalizer);
} else {
//bug # 2956674
//validation is moved into normalize to ensure that expressions are valid before we attempt to work with them
// super.normalize will call validateNode as well.
validateNode();
}
if ((this.operator.getSelector() != ExpressionOperator.Equal) &&
(this.operator.getSelector() != ExpressionOperator.NotEqual) &&
(this.operator.getSelector() != ExpressionOperator.In) &&
(this.operator.getSelector() != ExpressionOperator.NotIn)) {
throw QueryException.invalidOperatorForObjectComparison(this);
}
// Check for IN with objects, "object IN :objects", "objects IN (:object1, :object2 ...)", "object IN aList"
if ((this.operator.getSelector() == ExpressionOperator.In) || (this.operator.getSelector() == ExpressionOperator.NotIn)) {
// Switch object comparison to compare on primary key.
Expression left = this.firstChild;
if (!left.isObjectExpression()) {
throw QueryException.invalidExpression(this);
}
// Check if the left is for a 1-1 mapping, then optimize to compare on foreign key to avoid join.
DatabaseMapping mapping = null;
if (left.isQueryKeyExpression()) {
((ObjectExpression)left).getBaseExpression().normalize(normalizer);
mapping = ((ObjectExpression)left).getMapping();
}
ClassDescriptor descriptor = null;
List sourceFields = null;
List targetFields = null;
if ((mapping != null) && mapping.isOneToOneMapping()
&& (!((OneToOneMapping)mapping).hasRelationTableMechanism())
&& (!((OneToOneMapping)mapping).hasCustomSelectionQuery())) {
left = ((ObjectExpression)left).getBaseExpression();
descriptor = mapping.getReferenceDescriptor();
left = left.normalize(normalizer);
Map targetToSourceKeyFields = ((OneToOneMapping)mapping).getTargetToSourceKeyFields();
sourceFields = new ArrayList<>(targetToSourceKeyFields.size());
targetFields = new ArrayList<>(targetToSourceKeyFields.size());
for (Map.Entry entry : targetToSourceKeyFields.entrySet()) {
sourceFields.add(entry.getValue());
targetFields.add(entry.getKey());
}
} else {
mapping = null;
left = left.normalize(normalizer);
descriptor = ((ObjectExpression)left).getDescriptor();
sourceFields = descriptor.getPrimaryKeyFields();
targetFields = sourceFields;
}
boolean composite = sourceFields.size() > 1;
DatabaseField sourceField = sourceFields.get(0);
DatabaseField targetField = targetFields.get(0);
Expression newLeft = null;
if (composite) {
// For composite ids an array comparison is used, this only works on some databases.
List fieldExpressions = new ArrayList<>();
for (DatabaseField field : sourceFields) {
fieldExpressions.add(left.getField(field));
}
newLeft = getBuilder().value(fieldExpressions);
} else {
newLeft = left.getField(sourceField);
}
setFirstChild(newLeft);
Expression right = this.secondChild;
if (right.isConstantExpression()) {
// Check for a constant with a List of objects, need to collect the ids (also allow a list of ids).
ConstantExpression constant = (ConstantExpression)right;
if (constant.getValue() instanceof Collection> objects) {
List