2

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>);

1 Answer 1

1

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.

Sign up to request clarification or add additional context in comments.

2 Comments

I was hoping to avoid Specifications and build queries programmatically but obviously this is not possible. Thanks a lot for the detailed answer.
You're welcome. And you can certainly use the JPA criteria API directly if you wish to avoid using a Specification, but you wanted to use Spring Data JPA?

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.