Skip to content

Commit 1761ab2

Browse files
committed
Move Table and Column creation into factory methods.
Decouple default implementation from the creation of single table and column objects. Add TODO markers for future revision.
1 parent fcc7a92 commit 1761ab2

File tree

5 files changed

+202
-149
lines changed

5 files changed

+202
-149
lines changed

spring-data-relational/src/main/java/org/springframework/data/relational/core/mapping/AggregatePath.java

+85-29
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@
2424
import org.springframework.data.mapping.PersistentPropertyPath;
2525
import org.springframework.data.relational.core.sql.SqlIdentifier;
2626
import org.springframework.lang.Nullable;
27+
import org.springframework.util.Assert;
2728

2829
/**
2930
* Represents a path within an aggregate starting from the aggregate root. The path can be iterated from the leaf to its
@@ -81,7 +82,9 @@ default int getLength() {
8182

8283
/**
8384
* Returns {@literal true} if there are multiple values for this path, i.e. if the path contains at least one element
84-
* that is a collection and array or a map.
85+
* that is a collection and array or a map. // TODO: why does this return true if the parent entity is a collection?
86+
* This seems to mix some concepts that belong to somewhere else. // TODO: Multi-valued could be understood for
87+
* embeddables with more than one column (i.e. composite primary keys)
8588
*
8689
* @return {@literal true} if the path contains a multivalued element.
8790
*/
@@ -179,9 +182,13 @@ default RelationalPersistentEntity<?> getRequiredLeafEntity() {
179182
// TODO: Conceptually, AggregatePath works with properties. The mapping into columns and tables should reside in a
180183
// utility that can distinguish whether a property maps to one or many columns (e.g. embedded) and the same for
181184
// identifier columns.
182-
TableInfo getTableInfo();
185+
default TableInfo getTableInfo() {
186+
return TableInfo.of(this);
187+
}
183188

184-
ColumnInfo getColumnInfo();
189+
default ColumnInfo getColumnInfo() {
190+
return ColumnInfo.of(this);
191+
}
185192

186193
/**
187194
* Filter the AggregatePath returning the first item matching the given {@link Predicate}.
@@ -222,65 +229,114 @@ default Stream<AggregatePath> stream() {
222229

223230
record TableInfo(
224231

225-
/**
232+
/*
226233
* The fully qualified name of the table this path is tied to or of the longest ancestor path that is actually
227234
* tied to a table.
228-
*
229-
* @return the name of the table. Guaranteed to be not {@literal null}.
230-
* @since 3.0
231235
*/
232236
SqlIdentifier qualifiedTableName,
233237

234-
/**
238+
/*
235239
* The alias used for the table on which this path is based.
236-
*
237-
* @return a table alias, {@literal null} if the table owning path is the empty path.
238240
*/
239241
@Nullable SqlIdentifier tableAlias,
240242

241243
ColumnInfo reverseColumnInfo,
242244

243-
/**
245+
/*
244246
* The column used for the list index or map key of the leaf property of this path.
245-
*
246-
* @return May be {@literal null}.
247247
*/
248248
@Nullable ColumnInfo qualifierColumnInfo,
249249

250-
/**
250+
/*
251251
* The type of the qualifier column of the leaf property of this path or {@literal null} if this is not
252252
* applicable.
253-
*
254-
* @return may be {@literal null}.
255253
*/
256254
@Nullable Class<?> qualifierColumnType,
257255

258-
/**
256+
/*
259257
* The column name of the id column of the ancestor path that represents an actual table.
260258
*/
261259
SqlIdentifier idColumnName,
262260

263-
/**
261+
/*
264262
* If the table owning ancestor has an id the column name of that id property is returned. Otherwise the reverse
265263
* column is returned.
266264
*/
267265
SqlIdentifier effectiveIdColumnName) {
268266

267+
static TableInfo of(AggregatePath path) {
268+
269+
AggregatePath tableOwner = AggregatePathTraversal.getTableOwningPath(path);
270+
271+
RelationalPersistentEntity<?> leafEntity = tableOwner.getRequiredLeafEntity();
272+
SqlIdentifier qualifiedTableName = leafEntity.getQualifiedTableName();
273+
274+
SqlIdentifier tableAlias = tableOwner.isRoot() ? null : AggregatePathTableUtils.constructTableAlias(tableOwner);
275+
276+
ColumnInfo reverseColumnInfo = null;
277+
if (!tableOwner.isRoot()) {
278+
279+
AggregatePath idDefiningParentPath = tableOwner.getIdDefiningParentPath();
280+
RelationalPersistentProperty leafProperty = tableOwner.getRequiredLeafProperty();
281+
282+
SqlIdentifier reverseColumnName = leafProperty
283+
.getReverseColumnName(idDefiningParentPath.getRequiredLeafEntity());
284+
285+
reverseColumnInfo = new ColumnInfo(reverseColumnName,
286+
AggregatePathTableUtils.prefixWithTableAlias(path, reverseColumnName));
287+
}
288+
289+
ColumnInfo qualifierColumnInfo = null;
290+
if (!path.isRoot()) {
291+
292+
SqlIdentifier keyColumn = path.getRequiredLeafProperty().getKeyColumn();
293+
if (keyColumn != null) {
294+
qualifierColumnInfo = new ColumnInfo(keyColumn, keyColumn);
295+
}
296+
}
297+
298+
Class<?> qualifierColumnType = null;
299+
if (!path.isRoot() && path.getRequiredLeafProperty().isQualified()) {
300+
qualifierColumnType = path.getRequiredLeafProperty().getQualifierColumnType();
301+
}
302+
303+
SqlIdentifier idColumnName = leafEntity.hasIdProperty() ? leafEntity.getIdColumn() : null;
304+
305+
SqlIdentifier effectiveIdColumnName = tableOwner.isRoot() ? idColumnName : reverseColumnInfo.name();
306+
307+
return new TableInfo(qualifiedTableName, tableAlias, reverseColumnInfo, qualifierColumnInfo, qualifierColumnType,
308+
idColumnName, effectiveIdColumnName);
309+
310+
}
269311
}
270312

271313
record ColumnInfo(
272314

273-
/**
274-
* The name of the column used to represent this property in the database.
275-
*
276-
* @throws IllegalStateException when called on an empty path.
277-
*/
278-
SqlIdentifier name,
279-
/**
280-
* The alias for the column used to represent this property in the database.
281-
*
282-
* @throws IllegalStateException when called on an empty path.
283-
*/
315+
/* The name of the column used to represent this property in the database. */
316+
SqlIdentifier name, /* The alias for the column used to represent this property in the database. */
284317
SqlIdentifier alias) {
318+
319+
/**
320+
* Create a {@link ColumnInfo} from an aggregate path. ColumnInfo can be created for simple type single-value
321+
* properties only.
322+
*
323+
* @param path
324+
* @return the {@link ColumnInfo}.
325+
* @throws IllegalArgumentException if the path is {@link #isRoot()}, {@link #isEmbedded()} or
326+
* {@link #isMultiValued()}.
327+
*/
328+
static ColumnInfo of(AggregatePath path) {
329+
330+
Assert.notNull(path, "AggregatePath must not be null");
331+
Assert.isTrue(!path.isRoot(), () -> "Cannot obtain ColumnInfo for root path");
332+
Assert.isTrue(!path.isEmbedded(), () -> "Cannot obtain ColumnInfo for embedded path");
333+
334+
// TODO: Multi-valued paths cannot be represented with a single column
335+
// Assert.isTrue(!path.isMultiValued(), () -> "Cannot obtain ColumnInfo for multi-valued path");
336+
337+
SqlIdentifier name = AggregatePathTableUtils.assembleColumnName(path,
338+
path.getRequiredLeafProperty().getColumnName());
339+
return new ColumnInfo(name, AggregatePathTableUtils.prefixWithTableAlias(path, name));
340+
}
285341
}
286342
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,94 @@
1+
/*
2+
* Copyright 2023 the original author or authors.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* https://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
package org.springframework.data.relational.core.mapping;
17+
18+
import java.util.Collections;
19+
import java.util.Set;
20+
import java.util.function.BiConsumer;
21+
import java.util.function.BinaryOperator;
22+
import java.util.function.Function;
23+
import java.util.function.Supplier;
24+
import java.util.stream.Collector;
25+
26+
import org.springframework.data.relational.core.sql.SqlIdentifier;
27+
28+
/**
29+
* Utility methods to derive table and column information from {@link AggregatePath}.
30+
*
31+
* @author Mark Paluch
32+
* @since 3.2
33+
*/
34+
class AggregatePathTableUtils {
35+
36+
public static SqlIdentifier assembleColumnName(AggregatePath path, SqlIdentifier suffix) {
37+
return suffix.transform(constructEmbeddedPrefix(path)::concat);
38+
}
39+
40+
private static String constructEmbeddedPrefix(AggregatePath path) {
41+
42+
return path.stream() //
43+
.filter(p -> p != path) //
44+
.takeWhile(AggregatePath::isEmbedded).map(p -> p.getRequiredLeafProperty().getEmbeddedPrefix()) //
45+
.collect(new ReverseJoinCollector());
46+
}
47+
48+
public static SqlIdentifier prefixWithTableAlias(AggregatePath path, SqlIdentifier columnName) {
49+
50+
AggregatePath tableOwner = AggregatePathTraversal.getTableOwningPath(path);
51+
SqlIdentifier tableAlias = tableOwner.isRoot() ? null : constructTableAlias(tableOwner);
52+
53+
return tableAlias == null ? columnName : columnName.transform(name -> tableAlias.getReference() + "_" + name);
54+
}
55+
56+
public static SqlIdentifier constructTableAlias(AggregatePath path) {
57+
58+
String alias = path.stream() //
59+
.filter(p -> !p.isRoot()) //
60+
.map(p -> p.isEmbedded() //
61+
? p.getRequiredLeafProperty().getEmbeddedPrefix()//
62+
: p.getRequiredLeafProperty().getName() + (p == path ? "" : "_") //
63+
) //
64+
.collect(new ReverseJoinCollector());
65+
return SqlIdentifier.quoted(alias);
66+
}
67+
68+
private static class ReverseJoinCollector implements Collector<String, StringBuilder, String> {
69+
@Override
70+
public Supplier<StringBuilder> supplier() {
71+
return StringBuilder::new;
72+
}
73+
74+
@Override
75+
public BiConsumer<StringBuilder, String> accumulator() {
76+
return ((stringBuilder, s) -> stringBuilder.insert(0, s));
77+
}
78+
79+
@Override
80+
public BinaryOperator<StringBuilder> combiner() {
81+
return (a, b) -> b.append(a);
82+
}
83+
84+
@Override
85+
public Function<StringBuilder, String> finisher() {
86+
return StringBuilder::toString;
87+
}
88+
89+
@Override
90+
public Set<Characteristics> characteristics() {
91+
return Collections.emptySet();
92+
}
93+
}
94+
}

spring-data-relational/src/main/java/org/springframework/data/relational/core/mapping/AggregatePathTraversal.java

+12-1
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,7 @@
2323
*/
2424
class AggregatePathTraversal {
2525

26-
public static AggregatePath findIdDefiningPath(AggregatePath aggregatePath) {
26+
public static AggregatePath getIdDefiningPath(AggregatePath aggregatePath) {
2727

2828
Predicate<AggregatePath> idDefiningPathFilter = ap -> !ap.equals(aggregatePath)
2929
&& (ap.isRoot() || ap.hasIdProperty());
@@ -35,4 +35,15 @@ public static AggregatePath findIdDefiningPath(AggregatePath aggregatePath) {
3535
return result;
3636
}
3737

38+
public static AggregatePath getTableOwningPath(AggregatePath aggregatePath) {
39+
40+
Predicate<AggregatePath> idDefiningPathFilter = ap -> ap.isEntity() && !ap.isEmbedded();
41+
42+
AggregatePath result = aggregatePath.filter(idDefiningPathFilter);
43+
if (result == null) {
44+
throw new NoSuchElementException();
45+
}
46+
return result;
47+
}
48+
3849
}

0 commit comments

Comments
 (0)