orderBy) {
+ return orderBy(orderBy.toArray(new Expression[0]));
+ }
+
+ /**
+ * Specify the {@literal ORDER BY} clause of an analytic function
+ *
+ * @param orderBy array of {@link Expression}. Typically, column but other expressions are fine to.
+ * @return a new {@literal AnalyticFunction} is ordered by the given expressions, overwriting any expression
+ * previously present.
+ */
+ public AnalyticFunction orderBy(Expression... orderBy) {
- final OrderByField[] orderByFields = Arrays.stream(orderByExpression) //
+ final OrderByField[] orderByFields = Arrays.stream(orderBy) //
.map(OrderByField::from) //
.toArray(OrderByField[]::new);
diff --git a/spring-data-relational/src/main/java/org/springframework/data/relational/core/sql/Conditions.java b/spring-data-relational/src/main/java/org/springframework/data/relational/core/sql/Conditions.java
index c4eb4a463f6..62dc6d86ba7 100644
--- a/spring-data-relational/src/main/java/org/springframework/data/relational/core/sql/Conditions.java
+++ b/spring-data-relational/src/main/java/org/springframework/data/relational/core/sql/Conditions.java
@@ -247,7 +247,7 @@ public static In in(Expression columnOrExpression, Expression... expressions) {
* @param subselect the subselect.
* @return the {@link In} condition.
*/
- public static In in(Column column, Select subselect) {
+ public static In in(Expression column, Select subselect) {
Assert.notNull(column, "Column must not be null");
Assert.notNull(subselect, "Subselect must not be null");
diff --git a/spring-data-relational/src/main/java/org/springframework/data/relational/core/sql/TupleExpression.java b/spring-data-relational/src/main/java/org/springframework/data/relational/core/sql/TupleExpression.java
new file mode 100644
index 00000000000..e1699197ea4
--- /dev/null
+++ b/spring-data-relational/src/main/java/org/springframework/data/relational/core/sql/TupleExpression.java
@@ -0,0 +1,54 @@
+package org.springframework.data.relational.core.sql;
+
+import org.jetbrains.annotations.NotNull;
+
+import static java.util.stream.Collectors.*;
+
+import java.util.List;
+
+/**
+ * A tuple as used in conditions like
+ *
+ *
+ * WHERE (one, two) IN (select x, y from some_table)
+ *
+ *
+ * @author Jens Schauder
+ * @since 3.5
+ */
+public class TupleExpression extends AbstractSegment implements Expression {
+
+ private final List extends Expression> expressions;
+
+ private static Segment[] children(List extends Expression> expressions) {
+ return expressions.toArray(new Segment[0]);
+ }
+
+ private TupleExpression(List extends Expression> expressions) {
+
+ super(children(expressions));
+
+ this.expressions = expressions;
+ }
+
+ public static TupleExpression create(Expression... expressions) {
+ return new TupleExpression(List.of(expressions));
+ }
+
+ public static TupleExpression create(List extends Expression> expressions) {
+ return new TupleExpression(expressions);
+ }
+
+ public static Expression maybeWrap(List columns) {
+
+ if (columns.size() == 1) {
+ return columns.get(0);
+ }
+ return new TupleExpression(columns);
+ }
+
+ @Override
+ public String toString() {
+ return "(" + expressions.stream().map(Expression::toString).collect(joining(", ")) + ")";
+ }
+}
diff --git a/spring-data-relational/src/main/java/org/springframework/data/relational/core/sql/render/ExpressionVisitor.java b/spring-data-relational/src/main/java/org/springframework/data/relational/core/sql/render/ExpressionVisitor.java
index 65843cd3400..f4da4adc8d8 100644
--- a/spring-data-relational/src/main/java/org/springframework/data/relational/core/sql/render/ExpressionVisitor.java
+++ b/spring-data-relational/src/main/java/org/springframework/data/relational/core/sql/render/ExpressionVisitor.java
@@ -48,7 +48,7 @@ class ExpressionVisitor extends TypedSubtreeVisitor implements PartR
/**
* Creates an {@code ExpressionVisitor}.
*
- * @param context must not be {@literal null}.
+ * @param context must not be {@literal null}.
* @param aliasHandling controls if columns should be rendered as their alias or using their table names.
* @since 2.3
*/
@@ -78,6 +78,13 @@ Delegation enterMatched(Expression segment) {
return Delegation.delegateTo(visitor);
}
+ if (segment instanceof TupleExpression) {
+
+ TupleVisitor visitor = new TupleVisitor(context);
+ partRenderer = visitor;
+ return Delegation.delegateTo(visitor);
+ }
+
if (segment instanceof AnalyticFunction) {
AnalyticFunctionVisitor visitor = new AnalyticFunctionVisitor(context);
diff --git a/spring-data-relational/src/main/java/org/springframework/data/relational/core/sql/render/TupleVisitor.java b/spring-data-relational/src/main/java/org/springframework/data/relational/core/sql/render/TupleVisitor.java
new file mode 100644
index 00000000000..fef8d8f6886
--- /dev/null
+++ b/spring-data-relational/src/main/java/org/springframework/data/relational/core/sql/render/TupleVisitor.java
@@ -0,0 +1,72 @@
+/*
+ * Copyright 2019-2024 the original author or authors.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * https://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.springframework.data.relational.core.sql.render;
+
+import org.springframework.data.relational.core.sql.TupleExpression;
+import org.springframework.data.relational.core.sql.Visitable;
+
+/**
+ * Visitor for rendering tuple expressions.
+ *
+ * @author Jens Schauder
+ * @since 3.5
+ */
+class TupleVisitor extends TypedSingleConditionRenderSupport implements PartRenderer {
+
+ private final StringBuilder part = new StringBuilder();
+ private boolean needsComma = false;
+
+ TupleVisitor(RenderContext context) {
+ super(context);
+ }
+
+ @Override
+ Delegation leaveNested(Visitable segment) {
+
+ if (hasDelegatedRendering()) {
+
+ if (needsComma) {
+ part.append(", ");
+ }
+
+ part.append(consumeRenderedPart());
+ needsComma = true;
+ }
+
+ return super.leaveNested(segment);
+ }
+
+ @Override
+ Delegation enterMatched(TupleExpression segment) {
+
+ part.append("(");
+
+ return super.enterMatched(segment);
+ }
+
+ @Override
+ Delegation leaveMatched(TupleExpression segment) {
+
+ part.append(")");
+
+ return super.leaveMatched(segment);
+ }
+
+ @Override
+ public CharSequence getRenderedPart() {
+ return part;
+ }
+}
diff --git a/spring-data-relational/src/main/java/org/springframework/data/relational/core/sqlgeneration/SingleQuerySqlGenerator.java b/spring-data-relational/src/main/java/org/springframework/data/relational/core/sqlgeneration/SingleQuerySqlGenerator.java
index 6d65ce825e3..01f436c635a 100644
--- a/spring-data-relational/src/main/java/org/springframework/data/relational/core/sqlgeneration/SingleQuerySqlGenerator.java
+++ b/spring-data-relational/src/main/java/org/springframework/data/relational/core/sqlgeneration/SingleQuerySqlGenerator.java
@@ -167,9 +167,13 @@ private QueryMeta createInlineQuery(AggregatePath basePath, @Nullable Condition
columns.add(rownumber);
String rowCountAlias = aliases.getRowCountAlias(basePath);
- Expression count = basePath.isRoot() ? new AliasedExpression(SQL.literalOf(1), rowCountAlias)
- : AnalyticFunction.create("count", Expressions.just("*"))
- .partitionBy(table.column(basePath.getTableInfo().reverseColumnInfo().name())).as(rowCountAlias);
+ Expression count = basePath.isRoot() ? new AliasedExpression(SQL.literalOf(1), rowCountAlias) //
+ : AnalyticFunction.create("count", Expressions.just("*")) //
+ .partitionBy( //
+ basePath.getTableInfo().reverseColumnInfos().toList( //
+ ci -> table.column(ci.name()) //
+ ) //
+ ).as(rowCountAlias);
columns.add(count);
String backReferenceAlias = null;
@@ -178,7 +182,7 @@ private QueryMeta createInlineQuery(AggregatePath basePath, @Nullable Condition
if (!basePath.isRoot()) {
backReferenceAlias = aliases.getBackReferenceAlias(basePath);
- columns.add(table.column(basePath.getTableInfo().reverseColumnInfo().name()).as(backReferenceAlias));
+ columns.add(table.column(basePath.getTableInfo().reverseColumnInfos().unique().name()).as(backReferenceAlias));
keyAlias = aliases.getKeyAlias(basePath);
Expression keyExpression = basePath.isQualified()
@@ -238,9 +242,10 @@ private String getIdentifierProperty(List paths) {
private static AnalyticFunction createRowNumberExpression(AggregatePath basePath, Table table,
String rowNumberAlias) {
+ AggregatePath.ColumnInfos reverseColumnInfos = basePath.getTableInfo().reverseColumnInfos();
return AnalyticFunction.create("row_number") //
- .partitionBy(table.column(basePath.getTableInfo().reverseColumnInfo().name())) //
- .orderBy(table.column(basePath.getTableInfo().reverseColumnInfo().name())) //
+ .partitionBy(reverseColumnInfos.toList(ci -> table.column(ci.name()))) //
+ .orderBy(reverseColumnInfos.toList(ci -> table.column(ci.name()))) //
.as(rowNumberAlias);
}
diff --git a/spring-data-relational/src/test/java/org/springframework/data/relational/core/mapping/BasicRelationalPersistentEntityUnitTests.java b/spring-data-relational/src/test/java/org/springframework/data/relational/core/mapping/BasicRelationalPersistentEntityUnitTests.java
index 83f56e80121..fb6e822346a 100644
--- a/spring-data-relational/src/test/java/org/springframework/data/relational/core/mapping/BasicRelationalPersistentEntityUnitTests.java
+++ b/spring-data-relational/src/test/java/org/springframework/data/relational/core/mapping/BasicRelationalPersistentEntityUnitTests.java
@@ -175,8 +175,9 @@ private static class EntityWithSchemaAndName {
@Id private Long id;
}
- @Table(schema = "#{T(org.springframework.data.relational.core.mapping."
- + "BasicRelationalPersistentEntityUnitTests$EntityWithSchemaAndTableSpelExpression).desiredSchemaName}",
+ @Table(
+ schema = "#{T(org.springframework.data.relational.core.mapping."
+ + "BasicRelationalPersistentEntityUnitTests$EntityWithSchemaAndTableSpelExpression).desiredSchemaName}",
name = "#{T(org.springframework.data.relational.core.mapping."
+ "BasicRelationalPersistentEntityUnitTests$EntityWithSchemaAndTableSpelExpression).desiredTableName}")
private static class EntityWithSchemaAndTableSpelExpression {
@@ -185,10 +186,11 @@ private static class EntityWithSchemaAndTableSpelExpression {
public static String desiredSchemaName = "HELP_ME_OBI_WON";
}
- @Table(schema = "#{T(org.springframework.data.relational.core.mapping."
- + "BasicRelationalPersistentEntityUnitTests$LittleBobbyTables).desiredSchemaName}",
+ @Table(
+ schema = "#{T(org.springframework.data.relational.core.mapping."
+ + "BasicRelationalPersistentEntityUnitTests$LittleBobbyTables).desiredSchemaName}",
name = "#{T(org.springframework.data.relational.core.mapping."
- + "BasicRelationalPersistentEntityUnitTests$LittleBobbyTables).desiredTableName}")
+ + "BasicRelationalPersistentEntityUnitTests$LittleBobbyTables).desiredTableName}")
private static class LittleBobbyTables {
@Id private Long id;
public static String desiredTableName = "Robert'); DROP TABLE students;--";
diff --git a/spring-data-relational/src/test/java/org/springframework/data/relational/core/mapping/ColumnInfosUnitTests.java b/spring-data-relational/src/test/java/org/springframework/data/relational/core/mapping/ColumnInfosUnitTests.java
new file mode 100644
index 00000000000..54f0bf04820
--- /dev/null
+++ b/spring-data-relational/src/test/java/org/springframework/data/relational/core/mapping/ColumnInfosUnitTests.java
@@ -0,0 +1,102 @@
+/*
+ * Copyright 2024 the original author or authors.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * https://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.springframework.data.relational.core.mapping;
+
+import static org.assertj.core.api.Assertions.*;
+import static org.junit.jupiter.api.Assertions.*;
+
+import java.util.ArrayList;
+import java.util.List;
+import java.util.NoSuchElementException;
+
+import org.junit.jupiter.api.Test;
+import org.springframework.data.annotation.Id;
+import org.springframework.data.relational.core.sql.SqlIdentifier;
+
+/**
+ * Unit tests for the construction of {@link org.springframework.data.relational.core.mapping.AggregatePath.ColumnInfos}
+ *
+ * @author Jens Schauder
+ */
+class ColumnInfosUnitTests {
+
+ static final SqlIdentifier ID = SqlIdentifier.quoted("ID");
+ RelationalMappingContext context = new RelationalMappingContext();
+
+ @Test // GH-574
+ void emptyColumnInfos() {
+
+ AggregatePath.ColumnInfos columnInfos = AggregatePath.ColumnInfos.empty(basePath(DummyEntity.class));
+
+ assertThat(columnInfos.isEmpty()).isTrue();
+ assertThrows(NoSuchElementException.class, columnInfos::any);
+ assertThrows(IllegalStateException.class, columnInfos::unique);
+ assertThat(columnInfos.toList(ci -> {
+ throw new IllegalStateException("This should never get called");
+ })).isEmpty();
+ }
+
+ @Test // GH-574
+ void singleElementColumnInfos() {
+
+ AggregatePath.ColumnInfos columnInfos = basePath(DummyEntity.class).getTableInfo().idColumnInfos();
+
+ assertThat(columnInfos.isEmpty()).isFalse();
+ assertThat(columnInfos.any().name()).isEqualTo(ID);
+ assertThat(columnInfos.unique().name()).isEqualTo(ID);
+ assertThat(columnInfos.toList(ci -> ci.name())).containsExactly(ID);
+ }
+
+ @Test // GH-574
+ void multiElementColumnInfos() {
+
+ AggregatePath.ColumnInfos columnInfos = basePath(DummyEntityWithCompositeId.class).getTableInfo().idColumnInfos();
+
+ assertThat(columnInfos.isEmpty()).isFalse();
+ assertThat(columnInfos.any().name()).isEqualTo(SqlIdentifier.quoted("ONE"));
+ assertThrows(IllegalStateException.class, columnInfos::unique);
+ assertThat(columnInfos.toList(ci -> ci.name())) //
+ .containsExactly( //
+ SqlIdentifier.quoted("ONE"), //
+ SqlIdentifier.quoted("TWO") //
+ );
+
+ List collector = new ArrayList<>();
+ columnInfos.forEach((ap, ci) -> collector.add(ap.toDotPath() + "+" + ci.name()));
+ assertThat(collector).containsExactly("one+\"ONE\"", "two+\"TWO\"");
+
+ columnInfos.get(getPath(CompositeId.class, "one"));
+
+ }
+
+ private AggregatePath getPath(Class> type, String name) {
+ return basePath(type).append(context.getPersistentEntity(type).getPersistentProperty(name));
+ }
+
+ private AggregatePath basePath(Class> type) {
+ return context.getAggregatePath(context.getPersistentEntity(type));
+ }
+
+ record DummyEntity(@Id String id, String name) {
+ }
+
+ record CompositeId(String one, String two) {
+ }
+
+ record DummyEntityWithCompositeId(@Id @Embedded.Nullable CompositeId id, String name) {
+ }
+}
diff --git a/spring-data-relational/src/test/java/org/springframework/data/relational/core/mapping/DefaultAggregatePathUnitTests.java b/spring-data-relational/src/test/java/org/springframework/data/relational/core/mapping/DefaultAggregatePathUnitTests.java
index 837cec98328..4efea298a17 100644
--- a/spring-data-relational/src/test/java/org/springframework/data/relational/core/mapping/DefaultAggregatePathUnitTests.java
+++ b/spring-data-relational/src/test/java/org/springframework/data/relational/core/mapping/DefaultAggregatePathUnitTests.java
@@ -22,6 +22,8 @@
import java.util.List;
import java.util.Map;
+import java.util.Set;
+import java.util.TreeSet;
import java.util.stream.Collectors;
import org.junit.jupiter.api.Test;
@@ -62,9 +64,9 @@ void getParentPath() {
assertSoftly(softly -> {
- softly.assertThat(path("second.third2.value").getParentPath()).isEqualTo(path("second.third2"));
- softly.assertThat(path("second.third2").getParentPath()).isEqualTo(path("second"));
- softly.assertThat(path("second").getParentPath()).isEqualTo(path());
+ softly.assertThat((Object) path("second.third2.value").getParentPath()).isEqualTo(path("second.third2"));
+ softly.assertThat((Object) path("second.third2").getParentPath()).isEqualTo(path("second"));
+ softly.assertThat((Object) path("second").getParentPath()).isEqualTo(path());
softly.assertThatThrownBy(() -> path().getParentPath()).isInstanceOf(IllegalStateException.class);
});
@@ -94,14 +96,15 @@ void idDefiningPath() {
assertSoftly(softly -> {
- softly.assertThat(path("second.third2.value").getIdDefiningParentPath()).isEqualTo(path());
- softly.assertThat(path("second.third.value").getIdDefiningParentPath()).isEqualTo(path());
- softly.assertThat(path("secondList.third2.value").getIdDefiningParentPath()).isEqualTo(path());
- softly.assertThat(path("secondList.third.value").getIdDefiningParentPath()).isEqualTo(path());
- softly.assertThat(path("second2.third2.value").getIdDefiningParentPath()).isEqualTo(path());
- softly.assertThat(path("second2.third.value").getIdDefiningParentPath()).isEqualTo(path());
- softly.assertThat(path("withId.second.third2.value").getIdDefiningParentPath()).isEqualTo(path("withId"));
- softly.assertThat(path("withId.second.third.value").getIdDefiningParentPath()).isEqualTo(path("withId"));
+ softly.assertThat((Object) path("second.third2.value").getIdDefiningParentPath()).isEqualTo(path());
+ softly.assertThat((Object) path("second.third.value").getIdDefiningParentPath()).isEqualTo(path());
+ softly.assertThat((Object) path("secondList.third2.value").getIdDefiningParentPath()).isEqualTo(path());
+ softly.assertThat((Object) path("secondList.third.value").getIdDefiningParentPath()).isEqualTo(path());
+ softly.assertThat((Object) path("second2.third2.value").getIdDefiningParentPath()).isEqualTo(path());
+ softly.assertThat((Object) path("second2.third.value").getIdDefiningParentPath()).isEqualTo(path());
+ softly.assertThat((Object) path("withId.second.third2.value").getIdDefiningParentPath())
+ .isEqualTo(path("withId"));
+ softly.assertThat((Object) path("withId.second.third.value").getIdDefiningParentPath()).isEqualTo(path("withId"));
});
}
@@ -121,13 +124,13 @@ void reverseColumnName() {
assertSoftly(softly -> {
- softly.assertThat(path("second.third2").getTableInfo().reverseColumnInfo().name())
+ softly.assertThat((Object) path("second.third2").getTableInfo().reverseColumnInfo().name())
.isEqualTo(quoted("DUMMY_ENTITY"));
- softly.assertThat(path("second.third").getTableInfo().reverseColumnInfo().name())
+ softly.assertThat((Object) path("second.third").getTableInfo().reverseColumnInfo().name())
.isEqualTo(quoted("DUMMY_ENTITY"));
- softly.assertThat(path("secondList.third2").getTableInfo().reverseColumnInfo().name())
+ softly.assertThat((Object) path("secondList.third2").getTableInfo().reverseColumnInfo().name())
.isEqualTo(quoted("DUMMY_ENTITY"));
- softly.assertThat(path("secondList.third").getTableInfo().reverseColumnInfo().name())
+ softly.assertThat((Object) path("secondList.third").getTableInfo().reverseColumnInfo().name())
.isEqualTo(quoted("DUMMY_ENTITY"));
softly.assertThat(path("second2.third").getTableInfo().reverseColumnInfo().name())
.isEqualTo(quoted("DUMMY_ENTITY"));
@@ -140,6 +143,17 @@ void reverseColumnName() {
});
}
+ @Test // GH-574
+ void reverseColumnNames() {
+
+ assertSoftly(softly -> {
+ softly.assertThat(path(CompoundIdEntity.class, "second").getTableInfo().reverseColumnInfos().toList(x -> x))
+ .extracting(AggregatePath.ColumnInfo::name)
+ .containsExactlyInAnyOrder(quoted("COMPOUND_ID_ENTITY_ONE"), quoted("COMPOUND_ID_ENTITY_TWO"));
+
+ });
+ }
+
@Test // GH-1525
void getQualifierColumn() {
@@ -172,8 +186,9 @@ void extendBy() {
assertSoftly(softly -> {
- softly.assertThat(path().append(entity.getRequiredPersistentProperty("withId"))).isEqualTo(path("withId"));
- softly.assertThat(path("withId").append(path("withId").getRequiredIdProperty()))
+ softly.assertThat((Object) path().append(entity.getRequiredPersistentProperty("withId")))
+ .isEqualTo(path("withId"));
+ softly.assertThat((Object) path("withId").append(path("withId").getRequiredIdProperty()))
.isEqualTo(path("withId.withIdId"));
});
}
@@ -229,9 +244,9 @@ void isMultiValued() {
softly.assertThat(path("second").isMultiValued()).isFalse();
softly.assertThat(path("second.third2").isMultiValued()).isFalse();
softly.assertThat(path("secondList.third2").isMultiValued()).isTrue(); // this seems wrong as third2 is an
- // embedded path into Second, held by
- // List (so the parent is
- // multi-valued but not third2).
+ // embedded path into Second, held by
+ // List (so the parent is
+ // multi-valued but not third2).
// TODO: This test fails because MultiValued considers parents.
// softly.assertThat(path("secondList.third.value").isMultiValued()).isFalse();
softly.assertThat(path("secondList").isMultiValued()).isTrue();
@@ -306,13 +321,13 @@ void getTableAlias() {
softly.assertThat(path("second.third2").getTableInfo().tableAlias()).isEqualTo(quoted("second"));
softly.assertThat(path("second.third2.value").getTableInfo().tableAlias()).isEqualTo(quoted("second"));
softly.assertThat(path("second.third").getTableInfo().tableAlias()).isEqualTo(quoted("second_third")); // missing
- // _
+ // _
softly.assertThat(path("second.third.value").getTableInfo().tableAlias()).isEqualTo(quoted("second_third")); // missing
- // _
+ // _
softly.assertThat(path("secondList.third2").getTableInfo().tableAlias()).isEqualTo(quoted("secondList"));
softly.assertThat(path("secondList.third2.value").getTableInfo().tableAlias()).isEqualTo(quoted("secondList"));
softly.assertThat(path("secondList.third").getTableInfo().tableAlias()).isEqualTo(quoted("secondList_third")); // missing
- // _
+ // _
softly.assertThat(path("secondList.third.value").getTableInfo().tableAlias())
.isEqualTo(quoted("secondList_third")); // missing _
softly.assertThat(path("secondList").getTableInfo().tableAlias()).isEqualTo(quoted("secondList"));
@@ -416,20 +431,6 @@ void getBaseProperty() {
});
}
- @Test // GH-1525
- void getIdColumnName() {
-
- assertSoftly(softly -> {
-
- softly.assertThat(path().getTableInfo().idColumnName()).isEqualTo(quoted("ENTITY_ID"));
- softly.assertThat(path("withId").getTableInfo().idColumnName()).isEqualTo(quoted("WITH_ID_ID"));
-
- softly.assertThat(path("second").getTableInfo().idColumnName()).isNull();
- softly.assertThat(path("second.third2").getTableInfo().idColumnName()).isNull();
- softly.assertThat(path("withId.second").getTableInfo().idColumnName()).isNull();
- });
- }
-
@Test // GH-1525
void toDotPath() {
@@ -452,43 +453,89 @@ void getRequiredPersistentPropertyPath() {
});
}
- @Test // GH-1525
- void getEffectiveIdColumnName() {
+ @Test
+ // GH-1525
+ void getLength() {
assertSoftly(softly -> {
+ softly.assertThat(path().getLength()).isEqualTo(1);
+ softly.assertThat(path().stream().collect(Collectors.toList())).hasSize(1);
- softly.assertThat(path().getTableInfo().effectiveIdColumnName()).isEqualTo(quoted("ENTITY_ID"));
- softly.assertThat(path("second.third2").getTableInfo().effectiveIdColumnName()).isEqualTo(quoted("DUMMY_ENTITY"));
- softly.assertThat(path("withId.second.third").getTableInfo().effectiveIdColumnName())
- .isEqualTo(quoted("WITH_ID"));
- softly.assertThat(path("withId.second.third2.value").getTableInfo().effectiveIdColumnName())
- .isEqualTo(quoted("WITH_ID"));
+ softly.assertThat(path("second.third2").getLength()).isEqualTo(3);
+ softly.assertThat(path("second.third2").stream().collect(Collectors.toList())).hasSize(3);
+
+ softly.assertThat(path("withId.second.third").getLength()).isEqualTo(4);
+ softly.assertThat(path("withId.second.third2.value").getLength()).isEqualTo(5);
});
}
- @Test // GH-1525
- void getLength() {
+ @Test // GH-574
+ void getTail() {
- assertThat(path().getLength()).isEqualTo(1);
- assertThat(path().stream().collect(Collectors.toList())).hasSize(1);
+ assertSoftly(softly -> {
- assertThat(path("second.third2").getLength()).isEqualTo(3);
- assertThat(path("second.third2").stream().collect(Collectors.toList())).hasSize(3);
+ softly.assertThat((Object) path().getTail()).isEqualTo(null);
+ softly.assertThat((Object) path("second").getTail()).isEqualTo(null);
+ softly.assertThat(path("second.third").getTail().toDotPath()).isEqualTo("third");
+ softly.assertThat(path("second.third.value").getTail().toDotPath()).isEqualTo("third.value");
+ });
+ }
+
+ @Test // GH-74
+ void append() {
+
+ assertSoftly(softly -> {
+
+ softly.assertThat(path("second").append(path()).toDotPath()).isEqualTo("second");
+ softly.assertThat(path().append(path("second")).toDotPath()).isEqualTo("second");
+ softly.assertThat(path().append(path("second.third")).toDotPath()).isEqualTo("second.third");
+ AggregatePath value = path("second.third.value").getTail().getTail();
+ softly.assertThat(path("second.third").append(value).toDotPath()).isEqualTo("second.third.value");
+ });
+ }
+
+ @Test // GH-574
+ void sortPaths() {
+
+ Set sorted = new TreeSet<>();
+
+ AggregatePath alpha = path();
+ AggregatePath as = path("second");
+ AggregatePath ast = path("second.third");
+ AggregatePath aw = path("withId");
+
+ sorted.add(aw);
+ sorted.add(ast);
+ sorted.add(as);
+ sorted.add(alpha);
+
+ assertThat(sorted).containsExactly(alpha, as, ast, aw);
- assertThat(path("withId.second.third").getLength()).isEqualTo(4);
- assertThat(path("withId.second.third2.value").getLength()).isEqualTo(5);
}
private AggregatePath path() {
return context.getAggregatePath(entity);
}
+ private AggregatePath path(RelationalPersistentEntity> entity) {
+ return context.getAggregatePath(entity);
+ }
+
+ private AggregatePath path(Class> entityType, String path) {
+ return context.getAggregatePath(createSimplePath(entityType, path));
+ }
+
private AggregatePath path(String path) {
return context.getAggregatePath(createSimplePath(path));
}
PersistentPropertyPath createSimplePath(String path) {
- return PersistentPropertyPathTestUtils.getPath(context, path, DummyEntity.class);
+ return createSimplePath(entity.getType(), path);
+ }
+
+ PersistentPropertyPath createSimplePath(Class> entityType, String path) {
+
+ return PersistentPropertyPathTestUtils.getPath(context, path, entityType);
}
@SuppressWarnings("unused")
@@ -502,6 +549,12 @@ static class DummyEntity {
WithId withId;
}
+ record CompoundId(Long one, String two) {
+ }
+
+ record CompoundIdEntity(@Id CompoundId id, Second second) {
+ }
+
@SuppressWarnings("unused")
static class Second {
Third third;
diff --git a/spring-data-relational/src/test/java/org/springframework/data/relational/core/mapping/RelationalMappingContextUnitTests.java b/spring-data-relational/src/test/java/org/springframework/data/relational/core/mapping/RelationalMappingContextUnitTests.java
index 14316048e41..f2b04d70b7f 100644
--- a/spring-data-relational/src/test/java/org/springframework/data/relational/core/mapping/RelationalMappingContextUnitTests.java
+++ b/spring-data-relational/src/test/java/org/springframework/data/relational/core/mapping/RelationalMappingContextUnitTests.java
@@ -60,7 +60,7 @@ public void canObtainAggregatePath() {
EntityWithUuid.class);
AggregatePath aggregatePath = context.getAggregatePath(path);
- assertThat(aggregatePath).isNotNull();
+ assertThat((Object) aggregatePath).isNotNull();
}
@Test // GH-1525
@@ -75,7 +75,7 @@ public void innerAggregatePathsGetCached() {
AggregatePath one = context.getAggregatePath(path);
AggregatePath two = context.getAggregatePath(path);
- assertThat(one).isSameAs(two);
+ assertThat((Object) one).isSameAs(two);
}
@Test // GH-1525
@@ -87,7 +87,7 @@ public void rootAggregatePathsGetCached() {
AggregatePath one = context.getAggregatePath(context.getRequiredPersistentEntity(EntityWithUuid.class));
AggregatePath two = context.getAggregatePath(context.getRequiredPersistentEntity(EntityWithUuid.class));
- assertThat(one).isSameAs(two);
+ assertThat((Object) one).isSameAs(two);
}
@Test // GH-1586
@@ -117,7 +117,7 @@ void aggregatePathsOfBasePropertyForDifferentInheritedEntitiesAreDifferent() {
AggregatePath aggregatePath1 = context.getAggregatePath(path1);
AggregatePath aggregatePath2 = context.getAggregatePath(path2);
- assertThat(aggregatePath1).isNotEqualTo(aggregatePath2);
+ assertThat((Object) aggregatePath1).isNotEqualTo(aggregatePath2);
}
static class EntityWithUuid {
@@ -128,6 +128,14 @@ static class WithEmbedded {
@Embedded.Empty(prefix = "prnt_") Parent parent;
}
+ static class WithEmbeddedId {
+ @Embedded.Nullable
+ @Id CompositeId id;
+ }
+
+ private record CompositeId(int a, int b) {
+ }
+
static class Parent {
@Embedded.Empty(prefix = "chld_") Child child;
@@ -144,5 +152,4 @@ static class Base {
static class Inherit1 extends Base {}
static class Inherit2 extends Base {}
-
}
diff --git a/spring-data-relational/src/test/java/org/springframework/data/relational/core/sql/TupleExpressionUnitTests.java b/spring-data-relational/src/test/java/org/springframework/data/relational/core/sql/TupleExpressionUnitTests.java
new file mode 100644
index 00000000000..b69a9ee10eb
--- /dev/null
+++ b/spring-data-relational/src/test/java/org/springframework/data/relational/core/sql/TupleExpressionUnitTests.java
@@ -0,0 +1,48 @@
+/*
+ * Copyright 2024 the original author or authors.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * https://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.springframework.data.relational.core.sql;
+
+import static org.assertj.core.api.Assertions.*;
+
+import java.util.List;
+
+import org.junit.jupiter.api.Test;
+
+class TupleExpressionUnitTests {
+
+ @Test // GH-574
+ void singleExpressionDoesNotGetWrapped() {
+
+ Column testColumn = Column.create("name", Table.create("employee"));
+
+ Expression wrapped = TupleExpression.maybeWrap(List.of(testColumn));
+
+ assertThat(wrapped).isSameAs(testColumn);
+ }
+
+ @Test // GH-574
+ void multipleExpressionsDoGetWrapped() {
+
+ Column testColumn1 = Column.create("first", Table.create("employee"));
+ Column testColumn2 = Column.create("last", Table.create("employee"));
+
+ Expression wrapped = TupleExpression.maybeWrap(List.of(testColumn1, testColumn2));
+
+ assertThat(wrapped).isInstanceOf(TupleExpression.class);
+ }
+
+}
diff --git a/spring-data-relational/src/test/java/org/springframework/data/relational/core/sql/render/DeleteRendererUnitTests.java b/spring-data-relational/src/test/java/org/springframework/data/relational/core/sql/render/DeleteRendererUnitTests.java
index 737932c5131..0b9dc35e160 100644
--- a/spring-data-relational/src/test/java/org/springframework/data/relational/core/sql/render/DeleteRendererUnitTests.java
+++ b/spring-data-relational/src/test/java/org/springframework/data/relational/core/sql/render/DeleteRendererUnitTests.java
@@ -27,10 +27,10 @@
*
* @author Mark Paluch
*/
-public class DeleteRendererUnitTests {
+class DeleteRendererUnitTests {
@Test // DATAJDBC-335
- public void shouldRenderWithoutWhere() {
+ void shouldRenderWithoutWhere() {
Table bar = SQL.table("bar");
@@ -40,7 +40,7 @@ public void shouldRenderWithoutWhere() {
}
@Test // DATAJDBC-335
- public void shouldRenderWithCondition() {
+ void shouldRenderWithCondition() {
Table table = Table.create("bar");
@@ -52,7 +52,7 @@ public void shouldRenderWithCondition() {
}
@Test // DATAJDBC-335
- public void shouldConsiderTableAlias() {
+ void shouldConsiderTableAlias() {
Table table = Table.create("bar").as("my_bar");
diff --git a/spring-data-relational/src/test/java/org/springframework/data/relational/core/sql/render/SelectRendererUnitTests.java b/spring-data-relational/src/test/java/org/springframework/data/relational/core/sql/render/SelectRendererUnitTests.java
index 2ec941640ab..ad0ac531abd 100644
--- a/spring-data-relational/src/test/java/org/springframework/data/relational/core/sql/render/SelectRendererUnitTests.java
+++ b/spring-data-relational/src/test/java/org/springframework/data/relational/core/sql/render/SelectRendererUnitTests.java
@@ -17,6 +17,8 @@
import static org.assertj.core.api.Assertions.*;
+import java.util.List;
+
import org.junit.jupiter.api.Nested;
import org.junit.jupiter.api.Test;
import org.springframework.data.relational.core.dialect.PostgresDialect;
@@ -24,8 +26,6 @@
import org.springframework.data.relational.core.sql.*;
import org.springframework.util.StringUtils;
-import java.util.List;
-
/**
* Unit tests for {@link SqlRenderer}.
*
@@ -115,196 +115,6 @@ void shouldRenderCountFunctionWithAliasedColumn() {
assertThat(SqlRenderer.toString(select)).isEqualTo("SELECT COUNT(bar.foo), bar.foo AS foo_bar FROM bar");
}
- @Test // DATAJDBC-309
- void shouldRenderSimpleJoin() {
-
- Table employee = SQL.table("employee");
- Table department = SQL.table("department");
-
- Select select = Select.builder().select(employee.column("id"), department.column("name")).from(employee) //
- .join(department).on(employee.column("department_id")).equals(department.column("id")) //
- .build();
-
- assertThat(SqlRenderer.toString(select)).isEqualTo("SELECT employee.id, department.name FROM employee "
- + "JOIN department ON employee.department_id = department.id");
- }
-
- @Test // DATAJDBC-340
- void shouldRenderOuterJoin() {
-
- Table employee = SQL.table("employee");
- Table department = SQL.table("department");
-
- Select select = Select.builder().select(employee.column("id"), department.column("name")) //
- .from(employee) //
- .leftOuterJoin(department).on(employee.column("department_id")).equals(department.column("id")) //
- .build();
-
- assertThat(SqlRenderer.toString(select)).isEqualTo("SELECT employee.id, department.name FROM employee "
- + "LEFT OUTER JOIN department ON employee.department_id = department.id");
- }
-
- @Test // GH-1421
- void shouldRenderFullOuterJoin() {
-
- Table employee = SQL.table("employee");
- Table department = SQL.table("department");
-
- Select select = Select.builder().select(employee.column("id"), department.column("name")) //
- .from(employee) //
- .join(department, Join.JoinType.FULL_OUTER_JOIN).on(employee.column("department_id"))
- .equals(department.column("id")) //
- .build();
-
- assertThat(SqlRenderer.toString(select)).isEqualTo("SELECT employee.id, department.name FROM employee "
- + "FULL OUTER JOIN department ON employee.department_id = department.id");
- }
-
- @Test // DATAJDBC-309
- void shouldRenderSimpleJoinWithAnd() {
-
- Table employee = SQL.table("employee");
- Table department = SQL.table("department");
-
- Select select = Select.builder().select(employee.column("id"), department.column("name")).from(employee) //
- .join(department).on(employee.column("department_id")).equals(department.column("id")) //
- .and(employee.column("tenant")).equals(department.column("tenant")) //
- .build();
-
- assertThat(SqlRenderer.toString(select)).isEqualTo("SELECT employee.id, department.name FROM employee " //
- + "JOIN department ON employee.department_id = department.id " //
- + "AND employee.tenant = department.tenant");
- }
-
- @Test // #995
- void shouldRenderArbitraryJoinCondition() {
-
- Table employee = SQL.table("employee");
- Table department = SQL.table("department");
-
- Select select = Select.builder() //
- .select(employee.column("id"), department.column("name")) //
- .from(employee) //
- .join(department) //
- .on(Conditions.isEqual(employee.column("department_id"), department.column("id")) //
- .or(Conditions.isNotEqual(employee.column("tenant"), department.column("tenant")) //
- )).build();
-
- assertThat(SqlRenderer.toString(select)).isEqualTo("SELECT employee.id, department.name FROM employee " //
- + "JOIN department ON employee.department_id = department.id " //
- + "OR employee.tenant != department.tenant");
- }
-
- @Test // #1009
- void shouldRenderJoinWithJustExpression() {
-
- Table employee = SQL.table("employee");
- Table department = SQL.table("department");
-
- Select select = Select.builder().select(employee.column("id"), department.column("name")).from(employee) //
- .join(department).on(Expressions.just("alpha")).equals(Expressions.just("beta")) //
- .build();
-
- assertThat(SqlRenderer.toString(select))
- .isEqualTo("SELECT employee.id, department.name FROM employee " + "JOIN department ON alpha = beta");
- }
-
- @Test // DATAJDBC-309
- void shouldRenderMultipleJoinWithAnd() {
-
- Table employee = SQL.table("employee");
- Table department = SQL.table("department");
- Table tenant = SQL.table("tenant").as("tenant_base");
-
- Select select = Select.builder().select(employee.column("id"), department.column("name")).from(employee) //
- .join(department).on(employee.column("department_id")).equals(department.column("id")) //
- .and(employee.column("tenant")).equals(department.column("tenant")) //
- .join(tenant).on(tenant.column("tenant_id")).equals(department.column("tenant")) //
- .build();
-
- assertThat(SqlRenderer.toString(select)).isEqualTo("SELECT employee.id, department.name FROM employee " //
- + "JOIN department ON employee.department_id = department.id " //
- + "AND employee.tenant = department.tenant " //
- + "JOIN tenant tenant_base ON tenant_base.tenant_id = department.tenant");
- }
-
- @Test // GH-1003
- void shouldRenderJoinWithInlineQuery() {
-
- Table employee = SQL.table("employee");
- Table department = SQL.table("department");
-
- Select innerSelect = Select.builder()
- .select(employee.column("id"), employee.column("department_Id"), employee.column("name")).from(employee)
- .build();
-
- InlineQuery one = InlineQuery.create(innerSelect, "one");
-
- Select select = Select.builder().select(one.column("id"), department.column("name")).from(department) //
- .join(one).on(one.column("department_id")).equals(department.column("id")) //
- .build();
-
- String sql = SqlRenderer.toString(select);
-
- assertThat(sql).isEqualTo("SELECT one.id, department.name FROM department " //
- + "JOIN (SELECT employee.id, employee.department_Id, employee.name FROM employee) one " //
- + "ON one.department_id = department.id");
- }
-
- @Test // GH-1362
- void shouldRenderNestedJoins() {
-
- Table merchantCustomers = Table.create("merchants_customers");
- Table customerDetails = Table.create("customer_details");
-
- Select innerSelect = Select.builder().select(customerDetails.column("cd_user_id")).from(customerDetails)
- .join(merchantCustomers)
- .on(merchantCustomers.column("mc_user_id").isEqualTo(customerDetails.column("cd_user_id"))).build();
-
- InlineQuery innerTable = InlineQuery.create(innerSelect, "inner");
-
- Select select = Select.builder().select(merchantCustomers.asterisk()) //
- .from(merchantCustomers) //
- .join(innerTable).on(innerTable.column("i_user_id").isEqualTo(merchantCustomers.column("mc_user_id"))) //
- .build();
-
- String sql = SqlRenderer.toString(select);
-
- assertThat(sql).isEqualTo("SELECT merchants_customers.* FROM merchants_customers " + //
- "JOIN (" + //
- "SELECT customer_details.cd_user_id " + //
- "FROM customer_details " + //
- "JOIN merchants_customers ON merchants_customers.mc_user_id = customer_details.cd_user_id" + //
- ") inner " + //
- "ON inner.i_user_id = merchants_customers.mc_user_id");
- }
-
- @Test // GH-1003
- void shouldRenderJoinWithTwoInlineQueries() {
-
- Table employee = SQL.table("employee");
- Table department = SQL.table("department");
-
- Select innerSelectOne = Select.builder()
- .select(employee.column("id").as("empId"), employee.column("department_Id"), employee.column("name"))
- .from(employee).build();
- Select innerSelectTwo = Select.builder().select(department.column("id"), department.column("name")).from(department)
- .build();
-
- InlineQuery one = InlineQuery.create(innerSelectOne, "one");
- InlineQuery two = InlineQuery.create(innerSelectTwo, "two");
-
- Select select = Select.builder().select(one.column("empId"), two.column("name")).from(one) //
- .join(two).on(two.column("department_id")).equals(one.column("empId")) //
- .build();
-
- String sql = SqlRenderer.toString(select);
- assertThat(sql).isEqualTo("SELECT one.empId, two.name FROM (" //
- + "SELECT employee.id AS empId, employee.department_Id, employee.name FROM employee) one " //
- + "JOIN (SELECT department.id, department.name FROM department) two " //
- + "ON two.department_id = one.empId");
- }
-
@Test // DATAJDBC-309
void shouldRenderOrderByName() {
@@ -424,7 +234,6 @@ void shouldRenderSimpleFunctionWithSubselect() {
Table floo = SQL.table("floo");
Column bah = floo.column("bah");
-
Select subselect = Select.builder().select(bah).from(floo).build();
SimpleFunction func = SimpleFunction.create("func", List.of(SubselectExpression.of(subselect)));
@@ -435,8 +244,8 @@ void shouldRenderSimpleFunctionWithSubselect() {
.where(Conditions.isEqual(func, SQL.literalOf(23))) //
.build();
- assertThat(SqlRenderer.toString(select))
- .isEqualTo("SELECT func(SELECT floo.bah FROM floo) AS alias FROM foo WHERE func(SELECT floo.bah FROM floo) = 23");
+ assertThat(SqlRenderer.toString(select)).isEqualTo(
+ "SELECT func(SELECT floo.bah FROM floo) AS alias FROM foo WHERE func(SELECT floo.bah FROM floo) = 23");
}
@Test // DATAJDBC-309
@@ -709,7 +518,7 @@ void asteriskOfAliasedTableUsesAlias() {
assertThat(rendered).isEqualTo("SELECT e.*, e.id FROM employee e");
}
- @Test
+ @Test // GH-1844
void rendersCaseExpression() {
Table table = SQL.table("table");
@@ -724,7 +533,225 @@ void rendersCaseExpression() {
.build();
String rendered = SqlRenderer.toString(select);
- assertThat(rendered).isEqualTo("SELECT CASE WHEN table.name IS NULL THEN 1 WHEN table.name IS NOT NULL THEN table.name ELSE 3 END FROM table");
+ assertThat(rendered).isEqualTo(
+ "SELECT CASE WHEN table.name IS NULL THEN 1 WHEN table.name IS NOT NULL THEN table.name ELSE 3 END FROM table");
+ }
+
+ @Test // GH-574
+ void rendersTupleExpression() {
+
+ Table table = SQL.table("table");
+ Column first = table.column("first");
+ Column middle = table.column("middle");
+ Column last = table.column("last").as("anAlias");
+
+ TupleExpression tupleExpression = TupleExpression.create(first, SQL.literalOf(1), middle, last); //
+
+ Select select = StatementBuilder.select(first) //
+ .from(table) //
+ .where(Conditions.in(tupleExpression, Expressions.just("some expression"))).build();
+
+ String rendered = SqlRenderer.toString(select);
+ assertThat(rendered).isEqualTo(
+ "SELECT table.first FROM table WHERE (table.first, 1, table.middle, table.last) IN (some expression)");
+ }
+
+ /**
+ * Tests for rendering joins.
+ */
+ @Nested
+ class JoinsTests {
+
+ @Test // DATAJDBC-309
+ void shouldRenderSimpleJoin() {
+
+ Table employee = SQL.table("employee");
+ Table department = SQL.table("department");
+
+ Select select = Select.builder().select(employee.column("id"), department.column("name")).from(employee) //
+ .join(department).on(employee.column("department_id")).equals(department.column("id")) //
+ .build();
+
+ assertThat(SqlRenderer.toString(select)).isEqualTo("SELECT employee.id, department.name FROM employee "
+ + "JOIN department ON employee.department_id = department.id");
+ }
+
+ @Test // DATAJDBC-340
+ void shouldRenderOuterJoin() {
+
+ Table employee = SQL.table("employee");
+ Table department = SQL.table("department");
+
+ Select select = Select.builder().select(employee.column("id"), department.column("name")) //
+ .from(employee) //
+ .leftOuterJoin(department).on(employee.column("department_id")).equals(department.column("id")) //
+ .build();
+
+ assertThat(SqlRenderer.toString(select)).isEqualTo("SELECT employee.id, department.name FROM employee "
+ + "LEFT OUTER JOIN department ON employee.department_id = department.id");
+ }
+
+ @Test // GH-1421
+ void shouldRenderFullOuterJoin() {
+
+ Table employee = SQL.table("employee");
+ Table department = SQL.table("department");
+
+ Select select = Select.builder().select(employee.column("id"), department.column("name")) //
+ .from(employee) //
+ .join(department, Join.JoinType.FULL_OUTER_JOIN).on(employee.column("department_id"))
+ .equals(department.column("id")) //
+ .build();
+
+ assertThat(SqlRenderer.toString(select)).isEqualTo("SELECT employee.id, department.name FROM employee "
+ + "FULL OUTER JOIN department ON employee.department_id = department.id");
+ }
+
+ @Test // DATAJDBC-309
+ void shouldRenderSimpleJoinWithAnd() {
+
+ Table employee = SQL.table("employee");
+ Table department = SQL.table("department");
+
+ Select select = Select.builder().select(employee.column("id"), department.column("name")).from(employee) //
+ .join(department).on(employee.column("department_id")).equals(department.column("id")) //
+ .and(employee.column("tenant")).equals(department.column("tenant")) //
+ .build();
+
+ assertThat(SqlRenderer.toString(select)).isEqualTo("SELECT employee.id, department.name FROM employee " //
+ + "JOIN department ON employee.department_id = department.id " //
+ + "AND employee.tenant = department.tenant");
+ }
+
+ @Test // #995
+ void shouldRenderArbitraryJoinCondition() {
+
+ Table employee = SQL.table("employee");
+ Table department = SQL.table("department");
+
+ Select select = Select.builder() //
+ .select(employee.column("id"), department.column("name")) //
+ .from(employee) //
+ .join(department) //
+ .on(Conditions.isEqual(employee.column("department_id"), department.column("id")) //
+ .or(Conditions.isNotEqual(employee.column("tenant"), department.column("tenant")) //
+ )).build();
+
+ assertThat(SqlRenderer.toString(select)).isEqualTo("SELECT employee.id, department.name FROM employee " //
+ + "JOIN department ON employee.department_id = department.id " //
+ + "OR employee.tenant != department.tenant");
+ }
+
+ @Test // #1009
+ void shouldRenderJoinWithJustExpression() {
+
+ Table employee = SQL.table("employee");
+ Table department = SQL.table("department");
+
+ Select select = Select.builder().select(employee.column("id"), department.column("name")).from(employee) //
+ .join(department).on(Expressions.just("alpha")).equals(Expressions.just("beta")) //
+ .build();
+
+ assertThat(SqlRenderer.toString(select))
+ .isEqualTo("SELECT employee.id, department.name FROM employee " + "JOIN department ON alpha = beta");
+ }
+
+ @Test // DATAJDBC-309
+ void shouldRenderMultipleJoinWithAnd() {
+
+ Table employee = SQL.table("employee");
+ Table department = SQL.table("department");
+ Table tenant = SQL.table("tenant").as("tenant_base");
+
+ Select select = Select.builder().select(employee.column("id"), department.column("name")).from(employee) //
+ .join(department).on(employee.column("department_id")).equals(department.column("id")) //
+ .and(employee.column("tenant")).equals(department.column("tenant")) //
+ .join(tenant).on(tenant.column("tenant_id")).equals(department.column("tenant")) //
+ .build();
+
+ assertThat(SqlRenderer.toString(select)).isEqualTo("SELECT employee.id, department.name FROM employee " //
+ + "JOIN department ON employee.department_id = department.id " //
+ + "AND employee.tenant = department.tenant " //
+ + "JOIN tenant tenant_base ON tenant_base.tenant_id = department.tenant");
+ }
+
+ @Test // GH-1003
+ void shouldRenderJoinWithInlineQuery() {
+
+ Table employee = SQL.table("employee");
+ Table department = SQL.table("department");
+
+ Select innerSelect = Select.builder()
+ .select(employee.column("id"), employee.column("department_Id"), employee.column("name")).from(employee)
+ .build();
+
+ InlineQuery one = InlineQuery.create(innerSelect, "one");
+
+ Select select = Select.builder().select(one.column("id"), department.column("name")).from(department) //
+ .join(one).on(one.column("department_id")).equals(department.column("id")) //
+ .build();
+
+ String sql = SqlRenderer.toString(select);
+
+ assertThat(sql).isEqualTo("SELECT one.id, department.name FROM department " //
+ + "JOIN (SELECT employee.id, employee.department_Id, employee.name FROM employee) one " //
+ + "ON one.department_id = department.id");
+ }
+
+ @Test // GH-1362
+ void shouldRenderNestedJoins() {
+
+ Table merchantCustomers = Table.create("merchants_customers");
+ Table customerDetails = Table.create("customer_details");
+
+ Select innerSelect = Select.builder().select(customerDetails.column("cd_user_id")).from(customerDetails)
+ .join(merchantCustomers)
+ .on(merchantCustomers.column("mc_user_id").isEqualTo(customerDetails.column("cd_user_id"))).build();
+
+ InlineQuery innerTable = InlineQuery.create(innerSelect, "inner");
+
+ Select select = Select.builder().select(merchantCustomers.asterisk()) //
+ .from(merchantCustomers) //
+ .join(innerTable).on(innerTable.column("i_user_id").isEqualTo(merchantCustomers.column("mc_user_id"))) //
+ .build();
+
+ String sql = SqlRenderer.toString(select);
+
+ assertThat(sql).isEqualTo("SELECT merchants_customers.* FROM merchants_customers " + //
+ "JOIN (" + //
+ "SELECT customer_details.cd_user_id " + //
+ "FROM customer_details " + //
+ "JOIN merchants_customers ON merchants_customers.mc_user_id = customer_details.cd_user_id" + //
+ ") inner " + //
+ "ON inner.i_user_id = merchants_customers.mc_user_id");
+ }
+
+ @Test // GH-1003
+ void shouldRenderJoinWithTwoInlineQueries() {
+
+ Table employee = SQL.table("employee");
+ Table department = SQL.table("department");
+
+ Select innerSelectOne = Select.builder()
+ .select(employee.column("id").as("empId"), employee.column("department_Id"), employee.column("name"))
+ .from(employee).build();
+ Select innerSelectTwo = Select.builder().select(department.column("id"), department.column("name"))
+ .from(department).build();
+
+ InlineQuery one = InlineQuery.create(innerSelectOne, "one");
+ InlineQuery two = InlineQuery.create(innerSelectTwo, "two");
+
+ Select select = Select.builder().select(one.column("empId"), two.column("name")).from(one) //
+ .join(two).on(two.column("department_id")).equals(one.column("empId")) //
+ .build();
+
+ String sql = SqlRenderer.toString(select);
+ assertThat(sql).isEqualTo("SELECT one.empId, two.name FROM (" //
+ + "SELECT employee.id AS empId, employee.department_Id, employee.name FROM employee) one " //
+ + "JOIN (SELECT department.id, department.name FROM department) two " //
+ + "ON two.department_id = one.empId");
+ }
+
}
/**
@@ -742,8 +769,8 @@ class AnalyticFunctionsTests {
void renderEmptyOver() {
Select select = StatementBuilder.select( //
- AnalyticFunction.create("MAX", salary) //
- ) //
+ AnalyticFunction.create("MAX", salary) //
+ ) //
.from(employee) //
.build();
diff --git a/spring-data-relational/src/test/java/org/springframework/data/relational/core/sqlgeneration/AliasFactoryUnitTests.java b/spring-data-relational/src/test/java/org/springframework/data/relational/core/sqlgeneration/AliasFactoryUnitTests.java
index b547aae17d0..28da149fb17 100644
--- a/spring-data-relational/src/test/java/org/springframework/data/relational/core/sqlgeneration/AliasFactoryUnitTests.java
+++ b/spring-data-relational/src/test/java/org/springframework/data/relational/core/sqlgeneration/AliasFactoryUnitTests.java
@@ -20,11 +20,13 @@
import org.junit.jupiter.api.Nested;
import org.junit.jupiter.api.Test;
+import org.springframework.data.annotation.Id;
import org.springframework.data.relational.core.mapping.Column;
import org.springframework.data.relational.core.mapping.RelationalMappingContext;
/**
* Unit tests for the {@link AliasFactory}.
+ *
* @author Jens Schauder
*/
class AliasFactoryUnitTests {
@@ -55,8 +57,8 @@ void aliasSimpleProperty() {
@Test // GH-1446
void nameGetsSanitized() {
- String alias = aliasFactory.getColumnAlias(
- context.getAggregatePath( context.getPersistentPropertyPath("evil", DummyEntity.class)));
+ String alias = aliasFactory
+ .getColumnAlias(context.getAggregatePath(context.getPersistentPropertyPath("evil", DummyEntity.class)));
assertThat(alias).isEqualTo("c_ameannamecontains3illegal_characters_1");
}
@@ -64,10 +66,10 @@ void nameGetsSanitized() {
@Test // GH-1446
void aliasIsStable() {
- String alias1 = aliasFactory.getColumnAlias(
- context.getAggregatePath( context.getRequiredPersistentEntity(DummyEntity.class)));
- String alias2 = aliasFactory.getColumnAlias(
- context.getAggregatePath( context.getRequiredPersistentEntity(DummyEntity.class)));
+ String alias1 = aliasFactory
+ .getColumnAlias(context.getAggregatePath(context.getRequiredPersistentEntity(DummyEntity.class)));
+ String alias2 = aliasFactory
+ .getColumnAlias(context.getAggregatePath(context.getRequiredPersistentEntity(DummyEntity.class)));
assertThat(alias1).isEqualTo(alias2);
}
@@ -79,10 +81,10 @@ class RnAlias {
@Test // GH-1446
void aliasIsStable() {
- String alias1 = aliasFactory.getRowNumberAlias(
- context.getAggregatePath(context.getRequiredPersistentEntity(DummyEntity.class)));
- String alias2 = aliasFactory.getRowNumberAlias(
- context.getAggregatePath( context.getRequiredPersistentEntity(DummyEntity.class)));
+ String alias1 = aliasFactory
+ .getRowNumberAlias(context.getAggregatePath(context.getRequiredPersistentEntity(DummyEntity.class)));
+ String alias2 = aliasFactory
+ .getRowNumberAlias(context.getAggregatePath(context.getRequiredPersistentEntity(DummyEntity.class)));
assertThat(alias1).isEqualTo(alias2);
}
@@ -90,11 +92,11 @@ void aliasIsStable() {
@Test // GH-1446
void aliasProjectsOnTableReferencingPath() {
- String alias1 = aliasFactory.getRowNumberAlias(
- context.getAggregatePath(context.getRequiredPersistentEntity(DummyEntity.class)));
+ String alias1 = aliasFactory
+ .getRowNumberAlias(context.getAggregatePath(context.getRequiredPersistentEntity(DummyEntity.class)));
- String alias2 = aliasFactory.getRowNumberAlias(
- context.getAggregatePath(context.getPersistentPropertyPath("evil", DummyEntity.class)));
+ String alias2 = aliasFactory
+ .getRowNumberAlias(context.getAggregatePath(context.getPersistentPropertyPath("evil", DummyEntity.class)));
assertThat(alias1).isEqualTo(alias2);
}
@@ -102,10 +104,10 @@ void aliasProjectsOnTableReferencingPath() {
@Test // GH-1446
void rnAliasIsIndependentOfTableAlias() {
- String alias1 = aliasFactory.getRowNumberAlias(
- context.getAggregatePath(context.getRequiredPersistentEntity(DummyEntity.class)));
- String alias2 = aliasFactory.getColumnAlias(
- context.getAggregatePath(context.getRequiredPersistentEntity(DummyEntity.class)));
+ String alias1 = aliasFactory
+ .getRowNumberAlias(context.getAggregatePath(context.getRequiredPersistentEntity(DummyEntity.class)));
+ String alias2 = aliasFactory
+ .getColumnAlias(context.getAggregatePath(context.getRequiredPersistentEntity(DummyEntity.class)));
assertThat(alias1).isNotEqualTo(alias2);
}
@@ -117,8 +119,8 @@ class BackReferenceAlias {
@Test // GH-1446
void testBackReferenceAlias() {
- String alias = aliasFactory.getBackReferenceAlias(
- context.getAggregatePath(context.getPersistentPropertyPath("dummy", Reference.class)));
+ String alias = aliasFactory
+ .getBackReferenceAlias(context.getAggregatePath(context.getPersistentPropertyPath("dummy", Reference.class)));
assertThat(alias).isEqualTo("br_dummy_entity_1");
}
@@ -129,8 +131,8 @@ class KeyAlias {
@Test // GH-1446
void testKeyAlias() {
- String alias = aliasFactory.getKeyAlias(
- context.getAggregatePath(context.getPersistentPropertyPath("dummy", Reference.class)));
+ String alias = aliasFactory
+ .getKeyAlias(context.getAggregatePath(context.getPersistentPropertyPath("dummy", Reference.class)));
assertThat(alias).isEqualTo("key_dummy_entity_1");
}
@@ -141,11 +143,11 @@ class TableAlias {
@Test // GH-1448
void tableAliasIsDifferentForDifferentPathsToSameEntity() {
- String alias = aliasFactory.getTableAlias(
- context.getAggregatePath(context.getPersistentPropertyPath("dummy", Reference.class)));
+ String alias = aliasFactory
+ .getTableAlias(context.getAggregatePath(context.getPersistentPropertyPath("dummy", Reference.class)));
- String alias2 = aliasFactory.getTableAlias(
- context.getAggregatePath(context.getPersistentPropertyPath("dummy2", Reference.class)));
+ String alias2 = aliasFactory
+ .getTableAlias(context.getAggregatePath(context.getPersistentPropertyPath("dummy2", Reference.class)));
assertThat(alias).isNotEqualTo(alias2);
}
@@ -158,6 +160,7 @@ static class DummyEntity {
}
static class Reference {
+ @Id Long id;
DummyEntity dummy;
DummyEntity dummy2;
}
diff --git a/src/main/antora/modules/ROOT/pages/jdbc/mapping.adoc b/src/main/antora/modules/ROOT/pages/jdbc/mapping.adoc
index c3bba01ca0d..c41b6cd42bb 100644
--- a/src/main/antora/modules/ROOT/pages/jdbc/mapping.adoc
+++ b/src/main/antora/modules/ROOT/pages/jdbc/mapping.adoc
@@ -106,6 +106,9 @@ Also, the type of that aggregate is encoded in a type parameter.
All references in an aggregate result in a foreign key relationship in the opposite direction in the database.
By default, the name of the foreign key column is the table name of the referencing entity.
+If the referenced id is an `@Embedded` id, the back reference consists of multiple columns, each named by a concatenation of + `_` + .
+E.g. the back reference to a `Person` entity, with a composite id with the properties `firstName` and `lastName` will consist of the two columns `PERSON_FIRST_NAME` and `PERSON_LAST_NAME`.
+
Alternatively you may choose to have them named by the entity name of the referencing entity ignoring `@Table` annotations.
You activate this behaviour by calling `setForeignKeyNaming(ForeignKeyNaming.IGNORE_RENAMING)` on the `RelationalMappingContext`.
diff --git a/src/main/antora/modules/ROOT/partials/mapping.adoc b/src/main/antora/modules/ROOT/partials/mapping.adoc
index 7e864516e2a..ed80c37fabf 100644
--- a/src/main/antora/modules/ROOT/partials/mapping.adoc
+++ b/src/main/antora/modules/ROOT/partials/mapping.adoc
@@ -149,6 +149,13 @@ Embedded entities containing a `Collection` or a `Map` will always be considered
Such an entity will therefore never be `null` even when using @Embedded(onEmpty = USE_NULL).
endif::[]
+[[entity-persistence.embedded-ids]]
+=== Embedded Ids
+
+Entities may be annotated with `@Id` and `@Embedded`, resulting in a composite id on the database side.
+The full embedded entity is considered the id, and therefore the check for determining if an aggregate is considered a new aggregate requiring an insert or an existing one, asking for an update is based on that entity, not its elements.
+Most use cases will require a custom `BeforeConvertCallback` to set the id for new aggregate.
+
[[entity-persistence.read-only-properties]]
== Read Only Properties