CriteriaQuery Implementation - java

I have 4 tables in DB, such as
Product => PK productid
Variant => PK variantid FK productid
Images => PK imageid FK variantid
Attribute => PK attributid FK variantid
So need to perform following action in Java Rest API
Pagination with sorting and filtering
One Product Object has different Variant with there images and attributes.
Below is the #Repository code for single table pagination i.e Product
#Repository
public class PaginProductCriteriaRepository {
private final EntityManager entityManager;
private final CriteriaBuilder criteriaBuilder;
public PaginProductCriteriaRepository(EntityManager entityManager) {
this.entityManager = entityManager;
this.criteriaBuilder = entityManager.getCriteriaBuilder();
}
public Page<Product> findAllWithFilter(PaginProductPage paginPage, PaginProductSearchCriteria paginSearchCriteria){
CriteriaQuery<Product> productCriteriaQuery = criteriaBuilder.createQuery(Product.class);
Root<Product> rootProduct = productCriteriaQuery.from(Product.class);
Predicate predicate = getPrediate(paginSearchCriteria,rootProduct);
productCriteriaQuery.where(predicate);
setOrder(paginPage,productCriteriaQuery,rootProduct);
TypedQuery<Product> typedQuery = entityManager.createQuery(productCriteriaQuery);
typedQuery.setFirstResult(paginPage.getPageNumber() * paginPage.getPageSize());
typedQuery.setMaxResults(paginPage.getPageSize());
Pageable pageable = getPageable(paginPage);
long paginCount = getPaginCountMethod(predicate);
return new PageImpl<>(typedQuery.getResultList(),pageable,paginCount);
}
private Predicate getPrediate(PaginProductSearchCriteria paginSearchCriteria, Root<Product> root) {
List<Predicate> predicates = new ArrayList<>();
if (Objects.nonNull(paginSearchCriteria.getpName())) {
predicates.add(criteriaBuilder.like(root.get("pName"),"%" + paginSearchCriteria.getpName() + "%"));
}
return criteriaBuilder.and(predicates.toArray(new Predicate[0]));
}
private void setOrder(PaginProductPage paginPage, CriteriaQuery<Product> criteriaQuery, Root<Product> root) {
if (paginPage.getSortDirection().equals(Sort.Direction.ASC)) {
criteriaQuery.orderBy(criteriaBuilder.asc(root.get(paginPage.getSortBy())));
} else {
criteriaQuery.orderBy(criteriaBuilder.desc(root.get(paginPage.getSortBy())));
}
}
private Pageable getPageable(PaginProductPage paginPage) {
Sort sort = Sort.by(paginPage.getSortDirection(),paginPage.getSortBy());
return PageRequest.of(paginPage.getPageNumber(),paginPage.getPageSize(),sort);
}
private long getPaginCountMethod(Predicate predicate) {
CriteriaQuery<Long> countQuery = criteriaBuilder.createQuery(Long.class);
Root<Product> countRoot = countQuery.from(Product.class);
countQuery.select(criteriaBuilder.count(countRoot)).where(predicate);
return entityManager.createQuery(countQuery).getSingleResult();
}
}
Expectation:
Using above code pagination is working fine using single Table i.e Product BUT I need to join multiple tables with product and get full product object contain variants, images and attributes.
Note: Single Product has multiple variants, variant has muluple attribute and images

Related

when query at JPA - JPQL

My system required to add filters,and I'm wonder if there any query that like this
SELECT *
FROM posts p
when byDate is not null then (where p.createAt BETWEEN :startDate AND :endDate)
when byType is not null then (where p.type = :type)
I knew that the query is not valid, but I want at one query to get the data wherever the request has (no filter or all filters or some of filters).
My goal is to create one query to achieve all cases.
It's usually not a good idea to write a big SQL query when you can tell in advance the actual query you want to run.
If you want to run a different query based on conditions you know before running the query, there are different approaches in JPA or Spring that you can use
Spring
You can define the different queries using Spring Data query methods?
public class PostRepository implements JpaRepository<Post, Long> {
List<Post> findByCreatedAtBetween(Date startDate, Date endDate);
List<Post> findByTypeIs(String type);
}
And then somewhere in the code, you can:
List<Post> results = null;
if (byDate != null) {
results = repository.findByCreatedAtBetween(startDate, endDate);
} else if (byType != null) {
results = repository.findByTypeIs(type);
} else {
results = repository.findAll();
}
Criteria
With criteria you can create a dynamic query at runtime and execute it:
public class PostRepository implements PostRepositoryCustom {
#PersistenceContext
private EntityManager em;
#Override
public List<Post> findPosts(Filter filter) {
CriteriaBuilder cb = entityManager.getCriteriaBuilder();
CriteriaQuery<User> query = cb.createQuery(Post.class);
Root<User> user = query.from(Post.class);
if ((filter.getByDate() != null)) {
// byDate is not null
ParameterExpression<Date> startDate = builder.parameter( Date.class );
ParameterExpression<Date> endDate = builder.parameter( Date.class );
query.where(builder.between( b.get( "createdAt" ), startDate, endDate));
return em.createQuery(query)
.setParameter(startDate, ...)
.setParameter(endDate, ...)
.getResultList();
}
if (filter.getByType() != null) {
ParameterExpression<Date> typeParam = builder.parameter( Date.class );
query.where(builder.and(root.get("type"), typeParam));
return em.createQuery(query)
.setParameter(typeParam, ...)
.getResultList();
}
return entityManager.createQuery(query)
.getResultList();
}
}
Assuming that your entity has the fields type and createdAt.
This approach works well if you don't know in advance what's your query looks like. For example, when you don't know how many conditions you will have to add to it.
But, if I know already which query I want to run, then I prefer to use HQL/JPQL.
HQL
If your queries don't change and you already know what they look like,
I find it easier to define them with HQL:
public class PostRepository implements PostRepositoryCustom {
#PersistenceContext
private EntityManager em;
#Override
public List<Post> findPosts(Filter filter) {
if (filter.getByDate() != null) {
return em.createQuery("from Post p where p.createdAt between :startDate and :endDate", Post.class)
.setParameter("startDate", ...)
.setParameter("endDate", ...)
.getResultList();
}
if (filter.getByType() != null) {
return em.createQuery("from Post p where p.type =:type", Post.class)
.setParameter("type", ...)
.getResultList();
}
return em.createQuery("from Post", Post.class)
.getResultList();
}
}
You can refactor the code to make it more elegant, but it should give you an idea. Note that if you need to reuse the same queries in different services, it might be helpful to define them using the annotation #NamedQuery.
Filters
In Hibernate (not JPA) you can also define filters. They are SQL filter conditions that one can apply at runtime:
#Entity
#FilterDef(name = Post.BY_DATE, defaultCondition = "createdAt between :startDate and :endDate", parameters = {#ParamDef(name = "startDate", type = "date"), #ParamDef(name = "startDate", type = "date") })
#FilterDef(name = Post.BY_TYPE, defaultCondition = "type = :type", parameters = #ParamDef(name = "startDate", type = "date"))
#Filter(name = Post.BY_DATE)
#Filter(name = Post.BY_TYPE)
class Post {
static final String BY_DATE = "Post.byDateFilter";
static final String BY_TYPE = "Post.byFilter"
private String type;
private Date createdAt;
...
}
Then:
public class PostRepository implements PostRepositoryCustom {
#PersistenceContext
private EntityManager em;
#Override
public List<Post> findPosts(Filter filter) {
enableFilters(em);
return em.createQuery("from Post", Post.class).getResultList();
}
private void enableFilters(Filter filter, EntityManager em) {
if (filter.getByDate() != null) {
em.unwrap(Session.class)
.enableFilter( Post.BY_DATE )
.setParameter("startDate", ...)
.setParameter("endDate", ...);
} else if (filter.getByType() != null) {
em.unwrap(Session.class)
.enableFilter( Post.BY_TYPE )
.setParameter("type", ...);
}
}
}

How can I add and build dynamic predicates to subquery using jpa specification?

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.

Is there a way to return Specifications for Parent Entity for org.springframework.data.jpa.domain.Specification?

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);
}
};
}
}

cross join in spring-data-jpa

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;
}

Using JPA Criteria Api and hibernate spatial 4 together

Given the query example here: http://www.hibernatespatial.org/tutorial-hs4.html
Query query = em.createQuery("select e from Event e where within(e.location, :filter) = true", Event.class);
query.setParameter("filter", filter);
Is it possible to rewrite the query using jpa 2 criteria api?( I am unsure how i should deal with the within(e.location, :filter) part.
I recently work at the exact same problem. My solution is a own Predicate for the within-keyword.
public class WithinPredicate extends AbstractSimplePredicate implements Serializable {
private final Expression<Point> matchExpression;
private final Expression<Geometry> area;
public WithinPredicate(CriteriaBuilderImpl criteriaBuilder, Expression<Point> matchExpression, Geometry area) {
this(criteriaBuilder, matchExpression, new LiteralExpression<Geometry>(criteriaBuilder, area));
}
public WithinPredicate(CriteriaBuilderImpl criteriaBuilder, Expression<Point> matchExpression, Expression<Geometry> area) {
super(criteriaBuilder);
this.matchExpression = matchExpression;
this.area = area;
}
public Expression<Point> getMatchExpression() {
return matchExpression;
}
public Expression<Geometry> getArea() {
return area;
}
public void registerParameters(ParameterRegistry registry) {
// Nothing to register
}
#Override
public String render(boolean isNegated, RenderingContext renderingContext) {
StringBuilder buffer = new StringBuilder();
buffer.append(" within(")
.append(((Renderable) getMatchExpression()).render(renderingContext))
.append(", ")
.append(((Renderable) getArea()).render(renderingContext))
.append(") = true ");
return buffer.toString();
}
}
Your query would look like this:
public List<Event> findEventInArea(Geometry area){
CriteriaBuilder cb = em.getCriteriaBuilder();
CriteriaQuery<Event> c = cb.createQuery(Event.class);
Root<Event> event = c.from(Event.class);
c.where(new WithinPredicate((CriteriaBuilderImpl) cb, event.get(Event_.location), area));
Query query = entityManager.createQuery(c);
return query.getResultList();
}
JPA does not support spatial. However, you can unwrap the hibernate session from your JPA EntityManager and run spatial criteria.
The lat lon bounds in this code sample is arbitrary.
#PersistenceContext(unitName = "myPuName")
private EntityManager entityManager;
#Override
public List<City> findCities() {
CriteriaBuilder criteriaBuilder = entityManager.getCriteriaBuilder();
Session session = entityManager.unwrap(Session.class);
Criteria criteria = session.createCriteria(City.class);
GeometryFactory geometryFactory = new GeometryFactory();
Coordinate[] coordinates = {new Coordinate(-9,-9,0),new Coordinate(-9,9,0),new Coordinate(9,9,0),new Coordinate(9,-9,0),new Coordinate(-9,-9,0)};
LinearRing polygon = geometryFactory.createLinearRing(coordinates);
Polygon po = geometryFactory.createPolygon(polygon,null);
criteria.add(SpatialRestrictions.within(City_.location.getName(), po));
List list = criteria.list();
return list;
}
Here is some more code not directly related to the question. This class can be used as an "Order" criteria to be added to a hibernate criteria. It will sort results by distance from the argument location:
public class KnnOrder extends Order {
private final Point fromPoint;
public KnnOrder(String propertyName, boolean ascending, Point fromPoint) {
super(propertyName, ascending);
this.fromPoint = fromPoint;
}
#Override
public String toSqlString(Criteria criteria, CriteriaQuery criteriaQuery) {
Dialect dialect = criteriaQuery.getFactory().getDialect();
if (!dialect.getClass().isAssignableFrom(PostgisDialect.class)) {
throw new UnsupportedOperationException("This supports only postgis dialect. Was requested: " + dialect.toString());
}
// final String[] columns = criteriaQuery.getColumnsUsingProjection(criteria, super.getPropertyName());
// String fromPointWkt = WKTWriter.toPoint(fromPoint.getCoordinate());
return "location <-> st_setsrid(st_makepoint(" + fromPoint.getX() + "," + fromPoint.getY() + "),4326)";
}
}
In JPA2 you can use the function expression builder. No dedicated stuff required anymore. Works for the order expression too.
public List<Event> listThem(Geometry area) {
CriteriaBuilder cb = em.getCriteriaBuilder();
CriteriaQuery<Event> cq = cb.createQuery(Event.class);
Root<Event> root = cq.from(Event.class);
ParameterExpression<Geometry> circleParm = cb.parameter(Geometry.class);
cq.where(cb.isTrue(cb.function("st_within", Boolean.class,
root.get(Event_.location), circleParm)));
TypedQuery<Event> tq = em.createQuery(cq);
tq.setParameter(circleParm, area);
return tq.getResultList();
}
Small price: The function name is database-dependent. In PostgreSQL the within function is called st_within.

Categories