Skip to content
Open
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
2 changes: 1 addition & 1 deletion pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@

<groupId>org.springframework.data</groupId>
<artifactId>spring-data-relational-parent</artifactId>
<version>4.1.0-SNAPSHOT</version>
<version>4.1.x-GH-2276-SNAPSHOT</version>
<packaging>pom</packaging>

<name>Spring Data Relational Parent</name>
Expand Down
2 changes: 1 addition & 1 deletion spring-data-jdbc-distribution/pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@
<parent>
<groupId>org.springframework.data</groupId>
<artifactId>spring-data-relational-parent</artifactId>
<version>4.1.0-SNAPSHOT</version>
<version>4.1.x-GH-2276-SNAPSHOT</version>
<relativePath>../pom.xml</relativePath>
</parent>

Expand Down
4 changes: 2 additions & 2 deletions spring-data-jdbc/pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@
<modelVersion>4.0.0</modelVersion>

<artifactId>spring-data-jdbc</artifactId>
<version>4.1.0-SNAPSHOT</version>
<version>4.1.x-GH-2276-SNAPSHOT</version>

<name>Spring Data JDBC</name>
<description>Spring Data module for JDBC repositories.</description>
Expand All @@ -15,7 +15,7 @@
<parent>
<groupId>org.springframework.data</groupId>
<artifactId>spring-data-relational-parent</artifactId>
<version>4.1.0-SNAPSHOT</version>
<version>4.1.x-GH-2276-SNAPSHOT</version>
</parent>

<properties>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -22,14 +22,15 @@
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.function.Function;
import java.util.regex.Pattern;

import org.jspecify.annotations.Nullable;

import org.springframework.data.core.PropertyPath;
import org.springframework.data.core.PropertyReferenceException;
import org.springframework.data.core.TypeInformation;
import org.springframework.data.domain.Sort;
import org.springframework.data.jdbc.core.mapping.AggregateReference;
import org.springframework.data.jdbc.core.mapping.JdbcValue;
import org.springframework.data.jdbc.support.JdbcUtil;
import org.springframework.data.mapping.MappingException;
Expand All @@ -41,12 +42,28 @@
import org.springframework.data.relational.core.query.CriteriaDefinition;
import org.springframework.data.relational.core.query.CriteriaDefinition.Comparator;
import org.springframework.data.relational.core.query.ValueFunction;
import org.springframework.data.relational.core.sql.*;
import org.springframework.data.relational.core.sql.Aliased;
import org.springframework.data.relational.core.sql.AndCondition;
import org.springframework.data.relational.core.sql.AsteriskFromTable;
import org.springframework.data.relational.core.sql.Column;
import org.springframework.data.relational.core.sql.Condition;
import org.springframework.data.relational.core.sql.Conditions;
import org.springframework.data.relational.core.sql.Expression;
import org.springframework.data.relational.core.sql.Expressions;
import org.springframework.data.relational.core.sql.Functions;
import org.springframework.data.relational.core.sql.OrderByField;
import org.springframework.data.relational.core.sql.OrCondition;
import org.springframework.data.relational.core.sql.SQL;
import org.springframework.data.relational.core.sql.SimpleFunction;
import org.springframework.data.relational.core.sql.SqlIdentifier;
import org.springframework.data.relational.core.sql.Table;
import org.springframework.data.relational.core.sql.TableLike;
import org.springframework.data.relational.domain.SqlSort;
import org.springframework.data.util.Pair;
import org.springframework.jdbc.core.namedparam.MapSqlParameterSource;
import org.springframework.util.Assert;
import org.springframework.util.ClassUtils;
import org.springframework.util.CollectionUtils;

/**
* Maps {@link CriteriaDefinition} and {@link Sort} objects considering mapping metadata and dialect-specific
Expand Down Expand Up @@ -303,42 +320,11 @@ private Condition mapCondition(CriteriaDefinition criteria, MapSqlParameterSourc
// Single embedded entity
if (propertyField.isEmbedded()) {

// IN/NOT_IN with collection of composite/embedded values: expand to (… AND …) OR (…)
if ((Comparator.IN.equals(comparator) || Comparator.NOT_IN.equals(comparator))
&& value instanceof Collection<?> collection) {

Condition condition = null;

for (Object o : collection) {

CriteriaWrapper cw = new CriteriaWrapper(criteria) {

@Override
public @Nullable Comparator getComparator() {
return Comparator.IN.equals(comparator) ? Comparator.EQ : Comparator.NEQ;
}

@Nullable
@Override
public Object getValue() {
return o;
}

@Override
public @Nullable SqlIdentifier getColumn() {
return criteriaColumn;
}
};

Condition c = Conditions.nest(mapCondition(cw, parameterSource, table, entity, true));
condition = condition == null ? c : condition.or(c);
}

if (condition == null) {
return Comparator.IN.equals(comparator) ? Conditions.unrestricted().not() : Conditions.unrestricted();
}
&& value instanceof Collection<?> collection) {

return condition;
return expandInCollectionComparison(comparator, collection,
element -> mapCondition(new ListElementCriteria(criteria, element), parameterSource, table, entity, true));
}

PersistentPropertyPath<RelationalPersistentProperty> path = ((MetadataBackedField) propertyField).getPath();
Expand All @@ -347,9 +333,35 @@ public Object getValue() {
.getRequiredPersistentEntity(propertyField.getRequiredProperty());

Condition condition = mapEmbeddedObjectCondition(criteria, parameterSource, table, embeddedEntity, embedded);
if (!embedded && condition instanceof OrCondition) {
return Conditions.nest(condition);
}
return embedded || !(condition instanceof AndCondition) ? condition : Conditions.nest(condition);
}

// AggregateReference (and similar associations) to a composite identifier: expand IN/NOT_IN like embedded
if (propertyField instanceof MetadataBackedField metadataBackedField && metadataBackedField.property != null) {

RelationalPersistentProperty associationProperty = metadataBackedField.property;

if (Association.isAssociation(associationProperty)) {

Association association = Association.from(associationProperty, converter);

if (association.isComplexIdentifier() //
&& (Comparator.IN.equals(comparator) || Comparator.NOT_IN.equals(comparator))
&& value instanceof Collection<?> collection) {

RelationalPersistentEntity<?> identifierEntity = association.getRequiredTargetIdentifierEntity();

return expandInCollectionComparison(comparator, collection,
element -> mapEmbeddedObjectCondition(
new ListElementCriteria(criteria, unwrapAssociationCriteriaValue(element)), parameterSource, table,
identifierEntity, true));
}
}
}

TypeInformation<?> actualType = propertyField.getTypeHint().getRequiredActualType();
Column column = table.column(propertyField.getMappedColumnName());
Object mappedValue;
Expand Down Expand Up @@ -384,6 +396,29 @@ public Object getValue() {
return createCondition(column, mappedValue, sqlType, parameterSource, comparator, criteria.isIgnoreCase());
}

/**
* Expands {@link Comparator#IN}/{@link Comparator#NOT_IN} over a collection to a disjunction of nested conditions,
* one per element (used for embedded types and composite association identifiers).
* <p>
* IN: (col = v AND …) OR (…) per element.
* NOT_IN: AND over tuple negations; each negation is (col != v OR …), i.e. NOT (c1 = v1 AND c2 = v2) ≡ (c1 != v1 OR c2 != v2).
*/
@SuppressWarnings("NullAway")
private Condition expandInCollectionComparison(Comparator comparator, Collection<?> collection,
Function<Object, Condition> nestedConditionFactory) {

if (CollectionUtils.isEmpty(collection)) {
return Comparator.IN.equals(comparator) ? Conditions.unrestricted().not() : Conditions.unrestricted();
}

Condition condition = null;
for (Object element : collection) {
Condition next = Conditions.nest(nestedConditionFactory.apply(element));
condition = condition == null ? next : (Comparator.IN.equals(comparator) ? condition.or(next) : condition.and(next));
}
return condition;
}

/**
* Converts values while taking specific value types like arrays, {@link Iterable}, or {@link Pair}.
*
Expand Down Expand Up @@ -469,6 +504,15 @@ private JdbcValue getWriteValue(RelationalPersistentProperty property, Object va
);
}

private static Object unwrapAssociationCriteriaValue(Object value) {

if (value instanceof AggregateReference<?, ?> aggregateReference) {
return aggregateReference.getId();
}

return value;
}

private Condition mapEmbeddedObjectCondition(CriteriaDefinition criteria, MapSqlParameterSource parameterSource,
Table table, RelationalPersistentEntity<?> embeddedEntity, boolean embedded) {

Expand All @@ -477,30 +521,22 @@ private Condition mapEmbeddedObjectCondition(CriteriaDefinition criteria, MapSql

PersistentPropertyAccessor<Object> embeddedAccessor = embeddedEntity.getPropertyAccessor(criteria.getValue());

Condition condition = Conditions.unrestricted();
for (RelationalPersistentProperty embeddedProperty : embeddedEntity) {

Object propertyValue = embeddedAccessor.getProperty(embeddedProperty);
Comparator tupleComparator = criteria.getComparator();
Assert.notNull(tupleComparator, "Comparator must not be null");

CriteriaWrapper cw = new CriteriaWrapper(criteria) {
boolean negateTuple = Comparator.NEQ.equals(tupleComparator);

@Override
public SqlIdentifier getColumn() {
return SqlIdentifier.unquoted(embeddedProperty.getName());
}
Condition condition = null;
for (RelationalPersistentProperty embeddedProperty : embeddedEntity) {

@Nullable
@Override
public Object getValue() {
return propertyValue;
}
};
Object propertyValue = embeddedAccessor.getProperty(embeddedProperty);

CriteriaDefinition cw = new EmbeddedPropertyCriteria(criteria, embeddedProperty, propertyValue);
Condition mapped = mapCondition(cw, parameterSource, table, embeddedEntity, embedded);
condition = condition.and(mapped);
condition = condition == null ? mapped : (negateTuple ? condition.or(mapped) : condition.and(mapped));
}

return condition;
return condition != null ? condition : Conditions.unrestricted();
}

@Nullable
Expand Down Expand Up @@ -742,6 +778,66 @@ private static String getUniqueName(MapSqlParameterSource parameterSource, Strin
return uniqueName;
}

/**
* {@link CriteriaDefinition} view of one element when expanding {@code IN}/{@code NOT IN} over embedded or composite
* identifier values.
*/
private static final class ListElementCriteria extends CriteriaWrapper {

// private final SqlIdentifier column;
// private final Comparator comparator;
private final Object elementValue;

ListElementCriteria(CriteriaDefinition delegate, Object elementValue) {
super(delegate);
// this.column = column;
// this.comparator = comparator;
this.elementValue = elementValue;
}

@Override
public @Nullable Comparator getComparator() {
Comparator elementComparator = getDelegate().getComparator();
return Comparator.IN.equals(elementComparator) ? Comparator.EQ : Comparator.NEQ;
}

@Override
public @Nullable SqlIdentifier getColumn() {
return getDelegate().getColumn();
}

@Override
public @Nullable Object getValue() {
return this.elementValue;
}
}

/**
* {@link CriteriaDefinition} for a single property of an embedded object, delegating flags to the outer criteria.
*/
private static final class EmbeddedPropertyCriteria extends CriteriaWrapper {

private final SqlIdentifier propertyColumn;
private final @Nullable Object propertyValue;

EmbeddedPropertyCriteria(CriteriaDefinition delegate, RelationalPersistentProperty embeddedProperty,
@Nullable Object propertyValue) {
super(delegate);
this.propertyColumn = SqlIdentifier.unquoted(embeddedProperty.getName());
this.propertyValue = propertyValue;
}

@Override
public SqlIdentifier getColumn() {
return this.propertyColumn;
}

@Override
public @Nullable Object getValue() {
return this.propertyValue;
}
}

abstract static class CriteriaWrapper implements CriteriaDefinition {

private final CriteriaDefinition delegate;
Expand All @@ -750,6 +846,10 @@ public CriteriaWrapper(CriteriaDefinition delegate) {
this.delegate = delegate;
}

protected CriteriaDefinition getDelegate() {
return delegate;
}

@Nullable
@Override
public Comparator getComparator() {
Expand All @@ -760,6 +860,7 @@ public Comparator getComparator() {
public boolean isIgnoreCase() {
return delegate.isIgnoreCase();
}

@Override
public boolean isGroup() {
return false;
Expand All @@ -776,7 +877,6 @@ public SqlIdentifier getColumn() {
return null;
}


@Nullable
@Override
public Object getValue() {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -236,7 +236,7 @@ void upsertInsertsWhenIdDoesNotExistAndUpdatesWhenItExists() {
void upsertUpdatesExistingWithNullValues() {

long id = 8891L;
withSqlServerIdentityInsertOn(template, List.of("LEGO_SET", "MANUAL"), () -> {
withSqlServerIdentityInsertOn(template, List.of("LEGO_SET"), () -> {

LegoSet lego = new LegoSet();
lego.id = id;
Expand All @@ -245,7 +245,11 @@ void upsertUpdatesExistingWithNullValues() {
Manual manual = new Manual();
manual.id = 42L;
manual.content = "Accelerates to 99% of light speed; Destroys almost everything. See https://what-if.xkcd.com/1/";
lego.manual = manual;

// Only one table with identity insert on at the time guard
if (!(template.getDataAccessStrategy().getDialect() instanceof SqlServerDialect)) {
lego.manual = manual;
}

template.upsert(lego);

Expand All @@ -255,8 +259,12 @@ void upsertUpdatesExistingWithNullValues() {
LegoSet loaded = template.findById(id, LegoSet.class);

assertThat(loaded.name).isEqualTo(null);
assertThat(loaded.manual).isNotNull();
assertThat(loaded.manual.content).isEqualTo(manual.content);

if (!(template.getDataAccessStrategy().getDialect() instanceof SqlServerDialect)) {
assertThat(loaded.manual).isNotNull();
assertThat(loaded.manual.content).isEqualTo(manual.content);
}

});
}

Expand Down
Loading
Loading