Skip to content

Commit 3eb7821

Browse files
committed
Add support for Query by Example.
Resolves #929.
1 parent ad67ec0 commit 3eb7821

File tree

2 files changed

+573
-0
lines changed

2 files changed

+573
-0
lines changed
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,167 @@
1+
/*
2+
* Copyright 2021 the original author or authors.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* https://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
package org.springframework.data.relational.repository.query;
18+
19+
import static org.springframework.data.domain.ExampleMatcher.*;
20+
21+
import java.beans.PropertyDescriptor;
22+
23+
import org.springframework.beans.BeanWrapper;
24+
import org.springframework.beans.BeanWrapperImpl;
25+
import org.springframework.beans.NotReadablePropertyException;
26+
import org.springframework.data.domain.Example;
27+
import org.springframework.data.mapping.context.MappingContext;
28+
import org.springframework.data.relational.core.mapping.RelationalMappingContext;
29+
import org.springframework.data.relational.core.mapping.RelationalPersistentEntity;
30+
import org.springframework.data.relational.core.mapping.RelationalPersistentProperty;
31+
import org.springframework.data.relational.core.query.Criteria;
32+
import org.springframework.data.relational.core.query.Query;
33+
import org.springframework.util.Assert;
34+
35+
/**
36+
* Transform an {@link Example} into a {@link Query}.
37+
*
38+
* @author Greg Turnquist
39+
*/
40+
public class RelationalExampleMapper {
41+
42+
private final MappingContext<? extends RelationalPersistentEntity<?>, ? extends RelationalPersistentProperty> mappingContext;
43+
44+
public RelationalExampleMapper(
45+
MappingContext<? extends RelationalPersistentEntity<?>, ? extends RelationalPersistentProperty> mappingContext) {
46+
this.mappingContext = mappingContext;
47+
}
48+
49+
/**
50+
* Use the {@link Example} to extract a {@link Query}.
51+
*
52+
* @param example
53+
* @return query
54+
*/
55+
public <T> Query getMappedExample(Example<T> example) {
56+
return getMappedExample(example, mappingContext.getRequiredPersistentEntity(example.getProbeType()));
57+
}
58+
59+
/**
60+
* Transform each property of the {@link Example}'s probe into a {@link Criteria} and assemble them into a
61+
* {@link Query}.
62+
*
63+
* @param example
64+
* @param entity
65+
* @return query
66+
*/
67+
private <T> Query getMappedExample(Example<T> example, RelationalPersistentEntity<?> entity) {
68+
69+
Assert.notNull(example, "Example must not be null!");
70+
Assert.notNull(entity, "RelationalPersistentEntity must not be null!");
71+
72+
Criteria criteria = Criteria.empty();
73+
74+
BeanWrapper beanWrapper = new BeanWrapperImpl(example.getProbe());
75+
76+
for (PropertyDescriptor propertyDescriptor : beanWrapper.getPropertyDescriptors()) {
77+
78+
// "class" isn't grounds for a query criteria
79+
if (propertyDescriptor.getName().equals("class")) {
80+
continue;
81+
}
82+
83+
// if this property descriptor is part of the ignoredPaths set, skip over it.
84+
if (example.getMatcher().getIgnoredPaths().contains(propertyDescriptor.getName())) {
85+
continue;
86+
}
87+
88+
Object propertyValue = null;
89+
try {
90+
propertyValue = beanWrapper.getPropertyValue(propertyDescriptor.getName());
91+
} catch (NotReadablePropertyException e) {}
92+
93+
if (propertyValue != null) {
94+
95+
String columnName = entity.getPersistentProperty(propertyDescriptor.getName()).getColumnName().getReference();
96+
97+
Criteria propertyCriteria;
98+
99+
// First, check the overall matcher for settings
100+
StringMatcher stringMatcher = example.getMatcher().getDefaultStringMatcher();
101+
boolean ignoreCase = example.getMatcher().isIgnoreCaseEnabled();
102+
103+
// Then, apply any property-specific overrides
104+
if (example.getMatcher().getPropertySpecifiers().hasSpecifierForPath(propertyDescriptor.getName())) {
105+
106+
PropertySpecifier propertySpecifier = example.getMatcher().getPropertySpecifiers()
107+
.getForPath(propertyDescriptor.getName());
108+
109+
if (propertySpecifier.getStringMatcher() != null) {
110+
stringMatcher = propertySpecifier.getStringMatcher();
111+
}
112+
113+
if (propertySpecifier.getIgnoreCase() != null) {
114+
ignoreCase = propertySpecifier.getIgnoreCase();
115+
}
116+
}
117+
118+
// Assemble the property's criteria
119+
switch (stringMatcher) {
120+
case DEFAULT:
121+
case EXACT:
122+
propertyCriteria = includeNulls((Example<T>) example) //
123+
? Criteria.where(columnName).isNull().or(columnName).is(propertyValue).ignoreCase(ignoreCase)
124+
: Criteria.where(columnName).is(propertyValue).ignoreCase(ignoreCase);
125+
break;
126+
case ENDING:
127+
propertyCriteria = includeNulls(example) //
128+
? Criteria.where(columnName).isNull().or(columnName).like("%" + propertyValue).ignoreCase(ignoreCase)
129+
: Criteria.where(columnName).like("%" + propertyValue).ignoreCase(ignoreCase);
130+
break;
131+
case STARTING:
132+
propertyCriteria = includeNulls(example) //
133+
? Criteria.where(columnName).isNull().or(columnName).like(propertyValue + "%").ignoreCase(ignoreCase)
134+
: Criteria.where(columnName).like(propertyValue + "%").ignoreCase(ignoreCase);
135+
break;
136+
case CONTAINING:
137+
propertyCriteria = includeNulls(example) //
138+
? Criteria.where(columnName).isNull().or(columnName).like("%" + propertyValue + "%")
139+
.ignoreCase(ignoreCase)
140+
: Criteria.where(columnName).like("%" + propertyValue + "%").ignoreCase(ignoreCase);
141+
break;
142+
default:
143+
throw new IllegalStateException(example.getMatcher().getDefaultStringMatcher() + " is not supported!");
144+
}
145+
146+
// Add this criteria based on any/all
147+
if (example.getMatcher().isAllMatching()) {
148+
criteria = criteria.and(propertyCriteria);
149+
} else {
150+
criteria = criteria.or(propertyCriteria);
151+
}
152+
}
153+
}
154+
155+
return Query.query(criteria);
156+
}
157+
158+
/**
159+
* Does this {@link Example} need to include {@literal NULL} values in its {@link Criteria}?
160+
*
161+
* @param example
162+
* @return whether or not to include nulls.
163+
*/
164+
private static <T> boolean includeNulls(Example<T> example) {
165+
return example.getMatcher().getNullHandler() == NullHandler.INCLUDE;
166+
}
167+
}

0 commit comments

Comments
 (0)