简体   繁体   中英

spring-data query method with a list as arguments

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.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM