2222import java .util .List ;
2323import java .util .Optional ;
2424
25+ import org .jetbrains .annotations .NotNull ;
26+ import org .jspecify .annotations .NonNull ;
27+ import org .jspecify .annotations .Nullable ;
28+
2529import org .springframework .data .domain .Example ;
30+ import org .springframework .data .domain .ExampleMatcher ;
2631import org .springframework .data .mapping .PersistentPropertyAccessor ;
2732import org .springframework .data .mapping .PropertyHandler ;
33+ import org .springframework .data .mapping .PropertyPath ;
2834import org .springframework .data .mapping .context .MappingContext ;
2935import org .springframework .data .relational .core .mapping .RelationalPersistentEntity ;
3036import org .springframework .data .relational .core .mapping .RelationalPersistentProperty ;
3137import org .springframework .data .relational .core .query .Criteria ;
3238import org .springframework .data .relational .core .query .Query ;
3339import org .springframework .data .support .ExampleMatcherAccessor ;
3440import org .springframework .util .Assert ;
41+ import org .springframework .util .StringUtils ;
3542
3643/**
3744 * Transform an {@link Example} into a {@link Query}.
3845 *
3946 * @since 2.2
4047 * @author Greg Turnquist
4148 * @author Jens Schauder
49+ * @author Mikhail Polivakha
4250 */
4351public class RelationalExampleMapper {
4452
@@ -64,92 +72,194 @@ public <T> Query getMappedExample(Example<T> example) {
6472 * {@link Query}.
6573 *
6674 * @param example
67- * @param entity
75+ * @param persistentEntity
6876 * @return query
6977 */
70- private <T > Query getMappedExample (Example <T > example , RelationalPersistentEntity <?> entity ) {
78+ private <T > Query getMappedExample (Example <T > example , RelationalPersistentEntity <?> persistentEntity ) {
7179
7280 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" );
7482
75- PersistentPropertyAccessor <T > propertyAccessor = entity .getPropertyAccessor (example .getProbe ());
83+ PersistentPropertyAccessor <T > probePropertyAccessor = persistentEntity .getPropertyAccessor (example .getProbe ());
7684 ExampleMatcherAccessor matcherAccessor = new ExampleMatcherAccessor (example .getMatcher ());
7785
78- final List <Criteria > criteriaBasedOnProperties = new ArrayList <>();
86+ final List <Criteria > criteriaBasedOnProperties = buildCriteriaRecursive ( //
87+ persistentEntity , //
88+ matcherAccessor , //
89+ probePropertyAccessor //
90+ );
7991
80- entity .doWithProperties ((PropertyHandler <RelationalPersistentProperty >) property -> {
92+ // Criteria, assemble!
93+ Criteria criteria = Criteria .empty ();
8194
82- if (property .isCollectionLike () || property .isMap ()) {
83- return ;
84- }
95+ for (Criteria propertyCriteria : criteriaBasedOnProperties ) {
8596
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 );
88101 }
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+ }
89159
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 {
90171 Optional <?> optionalConvertedPropValue = matcherAccessor //
91- .getValueTransformerForPath (property . getName () ) //
92- .apply (Optional .ofNullable (propertyAccessor . getProperty ( property ) ));
172+ .getValueTransformerForPath (currentPropertyDotPath ) //
173+ .apply (Optional .ofNullable (actualPropertyValue ));
93174
94175 // If the value is empty, don't try to match against it
95- if (! optionalConvertedPropValue .isPresent ()) {
176+ if (optionalConvertedPropValue .isEmpty ()) {
96177 return ;
97178 }
98179
99180 Object convPropValue = optionalConvertedPropValue .get ();
100- boolean ignoreCase = matcherAccessor .isIgnoreCaseForPath (property . getName () );
181+ boolean ignoreCase = matcherAccessor .isIgnoreCaseForPath (currentPropertyDotPath );
101182
102183 String column = property .getName ();
103184
104- switch (matcherAccessor .getStringMatcherForPath (property . getName () )) {
185+ switch (matcherAccessor .getStringMatcherForPath (currentPropertyDotPath )) {
105186 case DEFAULT :
106187 case EXACT :
107- criteriaBasedOnProperties .add (includeNulls (example ) //
188+ criteriaBasedOnProperties .add (includeNulls (matcherAccessor ) //
108189 ? Criteria .where (column ).isNull ().or (column ).is (convPropValue ).ignoreCase (ignoreCase )
109190 : Criteria .where (column ).is (convPropValue ).ignoreCase (ignoreCase ));
110191 break ;
111192 case ENDING :
112- criteriaBasedOnProperties .add (includeNulls (example ) //
193+ criteriaBasedOnProperties .add (includeNulls (matcherAccessor ) //
113194 ? Criteria .where (column ).isNull ().or (column ).like ("%" + convPropValue ).ignoreCase (ignoreCase )
114195 : Criteria .where (column ).like ("%" + convPropValue ).ignoreCase (ignoreCase ));
115196 break ;
116197 case STARTING :
117- criteriaBasedOnProperties .add (includeNulls (example ) //
198+ criteriaBasedOnProperties .add (includeNulls (matcherAccessor ) //
118199 ? Criteria .where (column ).isNull ().or (column ).like (convPropValue + "%" ).ignoreCase (ignoreCase )
119200 : Criteria .where (column ).like (convPropValue + "%" ).ignoreCase (ignoreCase ));
120201 break ;
121202 case CONTAINING :
122- criteriaBasedOnProperties .add (includeNulls (example ) //
203+ criteriaBasedOnProperties .add (includeNulls (matcherAccessor ) //
123204 ? Criteria .where (column ).isNull ().or (column ).like ("%" + convPropValue + "%" ).ignoreCase (ignoreCase )
124205 : Criteria .where (column ).like ("%" + convPropValue + "%" ).ignoreCase (ignoreCase ));
125206 break ;
126207 default :
127- throw new IllegalStateException (example . getMatcher () .getDefaultStringMatcher () + " is not supported" );
208+ throw new IllegalStateException (matcherAccessor .getDefaultStringMatcher () + " is not supported" );
128209 }
129- });
210+ }
130211
131- // Criteria, assemble!
132- Criteria criteria = Criteria .empty ();
212+ }
133213
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 ());
135231
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 );
142233
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 ;
144255 }
145256
146257 /**
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}?
148259 *
149- * @param example
150- * @return whether or not to include nulls.
260+ * @return whether to include nulls.
151261 */
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 ;
154264 }
155265}
0 commit comments