Can I use dynamic Join in queryDSL? - spring

I'm new in QueryDSL and having concern creating a QueryDSL with joins. I want to use one method and change dynamically query's join. One query joins A, B tables and another query joins A, B, C tables. I can't find these from querydsl reference. Can I do this?
My code is below.
#Override
public List<PostPreviewDto> findPostByCategoryName(String account, String categoryName, int cursor) {
return query.select(
Projections.constructor(PostPreviewDto.class,
post.id, post.title, post.time,
user.name, user.image, layout, like.count())
)
.from(post)
.innerJoin(user).on(post.user.eq(user).and(user.account.eq(account)))
.innerJoin(category).on(category.eq(post.category).and(category.name.eq(categoryName)))
.innerJoin(like).on(post.eq(like.post))
.innerJoin(mold).on(mold.eq(post.mold))
.innerJoin(layout).on(mold.eq(layout.mold).and(layout.main.eq(true)))
.where(greaterThanCursor(cursor))
.groupBy(post)
.orderBy(post.id.desc())
.limit(PAGE_SIZE)
.fetch();
}
#Override
public List<PostPreviewDto> findPostByUserId(Long userId, int cursor) {
return query.select(
Projections.constructor(PostPreviewDto.class,
post.id, post.title, post.time,
user.name, user.image, layout, like.count())
)
.from(post)
.innerJoin(user).on(post.user.eq(user).and(user.id.eq(userId)))
.innerJoin(like).on(post.eq(like.post))
.innerJoin(mold).on(mold.eq(post.mold))
.innerJoin(layout).on(mold.eq(layout.mold).and(layout.main.eq(true)))
.where(greaterThanCursor(cursor))
.groupBy(post)
.orderBy(post.id.desc())
.limit(PAGE_SIZE)
.fetch();
}

You should be able to do something like this:
private JPQLQuery<PostPreviewDto> getBaseQuery(int cursor) {
return query.select(Projections.constructor(PostPreviewDto.class,
post.id, post.title, post.time,
user.name, user.image, layout, like.count())
)
.from(post)
.innerJoin(user).on(post.user.eq(user))
.innerJoin(like).on(post.eq(like.post))
.innerJoin(mold).on(mold.eq(post.mold))
.innerJoin(layout).on(mold.eq(layout.mold).and(layout.main.eq(true)))
.where(greaterThanCursor(cursor))
.groupBy(post)
.orderBy(post.id.desc())
.limit(PAGE_SIZE);
}
#Override
public List<PostPreviewDto> findPostByCategoryName(String account, String categoryName, int cursor) {
return getBaseQuery(cursor)
.innerJoin(category).on(category.eq(post.category))
.where(user.account.eq(account).and(category.name.eq(categoryName)))
.fetch();
}
#Override
public List<PostPreviewDto> findPostByUserId(Long userId, int cursor) {
return getBaseQuery(cursor)
.where(user.id.eq(userId))
.fetch();
}

Related

How to iterate from the JPA query in spring boot based on the virtual table such as (the count of students for courses tutorials taken)

I need to retrieve each student with number of courses taken, for example :
count
student
5
John
8
Jenny
And I need the following JSON response from my rest end-point:
[
{
"name_student": John,
"count_course": 5,
},
{
"name_student": Jenny,
"count_course": 8,
},
]
I have made the query to fetch the required data inside the Repository:
#Query(value ="Select count(*), s.student_name from student s INNER JOIN courses c on c.student_id= s.student_id group by s.student_name", nativeQuery = true )
List<StudentDTO> getIncidentCountByOfficerKPI ()
My DTO:
#Data
#NoArgsConstructor
public class StudentDTO {
Integer count;
String StudentName;
}
Service:
public List<StudentDTO> getStudentCount() {
return p2StudentRepository.getIncidentCountByOfficerKPI ();
}
Controller:
#GetMapping("/Student-count")
public ResponseEntity<StudentDTO> getAllStudentCount() {
List<StudentDTO> selectedCount = p2StudentService.getStudentCount();
List<StudentDTO> result = new ArrayList<>();
selectedCount.forEach(i -> {
StudentDTOitem = new StudentDTO();
item.setUserName(i.getStudentName());
item.setCount(i.getCount());
result.add(item);
});
return ResponseEntity.ok(result);
}
when I test with postman, it returns to nothing like below
[]
How can I fix that?
The return type of your rest end-point is incorrect, it should be ResponseEntity<List<StudentDTO>> (since you need to place multiple StudentDTO in the response).
And to provide ResponseEntity with a response body, you can use body() method of the ResponseEntity.BodyBuilder (that's a builder-object produced by the static method ResponseEntity.ok()), or it can be done by using of the parameterized constructors of the ResponseEntity.
And since you're actually not transforming the data returned by getStudentCount() you can attach this list as the response body directly.
#GetMapping("/Student-count")
public ResponseEntity<List<StudentDTO>> getAllStudentCount() { // <- change the return type
List<StudentDTO> selectedCount = p2StudentService.getStudentCount();
return ResponseEntity.ok()
.body(selectedCount); // <- add the response body
}

CriteraQuery in toPredicate method is not working

I wanted to fetch only select column from the DB using multiple where clause below is my simple implementation. Am getting the result but am getting data for all the column instead of my requested fileName column alone.
List<ImageSiloVO> queryResult = imageSiloRepo.findAll(new Specification<ImageSiloVO>() {
#Override
public Predicate toPredicate(Root<ImageSiloVO> root, CriteriaQuery<?>query, CriteriaBuilder criteriaBuilder) {
query.select(root.get("fileName"));
List<Predicate> predicates = new ArrayList<>();
if(StringUtils.isNoneBlank(imageSiloVO.getEntryNumber())) {
predicates.add(criteriaBuilder.and(criteriaBuilder.equal(root.get("entryNumber"), imageSiloVO.getEntryNumber())));
}
return criteriaBuilder.and(predicates.toArray(new Predicate[predicates.size()]));
}
});

Merge specifications of different types in Criteria Query Specifications

I'm having an Activity entity which is in #ManyToOne relationship with Event entity and their corresponding metamodels - Activity_ and Event_ were generated by JPA model generator.
I've created specialized classes ActivitySpecifications and EventSpecifications. Those classes contain only static methods whose return Specification. For example:
public interface EventSpecifications {
static Specification<Event> newerThan(LocalDateTime date) {
return (root, cq, cb) -> cb.gt(Event_.date, date);
}
...
}
so when I want to build query matching multiple specifications, I can execute following statement using findAll on JpaSpecificationExecutor<Event> repository.
EventSpecifications.newerThan(date).and(EventSpecifications.somethingElse())
and ActivitySpecifications example:
static Specification<Activity> forActivityStatus(int status) { ... }
How do I use EventSpecifications from ActivitySpecifications ? I mean like merge specifications of different type. I'm sorry, but I don't even know how to ask it properly, but theres simple example:
I want to select all activities with status = :status and where activity.event.date is greater than :date
static Specification<Activity> forStatusAndNewerThan(int status, LocalDateTime date) {
return forActivityStatus(status)
.and((root, cq, cb) -> root.get(Activity_.event) ....
// use EventSpecifications.newerThan(date) somehow up there
}
Is something like this possible?
The closest thing that comes to my mind is using the following:
return forActivityStatus(status)
.and((root, cq, cb) -> cb.isTrue(EventSpecifications.newerThan(date).toPredicate(???, cq, cb));
where ??? requires Root<Event>, but I can only get Path<Event> using root.get(Activity_.event).
In its basic form, specifications are designed to be composable only if they refer to the same root.
However, it shouldn't be too difficult to introduce your own interface which is easily convertible to Specification and which allows for specifications refering to arbitrary entities to be composed.
First, you add the following interface:
#FunctionalInterface
public interface PathSpecification<T> {
default Specification<T> atRoot() {
return this::toPredicate;
}
default <S> Specification<S> atPath(final SetAttribute<S, T> pathAttribute) {
// you'll need a couple more methods like this one for all flavors of attribute types in order to make it fully workable
return (root, query, cb) -> {
return toPredicate(root.join(pathAttribute), query, cb);
};
}
#Nullable
Predicate toPredicate(Path<T> path, CriteriaQuery<?> query, CriteriaBuilder criteriaBuilder);
}
You then rewrite the specifications as follows:
public class ActivitySpecifications {
public static PathSpecification<Activity> forActivityStatus(ActivityStatus status) {
return (path, query, cb) -> cb.equal(path.get(Activity_.status), cb.literal(status));
}
}
public class EventSpecifications {
public static PathSpecification<Event> newerThan(LocalDateTime date) {
return (path, cq, cb) -> cb.greaterThanOrEqualTo(path.get(Event_.createdDate), date);
}
}
Once you've done that, you should be able to compose specifications in the following manner:
activityRepository.findAll(
forActivityStatus(ActivityStatus.IN_PROGRESS).atRoot()
.and(newerThan(LocalDateTime.of(2019, Month.AUGUST, 1, 0, 0)).atPath(Activity_.events))
)
The above solution has the additional advantage in that specifying WHERE criteria is decoupled from specifying paths, so if you have multiple associations between Activity and Event, you can reuse Event specifications for all of them.
Consider the following :
ClassA {
id;
}
ClassB {
foreignId; //id of A
}
For combining Specification<ClassA> specA, Specification<ClassB> specB
specB = specB.and(combineSpecs(specA);
private static Specification<ClassB> combineSpecs(Specification<ClassA> specA) {
return (root_b,query,builder) {
Subquery<ClassA> sub = query.subquery(ClassA.class);
Root<ClassA> root_a = sub.from(ClassA.class);
Predicate p1 = specA.toPredicate(root_a,query,builder);
Predicate p2 = builder.equal(root_a.get("id"),root_b.get("foreignId"));
Predicate predicate = builder.and(p1,p2);
sub.select(root_a).where(predicate);
return builder.exists(sub);
};
}

JPA Criteria api - Total records for concrete query within pagination

I am programming function for pagination in my repository layer. Function receive as parameters spring's pageable object and some value like this:
public Page<Foo> filterFoo(Pageable pageable, String value) {
CriteriaBuilder cb = entityManager.getCriteriaBuilder();
CriteriaQuery<Foo> fooQuery = cb.createQuery(Foo.class);
Root<Foo> foo = fooQuery .from(Foo.class);
fooQuery .where(adding predicate for match value);
List<Foo> result = entityManager.createQuery(fooQuery )
.setFirstResult((pageable.getPageNumber() - 1) * pageable.getPageSize())
.setMaxResults(pageable.getPageSize())
.getResultList();
return new PageImpl<>(result, pageable, xxxx);
}
Function return spring's PageImpl object filled with my result. To PageImpl I also need set total count of objects which suit predicates. This count number have to be of course without maxResult and firstResult. Is possible create another database call with my fooQuery to get total database records for that query without limit? What is the best practise to use pageable and criteria api in JPA? Thank you in advice.
Because generated SQL uses aliases - you may need make separate query for get total count of rows.
For example:
CriteriaQuery<Long> countQuery = cb.createQuery(Long.class);
countQuery.select(cb.count(countQuery.from(Foo.class)));
if (Objects.nonNull(filters)) {
countQuery.where(filters);
}
return new PageImpl<>(result, pageable, em.createQuery(countQuery).getSingleResult());
where filters is equal to your adding predicate for match value expression.
Also, you may use a TupleQuery with custom SQL function for calculate count of rows in one select query.
Like this:
public class SqlFunctionsMetadataBuilderContributor implements MetadataBuilderContributor {
#Override
public void contribute(MetadataBuilder metadataBuilder) {
metadataBuilder.applySqlFunction(
"count_over",
new SQLFunctionTemplate(
StandardBasicTypes.LONG,
"(count(?1) over())"
)
);
}
}
and Criteria:
public Page<Foo> findAll(Specification<Foo> specification, Pageable pageable) {
CriteriaQuery<Tuple> cq = cb.createTupleQuery();
Root<Foo.class> fooRoot = cq.from(Foo.class);
cq.select(cb.tuple(fooRoot, cb.function("count_over", Long.class, fooRoot.get("id"))));
Predicate filters = specification.toPredicate(fooRoot, cq, cb);
if (Objects.nonNull(filters)) {
cq.where(filters);
}
TypedQuery<Tuple> query = em.createQuery(cq);
query.setFirstResult((int) pageable.getOffset());
query.setMaxResults(pageable.getPageSize());
List<Tuple> result = query.getResultList();
if (result.isEmpty()) {
return new PageImpl<>(List.of());
}
return new PageImpl<>(
result.stream().map(tuple -> (Foo) tuple.get(0)).collect(toUnmodifiableList()),
pageable,
(long) result.get(0).get(1)
);
}
See more about SQLFunction: https://vladmihalcea.com/hibernate-sql-function-jpql-criteria-api-query/ and Custom SQL for Order in JPA Criteria API

Trouble with a LINQ 'filter' code throwing an error

I've got the following code in my Services project, which is trying to grab a list of posts based on the tag ... just like what we have here at SO (without making this a meta.stackoverflow.com question, with all due respect....)
This service code creates a linq query, passes it to the repository and then returns the result. Nothing too complicated. My LINQ filter method is failing with the following error :-
Method 'Boolean
Contains(System.String)' has no
supported translation to SQL.
I'm not sure how i should be changing my linq filter method :( Here's the code...
public IPagedList<Post> GetPosts(string tag, int index, int pageSize)
{
var query = _postRepository.GetPosts()
.WithMostRecent();
if (!string.IsNullOrEmpty(tag))
{
query = from q in query
.WithTag(tag) // <--- HERE'S THE FILTER
select q;
}
return query.ToPagedListOrNull(index, pageSize);
}
and the Filter method...
public static IQueryable<Post> WithTag(this IQueryable<Post> query,
string tag)
{
// 'TagList' (property) is an IList<string>
return from p in query
where p.TagList.Contains(tag)
select p;
}
Any ideas? I'm at a loss :(
Try with Any:
public static IQueryable<Post> WithTag(this IQueryable<Post> query,
string tag)
{
// 'TagList' (property) is an IList<string>
return from p in query
where p.TagList.Any(t => t == tag)
select p;
}
.
UPDATE (by PureKrome)
Another suggestion by Ahmad (in a comment below). This uses the Contains method so it will return all posts that contain the tag 'Test', eg. Post with Tag 'Testicle' :-
public static IQueryable<Post> WithTag(this IQueryable<Post> query,
string tag)
{
// 'TagList' (property) is an IList<string>
return from p in query
where p.TagList.Any(t => t.Contains(tag))
select p;
}
In WithTag try changing the query to use a List rather than an IList:
return from p in query
let taglist = p.TagList as List<string>
where taglist.Contains(tag)
select p;
Also check out this answer, which is similar to my suggestion: Stack overflow in LINQ to SQL and the Contains keyword

Resources