26
votes

Currently I have been using following Spring JPA Repository base custom query and it works fine,

 @Query("SELECT usr FROM User usr  WHERE usr.configurable = TRUE "
              + "AND (" +
                        "lower(usr.name) like lower(:filterText) OR lower(usr.userType.classType.displayName) like lower(:filterText) OR lower(usr.userType.model) like lower(:filterText)"
              +      ")"
              + "")
  public List<User> findByFilterText(@Param("filterText") String filterText, Sort sort);

I need to modify this query when filter text going to be a comma separated value. But as following manner it will be a dynamic query and how can I execute it.

Dynamic query I need to build,

String sql = "SELECT usr FROM User usr WHERE usr.configurable = TRUE";

for(String word : filterText.split(",")) {
                sql += " AND (lower(usr.name) like lower(:" + word + ") OR lower(usr.userType.classType.displayName) like lower(:" + word + ") OR lower(usr.userType.model) like lower(:" + word + "))";
}
3
look into JpaSpecificationExecutorguido
Provide an implementation for your DAO, and execute this dynamic query from the implementation. docs.spring.io/spring-data/jpa/docs/current/reference/html/…. The DAO shouldn't split. It should take a List<String> or a Set<String> as argument. The caller should deal with splitting.JB Nizet

3 Answers

17
votes

Per JB Nizet and the spring-data documentation, you should use a custom interface + repository implementation.

Create an interface with the method:

public interface MyEntityRepositoryCustom {
    List<User> findByFilterText(Set<String> words);
}

Create an implementation:

@Repository
public class MyEntityRepositoryImpl implements MyEntityRepositoryCustom {
    @PersistenceContext
    private EntityManager entityManager;

    public List<User> findByFilterText(Set<String> words) {
        // implementation below
    }
}

Extend the new interface in your existing Repository interface:

public interface MyEntityRepository extends JpaRepository<MyEntity, Long>, MyEntityRepositoryCustom {
    // other query methods
}

Finally, call the method somewhere else:

dao.findByFilterText(new HashSet<String>(Arrays.asList(filterText.split(","))));

Query implementation

Your method of producing the sql variable, namely by concatenating some strings into the query is bad. Do not do this.

The word which you are concatenating must be a valid JPQL identifier, namely a : followed by a java identifier start, optionally followed by some java identifier part. This means that if your CSV contains foo bar,baz, you will attempt to use foo bar as an identifier and you'll get an exception.

You can instead use CriteriaBuilder to construct the query in a safe way:

public List<User> findByFilterText(Set<String> words) {
    CriteriaBuilder cb = entityManager.getCriteriaBuilder();
    CriteriaQuery<User> q = cb.createQuery(User.class);
    Root<User> user = q.from(User.class);

    Path<String> namePath = user.get("name");
    Path<String> userTypeClassTypeDisplayName = 
                     user.get("userType").get("classType").get("displayName");
    Path<String> userTypeModel = user.get("userType").get("model");
    List<Predicate> predicates = new ArrayList<>();
    for(String word : words) {
        Expression<String> wordLiteral = cb.literal(word);
        predicates.add(
                cb.or(
                    cb.like(cb.lower(namePath), cb.lower(wordLiteral)),
                    cb.like(cb.lower(userTypeClassTypeDisplayName),
                            cb.lower(wordLiteral)),
                    cb.like(cb.lower(userTypeModel), cb.lower(wordLiteral))
                )
        );
    }
    q.select(doc).where(
            cb.and(predicates.toArray(new Predicate[predicates.size()]))
    );

    return entityManager.createQuery(q).getResultList();
}
7
votes

I've been looking for the solution myself : The naming of the "Custom" repository interface and implentation is very strict (as said there How to add custom method to Spring Data JPA)

So, to be clear, the whole code : (But @beerbajay was right)

The custom method interface

public interface MyEntityRepositoryCustom {
    List<MyEntity> findSpecial();
}

The custom method implementation

public class MyEntityRepositoryImpl implements MyEntityRepositoryCustom {
    @PersistenceContext
    private EntityManager em;

    //custom method implementation
    public List<Object> findSpecial() {
        List<Object> list = em.createNativeQuery("select name, value from T_MY_ENTITY").getResultList();
        return list;
    }
}

The "original" repository

@Repository
public interface MyEntityRepository extends JpaRepository<MyEntity,Long>, MyEntityRepositoryCustom {
    //original methods here... do not redefine findSpecial()...
}

You can now use the "original" repository with the new custom methods

@Service
public class MyService {
    @Autowired
    private DataRepository r;

    public void doStuff() {
        List<Object> list = r.findSpecial();
    }
}
4
votes

Spring Data JPA has a way to create Custom and Dynamic queries with "Specifications": Spring Data - Specifications

First, your interface which extends JpaRepository or CrudRepository should also implement JpaSpecificationExecutor<...> and that's all you need. Your repository now has a new method findAll which accepts a Specification<...> object, and your can use the method Beerbajay used to create Criteria Queries by overriding the method toPredicate(...) and there you are free to build (almost) any query you want like so:

Specification<...> spec = new Specification<...>() {
@Override
public Predicate toPredicate(Root<...> entity, CriteriaQuery<?> query, CriteriaBuilder cb) {                
    List<Predicate> conditions = buildManyPredicates(cb, entity);
    return cb.and(conditions.toArray(new Predicate[conditions.size()]));
}
 };

repository.findAll(spec, PageRequest.of(0, 10));

This solves the problem of Spring Data trying to parse the methods you added in the custom interface (because there is no custom interface)