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,194 @@ 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
+ }
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
+ }
89
159
160
+ Object actualPropertyValue = entityPropertiesAccessor .getProperty (property );
161
+
162
+ if (property .isEmbedded () && actualPropertyValue != null ) {
163
+ processEmbeddedRecursively ( //
164
+ matcherAccessor , //
165
+ actualPropertyValue ,
166
+ property , //
167
+ criteriaBasedOnProperties , //
168
+ currentPropertyPath //
169
+ );
170
+ } else {
90
171
Optional <?> optionalConvertedPropValue = matcherAccessor //
91
- .getValueTransformerForPath (property . getName () ) //
92
- .apply (Optional .ofNullable (propertyAccessor . getProperty ( property ) ));
172
+ .getValueTransformerForPath (currentPropertyDotPath ) //
173
+ .apply (Optional .ofNullable (actualPropertyValue ));
93
174
94
175
// If the value is empty, don't try to match against it
95
- if (! optionalConvertedPropValue .isPresent ()) {
176
+ if (optionalConvertedPropValue .isEmpty ()) {
96
177
return ;
97
178
}
98
179
99
180
Object convPropValue = optionalConvertedPropValue .get ();
100
- boolean ignoreCase = matcherAccessor .isIgnoreCaseForPath (property . getName () );
181
+ boolean ignoreCase = matcherAccessor .isIgnoreCaseForPath (currentPropertyDotPath );
101
182
102
183
String column = property .getName ();
103
184
104
- switch (matcherAccessor .getStringMatcherForPath (property . getName () )) {
185
+ switch (matcherAccessor .getStringMatcherForPath (currentPropertyDotPath )) {
105
186
case DEFAULT :
106
187
case EXACT :
107
- criteriaBasedOnProperties .add (includeNulls (example ) //
188
+ criteriaBasedOnProperties .add (includeNulls (matcherAccessor ) //
108
189
? Criteria .where (column ).isNull ().or (column ).is (convPropValue ).ignoreCase (ignoreCase )
109
190
: Criteria .where (column ).is (convPropValue ).ignoreCase (ignoreCase ));
110
191
break ;
111
192
case ENDING :
112
- criteriaBasedOnProperties .add (includeNulls (example ) //
193
+ criteriaBasedOnProperties .add (includeNulls (matcherAccessor ) //
113
194
? Criteria .where (column ).isNull ().or (column ).like ("%" + convPropValue ).ignoreCase (ignoreCase )
114
195
: Criteria .where (column ).like ("%" + convPropValue ).ignoreCase (ignoreCase ));
115
196
break ;
116
197
case STARTING :
117
- criteriaBasedOnProperties .add (includeNulls (example ) //
198
+ criteriaBasedOnProperties .add (includeNulls (matcherAccessor ) //
118
199
? Criteria .where (column ).isNull ().or (column ).like (convPropValue + "%" ).ignoreCase (ignoreCase )
119
200
: Criteria .where (column ).like (convPropValue + "%" ).ignoreCase (ignoreCase ));
120
201
break ;
121
202
case CONTAINING :
122
- criteriaBasedOnProperties .add (includeNulls (example ) //
203
+ criteriaBasedOnProperties .add (includeNulls (matcherAccessor ) //
123
204
? Criteria .where (column ).isNull ().or (column ).like ("%" + convPropValue + "%" ).ignoreCase (ignoreCase )
124
205
: Criteria .where (column ).like ("%" + convPropValue + "%" ).ignoreCase (ignoreCase ));
125
206
break ;
126
207
default :
127
- throw new IllegalStateException (example . getMatcher () .getDefaultStringMatcher () + " is not supported" );
208
+ throw new IllegalStateException (matcherAccessor .getDefaultStringMatcher () + " is not supported" );
128
209
}
129
- });
210
+ }
130
211
131
- // Criteria, assemble!
132
- Criteria criteria = Criteria .empty ();
212
+ }
133
213
134
- for (Criteria propertyCriteria : criteriaBasedOnProperties ) {
214
+ /**
215
+ * Processes an embedded entity's properties recursively.
216
+ *
217
+ * @param matcherAccessor the input matcher on the {@link Example#getProbe() original probe}.
218
+ * @param value the actual embedded object.
219
+ * @param property the embedded property.
220
+ * @param criteriaBasedOnProperties collection of {@link Criteria} objects to potentially enrich.
221
+ * @param currentPropertyPath the dot-separated path of the passed {@code property}.
222
+ */
223
+ private void processEmbeddedRecursively (
224
+ ExampleMatcherAccessor matcherAccessor ,
225
+ Object value ,
226
+ RelationalPersistentProperty property ,
227
+ List <Criteria > criteriaBasedOnProperties ,
228
+ PropertyPath currentPropertyPath
229
+ ) {
230
+ RelationalPersistentEntity <?> embeddedPersistentEntity = mappingContext .getPersistentEntity (property .getTypeInformation ());
135
231
136
- if (example .getMatcher ().isAllMatching ()) {
137
- criteria = criteria .and (propertyCriteria );
138
- } else {
139
- criteria = criteria .or (propertyCriteria );
140
- }
141
- }
232
+ PersistentPropertyAccessor <?> embeddedEntityPropertyAccessor = embeddedPersistentEntity .getPropertyAccessor (value );
142
233
143
- return Query .query (criteria );
234
+ embeddedPersistentEntity .doWithProperties ((PropertyHandler <RelationalPersistentProperty >) embeddedProperty ->
235
+ potentiallyEnrichCriteriaByProcessingProperty (
236
+ currentPropertyPath ,
237
+ matcherAccessor ,
238
+ embeddedEntityPropertyAccessor ,
239
+ embeddedProperty ,
240
+ criteriaBasedOnProperties
241
+ )
242
+ );
243
+ }
244
+
245
+ @ NonNull
246
+ private static PropertyPath resolveCurrentPropertyPath (@ Nullable PropertyPath propertyPath , RelationalPersistentProperty property ) {
247
+ PropertyPath currentPropertyPath ;
248
+
249
+ if (propertyPath == null ) {
250
+ currentPropertyPath = PropertyPath .from (property .getName (), property .getOwner ().getTypeInformation ());
251
+ } else {
252
+ currentPropertyPath = propertyPath .nested (property .getName ());
253
+ }
254
+ return currentPropertyPath ;
144
255
}
145
256
146
257
/**
147
- * Does this {@link Example } need to include {@literal NULL} values in its {@link Criteria}?
258
+ * Does this {@link ExampleMatcherAccessor } need to include {@literal NULL} values in its {@link Criteria}?
148
259
*
149
- * @param example
150
- * @return whether or not to include nulls.
260
+ * @return whether to include nulls.
151
261
*/
152
- private static <T > boolean includeNulls (Example < T > example ) {
153
- return example . getMatcher () .getNullHandler () == NullHandler .INCLUDE ;
262
+ private static <T > boolean includeNulls (ExampleMatcherAccessor exampleMatcher ) {
263
+ return exampleMatcher .getNullHandler () == NullHandler .INCLUDE ;
154
264
}
155
265
}
0 commit comments