We have to query data from database where we need to find entities matching a list of key value pairs. We thought it would be a nice idea to use Spring Data JPA as we need also pagination.
The tables we created are like below:
terminal(ID,NUMBER,NAME);
terminal_properties(ID,KEY,VALUE,TERMINAL_FK);
Is it possible to define a query method to fetch all terminals with properties containing given key/value pairs ?
Something like this: List<Terminal> findByPropertiesKeyAndValue(List<Property>);
I didn't execute the code, but given the correct import statements, this at least compiles. Depending on your entity definition, some properties may need to be adapted and in any case, you should get an idea of how to approach this.
My criteria query is based on the following SQL:
SELECT * FROM TERMINAL
WHERE ID IN (
SELECT TERMINAL_FK FROM TERMINAL_PROPERTIES
WHERE (KEY = 'key1' AND VALUE = 'value1')
OR (KEY = 'key2' AND VALUE = 'value2')
...
GROUP BY TERMINAL_FK
HAVING COUNT(*) = 42
)
Where you list each name/value pair and 42
simply represents the number of name/value pairs.
So I assume you defined a repository like this:
public interface TerminalRepository extends CrudRepository<Terminal, Long>, JpaSpecificationExecutor {
}
It's important to extend JpaSpecificationExecutor
in order to make use of the criteria API.
Then you can build a criteria query like this:
public class TerminalService {
private static Specification<Terminal> hasProperties(final Map<String, String> properties) {
return new Specification<Terminal>() {
@Override
public Predicate toPredicate(Root<Terminal> root, CriteriaQuery<?> query, CriteriaBuilder builder) {
// SELECT TERMINAL_FK FROM TERMINAL_PROPERTIES
Subquery<TerminalProperty> subQuery = query.subquery(TerminalProperty.class);
Root propertyRoot = subQuery.from(TerminalProperty.class);
subQuery.select(propertyRoot.get("terminal.id"));
Predicate whereClause = null;
for (Map.Entry<String, String> entry : properties.entrySet()) {
// (KEY = 'key1' AND VALUE = 'value1')
Predicate predicate = builder.and(builder.equal(propertyRoot.get("key"),
entry.getKey()), builder.equal(propertyRoot.get("value"), entry.getValue()));
if (whereClause == null) {
whereClause = predicate;
} else {
// (...) OR (...)
whereClause = builder.or(whereClause, predicate);
}
}
subQuery.where(whereClause);
// GROUP BY TERMINAL_FK
subQuery.groupBy(propertyRoot.get("terminal.id"));
// HAVING COUNT(*) = 42
subQuery.having(builder.equal(builder.count(propertyRoot), properties.size()));
// WHERE ID IN (...)
return query.where(builder.in(root.get("id")).value(subQuery)).getRestriction();
}
};
}
@Autowired
private TerminalRepository terminalRepository;
public Iterable<Terminal> findTerminalsWith(Map<String, String> properties) {
// this works only because our repository implements JpaSpecificationExecutor
return terminalRepository.findAll(hasProperties(properties));
}
}
You can obviously replace Map<String, String>
with Iterable<TerminalProperty>
, although that would feel odd because they seem to be bound to a specific Terminal
.
The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.