Skip to content

Commit eafde8a

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

File tree

2 files changed

+570
-0
lines changed

2 files changed

+570
-0
lines changed
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,164 @@
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.relational.core.mapping.RelationalMappingContext;
28+
import org.springframework.data.relational.core.mapping.RelationalPersistentEntity;
29+
import org.springframework.data.relational.core.query.Criteria;
30+
import org.springframework.data.relational.core.query.Query;
31+
import org.springframework.util.Assert;
32+
33+
/**
34+
* Transform an {@link Example} into a {@link Query}.
35+
*
36+
* @author Greg Turnquist
37+
*/
38+
public class RelationalExampleMapper {
39+
40+
private final RelationalMappingContext mappingContext;
41+
42+
public RelationalExampleMapper(RelationalMappingContext mappingContext) {
43+
this.mappingContext = mappingContext;
44+
}
45+
46+
/**
47+
* Use the {@link Example} to extract a {@link Query}.
48+
*
49+
* @param example
50+
* @return query
51+
*/
52+
public <T> Query getMappedExample(Example<T> example) {
53+
return getMappedExample(example, mappingContext.getRequiredPersistentEntity(example.getProbeType()));
54+
}
55+
56+
/**
57+
* Transform each property of the {@link Example}'s probe into a {@link Criteria} and assemble them into a
58+
* {@link Query}.
59+
*
60+
* @param example
61+
* @param entity
62+
* @return query
63+
*/
64+
private <T> Query getMappedExample(Example<T> example, RelationalPersistentEntity<?> entity) {
65+
66+
Assert.notNull(example, "Example must not be null!");
67+
Assert.notNull(entity, "RelationalPersistentEntity must not be null!");
68+
69+
Criteria criteria = Criteria.empty();
70+
71+
BeanWrapper beanWrapper = new BeanWrapperImpl(example.getProbe());
72+
73+
for (PropertyDescriptor propertyDescriptor : beanWrapper.getPropertyDescriptors()) {
74+
75+
// "class" isn't grounds for a query criteria
76+
if (propertyDescriptor.getName().equals("class")) {
77+
continue;
78+
}
79+
80+
// if this property descriptor is part of the ignoredPaths set, skip over it.
81+
if (example.getMatcher().getIgnoredPaths().contains(propertyDescriptor.getName())) {
82+
continue;
83+
}
84+
85+
Object propertyValue = null;
86+
try {
87+
propertyValue = beanWrapper.getPropertyValue(propertyDescriptor.getName());
88+
} catch (NotReadablePropertyException e) {}
89+
90+
if (propertyValue != null) {
91+
92+
String columnName = entity.getPersistentProperty(propertyDescriptor.getName()).getColumnName().getReference();
93+
94+
Criteria propertyCriteria;
95+
96+
// First, check the overall matcher for settings
97+
StringMatcher stringMatcher = example.getMatcher().getDefaultStringMatcher();
98+
boolean ignoreCase = example.getMatcher().isIgnoreCaseEnabled();
99+
100+
// Then, apply any property-specific overrides
101+
if (example.getMatcher().getPropertySpecifiers().hasSpecifierForPath(propertyDescriptor.getName())) {
102+
103+
PropertySpecifier propertySpecifier = example.getMatcher().getPropertySpecifiers()
104+
.getForPath(propertyDescriptor.getName());
105+
106+
if (propertySpecifier.getStringMatcher() != null) {
107+
stringMatcher = propertySpecifier.getStringMatcher();
108+
}
109+
110+
if (propertySpecifier.getIgnoreCase() != null) {
111+
ignoreCase = propertySpecifier.getIgnoreCase();
112+
}
113+
}
114+
115+
// Assemble the property's criteria
116+
switch (stringMatcher) {
117+
case DEFAULT:
118+
case EXACT:
119+
propertyCriteria = includeNulls((Example<T>) example) //
120+
? Criteria.where(columnName).isNull().or(columnName).is(propertyValue).ignoreCase(ignoreCase)
121+
: Criteria.where(columnName).is(propertyValue).ignoreCase(ignoreCase);
122+
break;
123+
case ENDING:
124+
propertyCriteria = includeNulls(example) //
125+
? Criteria.where(columnName).isNull().or(columnName).like("%" + propertyValue).ignoreCase(ignoreCase)
126+
: Criteria.where(columnName).like("%" + propertyValue).ignoreCase(ignoreCase);
127+
break;
128+
case STARTING:
129+
propertyCriteria = includeNulls(example) //
130+
? Criteria.where(columnName).isNull().or(columnName).like(propertyValue + "%").ignoreCase(ignoreCase)
131+
: Criteria.where(columnName).like(propertyValue + "%").ignoreCase(ignoreCase);
132+
break;
133+
case CONTAINING:
134+
propertyCriteria = includeNulls(example) //
135+
? Criteria.where(columnName).isNull().or(columnName).like("%" + propertyValue + "%")
136+
.ignoreCase(ignoreCase)
137+
: Criteria.where(columnName).like("%" + propertyValue + "%").ignoreCase(ignoreCase);
138+
break;
139+
default:
140+
throw new IllegalStateException(example.getMatcher().getDefaultStringMatcher() + " is not supported!");
141+
}
142+
143+
// Add this criteria based on any/all
144+
if (example.getMatcher().isAllMatching()) {
145+
criteria = criteria.and(propertyCriteria);
146+
} else {
147+
criteria = criteria.or(propertyCriteria);
148+
}
149+
}
150+
}
151+
152+
return Query.query(criteria);
153+
}
154+
155+
/**
156+
* Does this {@link Example} need to include {@literal NULL} values in its {@link Criteria}?
157+
*
158+
* @param example
159+
* @return whether or not to include nulls.
160+
*/
161+
private static <T> boolean includeNulls(Example<T> example) {
162+
return example.getMatcher().getNullHandler() == NullHandler.INCLUDE;
163+
}
164+
}

0 commit comments

Comments
 (0)