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,192 @@ 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+ }
89103
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 {
90169 Optional <?> optionalConvertedPropValue = matcherAccessor //
91- .getValueTransformerForPath (property . getName () ) //
92- .apply (Optional .ofNullable (propertyAccessor .getProperty (property )));
170+ .getValueTransformerForPath (currentPropertyDotPath ) //
171+ .apply (Optional .ofNullable (entityPropertiesAccessor .getProperty (property )));
93172
94173 // If the value is empty, don't try to match against it
95- if (! optionalConvertedPropValue .isPresent ()) {
174+ if (optionalConvertedPropValue .isEmpty ()) {
96175 return ;
97176 }
98177
99178 Object convPropValue = optionalConvertedPropValue .get ();
100- boolean ignoreCase = matcherAccessor .isIgnoreCaseForPath (property . getName () );
179+ boolean ignoreCase = matcherAccessor .isIgnoreCaseForPath (currentPropertyDotPath );
101180
102181 String column = property .getName ();
103182
104- switch (matcherAccessor .getStringMatcherForPath (property . getName () )) {
183+ switch (matcherAccessor .getStringMatcherForPath (currentPropertyDotPath )) {
105184 case DEFAULT :
106185 case EXACT :
107- criteriaBasedOnProperties .add (includeNulls (example ) //
186+ criteriaBasedOnProperties .add (includeNulls (matcherAccessor ) //
108187 ? Criteria .where (column ).isNull ().or (column ).is (convPropValue ).ignoreCase (ignoreCase )
109188 : Criteria .where (column ).is (convPropValue ).ignoreCase (ignoreCase ));
110189 break ;
111190 case ENDING :
112- criteriaBasedOnProperties .add (includeNulls (example ) //
191+ criteriaBasedOnProperties .add (includeNulls (matcherAccessor ) //
113192 ? Criteria .where (column ).isNull ().or (column ).like ("%" + convPropValue ).ignoreCase (ignoreCase )
114193 : Criteria .where (column ).like ("%" + convPropValue ).ignoreCase (ignoreCase ));
115194 break ;
116195 case STARTING :
117- criteriaBasedOnProperties .add (includeNulls (example ) //
196+ criteriaBasedOnProperties .add (includeNulls (matcherAccessor ) //
118197 ? Criteria .where (column ).isNull ().or (column ).like (convPropValue + "%" ).ignoreCase (ignoreCase )
119198 : Criteria .where (column ).like (convPropValue + "%" ).ignoreCase (ignoreCase ));
120199 break ;
121200 case CONTAINING :
122- criteriaBasedOnProperties .add (includeNulls (example ) //
201+ criteriaBasedOnProperties .add (includeNulls (matcherAccessor ) //
123202 ? Criteria .where (column ).isNull ().or (column ).like ("%" + convPropValue + "%" ).ignoreCase (ignoreCase )
124203 : Criteria .where (column ).like ("%" + convPropValue + "%" ).ignoreCase (ignoreCase ));
125204 break ;
126205 default :
127- throw new IllegalStateException (example . getMatcher () .getDefaultStringMatcher () + " is not supported" );
206+ throw new IllegalStateException (matcherAccessor .getDefaultStringMatcher () + " is not supported" );
128207 }
129- });
208+ }
130209
131- // Criteria, assemble!
132- Criteria criteria = Criteria .empty ();
210+ }
133211
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 ());
135229
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 );
142231
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 ;
144253 }
145254
146255 /**
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}?
148257 *
149- * @param example
150- * @return whether or not to include nulls.
258+ * @return whether to include nulls.
151259 */
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 ;
154262 }
155263}
0 commit comments