How can I get specific item from collection? Criteria API - java

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

Related

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.

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

Selecting generic primary key with CriteriaQuery

When migrating from Hibernate Criteria api to CriteriaQuery I ran into a generic DAO for a abstract class that has a where on a common field but does a select on their id, even if the ids are totally different per class.
The old projection looks like this
criteria.setProjection(Projections.id());
Is there any way to do this in a similar way with CriteriaQuery?
Edit: Full criteria code
DetachedCriteria detachedCriteria = DetachedCriteria.forClass(MyEntity.class);
detachedCriteria.add(Restrictions.in("accountID", accounts));
detachedCriteria.setProjection(Projections.id());
EntityManager em = ...;
Criteria criteria = detachedCriteria.getExecutableCriteria((Session) em.getDelegate());
List<Integer> list = criteria.list();
I just managed to find it on my own.
criteriaQuery.select(
root.get(entityRoot.getModel().getDeclaredId(int.class))
);
Combining answers:
https://stackoverflow.com/a/16911313
https://stackoverflow.com/a/47793003
I created this method:
public String getIdAttribute(EntityManager em, String fullClassName) {
Class<? extends Object> clazz = null;
try {
clazz = Class.forName(fullClassName);
} catch (ClassNotFoundException e) {
e.printStackTrace();
}
Metamodel m = em.getMetamodel();
IdentifiableType<T> of = (IdentifiableType<T>) m.managedType(clazz);
return of.getId(of.getIdType().getJavaType()).getName();
}
then I have the entity manager injected
#PersistenceContext
private EntityManager em;
I get the root entity primary key like that:
String rootFullClassName = root.getModel().getJavaType().getName();
String primaryKeyName = getIdAttribute(em, rootFullClassName);
and I get the primary keys referenced on attributes like that:
return (Specification<T>) (root, query, builder) -> {
Set<Attribute<? super T, ?>> attributes = root.getModel().getAttributes();
for (Attribute a: attributes) {
if(a.isAssociation()) {
Path rootJoinGetName = root.join(a.getName());
String referencedClassName = rootJoinGetName.getJavaType().getName();
String referencedPrimaryKey = getIdAttribute(em, referencedClassName);
//then I can use it to see if it is equal to a value (e.g
//filtering actors by movies with id = 1 - in
//this case referencedPrimaryKey is "id")
Predicate p = rootJoinGetName.get(referencedPrimaryKey).in(1);
}
}
}
In this way I don't need to know the type of the primary key/referenced key in advance as it can be derived through the Entity Manager Meta Model. The above code can be used with CriteriaQuery as well as Specifications.

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.

Can not create specification that will check class of field

Entity Order contains field:
#ManyToOne(optional = false)
private AbstractRequester requester;
I wanna get data by type this field.
I created specification like in documentation, but on
page = orderRepository.findAll(spec, pageable);
Get Exception
org.springframework.dao.InvalidDataAccessApiUsageException: org.hibernate.QueryException: could not resolve property: class of: ...order.Order
My specs:
#Override
public Predicate toPredicate(Root<Order> root, CriteriaQuery<?> query, CriteriaBuilder cb) {
Predicate megaPredicate = cb.conjunction();`
...
megaPredicate = cb.and(megaPredicate, cb.and(
cb.equal(root.get("requester").type(), cb.literal(PersonRequester.class))
...
return megaPredicate;
}
You are trying to use the discriminator column in your query and that's illegal.
Instead, execute your query on the PersonRequester directly and remove the offending clause from megaPredicate.

Categories

Resources