I have a book class with a list of authors:
#Entity
#Table(name = "book")
public class Book extends Content {
#ManyToMany(fetch = FetchType.LAZY)
private List<Author> authors;
...}
Now, this is my BookSpecifications class:
public static Specification<Book> authorIdIs(Long authorId) {
return new Specification<Book>() {
#Override
public Predicate toPredicate(Root<Book> root, CriteriaQuery<?> criteriaQuery, CriteriaBuilder cb) {
return cb.isTrue(root.get("authors").get("id").in(authorId));
}
};
}
How can check the given authorId is in the list?
Error:
java.lang.IllegalStateException: Illegal attempt to dereference path source [null.authors] of basic type
at org.hibernate.jpa.criteria.path.AbstractPathImpl.illegalDereference(AbstractPathImpl.java:98) ~[hibernate-entitymanager-4.3.11.Final.jar:4.3.11.Final]
at org.hibernate.jpa.criteria.path.AbstractPathImpl.get(AbstractPathImpl.java:191) ~[hibernate-entitymanager-4.3.11.Final.jar:4.3.11.Final]
at com.tarameshgroup.derakht.service.specs.BookSpecifications$3.toPredicate(BookSpecifications.java:40) ~[classes/:na]
at org.springframework.data.jpa.domain.Specifications$ComposedSpecification.toPredicate(Specifications.java:189) ~[spring-data-jpa-1.9.2.RELEASE.jar:na]
For that you can use Join with predicate:
Refer below code,
public static Specification<Book> authorIdIs(Long authorId) {
return new Specification<Book>() {
#Override
public Predicate toPredicate(Root<Book> root, CriteriaQuery<?> criteriaQuery, CriteriaBuilder cb) {
Join join = root.join("authors");
return cb.equal(join.get("id"),authorId);
}
};
}
Related
I have an entity, says:
class MyEntity {
Long id;
String attr1;
String attr2;
String attr3;
String attr4;
Double attr5;
}
I use Specification to query the result filtered by attributes like:
class MySpecification implements Specification<MyEntity> {
private String attr1;
private String attr2;
private String attr3;
private String attr4;
#Override
public Predicate toPredicate(Root<MyEntity> root, CriteriaQuery<?> query, CriteriaBuilder criteriaBuilder) {
List<Predicate> restriction = new ArrayList<>();
if (!StringUtils.isEmpty(attr1)) {
restriction.add(criteriaBuilder.equal(root.get("attr1"), attr1));
}
if (!StringUtils.isEmpty(attr2)) {
restriction.add(criteriaBuilder.equal(root.get("attr2"), attr2));
}
// And so on
Predicate predicate = criteriaBuilder.disjunction();
predicate.getExpressions().add(criteriaBuilder.and(restriction.toArray(new Predicate[restriction.size()])));
return predicate;
}
}
Now I want to get sum of attr5 by the Specification, how can I do that ?
Thank you in advanced.
After some research, here is solution:
Create an interface:
interface MyRepositoryCustom {
<S extends Number> S sum(Specification<MyEntity> spec, Class<S> resultType, String fieldName);
}
Implementation:
#Repository
class MyRepositoryCustomImpl implements MyRepositoryCustom {
#Autowired
private EntityManager entityManager;
#Override
public <S extends Number> S sum(Specification<MyEntity> spec, Class<S> resultType, String fieldName) {
CriteriaBuilder builder = entityManager.getCriteriaBuilder();
CriteriaQuery<S> query = builder.createQuery(resultType);
Root<MyEntity> root = applySpecificationToCriteria(spec, query);
query.select(builder.sum(root.get(fieldName).as(resultType)));
TypedQuery<S> typedQuery = entityManager.createQuery(query);
return typedQuery.getSingleResult();
}
protected <S> Root<MyEntity> applySpecificationToCriteria(Specification<MyEntity> spec, CriteriaQuery<S> query) {
Root<MyEntity> root = query.from(MyEntity.class);
if (spec == null) {
return root;
}
CriteriaBuilder builder = entityManager.getCriteriaBuilder();
Predicate predicate = spec.toPredicate(root, query, builder);
if (predicate != null) {
query.where(predicate);
}
return root;
}
}
Main repository should extend both JpaRepository and MyRepositoryCustom:
#Repository
interface MyEntityRepository extends JpaRepository<MyEntity, Long>, MyRepositoryCustom {
}
I am creating a Spring Boot application having entities like Product, Category, Machinery, UsageLocation etc.. Thing that is common in all these entities is that they all have a String attribute called name and can be filtered from UI using name. I have written a specification for product to filter using name and it is working. Below is the code
public final class ProductSpecifications
{
public static Specification<Product> whereNameContains(String name)
{
Specification<Product> finalSpec = (Root<Product> root, CriteriaQuery<?> query, CriteriaBuilder cb)
-> cb.like(root.get(Product_.PRODUCT_NAME), "%"+name+"%");
return finalSpec;
}
public static Specification<Product> whereNameEqauls(String name)
{
Specification<Product> finalSpec = (Root<Product> root, CriteriaQuery<?> query, CriteriaBuilder cb)
-> cb.equal(root.get(Product_.PRODUCT_NAME), name);
return finalSpec;
}
}
Now problem is that I have to write same code again for filtering other entities with only difference being the class name(Product), field name(PRODUCT_NAME) and return type of method. Can I create a generic class and method to which I can pass class name and field name as parameters and it returns specification of respective return type.
First make your SpecificationsBuilder generic
#Service
public final class SpecificationsBuilder<T>
{
public static Specification<T> whereNameContains(String key,String name)
{
Specification<T> finalSpec = (Root<T> root, CriteriaQuery<?> query, CriteriaBuilder cb)
-> cb.like(root.get(key), "%"+name+"%");
return finalSpec;
}
}
Then in controller #Autowire the SpecificationsBuilder
#Autowire
private final SpecificationBuilder<Product> specificationBuilder;
public List<Product> getAll(String name) {
Specification<Product> specification =
specificationBuilder.whereNameContains(Product_.PRODUCT_NAME, name);
List<Product> products = productRepo.findAll(specification);// pass the specifications
return products;
}
You can create your own generic library for SpecificationsBuilder, I have one. You can find details here
I was able to solve this issue using Abinash's answer. Below is the working code for reusable specification class
import java.util.List;
import javax.persistence.criteria.CriteriaBuilder;
import javax.persistence.criteria.CriteriaQuery;
import javax.persistence.criteria.Root;
import org.springframework.data.jpa.domain.Specification;
import org.springframework.stereotype.Service;
import com.ec.application.model.Product;
import com.ec.common.Filters.FilterAttributeData;
import com.ec.common.Filters.FilterDataList;
public class SpecificationsBuilder<T>
{
//#######################################/#################//
// Level 0 - If the field that you want to query is parent entity //
//########################################################//
public Specification<T> whereDirectFieldContains(String key,List<String> names)
{
Specification<T> finalSpec = null;
for(String name:names)
{
Specification<T> internalSpec = (Root<T> root, CriteriaQuery<?> query, CriteriaBuilder cb)
-> cb.like(root.get(key), "%"+ name +"%");
finalSpec = specOrCondition(finalSpec,internalSpec); // to append specifications as I am filtering based on list of strings
}
return finalSpec;
}
//#######################################//
// Level 1 - If you want to query a child entity. // //
//#######################################//
public Specification<T> whereChildFieldContains(String childTable, String childFiledName,
List<String> names)
{
Specification<T> finalSpec = null;
for(String name:names)
{
Specification<T> internalSpec = (Root<T> root, CriteriaQuery<?> query, CriteriaBuilder cb)
-> cb.like(root.get(childTable).get(childFiledName), "%"+ name +"%");
finalSpec = specOrCondition(finalSpec,internalSpec); // to append specifications as I am filtering based on list of strings
}
return finalSpec;
}
//#######################################//
// Reusable Spec Setter to handle NULLs. //
//#######################################//
public Specification<T> specAndCondition(Specification<T> finalSpec, Specification<T> internalSpec)
{
if(finalSpec == null) return internalSpec;
else return finalSpec.and(internalSpec);
}
public Specification<T> specOrCondition(Specification<T> finalSpec, Specification<T> internalSpec)
{
if(finalSpec == null) return internalSpec;
else return finalSpec.or(internalSpec);
}
}
And this is the code for entity specification class
public static Specification<Product> getSpecification(FilterDataList filterDataList)
{
List<String> productNames = specbldr.fetchValueFromFilterList(filterDataList,"product");
List<String> categoryNames = specbldr.fetchValueFromFilterList(filterDataList,"category");
Specification<Product> finalSpec = null;
if(productNames != null && productNames.size()>0)
finalSpec = specbldr.specAndCondition(finalSpec, specbldr.whereDirectFieldContains(Product_.PRODUCT_NAME, productNames));
if(categoryNames != null && categoryNames.size()>0)
{
finalSpec = specbldr.specAndCondition(finalSpec,
specbldr.whereChildFieldContains(Product_.CATEGORY,Category_.CATEGORY_NAME, categoryNames));
}
return finalSpec;
}
I'm trying to build dynamic subquery with a JPA Specification. How can I add predicates to the subquery and build it?
for example, I'll have 2 tables:
User and Usercard:
#Entity
#Table(name = "users", schema = "someschema")
public class User {
#Id
#GeneratedValue(strategy = GenerationType.AUTO)
private Long id;
private String name;
private String surName;
private String email;
#OneToMany
private List<Usercard> usercardList;
//other methods...
}
and
#Entity
#Table(name = "usercard", schema = "someschema")
public class Usercard {
#Id
private Long id;
private String account;
private String value;
#ManyToOne
private User user;
//other methods...
}
I have my repo:
#Repository
public interface UserRepository extends JpaRepository<User, Long>,
JpaSpecificationExecutor<User> {}
And trying to build smth like:
public List<User> findByPredicate(String email) {
return userRepository.findAll((Specification<User>) (root,
criteriaQuery, criteriaBuilder) -> {
List<Predicate> predicates = new ArrayList<>();
if (email != null) {
predicates.add(criteriaBuilder.and(criteriaBuilder.equal(
root.get("email"), email)));
}
return criteriaBuilder.and(predicates.toArray(new
Predicate[predicates.size()]));
});
}
but for subquery with predicates.
I've tried methods like this:
public List<User> findByUsercardAccount(String email, String account) {
return userRepository.findAll(new Specification<User>() {
#Override
public Predicate toPredicate(Root<User> root, CriteriaQuery<?>
criteriaQuery, CriteriaBuilder criteriaBuilder) {
Subquery<Usercard> subquery =
criteriaQuery.subquery(Usercard.class);
Root<Usercard> subRoot = subquery.from(Usercard.class);
List<Predicate> predicates = new ArrayList<>();
//predicates for Users table
predicates.add(criteriaBuilder.and(criteriaBuilder.equal(
root.get("email"), email)));
//predicates for Usercard table
predicates.add(criteriaBuilder.equal(subRoot.get("account"),
account));
return criteriaBuilder.and(predicates.toArray(new
Predicate[predicates.size()]));
}
});
}
So, I need a method for dynamic search within a few tables where I can pass arguments for a dynamic query as well as a dynamic subquery. I would be grateful for any help.
Found this solution:
public List<User> findByUsercardAccount(String account, String email) {
return userRepository.findAll((Specification<User>) (root, criteriaQuery, criteriaBuilder) -> {
Subquery<User> subquery = criteriaQuery.subquery(User.class);
Root<Usercard> subRoot = subquery.from(Usercard.class);
List<Predicate> predicates = new ArrayList<>();
List<Predicate> subPredicates = new ArrayList<>();
if (account != null)
subPredicates.add(criteriaBuilder.equal(subRoot.get("account"), account));
if (email != null)
predicates.add(criteriaBuilder.and(criteriaBuilder.equal(root.get("email"), email)));
subquery.select(subRoot.get("id")).where(subPredicates.toArray(new Predicate[predicates.size()]));
predicates.add(criteriaBuilder.exists(subquery));
return criteriaBuilder.and(predicates.toArray(new Predicate[predicates.size()]));
});
}
}
So, I've created subroot and subquery, added some restrictions (subPredicates) and passed them to the main root as a predicate.
Suppose I have a bidirectional 1-1 association with the Person entity
#Entity
public class Person {
#OneToOne(optional=false)
#JoinColumn(name = "contact_id")
private Contact contact;
// getters/setters/constructors
}
And the Contact Entity
#Entity
public class Contact {
#OneToOne(mappedBy="contact")
private Person person;
// getters/setters/
}
I couldn't find a way to select parent object for Person Entity using the Contact entity. Like so...
criteriaQuery.select(root.get(Contact_.person));
I get this error:
Incompatible types. Required Selection<? extends capture of ?> but 'get' was inferred to Path<Y>: no instance(s) of type variable(s) exist so that Person conforms to capture of ?
Is there a way of doing this? I wanted to return a Predicate for Person Entity using the Contact root. For eg.
public static Specification<Person> phoneWithCountryCode(String countryCode) {
return new Specification<Person>() {
#Override
public Predicate toPredicate(
Root<Contact> root,
CriteriaQuery<?> criteriaQuery,
CriteriaBuilder criteriaBuilder
) {
String startsWithPattern = countryCode + "%";
criteriaQuery.select(root.get(Contact_.person));
return criteriaBuilder.like(
root.get(Contact_.phone), startsWithPattern
);
}
};
}
Yes, you can do.
I did it.I Have Relationship ( Book -- Review).
In your case create Specification<Person> and use join with contact.
like this,
Join joins = root.join("contact");
If help requires just follow my code.
public class BookSpecification {
public static Specification<Book> getBookByNameAndReviewId(){
return new Specification<Book> () {
#Override
public Predicate toPredicate(Root<Book> root, CriteriaQuery<?> query, CriteriaBuilder cb)
{
//List<javax.persistence.criteria.Predicate>
List<Predicate> predicates = new ArrayList<>();
predicates.add(cb.equal(root.get("name"), "spring boot"));
Join joins = root.join("reviews");
predicates.add(cb.equal(joins.get("no") , 584));
return cb.and(predicates.toArray(new Predicate[predicates.size()]));
// return cb.equal(root, predicates);
}
};
}
}
There are two entity
#Entity
public class Event{
...
#ManyToMany(fetch = FetchType.LAZY)
private Set<EventGroup> eventGroups;
}
#Entity
public class EventGroup {
...
#ManyToMany(fetch = FetchType.LAZY)
private Set<Event> events;
}
I need to get Events which has EventGroups with given ids.
Using spring data CrudRepository.
#Repository
public interface EventRepository extends CrudRepository<Event, Long>, JpaSpecificationExecutor {
}
Im calling
eventRepository.findAll(buildSpecification(filter);
This is how i build specification:
private Specification<Event> buildSpecification(final EventFilter filter) {
final Specification<Event> specification = new Specification<Event>() {
#Override
public Predicate toPredicate(Root<Event> root, CriteriaQuery<?> criteriaQuery, CriteriaBuilder cb) {
root = criteriaQuery.distinct(true).from(Event.class);
Predicate predicate = cb.conjunction();
if (filter.getEventGroupIds() != null) {
Join<Event, EventGroup> join = root.join(Event_.eventGroups);
predicate.getExpressions().add( join.get(EventGroup_.id).in(filter.getEventGroupIds()) );
}
return criteriaQuery.where(predicate).getRestriction();
}
};
return specification;
}
But result query is
SELECT DISTINCT
event0_.id AS id1_1_,
event0_.createdAt AS createdA2_1_,
event0_.date AS date3_1_,
event0_.image_id AS image_id6_1_,
event0_.moderated AS moderate4_1_,
event0_.name AS name5_1_,
event0_.owner_id AS owner_id7_1_
FROM Event event0_
CROSS JOIN Event event1_
INNER JOIN Event_EventGroup eventgroup2_ ON event1_.id = eventgroup2_.Event_id
INNER JOIN EventGroup eventgroup3_ ON eventgroup2_.eventGroups_id = eventgroup3_.id
WHERE eventgroup3_.id IN (15)
This cross join corrupt everything.
What should i do? May be there is another way to get it?
Solved
private Specification<Event> buildSpecification(final EventFilter filter) {
final Specification<Event> specification = new Specification<Event>() {
#Override
public Predicate toPredicate(Root<Event> root, CriteriaQuery<?> cq, CriteriaBuilder cb) {
cq.distinct(true);
Predicate predicate = cb.conjunction();
if (filter.getEventGroupIds() != null) {
Join<Event, EventGroup> join = root.join(Event_.eventGroups);
predicate.getExpressions().add(join.get(EventGroup_.id).in(filter.getEventGroupIds()) );
}
return predicate;
}
};
return specification;
}