How to avoid joins using subqueries inside Specification? - java

I need to fetch some entites based on data that can only be found after 5 associations. I would like to avoid joining all the tables on the way and use the IN clause.
Here is a simple implementantion I found using only a couple of entities:
#Entity
public class Foo {
#Id
private Long idFoo;
private String name;
}
#Entity
public class Bar {
#Id
private Long idBar;
#ManyToOne
#JoinColumn(name = "idFoo")
private Foo foo;
}
Let's suppose I need to list all Foo objects according to a Bar property, let's say the idBar:
class FilterFooByIdBar extends Specification<Foo> {
public Predicate toPredicate(Root<Foo> root, CriteriaQuery<?> query, CriteriaBuilder cb) {
var subquery = query.subquery(Foo.class);
var barRoot = subquery.from(Bar.class);
subquery.select(barRoot.get("foo"))
.where(builder.equal(barRoot.get("idBar"), 1L));
return root.in(subquery);
}
}
This works but the resulting SQL is something like that:
select foo0_.idFoo, foo0_.name
from Foo foo0_
where foo0_.idFoo in (
select bar1_.idFoo
from Bar bar1_ cross join Foo foo2_
where bar1_.idFoo=foo2_.idFoo
and bar1_.idBar=1
);
I think that the join inside the subquery is useless and goes against my goal, I would like to do something like:
select foo0_.idFoo, foo0_.name
from Foo foo0_
where foo0_.idFoo in (
select bar1_.idFoo
from bar bar1_
where bar1_.idBar=1
);
Is there anyway to change the Specification and achieve that?

Use an exists subquery instead which is usually faster anyway and will also allow to eliminate the join if you are using the latest hibernate version.
class FilterFooByIdBar extends Specification<Foo> {
public Predicate toPredicate(Root<Foo> root, CriteriaQuery<?> query, CriteriaBuilder cb) {
var subquery = query.subquery(Foo.class);
var barRoot = subquery.from(Bar.class);
subquery.select(cb.literal(1))
.where(cb.and(
cb.equal(barRoot.get("idBar"), 1L),
cb.equal(barRoot.get("foo"), root)
);
return cb.exists(subquery);
}
}

Related

How can I get specific item from collection? Criteria API

I have two entities with one-to-many relationships (simplified):
public class Action{
#OneToMany
private Set<ActionDetailParameter> detailParameters = new HashSet<>(0);
}
public class ActionDetailParameter {
private String parameterName;
private String parameterValue;
}
I need to select all Actions where detailParameters item has parameterName equals "newserviceDepartmentName". I tried using this code:
...
SetJoin<Action, ActionDetailParameter> detailParameters = actionRoot.joinSet("detailParameters", JoinType.LEFT);
Predicate namePredicate = criteriaBuilder.equal(detailParameters.get("parameterName"), "newserviceDepartmentName");
QueryBuildingCriteria<Action> queryBuildingCriteria = getQueryBuildingCriteria(Action.class);
CriteriaQuery<Action> query = (CriteriaQuery<Action>) queryBuildingCriteria.getQuery();
getResultList(createQuery(query.select(actionRoot).where(namePredicate)));
...
there was the following exception:
org.hibernate.hql.internal.ast.QuerySyntaxException: Invalid path: 'generatedAlias1.parameterName' [select generatedAlias0 from common.dao.entities.Action as generatedAlias0 where generatedAlias1.parameterName=:param0]
please tell me what I am doing wrong
I believe your problem to be that you construct a new CriteriaQuery after creating the actionRoot. As you don't show the whole code, this is some speculation.
QueryBuildingCriteria<Action> queryBuildingCriteria = getQueryBuildingCriteria(Action.class);
CriteriaQuery<Action> query = (CriteriaQuery<Action>) queryBuildingCriteria.getQuery();
I have adjusted your mapping to something I had existing in my system and have simply replaced the working classes/field names with the ones from your question:
public void test() {
CriteriaBuilder cb = getSessionFactory().getCriteriaBuilder();
CriteriaQuery<Action> createQuery = cb.createQuery(Action.class);
Root<Action> x = createQuery.from(Action.class);
createQuery.select(x)
.where(cb.equal(x.joinList("detailParameters").get("parameterName"),
"newserviceDepartmentName"));
List<Action> resultList = getSession().createQuery(createQuery).getResultList();
System.out.println(resultList);
}
This should return all Actions which have an ActionDetailParameter where the parameterName equals newserviceDepartmentName

Java Specification CriteriaBuilder complex query

I'm not very experienced with Specification, builder, query, I have to do a rather complex query like this:
select * from table where
code in ('code1', 'code2' //codes) and
(
(
date between "2020-03-23 //from" and "2020-03-30 //to"
and
status in ('Status1' , 'Status2' //status)
)
or
(
date between "2021-03-23" and "2021-03-30"
and
status in ('Status3' , 'Status4')
)
)
And i have a DTO like this:
public class SearchCriteria {
#Embedded
private Filters filters;
#Embeddable
#Getter
#Setter
public static class Filters {
private List<String> codes;
private List<TimePeriod> timePeriods;
}
#Embeddable
#Getter
#Setter
public static class TimePeriod {
private List<String> status;
private StartDate startDate;
}
#Embeddable
#Getter
#Setter
public static class StartDate {
private LocalDate from;
private LocalDate to;
}
It's very hard for me. I'm trying everything. I preferred to show you a specific case so as not to run into misunderstanding. Could someone help me? I would appreciate very much!
I don't need to use the Specification, I just need to be able to reproduce that query example, the Specification just seemed like the best choice.
Thank u all.
I think you're on the right track, the criteria class looks fine.
Here's how you could use it in a method to build the JPA criteria and execute a query using the repository corresponding to your entity:
public void query(List<String> codes, List<TimePeriod> timePeriods) {
// build the code filter
Specification<Table> codeSpec = (root, query, criteriaBuilder) -> {
Path<String> codeField = root.get("code");
var codePredicate = criteriaBuilder.in(codeField);
codes.forEach(code -> codePredicate.value(code));
return codePredicate;
};
// iterate over the time periods
var timePeriodSpec = timePeriods.stream().map(timePeriod -> {
Specification<Table> dateSpec = (root, query, criteriaBuilder) -> {
Path<LocalDate> dateField = root.get("date");
return criteriaBuilder.between(dateField, timePeriod.startDate.from, timePeriod.startDate.to);
};
Specification<Table> statusSpec = (root, query, criteriaBuilder) -> {
Path<String> statusField = root.get("status");
var statusPredicate = criteriaBuilder.in(statusField);
timePeriod.status.forEach(status -> statusPredicate.value(status));
return statusPredicate;
};
// combine the date and status filter
return dateSpec.and(statusSpec);
})
.reduce(Specification::or).get(); // chain the time period filters together
var fullSpec = codeSpec.and(timePeriodSpec);
var result = tableRepository.findAll(fullSpec, Pageable.unpaged());
}
You also need to make sure your repository implements the JpaSpecificationExecutor interface, but you've probably figured that out already.

Java - JPA-Specification: how to create criteria/specification on a field that belongs to a nested object?

I am using jdk 1.8 , hibernate and jpa in my project. And using specification/criteria to build my search query.
I have a class A ( an hibernate entity) which has class B as an attribute. So, roughly, it looks like :
#Entity
class A {
Long id;
String comment;
#OneToOne
B b;
}
and...
#Entity
class B {
Long id;
String type;
}
My repository class looks like (roughly):
public interface ARepository extends PagingAndSortingRepository<A, Integer>,
JpaSpecificationExecutor<A> {
}
Most of the simple JPA queries are working as expected. Even the specification/criteria based directly on Class A is working. However, I need to create a dynamic query and that should be executed under "findAll" method of PagingAndSortingRepository class. This query should be equivalent to
select * from A a left join B b on a.b_id = b.id
where b.type='final' and a.comment='blah';
I created a similar logic as above in a specification like :
public Specification<A> getSpecification() {
return (itemRoot, query, criteriaBuilder) -> {
.........
List<Predicate> partialQueries = new ArrayList<>();
partialQueries.add(criteriaBuilder.equal(itemRoot.get("b.type"), "final"));
partialQueries.add(criteriaBuilder.equal(itemRoot.get("comment"), "blah"));
//Other queries to be added...
return criteriaBuilder.and(partialQueries.toArray(new Predicate[0]));
};
}
And getting error :
Unable to locate Attribute with the the given name [b.type] on this ManagedType [com.something.domain.A]
Any insight on how to create criteria/specification on a field that belongs to a nested object?
If you want to filter nested object. You can write
itemRoot.get("NestedTableName").get("nestedfieldname")
In your case - itemRoot.get("B").get("type")
itemRoot.get("Name of the nested object field in the root class").get("nestedfieldname");
Example:
cb.equal(root.get("b").get("type"),value)
In your case - itemRoot.get("b").get("type");

Filtering in Spring Data JPA Repository using QueryDSL with a nullable foreign key

I've been having this issue where I am unable to properly filter on a table using querydsl which has a nullable foreign key. I stripped down my use case into a very simple scenario.
Say we have 2 entities, MyEntity and TimeRangeEntity. My Entity only has an ID and a foreign key to the TimeRangeEntity. The TimeRangeEntity only has a start and an end time and an ID. BaseEntity, that these both extend from, only has the ID set with the #Id annotation.
#Entity
#Table(name = "TEST_OBJECT")
public class MyEntity extends BaseEntity {
#OneToOne(cascade = { CascadeType.MERGE, CascadeType.PERSIST })
private TimeRangeEntity actionTime;
public TimeRangeEntity getActionTime() {
return actionTime;
}
public void setActionTime(TimeRangeEntity actionTime) {
this.actionTime = actionTime;
}
}
#Entity
#DiscriminatorValue("static")
public class TimeRangeEntity extends BaseEntity {
#Column(name = "START_TIME")
private Instant startTime;
#Column(name = "END_TIME")
private Instant endTime;
public Instant getStartTime() {
return startTime;
}
public void setStartTime(Instant startTime) {
this.startTime = startTime;
}
public Instant getEndTime() {
return endTime;
}
public void setEndTime(Instant endTime) {
this.endTime = endTime;
}
}
I've constructed a default method in my repository to run a findAll with a predicate using querydsl to build the SQL syntax
#Repository
public interface MyRepository extends JpaRepository<MyEntity, Long>, QueryDslPredicateExecutor<MyEntity> {
default Page<MyEntity> paginateFilter(PaginationInfo info, String filter){
int page = info.getOffset() > 0 ? info.getOffset() / info.getLimit() : 0;
PageRequest pageRequest = new PageRequest(page, info.getLimit(), new Sort(new Sort.Order(info.getSortDirection(), info.getSortProperty())));
return findAll(createFilterPredicate(filter, myEntity), pageRequest);
}
default Predicate createFilterPredicate(String filter, QMyEntity root){
BooleanBuilder filterBuilder = new BooleanBuilder();
filterBuilder.or(root.id.stringValue().containsIgnoreCase(filter));
filterBuilder.or(root.actionTime.startTime.isNotNull());
return filterBuilder.getValue();
}
}
I also wrote a test that should work given the code presented. What I'm trying to do is just filter based on ID. The caveat is that the FK to the TimeRange can be null. I'll note that this a contrived example to get my point across and the solution can't really be "just enforce the FK is not null."
#RunWith(SpringRunner.class)
#DataJpaTest(showSql = false)
#ContextConfiguration(classes = TestConfig.class)
#ActiveProfiles("test")
public class MyRepositoryTest {
#Autowired
private MyRepository sut;
private static final int count = 3;
#Before
public void setup(){
for (int i = 0; i < count; i++){
sut.save(new MyEntity());
}
}
#Test
public void testPaginationWithStringFilter(){
PaginationInfo info = new PaginationInfo();
info.setOffset(0);
info.setLimit(10);
info.setSortDirection(Sort.Direction.ASC);
info.setSortProperty("id");
Page<MyEntity> page = sut.paginateFilter(info, "1");
assertEquals(1, page.getTotalElements());
page = sut.paginateFilter(info, "10");
assertEquals(0, page.getTotalElements());
}
}
The problem that I'm running into is that it isn't filtering on the ID if the FK is null. All I'm doing when I save is setting the ID. I know the problem is because I can see the filtering work properly when I comment out the line filterBuilder.or(root.actionTime.startTime.isNotNull()); but it doesn't work when I have that in.
This generates the following queries. The first is for the "working" filtering where I can filter based on ID (line commented out). The second is for the filtering with the actionTime included.
select myentity0_.id as id2_38_, myentity0_.action_time_id as action_t3_38_ from test_object myentity0_ where lower(cast(myentity0_.id as char)) like ? escape '!' order by myentity0_.id asc limit ?
select myentity0_.id as id2_38_, myentity0_.action_time_id as action_t3_38_ from test_object myentity0_ cross join time_range_entity timerangee1_ where myentity0_.action_time_id=timerangee1_.id and (lower(cast(myentity0_.id as char)) like ? escape '!' or timerangee1_.start_time is not null) order by myentity0_.id asc limit ?
Looking at this, I'm almost certain that this is due to the snipper cross join time_range_entity timerangee1_ where myentity0_.action_time_id=timerangee1_.id since it validates that the entities match, which they cannot if the range foreign key is null.
I've been pulling my hair out trying to get this conditional working that only checks the time range's table properties IF the FK is not null but I cannot find a way using querydsl. Any advice/guidance/code snippets would be stellar.
EDIT: Just translating to straight SQL, I got this query for the generated JPQL(translated to this example since I used it with real data):
select * from test_object cross join time_range range where test_object.action_time_id=range.id and lower(cast(test_object.id as char)) like '%1%';
With a null FK, that didn't return a row as expected. Changing this to a left join from a cross join ended up working properly.
select * from test_object left join time_range on test_object.action_time_id=time_range.id where lower(cast(test_object.id as char)) like '%1%';
With that, is there any way to specify a left join with the querydsl predicate executor? This seems like it'd be the solution to my problem!
Try to use Specification instead of Predicate
private Specification<QMyEntity> createFilterPredicate(final String filter, final QMyEntity root) {
return new Specification<QMyEntity>() {
#Nullable
#Override
public Predicate toPredicate(Root<QMyEntity> root, CriteriaQuery<?> query,
CriteriaBuilder criteriaBuilder) {
Join<Object, Object> actionTime = root.join("actionTime", JoinType.LEFT);
return criteriaBuilder.or(criteriaBuilder.like(criteriaBuilder.lower(root.get("id")), "%" + filter + "%"), criteriaBuilder.isNotNull(actionTime.get("startTime")));
}
};
}

Spring Data JPA Specification groupBy

sorry for my english first.
i want use jpa to groupby, like : select scrip, dustup, count(*) from data flow group by scrip, dstip.
so, write these code:
public class DataflowSpec {
public static Specification<Dataflow> search(final String[] group, final String[] sort, final String[] desc) {
return new Specification<Dataflow>() {
#Override
public Predicate toPredicate(Root<Dataflow> root1, CriteriaQuery<?> query1, CriteriaBuilder builder) {
// TODO Auto-generated method stub
CriteriaQuery<Tuple> query = builder.createQuery(Tuple.class);
Root<Dataflow> root = query.from(Dataflow.class);
query.multiselect(root.get("srcip"), root.get("dstip"), builder.count(root));
query.groupBy(root.get("srcip"), root.get("dstip"));
query.orderBy(builder.desc(root.get("srcip").as(BigInteger.class)));
return query.getRestriction();
}
};
}
}
but , SQL log is:
Hibernate:
select
count(dataflow0_.id) as col_0_0_
from
Dataflow dataflow0_
Hibernate:
select
dataflow0_.id as id1_2_,
dataflow0_.byteall as byteall2_2_,
dataflow0_.bytedn as bytedn3_2_,
dataflow0_.byteup as byteup4_2_,
dataflow0_.dstip as dstip5_2_,
dataflow0_.dstport as dstport6_2_,
dataflow0_.engieid as engieid7_2_,
dataflow0_.flag as flag8_2_,
dataflow0_.netid as netid9_2_,
dataflow0_.pkgall as pkgall10_2_,
dataflow0_.pkgdn as pkgdn11_2_,
dataflow0_.pkgup as pkgup12_2_,
dataflow0_.protocolid as protoco17_2_,
dataflow0_.rtt as rtt13_2_,
dataflow0_.srcip as srcip14_2_,
dataflow0_.srcport as srcport15_2_,
dataflow0_.updatetime as updatet16_2_
from
Dataflow dataflow0_ limit ?
so, how to resolve it? thanks!
For people still looking for how to apply "group by" in Spring jpa Specification, you can use something like the following snippet:
...
private Dataflow dataflowFilter;
#Override
public Predicate toPredicate(Root&ltDataflow&gt root, CriteriaQuery&lt?&gt cq, CriteriaBuilder cb) {
Predicate predicate = cb.conjunction();
predicate.getExpressions().add(cb.equal(root.get("id"), dataflowFilter.getId()));
...
cq.groupBy(root.get("id"));
...
return predicate;
}
You can achieve spring data group by by specification, just follow
[section 2.6][1] or [section 3.6][2] for version before or after 2.0. For single repository manipulation, the two versions have identical solution. For the *all * repository solution, before 2.0 use [customized factory bean][3], while after 2.0 this factory bean manipulation is omitted.
public Map<AlarmMsg.AlarmLevel, Long> testSpecification(String neId) {
SingularAttribute attribute = AlarmData_.isClear;
Specification<Object> where = Specification.where(
(root, query, cb) -> cb.equal(root.get(attribute), false)
);
final Map<AlarmMsg.AlarmLevel, Long> result = alarmDataRepository.groupAndCount(AlarmData_.alarmLevel, where );
return result;
}
repository:
public interface AlarmDataRepository extends JpaRepository<AlarmData, Long>, JpaSpecificationExecutor<AlarmData>, CustomizedGroupCountRepository {
Fragment repository and its implementation:
public interface CustomizedGroupCountRepository {
Map<AlarmMsg.AlarmLevel, Long> groupAndCount(SingularAttribute singularAttribute, Specification where);
}
public class CustomizedGroupCountRepositoryImpl implements CustomizedGroupCountRepository {
private final EntityManager entityManager;
public CustomizedGroupCountRepositoryImpl(EntityManager entityManager) {
Assert.notNull(entityManager, "EntityManager must not be null!");
this.entityManager = entityManager;
}
#Override
public Map<AlarmMsg.AlarmLevel, Long> groupAndCount(SingularAttribute singularAttribute, Specification where) {
final CriteriaBuilder criteriaBuilder = entityManager.getCriteriaBuilder();
final CriteriaQuery<Tuple> query = criteriaBuilder.createQuery(Tuple.class);
final Root<AlarmData> root = query.from(AlarmData.class);
final Path<AlarmMsg.AlarmLevel> expression = root.get(singularAttribute);
query.multiselect(expression, criteriaBuilder.count(root));
query.select(criteriaBuilder.tuple(expression, criteriaBuilder.count(root)));
query.where(where.toPredicate(root, query, criteriaBuilder));
query.groupBy(expression);
final List<Tuple> resultList = entityManager.createQuery(query).getResultList();
return resultList.stream()
.collect(toMap(
t -> t.get(0, AlarmMsg.AlarmLevel.class),
t -> t.get(1, Long.class))
);
}
}
The main difference between one-for-all-repository and one-for-single-repository is, in one-for-single-repository case, it can access the real entity class, like User in spring reference document. So that you don't need to use generic types to refer an any-typed entity, while in one-for-all-repository case, the implementation of the customized method uses generic types, and its class information could (or must) be gained from an injected JpaEntityInformation as stated in both section 3.6.
[1]: https://docs.spring.io/spring-data/jpa/docs/1.8.0.RELEASE/reference/html/#repositories.single-repository-behaviour
[2]: https://docs.spring.io/spring-data/jpa/docs/2.0.5.RELEASE/reference/html/#repositories.single-repository-behavior
[3]: https://jeroenbellen.com/spring-data-extending-the-jpa-specification-executor/
Specification doesn't support groupBy.
SimpleJpaRepository replaced query.select/multiselect by query.select(root)

Categories

Resources