29

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 + "))";
}
Channa
  • 4,963
  • 14
  • 65
  • 97
  • 3
    look into `JpaSpecificationExecutor` – guido May 25 '15 at 04:11
  • 1
    Provide an implementation for your DAO, and execute this dynamic query from the implementation. http://docs.spring.io/spring-data/jpa/docs/current/reference/html/#repositories.single-repository-behaviour. The DAO shouldn't split. It should take a List or a Set as argument. The caller should deal with splitting. – JB Nizet May 25 '15 at 06:03

4 Answers4

20

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();
}
Dertalai
  • 198
  • 1
  • 9
beerbajay
  • 19,652
  • 6
  • 58
  • 75
  • 2
    Many thanks for your worth full answer. Due to compiler failure I have apply following corrections, cb.lower(word) --> cb.lower(wordLiteral ) q.select(doc) --> q.select(user) But with those corrections when I implement this solution I will get following error on application deploy time:- Caused by: org.springframework.data.mapping.PropertyReferenceException: No property filter found for type com.ord.model.User – Channa May 26 '15 at 02:54
  • @Channa Fixed. It sounds like spring-data is trying to interpret the function name `findByFilterText`, which it shouldn't be doing. Did you make your repository extend the custom repository class? – beerbajay May 26 '15 at 05:25
  • Yes I have done the things as you mention. That mean I have created "MyEntityRepositoryCustom" interface and then later extend it to existing Repository Interface as "public interface MyEntityRepository extends JpaRepository, MyEntityRepositoryCustom" – Channa May 26 '15 at 07:48
  • 2
    @Channa I had an error in the repository name, it must be `MyEntityRepositoryImpl`, not `MyEntityRepositoryCustomImpl`. See if making that change helps. – beerbajay May 26 '15 at 10:59
  • thanks for feedback. I have done the change as you mention, but still I am getting same exception :(. Have a nice time !. Thanks. – Channa May 27 '15 at 02:46
  • Can I implement this in case my query is a complex one? i.e- It includes `JOINS` and `subqueries`. https://stackoverflow.com/questions/47266362/add-dynamic-criteria-to-a-jpa-custom-query – Rajeev Ranjan Nov 13 '17 at 14:36
  • createQuery Does it support true paging ? (like mysql limit 0,10) – zy_sun Jan 10 '22 at 03:34
8

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();
    }
}
Community
  • 1
  • 1
Barium Scoorge
  • 1,938
  • 3
  • 27
  • 48
3

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)

Tom Elias
  • 751
  • 6
  • 15
0

try query DSL as illustrated in the official documents

SalutonMondo
  • 629
  • 9
  • 17