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 {
}
Related
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.
I want to implement pagination only for certain type of users. The table structure is:
#Entity
#Table(name = "users")
public class Users implements Serializable {
#Id
#GeneratedValue(strategy = GenerationType.IDENTITY)
#Column(name = "id", unique = true, updatable = false, nullable = false, length = 3)
private int id;
#Column(length = 255)
private String login;
#Column(length = 255)
private String email;
#Column(length = 64)
private String salt;
#Column(length = 255)
private String type;
.....
}
Repository:
#Repository
public interface UserRepository extends JpaRepository<Users, Integer> {
}
Query:
#Override
public Page<Users> findAll(int page, int size) {
return dao.findAll(PageRequest.of(page, size));
}
How I can limit the query only to select type admin, super_admin and etc.
I suppose that I need to add limitation like IN (admin, super_admin)?
probably the easiest way is to use the naming convention and use findAllByTypeIn:
interface UserRepository extends JpaRepository<User, Integer> {
Page<User> findAllByTypeIn(Pageable page,String... types));
}
but if you have more complex search criteria I would consider using specifications
Spring Data JPA supports specifications, you can extend your repository interface with the JpaSpecificationExecutor interface, as follows:
interface UserRepository extends JpaRepository<User, Integer>, JpaSpecificationExecutor<User> {
}
Write your specification
class UserSpecs {
public static Specification<User> isOfType(String type) {
return new Specification<User>() {
#Override
public Predicate toPredicate(Root<User> root, CriteriaQuery<?> query, CriteriaBuilder builder) {
return builder.equal(root.get("type"), type);
}
};
}
public static Specification<User> isInTypes(String... types) {
return new Specification<User>() {
#Override
public Predicate toPredicate(Root<User> root, CriteriaQuery<?> query, CriteriaBuilder builder) {
In<String> inClause = builder.in(root.get("type"));
for (String type: types) {
inClause.value(type);
}
return inClause;
}
};
}
}
and get the result via
dao.findAll(UserSpecs.isOfType("admin").or(UserSpecs.isOfType("super_admin")) ,PageRequest.of(page, size));
or if you prefer to use in
dao.findAll(UserSpecs.isInTypes("admin","super_admin")) ,PageRequest.of(page, size));
I have to implement a search method with a filter. This is the structure:
My Jpa interface:
public interface FooRepository extends JpaRepository<Foo, Long>, FooRepositoryCustom {
}
My custom interface:
public interface FooRepositoryCustom {
Page<Foo> findByFilter(FooFilter filter, Pageable pageable);
}
My custom implementation:
public class FooRepositoryImpl implements FooRepositoryCustom {
#PersistenceContext
private EntityManager em;
#Override
public Page<Foo> findByFilter(FppFilter filter, Pageable pageable) {
CriteriaQuery<Foo> criteriaQuery = em.getCriteriaBuilder().createQuery(Foo.class);
Root<Foo> root = criteriaQuery.from(Foo.class);
criteriaQuery.select(root);
List<Predicate> predicates = new ArrayList<Predicate>();
if (filter.getFooAttr1() != null) {
predicates.add(em.getCriteriaBuilder().equal(root.get("fooAttr1"), filter.getFooAttr1()));
}
if (filter.getOtherFooId() != null) {
Join<Foo, OtherFoo> join = root.join("otherFoo", JoinType.LEFT);
predicates.add(em.getCriteriaBuilder().equal(join.get("id"), filter.getOtherFooId()));
}
criteriaQuery.where(predicates.toArray(new Predicate[] {}));
// Order
List<Order> orderList = new ArrayList<Order>();
for (org.springframework.data.domain.Sort.Order order : pageable.getSort()) {
if (order.getDirection().equals(Direction.ASC)) {
orderList.add(em.getCriteriaBuilder().asc(root.get(order.getProperty())));
} else {
orderList.add(em.getCriteriaBuilder().desc(root.get(order.getProperty())));
}
}
criteriaQuery.orderBy(orderList);
int totalRows = em.createQuery(criteriaQuery).getResultList().size();
em.createQuery(criteriaQuery).setFirstResult(pageable.getPageNumber() * pageable.getPageSize());
em.createQuery(criteriaQuery).setMaxResults(pageable.getPageSize());
Page<Foo> page = new PageImpl<Foo>(em.createQuery(criteriaQuery).getResultList(), pageable, totalRows);
return page;
}
}
There is a simple way to return a Page <Foo> without Criteria. Otherwise to add sorting and paging?
I did not know the specifications. This is my solution:
I implemented my espeficicación:
public class FooSpecifications {
public static Specification<Foo> withFilter(final FooFilter filter) {
return new Specification<Foo>() {
#Override
public Predicate toPredicate(Root<Foo> root, CriteriaQuery<?> query,
CriteriaBuilder builder) {
List<Predicate> predicates = new ArrayList<Predicate>();
if (filter.getAttr1() != null) {
predicates.add(builder.equal(root.get("attr1"), filter.getAttr1()));
}
if (filter.getOtherFooId() != null) {
Join<Foo, OtherFoo> join = root.join("otherFoo", JoinType.LEFT);
predicates.add(builder.equal(join.get("id"), filter.getOtherFooId()));
}
return builder.and(predicates.toArray(new Predicate[] {}));
}
};
}
}
I add the method to the interface:
public interface FooRepository extends JpaRepository<Foo, Long>, FooRepositoryCustom {
Page<Foo> findAll(Specification<Foo> specification, Pageable pageable);
}
And for use in Service:
fooRepo.findAll(FooSpecifications.withFilter(filter), pageable);
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;
}