I am trying to build dynamic query (with like condition). When I try it with raw sql into the mysql , it worked very well. Here is an example:
select * from dbiwant.want_activity m where m.want like "%bike%"
Then, I've tried it with the CriteriaBuilder but I couldn't achieve to get the same results in Java. Just empty result. Here is my code:
Specifications specifications = Specifications.where(specificationService.isLike("want", search));
public Specification<WantActivity> isLike(String param, String search) {
return new Specification<WantActivity>() {
public javax.persistence.criteria.Predicate toPredicate(Root<WantActivity> root, CriteriaQuery<?> query,
CriteriaBuilder builder) {
return builder.like(root.get(param) ,"%" + search + "%");
}
};
}
What's the problem?
try this:
public Specification<WantActivity> isLike(String param, String search) {
return new Specification<WantActivity>() {
public javax.persistence.criteria.Predicate toPredicate(Root<WantActivity> root, CriteriaQuery<?> query,
CriteriaBuilder builder) {
final Predicate like = query.where(builder.like(root.<String> get(param), search)).getRestriction();
return like;
}
};
Related
In my app:
After opening a browser, user can use seven search fields, but doesn’t have to complete each one of them. Each field corresponds to one characteristic feature of the searched object.
A POST request is sent to the server (the html responsible for this and the previous step is shown below):
<main>
<section>
<form name="searchForm" th:action="#{'/myObjectsSearch'}" method="POST">
<fieldset>
<input type="text" id="inputName" th:name="name" minlength="3">
</fieldset>
<fieldset>
<input type="text" id="inputColor" th:name="color" minlength="3">
</fieldset>
<!--five more fieldsets-->
</form>
</section>
<section>
<div th:each="myObject: ${myObjects}"><p th:text="${myObject}"></p></div>
</section>
</main>
The request first goes to the method getByDifferentParameters() in controller - #RequestParam (required = false) annotations were used, because not every feature will be completed by the user:
#PostMapping()
public String getByDifferentParameters(
#RequestParam(name = "name", required = false) String name,
#RequestParam(name = "color", required = false) String color,
//five more ,
Model model) {
List<MyObject> myObjects = dbService.retrieveByDifferentParameters(name, color, //five more);
model.addAttribute("myObjects", myObjects);
return "myObjectsSearch";
}
... then the request goes to service and to class implementing the CrudRepository interface:
#Query(nativeQuery = true)
List<MyObject> retrieveByDifferentParameters(#RequestParam(value = "NAME", required = false) String name,
#RequestParam(value = "COLOR", required = false) String color, //five more);
... to finally send a query to the database from the domain class in the form of:
#NamedNativeQuery(
name = "MyObject.retrieveByDifferentParameters",
query = "SELECT * FROM myObjects WHERE " +
"IFNULL(:name, name) = IFNULL(name, name) AND " +
"IFNULL(:color, color) = IFNULL(color, color) AND " +
//five more
;",
resultClass = MyObject.class
)
The query from point 5) works perfect from the console, but not from the browser.
Should I somehow pass null value to RequestParams for mySQL to understand the query? Unless null value is already being passed to query and the issue lies somewhere else.
PS
there are 7 features, hence the repeated comment "//five more" to save some space
It seems like you want to build search&filter API. One of the best ways we've done this is to use Java Specification API with JPA. Baeldung has a great guide on this here. Let me know if this helps.
As per answer suggested from Frantz Romain, I would like to give some hints/ easy implementation, to solve your issue on (7 search fields),
I have 3 search fields, so I have made 3 methods, and 4th method to combine them, please know the usage of
criteriaBuilder.conjunction();
It returns true if we don't have values/ if null
please refer the below code.
public class PostSpecification implements Specification<Post> {
#Override
public Specification<Post> and(Specification<Post> other) {
return Specification.super.and(other);
}
#Override
public Specification<Post> or(Specification<Post> other) {
return Specification.super.or(other);
}
#Override
public Predicate toPredicate(Root<Post> root, CriteriaQuery<?> query, CriteriaBuilder criteriaBuilder) {
return null;
}
public static Specification<Post> bySearch(String search) {
return (root, query, criteriaBuilder) -> {
if (search == null) {
return criteriaBuilder.conjunction();
}
query.distinct(true);
Predicate titlePredicate = criteriaBuilder.like(root.<String>get("title"), "%" + search + "%");
Predicate contentPredicate = criteriaBuilder.like(root.<String>get("content"), "%" + search + "%");
Predicate authorPredicate = criteriaBuilder.like(root.<String>get("author"), "%" + search + "%");
Join<Post, Tag> postTagsTable = root.join("tags", JoinType.INNER);
Predicate tagPredicate = criteriaBuilder.like(postTagsTable.get("name"), "%" + search + "%");
return criteriaBuilder.or(titlePredicate, contentPredicate,
authorPredicate, tagPredicate);
};
}
public static Specification<Post> byPublished() {
return (root, query, criteriaBuilder) -> criteriaBuilder.equal(root.get("isPublished"),true);
}
public static Specification<Post> byAuthorNames(String[] authorNames) {
return (root, query, criteriaBuilder) -> {
if (authorNames == null || authorNames.length == 0) {
return criteriaBuilder.conjunction();
}
return root.<String>get("author").in(authorNames);
};
}
public static Specification<Post> byTagNames(String[] tagNames) {
return (root, query, criteriaBuilder) -> {
if (tagNames == null || tagNames.length > 0) {
return criteriaBuilder.conjunction();
}
Join<Post, Tag> postTagsTable = root.join("tags", JoinType.INNER);
return postTagsTable.<String>get("name").in(tagNames);
};
}
public Specification<Post> findByFilters(String search, String[] authorNames, String[] tagNames) {
return byPublished()
.and(bySearch(search))
.and(byAuthorNames(authorNames))
.and(byTagNames(tagNames));
}
}
You will need to add 7 different types of such functions
and call from controller or serviceImpl like this way
PostSpecification postSpecification = new PostSpecification();
Specification<Post> filteredSpecification = postSpecification.findByFilters(search, authorNames, tagNames);
posts = postRepository.findAll(filteredSpecification, pageable).getContent();
I hope it helps!
I'm not an expert in Data JPA Specs
but I want to build a specification that allow me to filter with my criteria and then make a distinct by on a field.
I build my specifcation like this
public static Specification<Mission> withCriteria(MissionCriteria criteria) {
Specification<Mission> specs = criteria(criteria);
if (criteria.getClient() != null) {
specs = specs.and(clientMethod(criteria.getClient()));
}
if (criteria.getProjet() != null) {
specs = specs.and(projetMethod(criteria.getProjet()));
}
with these functions called to apply my criteria to the query
private static Specification<Mission> client(Long client) {
return (root, query, builder) -> {
return builder.equal(root.get("client"), client);
};
}
private static Specification<Mission> projet(Long projet) {
return (root, query, builder) -> {
return builder.equal(root.get("projet").get("id"), projet);
};
}
And at the end of my withCriteria method, I want my query to make a distinct by on an external field of my initial table.
I have this
private static Specification<Mission> distinctByCollaboratorId(){
return (root, query, builder) -> {
query.distinct(true);
return builder.and(root.get("collaborator").get("id").isNotNull());
};
}
But that doesn't work and I litteraly have no idea how to build it clean with my root query builder :-) help appreciated
I try to create lambda in Kotlin.
I has following Java interface:
public interface Specification<T> extends Serializable {
#Nullable
Predicate toPredicate(Root<T> root, CriteriaQuery<?> query, CriteriaBuilder builder);
}
And in Java I can return new Specification from method like:
private Specification<Product> nameLike(String name){
return new Specification<Product>() {
#Override
public Predicate toPredicate(Root<Product> root, CriteriaQuery<?> query, CriteriaBuilder criteriaBuilder) {
return criteriaBuilder.like(root.get(Product_.NAME), "%"+name+"%");
}
};
}
And with Java 8 I can cut it to labda like:
private Specification<Product> nameLike(String name) {
return (root, query, criteriaBuilder)
-> criteriaBuilder.like(root.get(Product_.NAME), "%"+name+"%");
}
How to do it in Kotlin with lamba? I already tried many options, but they do not compile. help please.
Update:
Last option in Kotlin:
class ProductSpecification {
fun nameLike(name: String): (Root<Product>, CriteriaQuery<Product>, CriteriaBuilder) -> Predicate = {
root, query, builder -> builder.like(root.get("name"), "%$name%")
}
}
It compiles, but when I pass it in function with argument Specification, I have error None of the following functions can be called with the arguments supplied.. Code example of invoking:
repository.findAll(ProductSpecification().nameLike("fff"))
I found solution! When I started to implement roughly option like of the second code example in my question, IDE suggests to do like:
fun nameLike(name: String) = Specification { root: Root<Product>, query: CriteriaQuery<*>, builder: CriteriaBuilder ->
builder.like(root.get("name"), "%$name%")
}
That's fine for me
I have the following repository with 2 custom query methods:
#Repository
public interface CropVarietyNameDao extends JpaRepository<CropVarietyName, Long> {
//https://stackoverflow.com/questions/25362540/like-query-in-spring-jparepository
Set<CropVarietyName> findAllByNameIgnoreCaseContainingOrScientificNameIgnoreCaseContaining(String varietyName, String scientificName);
Set<CropVarietyName> findAllByNameIgnoreCaseStartsWithOrScientificNameIgnoreCaseStartsWith(String varietyName, String scientificName);
}
I need to add an additional condtion, namely parent entity called crop deleted = false.
If i change a method to the following:
findAllByNameIgnoreCaseContainingOrScientificNameIgnoreCaseContainingAndCrop_deletedIsFalse(String varietyName, String scientificName);
Then most likely it will interpret the query as (Name containing OR (scientificNameContaining AND crop_deleted = false), but thats not how i want it.
It needs to be (NameContaining OR scientificNameContaining) AND crop_deleted = false)
My guess is that I have to add the AND crop_deleted part to both parts, but that seems inefficient. How can i write a method thats essentially (NameContaining OR scientificNameContaining) AND crop_deleted = false) without having to use #Query?
You could try JPA Specification -
#Repository
public interface CropVarietyNameDao extends JpaRepository<CropVarietyName, Long>,
JpaSpecificationExecutor<CropVarietyName> {
}
public class CropVarietySpecs {
public static Specification<CropVarietyName> cropPredicate(String varietyName, String sciName, boolean cropStatus) {
return new Specification<CropVarietyName>() {
#Override
public Predicate toPredicate(Root<CropVarietyName> root, CriteriaQuery<?> query, CriteriaBuilder criteriaBuilder) {
List<Predicate> predicates = new ArrayList<>();
Predicate varietyContainingIgnoreCasePredicate = criteriaBuilder.like(criteriaBuilder.lower(root.get("<column_name>")), varietyName.toLowerCase());
Predicate scientificContainingIgnoreCasePredicate = criteriaBuilder.like(criteriaBuilder.lower(root.get("<column_name>")), sciName.toLowerCase());
Predicate cropStatusPredicate = criteriaBuilder.equal(root.get("<column_name>"), cropStatus);
predicates.add(varietyContainingIgnoreCasePredicate);
predicates.add(scientificContainingIgnoreCasePredicate);
criteriaBuilder.or(predicates.toArray(new Predicate[predicates.size()]));
return criteriaBuilder.and(cropStatusPredicate);
}
};
}
}
Then you can call the findAll() method of your repository like -
List<CropVarietyName> entities = cropVarietyNameDao.findAll(CropVarietySpecs.cropPredicate("varietyName", "sciName", false));
I have a method that does a search with filters, so I'm using Specification to build a dynamic query:
public Page<Foo> searchFoo(#NotNull Foo probe, #NotNull Pageable pageable) {
Specification<Foo> spec = Specification.where(null); // is this ok?
if(probe.getName() != null) {
spec.and(FooSpecs.containsName(probe.getName()));
}
if(probe.getState() != null) {
spec.and(FooSpecs.hasState(probe.getState()));
}
//and so on...
return fooRepo.findAll(spec, pageable);
}
There is the possibility that there are no filters specified, so I would list everything without filtering. So having that in mind, how I should initialize spec ? Right now, the code above doesn't work as it always returns me the same result: all the registers of the table, no filtering have been aplied althought and operations have been made.
FooSpecs:
public class PrescriptionSpecs {
public static Specification<Prescription> containsCode(String code) {
return (root, criteriaQuery, criteriaBuilder) ->
criteriaBuilder.like(root.get(Prescription_.code), "%" + code + "%");
}
// some methods matching objects...
public static Specification<Prescription> hasContractor(Contractor contractor) {
return (root, criteriaQuery, criteriaBuilder) ->
criteriaBuilder.equal(root.get(Prescription_.contractor), contractor);
}
//... also some methods that access nested objects, not sure about this
public static Specification<Prescription> containsUserCode(String userCode) {
return (root, criteriaQuery, criteriaBuilder) ->
criteriaBuilder.like(root.get(Prescription_.user).get(User_.code), "%" + userCode + "%");
}
}
Specification.where(null) works just fine.
It is annotated with #Nullable and the implementation handles null values as it should.
The problem is that you are using the and method as if it would modify the Specification, but it creates a new one. So you should use
spec = spec.and( ... );