Skip to content

Commit bfb1590

Browse files
refactor: flattened path processing - metadata-driven VI detection
Replace runtime heuristics in the flush pipeline with pre-computed segment classification cached at mapping load time.
1 parent 195547e commit bfb1590

10 files changed

Lines changed: 571 additions & 185 deletions

cayenne/src/main/java/org/apache/cayenne/access/flush/ArcValuesCreationHandler.java

Lines changed: 161 additions & 167 deletions
Large diffs are not rendered by default.

cayenne/src/main/java/org/apache/cayenne/access/flush/ValuesCreationHandler.java

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -51,9 +51,9 @@ public void nodePropertyChanged(Object nodeId, String property, Object oldValue,
5151
DbEntity dbEntity = entity.getDbEntity();
5252

5353
if(attribute.isFlattened()) {
54-
// get target row ID
55-
FlattenedPathProcessingResult result
56-
= processFlattenedPath(id, null, dbEntity, attribute.getDbAttributePath(), newValue != null);
54+
// get target row ID - use cached FlattenedPathInfo if available
55+
FlattenedPathProcessingResult result = processFlattenedAttributePath(id, dbEntity,
56+
attribute.getDbAttributePath(), newValue != null);
5757
if(result.isProcessed()) {
5858
id = result.getId();
5959
}
Lines changed: 138 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,138 @@
1+
/*****************************************************************
2+
* Licensed to the Apache Software Foundation (ASF) under one
3+
* or more contributor license agreements. See the NOTICE file
4+
* distributed with this work for additional information
5+
* regarding copyright ownership. The ASF licenses this file
6+
* to you under the Apache License, Version 2.0 (the
7+
* "License"); you may not use this file except in compliance
8+
* with the License. You may obtain a copy of the License at
9+
*
10+
* https://www.apache.org/licenses/LICENSE-2.0
11+
*
12+
* Unless required by applicable law or agreed to in writing,
13+
* software distributed under the License is distributed on an
14+
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
15+
* KIND, either express or implied. See the License for the
16+
* specific language governing permissions and limitations
17+
* under the License.
18+
****************************************************************/
19+
20+
package org.apache.cayenne.map;
21+
22+
import java.util.ArrayList;
23+
import java.util.List;
24+
25+
/**
26+
* Analyzes a flattened {@link ObjRelationship}'s {@link DbRelationship} path
27+
* and classifies each segment as one of the {@link FlattenedPathSegmentType} values.
28+
* <p>
29+
* This classification is performed once per relationship and cached in
30+
* {@link ObjRelationship} as a {@link FlattenedPathInfo}, so that downstream
31+
* consumers never need to re-derive the nature of each segment from raw
32+
* join metadata.
33+
* </p>
34+
*
35+
* @since 5.0
36+
*/
37+
public class FlattenedPathAnalyzer {
38+
39+
/**
40+
* Analyzes the given flattened relationship and returns a {@link FlattenedPathInfo}
41+
* with each segment classified.
42+
*
43+
* @param relationship a flattened ObjRelationship (must have more than one DbRelationship)
44+
* @return classified path info
45+
* @throws IllegalArgumentException if the relationship is not flattened
46+
*/
47+
public static FlattenedPathInfo analyze(ObjRelationship relationship) {
48+
List<DbRelationship> dbRels = relationship.getDbRelationships();
49+
if (dbRels.size() < 2) {
50+
throw new IllegalArgumentException(
51+
"FlattenedPathAnalyzer requires a flattened relationship (>1 DbRelationship), got: "
52+
+ relationship.getName());
53+
}
54+
55+
List<FlattenedPathInfo.AnnotatedSegment> segments = new ArrayList<>(dbRels.size());
56+
for (DbRelationship dbRel : dbRels) {
57+
FlattenedPathSegmentType type = classifySegment(dbRel);
58+
segments.add(new FlattenedPathInfo.AnnotatedSegment(dbRel, type));
59+
}
60+
61+
return new FlattenedPathInfo(segments);
62+
}
63+
64+
/**
65+
* Classifies a single {@link DbRelationship} segment.
66+
*
67+
* <p>Classification rules:</p>
68+
* <ol>
69+
* <li><b>VI_TO_CHILD</b>: the relationship is toDependentPK, toOne,
70+
* and has all-PK joins (parent → child in vertical inheritance).</li>
71+
* <li><b>VI_TO_PARENT</b>: the reverse relationship is toDependentPK, and
72+
* this relationship is toOne with all-PK joins (child → parent in vertical inheritance).</li>
73+
* <li><b>JOIN_TABLE</b>: the target entity has multiple incoming toDependentPK
74+
* relationships, indicating a many-to-many join table.</li>
75+
* <li><b>REGULAR</b>: everything else.</li>
76+
* </ol>
77+
*/
78+
public static FlattenedPathSegmentType classifySegment(DbRelationship dbRel) {
79+
if (dbRel.isToMany()) {
80+
// toMany can point to a join table
81+
if (isJoinTableTarget(dbRel.getTargetEntity())) {
82+
return FlattenedPathSegmentType.JOIN_TABLE;
83+
}
84+
return FlattenedPathSegmentType.REGULAR;
85+
}
86+
87+
// toOne: check for VI patterns
88+
if (dbRel.isToDependentPK() && hasAllPkJoins(dbRel)) {
89+
// parent - child (toDependentPK, all PK-to-PK joins)
90+
if (isJoinTableTarget(dbRel.getTargetEntity())) {
91+
return FlattenedPathSegmentType.JOIN_TABLE;
92+
}
93+
return FlattenedPathSegmentType.VI_TO_CHILD;
94+
}
95+
96+
DbRelationship reverseRel = dbRel.getReverseRelationship();
97+
if (reverseRel != null && reverseRel.isToDependentPK() && hasAllPkJoins(dbRel)) {
98+
// child - parent (reverse is toDependentPK, all PK-to-PK joins)
99+
return FlattenedPathSegmentType.VI_TO_PARENT;
100+
}
101+
102+
return FlattenedPathSegmentType.REGULAR;
103+
}
104+
105+
/**
106+
* Returns {@code true} if all joins in the relationship map PK columns on both sides.
107+
*/
108+
private static boolean hasAllPkJoins(DbRelationship dbRel) {
109+
for (DbJoin join : dbRel.getJoins()) {
110+
if (!join.getSource().isPrimaryKey() || !join.getTarget().isPrimaryKey()) {
111+
return false;
112+
}
113+
}
114+
return !dbRel.getJoins().isEmpty();
115+
}
116+
117+
/**
118+
* Returns {@code true} if the target entity is a join table, i.e. it has
119+
* more than one incoming toDependentPK relationship from other entities.
120+
* <p>
121+
* In vertical inheritance, a child table has exactly one incoming toDependentPK
122+
* (from the parent). A join table has two or more (from both sides of a many-to-many).
123+
* </p>
124+
*/
125+
private static boolean isJoinTableTarget(DbEntity target) {
126+
int toDependentPKCount = 0;
127+
for (DbRelationship rel : target.getRelationships()) {
128+
DbRelationship reverseRel = rel.getReverseRelationship();
129+
if (reverseRel != null && reverseRel.isToDependentPK()) {
130+
toDependentPKCount++;
131+
if (toDependentPKCount > 1) {
132+
return true;
133+
}
134+
}
135+
}
136+
return false;
137+
}
138+
}
Lines changed: 122 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,122 @@
1+
/*****************************************************************
2+
* Licensed to the Apache Software Foundation (ASF) under one
3+
* or more contributor license agreements. See the NOTICE file
4+
* distributed with this work for additional information
5+
* regarding copyright ownership. The ASF licenses this file
6+
* to you under the Apache License, Version 2.0 (the
7+
* "License"); you may not use this file except in compliance
8+
* with the License. You may obtain a copy of the License at
9+
*
10+
* https://www.apache.org/licenses/LICENSE-2.0
11+
*
12+
* Unless required by applicable law or agreed to in writing,
13+
* software distributed under the License is distributed on an
14+
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
15+
* KIND, either express or implied. See the License for the
16+
* specific language governing permissions and limitations
17+
* under the License.
18+
****************************************************************/
19+
20+
package org.apache.cayenne.map;
21+
22+
import java.util.Collections;
23+
import java.util.List;
24+
25+
/**
26+
* Pre-computed classification of a flattened {@link ObjRelationship}'s
27+
* {@link DbRelationship} path. Each segment in the path is annotated with a
28+
* {@link FlattenedPathSegmentType} that tells downstream consumers
29+
* (flush pipeline, prefetch routing, column extraction, etc.)
30+
* what kind of link they are dealing with — without re-discovering it
31+
* from raw join metadata every time.
32+
* <p>
33+
* Instances are <b>immutable</b> and created lazily by
34+
* {@link FlattenedPathAnalyzer#analyze(ObjRelationship)}.
35+
* </p>
36+
*
37+
* @since 5.0
38+
* @see FlattenedPathSegmentType
39+
* @see AnnotatedSegment
40+
*/
41+
public class FlattenedPathInfo {
42+
43+
private final List<AnnotatedSegment> segments;
44+
private final boolean fkThroughInheritance;
45+
46+
public FlattenedPathInfo(List<AnnotatedSegment> segments) {
47+
this.segments = Collections.unmodifiableList(segments);
48+
49+
boolean fkThroughVI = false;
50+
51+
if (segments.size() >= 2) {
52+
// FK through inheritance: all preceding segments are VI_TO_CHILD,
53+
// and the last segment is a regular FK-to-PK relationship
54+
boolean allPrecedingAreVI = true;
55+
for (int i = 0; i < segments.size() - 1; i++) {
56+
if (segments.get(i).getType() != FlattenedPathSegmentType.VI_TO_CHILD) {
57+
allPrecedingAreVI = false;
58+
break;
59+
}
60+
}
61+
if (allPrecedingAreVI) {
62+
FlattenedPathSegmentType lastType = segments.get(segments.size() - 1).getType();
63+
if (lastType == FlattenedPathSegmentType.REGULAR) {
64+
DbRelationship lastRel = segments.get(segments.size() - 1).getRelationship();
65+
fkThroughVI = lastRel.isToPK();
66+
}
67+
}
68+
}
69+
70+
this.fkThroughInheritance = fkThroughVI;
71+
}
72+
73+
/**
74+
* Returns the annotated segments of this flattened path, in order.
75+
*/
76+
public List<AnnotatedSegment> getSegments() {
77+
return segments;
78+
}
79+
80+
/**
81+
* Returns {@code true} if the FK is accessed through a vertical inheritance chain:
82+
* one or more {@link FlattenedPathSegmentType#VI_TO_CHILD} segments followed by
83+
* a {@link FlattenedPathSegmentType#REGULAR} FK-to-PK segment.
84+
*/
85+
public boolean isFkThroughInheritance() {
86+
return fkThroughInheritance;
87+
}
88+
89+
/**
90+
* A single segment of a flattened path, pairing a {@link DbRelationship}
91+
* with its classified {@link FlattenedPathSegmentType}.
92+
*
93+
* @since 5.0
94+
*/
95+
public static class AnnotatedSegment {
96+
97+
private final DbRelationship relationship;
98+
private final FlattenedPathSegmentType type;
99+
100+
public AnnotatedSegment(DbRelationship relationship, FlattenedPathSegmentType type) {
101+
this.relationship = relationship;
102+
this.type = type;
103+
}
104+
105+
public DbRelationship getRelationship() {
106+
return relationship;
107+
}
108+
109+
public FlattenedPathSegmentType getType() {
110+
return type;
111+
}
112+
113+
public boolean isVerticalInheritance() {
114+
return type.isVerticalInheritance();
115+
}
116+
117+
@Override
118+
public String toString() {
119+
return relationship.getName() + " [" + type + "]";
120+
}
121+
}
122+
}
Lines changed: 67 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,67 @@
1+
/*****************************************************************
2+
* Licensed to the Apache Software Foundation (ASF) under one
3+
* or more contributor license agreements. See the NOTICE file
4+
* distributed with this work for additional information
5+
* regarding copyright ownership. The ASF licenses this file
6+
* to you under the Apache License, Version 2.0 (the
7+
* "License"); you may not use this file except in compliance
8+
* with the License. You may obtain a copy of the License at
9+
*
10+
* https://www.apache.org/licenses/LICENSE-2.0
11+
*
12+
* Unless required by applicable law or agreed to in writing,
13+
* software distributed under the License is distributed on an
14+
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
15+
* KIND, either express or implied. See the License for the
16+
* specific language governing permissions and limitations
17+
* under the License.
18+
****************************************************************/
19+
20+
package org.apache.cayenne.map;
21+
22+
/**
23+
* Classifies a {@link DbRelationship} segment within a flattened path
24+
* according to its role in the data model.
25+
*
26+
* <ul>
27+
* <li>{@link #VI_TO_CHILD} — vertical inheritance: parent table → child table (toDependentPK, toOne)</li>
28+
* <li>{@link #VI_TO_PARENT} — vertical inheritance: child table → parent table (reverse of toDependentPK)</li>
29+
* <li>{@link #JOIN_TABLE} — many-to-many join table (target has multiple incoming toDependentPK)</li>
30+
* <li>{@link #REGULAR} — any other relationship (standard FK-to-PK, etc.)</li>
31+
* </ul>
32+
*
33+
* @since 5.0
34+
*/
35+
public enum FlattenedPathSegmentType {
36+
37+
/**
38+
* Vertical inheritance from parent to child table.
39+
* The relationship is toDependentPK, toOne, and the target entity
40+
* is a child in a vertical inheritance hierarchy sharing the parent's PK.
41+
*/
42+
VI_TO_CHILD,
43+
44+
/**
45+
* Vertical inheritance from child to parent table.
46+
* The reverse relationship is toDependentPK.
47+
*/
48+
VI_TO_PARENT,
49+
50+
/**
51+
* Relationship targeting a many-to-many join table.
52+
* The target entity has multiple incoming toDependentPK relationships.
53+
*/
54+
JOIN_TABLE,
55+
56+
/**
57+
* Any relationship that doesn't fall into the other categories.
58+
*/
59+
REGULAR;
60+
61+
/**
62+
* Returns {@code true} if this segment type represents a vertical inheritance link.
63+
*/
64+
public boolean isVerticalInheritance() {
65+
return this == VI_TO_CHILD || this == VI_TO_PARENT;
66+
}
67+
}

0 commit comments

Comments
 (0)