22
22
import java .util .List ;
23
23
import java .util .Optional ;
24
24
25
+ import org .jetbrains .annotations .NotNull ;
26
+ import org .jspecify .annotations .NonNull ;
27
+ import org .jspecify .annotations .Nullable ;
28
+
25
29
import org .springframework .data .domain .Example ;
30
+ import org .springframework .data .domain .ExampleMatcher ;
26
31
import org .springframework .data .mapping .PersistentPropertyAccessor ;
27
32
import org .springframework .data .mapping .PropertyHandler ;
33
+ import org .springframework .data .mapping .PropertyPath ;
28
34
import org .springframework .data .mapping .context .MappingContext ;
29
35
import org .springframework .data .relational .core .mapping .RelationalPersistentEntity ;
30
36
import org .springframework .data .relational .core .mapping .RelationalPersistentProperty ;
31
37
import org .springframework .data .relational .core .query .Criteria ;
32
38
import org .springframework .data .relational .core .query .Query ;
33
39
import org .springframework .data .support .ExampleMatcherAccessor ;
34
40
import org .springframework .util .Assert ;
41
+ import org .springframework .util .StringUtils ;
35
42
36
43
/**
37
44
* Transform an {@link Example} into a {@link Query}.
38
45
*
39
46
* @since 2.2
40
47
* @author Greg Turnquist
41
48
* @author Jens Schauder
49
+ * @author Mikhail Polivakha
42
50
*/
43
51
public class RelationalExampleMapper {
44
52
@@ -64,92 +72,192 @@ public <T> Query getMappedExample(Example<T> example) {
64
72
* {@link Query}.
65
73
*
66
74
* @param example
67
- * @param entity
75
+ * @param persistentEntity
68
76
* @return query
69
77
*/
70
- private <T > Query getMappedExample (Example <T > example , RelationalPersistentEntity <?> entity ) {
78
+ private <T > Query getMappedExample (Example <T > example , RelationalPersistentEntity <?> persistentEntity ) {
71
79
72
80
Assert .notNull (example , "Example must not be null" );
73
- Assert .notNull (entity , "RelationalPersistentEntity must not be null" );
81
+ Assert .notNull (persistentEntity , "RelationalPersistentEntity must not be null" );
74
82
75
- PersistentPropertyAccessor <T > propertyAccessor = entity .getPropertyAccessor (example .getProbe ());
83
+ PersistentPropertyAccessor <T > probePropertyAccessor = persistentEntity .getPropertyAccessor (example .getProbe ());
76
84
ExampleMatcherAccessor matcherAccessor = new ExampleMatcherAccessor (example .getMatcher ());
77
85
78
- final List <Criteria > criteriaBasedOnProperties = new ArrayList <>();
86
+ final List <Criteria > criteriaBasedOnProperties = buildCriteriaRecursive ( //
87
+ persistentEntity , //
88
+ matcherAccessor , //
89
+ probePropertyAccessor //
90
+ );
79
91
80
- entity .doWithProperties ((PropertyHandler <RelationalPersistentProperty >) property -> {
92
+ // Criteria, assemble!
93
+ Criteria criteria = Criteria .empty ();
81
94
82
- if (property .isCollectionLike () || property .isMap ()) {
83
- return ;
84
- }
95
+ for (Criteria propertyCriteria : criteriaBasedOnProperties ) {
85
96
86
- if (matcherAccessor .isIgnoredPath (property .getName ())) {
87
- return ;
97
+ if (example .getMatcher ().isAllMatching ()) {
98
+ criteria = criteria .and (propertyCriteria );
99
+ } else {
100
+ criteria = criteria .or (propertyCriteria );
88
101
}
102
+ }
89
103
104
+ return Query .query (criteria );
105
+ }
106
+
107
+ private <T > @ NotNull List <Criteria > buildCriteriaRecursive ( //
108
+ RelationalPersistentEntity <?> persistentEntity , //
109
+ ExampleMatcherAccessor matcherAccessor , //
110
+ PersistentPropertyAccessor <T > probePropertyAccessor //
111
+ ) {
112
+ final List <Criteria > criteriaBasedOnProperties = new ArrayList <>();
113
+
114
+ persistentEntity .doWithProperties ((PropertyHandler <RelationalPersistentProperty >) property -> {
115
+ potentiallyEnrichCriteriaByProcessingProperty (
116
+ null ,
117
+ matcherAccessor , //
118
+ probePropertyAccessor , //
119
+ property , //
120
+ criteriaBasedOnProperties //
121
+ );
122
+ });
123
+ return criteriaBasedOnProperties ;
124
+ }
125
+
126
+ /**
127
+ * Analyzes the incoming {@code property} and potentially enriches the {@code criteriaBasedOnProperties} with the new
128
+ * {@link Criteria} for this property.
129
+ * <p>
130
+ * This algorithm is recursive in order to take the embedded properties into account. The caller can expect that the result
131
+ * of this method call is fully processed subtree of an aggreagte where the passed {@code property} serves as the root.
132
+ *
133
+ * @param propertyPath the {@link PropertyPath} of the passed {@code property}.
134
+ * @param matcherAccessor the accessor for the original {@link ExampleMatcher}.
135
+ * @param entityPropertiesAccessor the accessor for the properties of the current entity that holds the given {@code property}
136
+ * @param property the property under analysis
137
+ * @param criteriaBasedOnProperties the {@link List} of criteria objects that potentially gets enriched as a
138
+ * result of the incoming {@code property} processing
139
+ */
140
+ private <T > void potentiallyEnrichCriteriaByProcessingProperty (
141
+ @ Nullable PropertyPath propertyPath ,
142
+ ExampleMatcherAccessor matcherAccessor , //
143
+ PersistentPropertyAccessor <T > entityPropertiesAccessor , //
144
+ RelationalPersistentProperty property , //
145
+ List <Criteria > criteriaBasedOnProperties //
146
+ ) {
147
+
148
+ // QBE do not support queries on Child aggregates yet
149
+ if (property .isCollectionLike () || property .isMap ()) {
150
+ return ;
151
+ }
152
+
153
+ PropertyPath currentPropertyPath = resolveCurrentPropertyPath (propertyPath , property );
154
+ String currentPropertyDotPath = currentPropertyPath .toDotPath ();
155
+
156
+ if (matcherAccessor .isIgnoredPath (currentPropertyDotPath )) {
157
+ return ;
158
+ }
159
+
160
+ if (property .isEmbedded ()) {
161
+ processEmbeddedRecursively ( //
162
+ matcherAccessor , //
163
+ entityPropertiesAccessor .getProperty (property ),
164
+ property , //
165
+ criteriaBasedOnProperties , //
166
+ currentPropertyPath //
167
+ );
168
+ } else {
90
169
Optional <?> optionalConvertedPropValue = matcherAccessor //
91
- .getValueTransformerForPath (property . getName () ) //
92
- .apply (Optional .ofNullable (propertyAccessor .getProperty (property )));
170
+ .getValueTransformerForPath (currentPropertyDotPath ) //
171
+ .apply (Optional .ofNullable (entityPropertiesAccessor .getProperty (property )));
93
172
94
173
// If the value is empty, don't try to match against it
95
- if (! optionalConvertedPropValue .isPresent ()) {
174
+ if (optionalConvertedPropValue .isEmpty ()) {
96
175
return ;
97
176
}
98
177
99
178
Object convPropValue = optionalConvertedPropValue .get ();
100
- boolean ignoreCase = matcherAccessor .isIgnoreCaseForPath (property . getName () );
179
+ boolean ignoreCase = matcherAccessor .isIgnoreCaseForPath (currentPropertyDotPath );
101
180
102
181
String column = property .getName ();
103
182
104
- switch (matcherAccessor .getStringMatcherForPath (property . getName () )) {
183
+ switch (matcherAccessor .getStringMatcherForPath (currentPropertyDotPath )) {
105
184
case DEFAULT :
106
185
case EXACT :
107
- criteriaBasedOnProperties .add (includeNulls (example ) //
186
+ criteriaBasedOnProperties .add (includeNulls (matcherAccessor ) //
108
187
? Criteria .where (column ).isNull ().or (column ).is (convPropValue ).ignoreCase (ignoreCase )
109
188
: Criteria .where (column ).is (convPropValue ).ignoreCase (ignoreCase ));
110
189
break ;
111
190
case ENDING :
112
- criteriaBasedOnProperties .add (includeNulls (example ) //
191
+ criteriaBasedOnProperties .add (includeNulls (matcherAccessor ) //
113
192
? Criteria .where (column ).isNull ().or (column ).like ("%" + convPropValue ).ignoreCase (ignoreCase )
114
193
: Criteria .where (column ).like ("%" + convPropValue ).ignoreCase (ignoreCase ));
115
194
break ;
116
195
case STARTING :
117
- criteriaBasedOnProperties .add (includeNulls (example ) //
196
+ criteriaBasedOnProperties .add (includeNulls (matcherAccessor ) //
118
197
? Criteria .where (column ).isNull ().or (column ).like (convPropValue + "%" ).ignoreCase (ignoreCase )
119
198
: Criteria .where (column ).like (convPropValue + "%" ).ignoreCase (ignoreCase ));
120
199
break ;
121
200
case CONTAINING :
122
- criteriaBasedOnProperties .add (includeNulls (example ) //
201
+ criteriaBasedOnProperties .add (includeNulls (matcherAccessor ) //
123
202
? Criteria .where (column ).isNull ().or (column ).like ("%" + convPropValue + "%" ).ignoreCase (ignoreCase )
124
203
: Criteria .where (column ).like ("%" + convPropValue + "%" ).ignoreCase (ignoreCase ));
125
204
break ;
126
205
default :
127
- throw new IllegalStateException (example . getMatcher () .getDefaultStringMatcher () + " is not supported" );
206
+ throw new IllegalStateException (matcherAccessor .getDefaultStringMatcher () + " is not supported" );
128
207
}
129
- });
208
+ }
130
209
131
- // Criteria, assemble!
132
- Criteria criteria = Criteria .empty ();
210
+ }
133
211
134
- for (Criteria propertyCriteria : criteriaBasedOnProperties ) {
212
+ /**
213
+ * Processes an embedded entity's properties recursively.
214
+ *
215
+ * @param matcherAccessor the input matcher on the {@link Example#getProbe() original probe}.
216
+ * @param value the actual embedded object.
217
+ * @param property the embedded property.
218
+ * @param criteriaBasedOnProperties collection of {@link Criteria} objects to potentially enrich.
219
+ * @param currentPropertyPath the dot-separated path of the passed {@code property}.
220
+ */
221
+ private void processEmbeddedRecursively (
222
+ ExampleMatcherAccessor matcherAccessor ,
223
+ Object value ,
224
+ RelationalPersistentProperty property ,
225
+ List <Criteria > criteriaBasedOnProperties ,
226
+ PropertyPath currentPropertyPath
227
+ ) {
228
+ RelationalPersistentEntity <?> embeddedPersistentEntity = mappingContext .getPersistentEntity (property .getTypeInformation ());
135
229
136
- if (example .getMatcher ().isAllMatching ()) {
137
- criteria = criteria .and (propertyCriteria );
138
- } else {
139
- criteria = criteria .or (propertyCriteria );
140
- }
141
- }
230
+ PersistentPropertyAccessor <?> embeddedEntityPropertyAccessor = embeddedPersistentEntity .getPropertyAccessor (value );
142
231
143
- return Query .query (criteria );
232
+ embeddedPersistentEntity .doWithProperties ((PropertyHandler <RelationalPersistentProperty >) embeddedProperty ->
233
+ potentiallyEnrichCriteriaByProcessingProperty (
234
+ currentPropertyPath ,
235
+ matcherAccessor ,
236
+ embeddedEntityPropertyAccessor ,
237
+ embeddedProperty ,
238
+ criteriaBasedOnProperties
239
+ )
240
+ );
241
+ }
242
+
243
+ @ NonNull
244
+ private static PropertyPath resolveCurrentPropertyPath (@ Nullable PropertyPath propertyPath , RelationalPersistentProperty property ) {
245
+ PropertyPath currentPropertyPath ;
246
+
247
+ if (propertyPath == null ) {
248
+ currentPropertyPath = PropertyPath .from (property .getName (), property .getOwner ().getTypeInformation ());
249
+ } else {
250
+ currentPropertyPath = propertyPath .nested (property .getName ());
251
+ }
252
+ return currentPropertyPath ;
144
253
}
145
254
146
255
/**
147
- * Does this {@link Example } need to include {@literal NULL} values in its {@link Criteria}?
256
+ * Does this {@link ExampleMatcherAccessor } need to include {@literal NULL} values in its {@link Criteria}?
148
257
*
149
- * @param example
150
- * @return whether or not to include nulls.
258
+ * @return whether to include nulls.
151
259
*/
152
- private static <T > boolean includeNulls (Example < T > example ) {
153
- return example . getMatcher () .getNullHandler () == NullHandler .INCLUDE ;
260
+ private static <T > boolean includeNulls (ExampleMatcherAccessor exampleMatcher ) {
261
+ return exampleMatcher .getNullHandler () == NullHandler .INCLUDE ;
154
262
}
155
263
}
0 commit comments