2222import java .util .List ;
2323import java .util .Optional ;
2424
25+ import org .jspecify .annotations .NonNull ;
26+ import org .jspecify .annotations .Nullable ;
27+
2528import org .springframework .data .domain .Example ;
29+ import org .springframework .data .domain .ExampleMatcher ;
2630import org .springframework .data .mapping .PersistentPropertyAccessor ;
2731import org .springframework .data .mapping .PropertyHandler ;
32+ import org .springframework .data .mapping .PropertyPath ;
2833import org .springframework .data .mapping .context .MappingContext ;
2934import org .springframework .data .relational .core .mapping .RelationalPersistentEntity ;
3035import org .springframework .data .relational .core .mapping .RelationalPersistentProperty ;
3944 * @since 2.2
4045 * @author Greg Turnquist
4146 * @author Jens Schauder
47+ * @author Mikhail Polivakha
4248 */
4349public class RelationalExampleMapper {
4450
@@ -64,92 +70,193 @@ public <T> Query getMappedExample(Example<T> example) {
6470 * {@link Query}.
6571 *
6672 * @param example
67- * @param entity
73+ * @param persistentEntity
6874 * @return query
6975 */
70- private <T > Query getMappedExample (Example <T > example , RelationalPersistentEntity <?> entity ) {
76+ private <T > Query getMappedExample (Example <T > example , RelationalPersistentEntity <?> persistentEntity ) {
7177
7278 Assert .notNull (example , "Example must not be null" );
73- Assert .notNull (entity , "RelationalPersistentEntity must not be null" );
79+ Assert .notNull (persistentEntity , "RelationalPersistentEntity must not be null" );
7480
75- PersistentPropertyAccessor <T > propertyAccessor = entity .getPropertyAccessor (example .getProbe ());
81+ PersistentPropertyAccessor <T > probePropertyAccessor = persistentEntity .getPropertyAccessor (example .getProbe ());
7682 ExampleMatcherAccessor matcherAccessor = new ExampleMatcherAccessor (example .getMatcher ());
7783
78- final List <Criteria > criteriaBasedOnProperties = new ArrayList <>();
84+ final List <Criteria > criteriaBasedOnProperties = buildCriteria ( //
85+ persistentEntity , //
86+ matcherAccessor , //
87+ probePropertyAccessor //
88+ );
7989
80- entity .doWithProperties ((PropertyHandler <RelationalPersistentProperty >) property -> {
90+ // Criteria, assemble!
91+ Criteria criteria = Criteria .empty ();
8192
82- if (property .isCollectionLike () || property .isMap ()) {
83- return ;
84- }
93+ for (Criteria propertyCriteria : criteriaBasedOnProperties ) {
8594
86- if (matcherAccessor .isIgnoredPath (property .getName ())) {
87- return ;
95+ if (example .getMatcher ().isAllMatching ()) {
96+ criteria = criteria .and (propertyCriteria );
97+ } else {
98+ criteria = criteria .or (propertyCriteria );
8899 }
100+ }
101+
102+ return Query .query (criteria );
103+ }
104+
105+ private <T > List <Criteria > buildCriteria ( //
106+ RelationalPersistentEntity <?> persistentEntity , //
107+ ExampleMatcherAccessor matcherAccessor , //
108+ PersistentPropertyAccessor <T > probePropertyAccessor //
109+ ) {
110+ final List <Criteria > criteriaBasedOnProperties = new ArrayList <>();
111+
112+ persistentEntity .doWithProperties ((PropertyHandler <RelationalPersistentProperty >) property -> {
113+ potentiallyEnrichCriteria (
114+ null ,
115+ matcherAccessor , //
116+ probePropertyAccessor , //
117+ property , //
118+ criteriaBasedOnProperties //
119+ );
120+ });
121+ return criteriaBasedOnProperties ;
122+ }
123+
124+ /**
125+ * Analyzes the incoming {@code property} and potentially enriches the {@code criteriaBasedOnProperties} with the new
126+ * {@link Criteria} for this property.
127+ * <p>
128+ * This algorithm is recursive in order to take the embedded properties into account. The caller can expect that the result
129+ * of this method call is fully processed subtree of an aggreagte where the passed {@code property} serves as the root.
130+ *
131+ * @param propertyPath the {@link PropertyPath} of the passed {@code property}.
132+ * @param matcherAccessor the accessor for the original {@link ExampleMatcher}.
133+ * @param entityPropertiesAccessor the accessor for the properties of the current entity that holds the given {@code property}
134+ * @param property the property under analysis
135+ * @param criteriaBasedOnProperties the {@link List} of criteria objects that potentially gets enriched as a
136+ * result of the incoming {@code property} processing
137+ */
138+ private <T > void potentiallyEnrichCriteria (
139+ @ Nullable PropertyPath propertyPath ,
140+ ExampleMatcherAccessor matcherAccessor , //
141+ PersistentPropertyAccessor <T > entityPropertiesAccessor , //
142+ RelationalPersistentProperty property , //
143+ List <Criteria > criteriaBasedOnProperties //
144+ ) {
145+
146+ // QBE do not support queries on Child aggregates yet
147+ if (property .isCollectionLike () || property .isMap ()) {
148+ return ;
149+ }
150+
151+ PropertyPath currentPropertyPath = resolveCurrentPropertyPath (propertyPath , property );
152+ String currentPropertyDotPath = currentPropertyPath .toDotPath ();
153+
154+ if (matcherAccessor .isIgnoredPath (currentPropertyDotPath )) {
155+ return ;
156+ }
89157
158+ Object actualPropertyValue = entityPropertiesAccessor .getProperty (property );
159+
160+ if (property .isEmbedded () && actualPropertyValue != null ) {
161+ processEmbeddedRecursively ( //
162+ matcherAccessor , //
163+ actualPropertyValue ,
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 (actualPropertyValue ));
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+ potentiallyEnrichCriteria (
234+ currentPropertyPath ,
235+ matcherAccessor ,
236+ embeddedEntityPropertyAccessor ,
237+ embeddedProperty ,
238+ criteriaBasedOnProperties
239+ )
240+ );
241+ }
242+
243+ private static PropertyPath resolveCurrentPropertyPath (@ Nullable PropertyPath propertyPath , RelationalPersistentProperty property ) {
244+ PropertyPath currentPropertyPath ;
245+
246+ if (propertyPath == null ) {
247+ currentPropertyPath = PropertyPath .from (property .getName (), property .getOwner ().getTypeInformation ());
248+ } else {
249+ currentPropertyPath = propertyPath .nested (property .getName ());
250+ }
251+ return currentPropertyPath ;
144252 }
145253
146254 /**
147- * Does this {@link Example } need to include {@literal NULL} values in its {@link Criteria}?
255+ * Does this {@link ExampleMatcherAccessor } need to include {@literal NULL} values in its {@link Criteria}?
148256 *
149- * @param example
150- * @return whether or not to include nulls.
257+ * @return whether to include nulls.
151258 */
152- private static <T > boolean includeNulls (Example < T > example ) {
153- return example . getMatcher () .getNullHandler () == NullHandler .INCLUDE ;
259+ private static <T > boolean includeNulls (ExampleMatcherAccessor exampleMatcher ) {
260+ return exampleMatcher .getNullHandler () == NullHandler .INCLUDE ;
154261 }
155262}
0 commit comments