Java Specification CriteriaBuilder complex query - java

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.

Related

Custom query projections with Spring Data neo4j: Two related nodes including the relationship

I'm struggling to understand how projections work in Spring Data neo4j or more precisely what can be done with them and what are the limitations (i.e. when to use other forms of mapping). Is there some documentation or examples for this topic?
Here is the concrete problem I'm having at the moment:
I have nodes linked by a relationship that represents their similarity. I want to list them ordered by relationship with as few queries as possible, of course.
Here is one way to query that
MATCH (evt1:Event)-[possibleDupe:POSSIBLE_DUPE]->(evt2:Event)
RETURN {evt1: evt1, possibleDupe: possibleDupe, evt2: evt2} AS duplicateEntry
ORDER BY possibleDupe.differenceScore
I thought I could just project that to a DTO:
public class DuplicateEntry {
public DuplicateEntry(Event evt1, PossibleDupe possibleDupe, Event evt2) {
this.evt1 = evt1;
this.possibleDupe = possibleDupe;
this.evt2 = evt2;
}
#Getter #Setter
private Event evt1;
#Getter #Setter
private PossibleDupe possibleDupe;
#Getter #Setter
private Event evt2;
}
The usual way would be to do something like this:
Statement statement = CypherParser.parse("MATCH (evt1:Event)-[possibleDupe:POSSIBLE_DUPE]->(evt2:Event) RETURN evt1, possibleDupe, evt2");
repository.findAll(statement, Event.class);
But that can not be mapped in that way, because both sides of the relation have the same type, so the mapper does not know which is which: More than one matching node in the record.
This is something SDN cannot do out-of-the-box because this outer view on a wrapper of domain entities does not fit to the entity-centric approach.
However, you can use the Neo4jClient to manually query the data and use the already existing mapper for your node entity class.
BiFunction<TypeSystem, MapAccessor, Event> mappingFunction = neo4jMappingContext.getRequiredMappingFunctionFor(Event.class);
client.query("MATCH (evt1:Event)-[possibleDupe:POSSIBLE_DUPE]->(evt2:Event) " +
"RETURN {evt1: evt1, possibleDupe: possibleDupe, evt2: evt2} AS duplicateEntry" +
"ORDER BY possibleDupe.differenceScore")
.fetchAs(DuplicateEntry.class)
.mappedBy((typeSystem, record) -> {
var duplicateEntry = (Map<String, Object>) record.get("duplicateEntry");
var event1 = mappingFunction.apply(typeSystem, duplicateEntry.get("evt1"));
var event2 = mappingFunction.apply(typeSystem, duplicateEntry.get("evt2"));
var possibleDupe = //... derive something from duplicateEntry.get("possibleDupe")
return new DuplicateEntry(event1, possibleDupe, event2);
}).all();
(just written down, might contain some typos)
More can be found here: https://docs.spring.io/spring-data/neo4j/docs/current/reference/html/#neo4j-client.result-objects.mapping-functions
meistermeier led me on the right path. Here is the final code.
The DTO:
public class DuplicateEntry {
public DuplicateEntry(Event evt1, int differenceScore, Event evt2) {
this.evt1 = evt1;
this.differenceScore = differenceScore;
this.evt2 = evt2;
}
#Getter #Setter
private Event evt1;
#Getter #Setter
private int differenceScore;
#Getter #Setter
private Event evt2;
}
The code in the service class for querying and mapping (I added pagination also):
public Collection<DuplicateEntry> getPossibleDupes(int page, int size) {
BiFunction<TypeSystem, MapAccessor, Event> mappingFunction =
neo4jMappingContext.getRequiredMappingFunctionFor(Event.class);
return neo4jClient.query("""
MATCH (evt1:Event)-[possibleDupe:POSSIBLE_DUPE]->(evt2:Event)
RETURN {evt1: evt1, possibleDupe: possibleDupe, evt2: evt2} AS duplicateEntry
ORDER BY possibleDupe.differenceScore
SKIP $skip LIMIT $size
""")
.bind(size).to("size")
.bind(page * size).to("skip")
.fetchAs(DuplicateEntry.class)
.mappedBy((typeSystem, record) -> {
MapValue duplicateEntry = (MapValue) record.get("duplicateEntry");
var event1 = mappingFunction.apply(typeSystem, duplicateEntry.get("evt1"));
var event2 = mappingFunction.apply(typeSystem, duplicateEntry.get("evt2"));
return new DuplicateEntry(
event1,
duplicateEntry.get("possibleDupe")
.get("differenceScore").asInt(),
event2);
}).all();
}

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

How to avoid joins using subqueries inside Specification?

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

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 Pageable doesn't work for ordering

On the internet I found that Spring can do pagination as well as ordering for a list of data retrieved from the database. Accordingly, I created my test class as following:
#Test
public void testPageable() {
int pageSize = 5;
Sort sort = new Sort( Direction.DESC, "someColumnA" );
Pageable pageable = new PageRequest( 0, pageSize, sort );
List<SomeObject> listOFSomeObject = getDao().getListData( "paramOne", pageable );
}
When I analyze the List I never get ordering of someColumnA in a DESC fashion, although I get back only 5 records which is correct.
Can someone please let me know what I might be doing wrong? Just as an FYI, I am using Hibernate for database access and Spring named query.
EDIT:
Code for getListData()->
public interface SomeRepository
extends JpaRepository<EntityMappedViaHibernate, String> {
List<Object[]> getListData( #Param(value = PARAM_ONE) final String paramOne, Pageable pageable );
}
My Hibernate entity is as follows:
#NamedQueries(value = {
#NamedQuery(name = "SomeRepository.getListData", query = "select id, someColumnA from EntityMappedViaHibernate where id = :paramOne")
})
#Entity
#Table(name = "entity_mapped_via_hibernate")
public class EntityMappedViaHibernate implements Serializable {
// Code Omitted on purpose
}
So those of you who are struggling like me the solution to this problem is that you need to have the query inside the Repository. In my case it has to be inside the SomeRepository. So in the code of the repo do the following:
public interface SomeRepository
extends JpaRepository<EntityMappedViaHibernate, String> {
#Query("select id, someColumnA from EntityMappedViaHibernate where id = :paramOne")
List<Object[]> getListData( #Param(value = PARAM_ONE) final String paramOne, Pageable pageable );
}
No need to have the NamedQuery inside the EntityMappedViaHibernate. That's it!!
Hope someone find the answer and do not struggle like I did.

Categories

Resources