Spring JPA: Use CriteriaBuilder to filter on a datetime between two instants - spring

I'm setting up a method to filter entries by the "lastupdated" column. I'm trying to filter entries of which the lastupdated value is between a given startTime and endTime.
I'm using the simplified code below:
public List<SomeEntity> getItemsByLastUpdated() {
CriteriaBuilder cb = entityManager.getCriteriaBuilder();
CriteriaQuery<SomeEntity> criteriaQuery = cb.createQuery(SomeEntity.class);
var root = criteriaQuery.from(SomeEntity.class);
var predicates = new ArrayList<>();
var startTime = Instant.now();
var endTime = Instant.now().plus(5, MINUTES);
predicates.add(cb.greaterThanOrEqualTo(root.get("lastupdated"), startTime));
predicates.add(cb.lessThan(root.get("lastupdated"), endTime));
criteriaQuery.where(predicates.toArray(new Predicate[0]));
return entityManager.createQuery(criteriaQuery).getResultList();
}
For some reason, the criteria for datetime fields are not (correctly) being applied: All the entities are being returned instead of only the items within the startTime-endTime range.
I'm guessing that I need to explicitly state that the "lastupdated" field is a datetime field; when I set startTime to Instant.MAX, I receive an error that seems to hint on a date filter instead of datetime:
Invalid value for EpochDay (valid values -365243219162 - 365241780471): 365241780838
Would anyone know how I can filter on a datetime field being between two given java Instants?
PS. I'm aware of using derived queries such as findByXGreaterThanOrEqualToAndXLessThan(Instant instant1, Instant instant2); but since there are several other criterias which I have not included, this option is not feasible.

I think you could easily do the same using EntityManager and JPQL instead of Criteria API:
public List<SomeEntity> getItemsByLastUpdated() {
var query = "select e from SomeEntity e
where e.lastupdated >= :from
and e.lastupdated < :to
and your_other_criteria";
return entityManager.createQuery(query)
.setParameter("from", startTime)
.setParameter("to", endTime)
.getResultList();
}

It took me quite some time to figure it out; turned out that the lack of filtering didn't have anything to do with the CriteriaBuilder setup.
I had created a test to verify the method, and prepared some entities before running the test. However, the "lastupdated" field in the entity class was annotated with #UpdateTimestamp, which would result in all the saved entities having pretty much the same 'lastupdated' value.

Related

Memory leak with Criteria API Pageable

I implemented pageable functionality into Criteria API query and I noticed increased memory usage during query execution. I also used spring-data-jpa method query to return same result, but there memory is cleaned up after every batch is processed. I tried detaching, flushing, clearing objects from EntityManager, but memory use would keep going up, occasionally it will drop but not as much as with method queries. My question is what could cause this memory use if objects are detached and how to deal with it?
Memory usage with Criteria API pageable:
Memory usage with method query:
Code
Since I'm also updating entities retrieved from DB, I use approach where I save ID of last processed entity, so when entity gets updated query doesen't skip next selected page. Below I provide code example that is not from real app I'm working on, but it just recreation of the issue I'm having.
Repository code:
#Override
public Slice<Player> getPlayers(int lastId, Pageable pageable) {
List<Predicate> predicates = new ArrayList<>();
CriteriaBuilder criteriaBuilder = entityManager.getCriteriaBuilder();
CriteriaQuery<Player> criteriaQuery = criteriaBuilder.createQuery(Player.class);
Root<Player> root = criteriaQuery.from(Player.class);
predicates.add(criteriaBuilder.greaterThan(root.get("id"), lastId));
criteriaQuery.where(criteriaBuilder.and(predicates.toArray(Predicate[]::new)));
criteriaQuery.orderBy(criteriaBuilder.asc(root.get("id")));
var query = entityManager.createQuery(criteriaQuery);
if (pageable.isPaged()) {
int pageSize = pageable.getPageSize();
int offset = pageable.getPageNumber() > 0 ? pageable.getPageNumber() * pageSize : 0;
// Fetch additional element and skip it based on the pageSize to know hasNext value.
query.setMaxResults(pageSize + 1);
query.setFirstResult(offset);
var resultList = query.getResultList();
boolean hasNext = pageable.isPaged() && resultList.size() > pageSize;
return new SliceImpl<>(hasNext ? resultList.subList(0, pageSize) : resultList, pageable, hasNext);
} else {
return new SliceImpl<>(query.getResultList(), pageable, false);
}
}
Iterating through pageables:
#Override
public Slice<Player> getAllPlayersPageable() {
int lastId = 0;
boolean hasNext = false;
Pageable pageable = PageRequest.of(0, 200);
do {
var players = playerCriteriaRepository.getPlayers(lastId, pageable);
if(!players.isEmpty()){
lastId = players.getContent().get(players.getContent().size() - 1).getId();
for(var player : players){
System.out.println(player.getFirstName());
entityManager.detach(player);
}
}
hasNext = players.hasNext();
} while (hasNext);
return null;
}
I think you are running into a query plan cache issue here that is related to the use of the JPA Criteria API and how numeric values are handled. Hibernate will render all numeric values as literals into an intermediary HQL query string which is then compiled. As you can imagine, every "scroll" to the next page will be a new query string so you gradually fill up the query plan cache.
One possible solution is to use a library like Blaze-Persistence which has a custom JPA Criteria API implementation and a Spring Data integration that will avoid these issues and at the same time improve the performance of your queries due to a better pagination implementation.
All your code would stay the same, you just have to include the integration and configure it as documented in the setup section.

Get last record with criteria api

everyone.
My task is to get the last added record from my database. Since the last of the records in the database has the maximum id, I wrote a method like this, but it always returns null to me.
#Override
public Address getLast() {
CriteriaBuilder criteriaBuilder = entityManager.getCriteriaBuilder();
CriteriaQuery<Address> criteriaQuery = criteriaBuilder.createQuery(Address.class);
Root<Address> root = criteriaQuery.from(Address.class);
criteriaQuery
.select(root)
.orderBy(criteriaBuilder.desc(root.get(Address_.id)));
TypedQuery<Address> findAllSizes = entityManager.createQuery(criteriaQuery);
return findAllSizes.getResultStream().findFirst().orElse(null);
}
All works fine. All I needed to add was add #Transactional on the method which calls the specified method

how to convert HAC flexible query to DAO query

I'm using below flexible query in HMC and it's working fine.
The same query needs to convert the DAO layer and input is a data parameter.
Please any help on this?
SELECT * FROM {Product} WHERE {creationtime} >= TO_DATE('2020/02/19','YYYY/MM/DD')
The complete and definitive guide for the creation of Flexiqueries and corresponding DAO code.
Refer DefaultProductDao and create one for your requirement or you can extend it if you want to reuse any function. I hope by looking at the class, you'll have an understanding of how to write and execute the flexi query in the SAP Hybris.
Converting your query to DAO
Here, I would suggest avoiding using TO_DATE or any DB function to ensure that the query is not DB dependent. In your case, you can parse string date to Java Date object and pass it to the query something like below
String query = "SELECT * FROM {"+ ProductModel._TYPECODE +"} WHERE {"+ ProductModel.CREATIONTIME +"} >= ?"+ProductModel.CREATIONTIME;
final FlexibleSearchQuery searchQuery = new FlexibleSearchQuery(query);
final Map<String, Object> params = new HashMap<String, Object>();
params.put(ProductModel.CREATIONTIME, getDateObject("2020/02/19"));
searchQuery.addQueryParameters(params);
final SearchResult searchResult = getFlexibleSearchService().search(searchQuery);
return searchResult.getResult();
Method
private Date getDateObject(String date)
{
// logic to parse your string date (YYYY/MM/DD) to java Date object
return new Date(); //return your parsed date object here
}

ObjectId.ToString not working in projection (LINQ)

I get an exception when executing this code:
List<TenantSelectorDto> foundSelectors = null;
//.....
foundSelectors = tenants
.Where(filter)
.Select(ts => new TenantSelectorDto
{
Id = ts.Id.ToString(),
Name = ts.Name,
ShortName = ts.ShortName,
TenantCode = ts.TenantCode,
})
.ToList();
The exception is:
NotSupportedException, Message: ToString of type System.Object is not supported in the expression tree {document}{_id}.ToString().
Id in the DTO is defined as string while the Id of the Tenant object is a MongoDB ObjectId. The DTO is part of a REST API and I do not want to force consumers to link with the entire MongoDB driver libraries just because of the data type ObjectId.
Is this a bug or why does .ToString not work when using in a projection?
Seems to be a LINQ problem! Instead of using LINQ I tried to use the Aggregation Framework and here it works as expected!
var filterDefinition = Builders<Tenant>.Filter.Where(<Build whatever filter you need here>);
var foundSelectors = tenantCollection
.Find(filterDefinition)
.Project(ts => new TenantSelectorDto
{
Id = ts.Id.ToString(),
Name = ts.Name,
ShortName = ts.ShortName,
TenantCode = ts.TenantCode,
})
.ToList();
Does the trick without exception!
Don't know if this is a general LINQ problem or a problem of the MongoDB LINQ implementation though.

Spring PagingAndSortingRepository returns all results instead of desired page size

I am using Spring Data JPA and I have a repository which extends PagingAndSortingRepository. My issue is that I have a query in which returns all of the results instead of the desired page size (100). I can't seem to find an issue that is wrong. Can anyone assist please?
#Test
public void testFindPageByStartAndEndDate() {
Timestamp endDate = Timestamp.valueOf("2017-06-14 09:18:42");
Timestamp startDate = Timestamp.valueOf("2017-05-19 01:31:23");
PageRequest pageRequest1 = new PageRequest(0, 100, Sort.Direction.ASC, "orderDate");
Page<Order> page1Orders = orderRepository.findPageByStartAndEndDate(startDate, endDate, pageRequest1);
assertThat(page1Orders.getTotalElements(), greaterThan(0L));
//
//
// Test Fails Here
// Expected: a value less than <101L>
// but: <139L> was greater than <101L>
//
//
assertThat(page1Orders.getTotalElements(), lessThan(101L));
}
This is the query that I am using.
#Query("SELECT o FROM Order o WHERE o.orderDate >= ?1 AND o.orderDate <= ?2")
Page<Order> findPageByStartAndEndDate(#Param("startDate") Timestamp startDate,
#Param("endDate") Timestamp endDate,
Pageable pageable);
getTotalElements() returns the count without pagination.
You get the number of elements present in the slice with getNumberOfElements().
See the implementation of Page as a reference here.

Resources