It's my first time working with Spring Reactor and I've faced the following challenge:
I have a service which allows to consume a number of records specified by page number and page size:
Mono<GetContactsForGroupResponse> getContactsForGroup(Integer page, Integer size);
Among other fields GetContactsForGroupResponse contains pagination metadata:
class GetContactsForGroupResponse {
private int totalPages;
private int totalElements;
private int numberOfElements;
private int size;
private int number;
private boolean first;
private boolean last;
//.....
}
Now I need to write another method that would read all of the pages from
Mono<GetContactsForGroupResponse> getContactsForGroup(Integer page, Integer size);
and combine results into one single collection:
Mono<Collection<GetContactsForGroupResponse>> getContactsForGroup();
So far I've written:
List<GetContactsForGroupResponse> groupContacts = new ArrayList<>();
AtomicBoolean allPagesConsumed = new AtomicBoolean(false);
int pageNumber = 0;
int pageSize = 10;
while(!allPagesConsumed.get()) {
allPagesConsumed.set(true);
GetContactsForGroupResponse getContactsForGroupResponse =
getContactsForGroup(accountId, g.getId(), 0, pageSize).block();
Optional.ofNullable(getContactsForGroupResponse)
.ifPresent(r -> {
allPagesConsumed.set(r.isLast());
groupContacts.add(r);
});
pageNumber ++;
I read results page by page until I reach the last page.
I wonder what is the right way of implementation from SpringReactor point of view
Any advice would be appreciated,
Thanks
there is no "right way", because this not a reactive question.
Since you are fetching "pages" you are dealing with a non reactive way of handling data. You have not disclosed anything about how you fetch this data, and from what type of database.
The easiest thing is to just make a query to the database and fetch everything in one go.
write a getAllContactsForGroup instead, and don't, do a while loop.
Related
I'm developping a REST API managing Module objects. For the UI, I need pagination and the total number of pages.
I know when Spring uses Page<T> an additional count query is used (to get the total number of pages) which is an overhead cost.
I need this total number of pages for the UI. But only once (no need to execute again the count query for each new page).
So I was thinking of exposing two endpoints :
getting the total number of elements
getting the data (so I'm returning a List<Module> instead of Page<Module> because I don't want to execute this extra count query for each page request)
Something like this :
#RestController
#RequestMapping("/api/modules")
public class ModuleApi {
private final ModuleService service;
#GetMapping("/count")
public Long count() {
return service.countModules();
}
#GetMapping
public List<Module> find(
#RequestParam("name") String name ,
#RequestParam(value = "page", required = false, defaultValue = "0") Integer page,
#RequestParam(value = "size", required = false, defaultValue = "10") Integer size
) {
return service.find(PageRequest.of(page, size));
}
}
Is this a good design ?
Counting once means your count will get outdated as more Modules are inserted into your database in which case the count is no longer relevant.
The better design would be to work with spring's Slice<Module>, forget the count altogether, and implement the solution on the front end side. Think of how some sites only fetch you more results when you are at the bottom of the page.
However this may cost a lot of effort and time on your architecture so your proposal should be fine.
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.
Is it possible to fetch data in user defined ranges [int starting record -int last record]?
In my case user will define in query String in which range he wants to fetch data.
I have tried something like this
Pageable pageable = new PageRequest(0, 10);
Page<Project> list = projectRepository.findAll(spec, pageable);
Where spec is my defined specification but unfortunately this do not help.
May be I am doing something wrong here.
I have seen other spring jpa provided methods but nothing are of much help.
user can enter something like this localhost:8080/Section/employee? range{"columnName":name,"from":6,"to":20}
So this says to fetch employee data and it will fetch the first 15 records (sorted by columnName ) does not matter as of now.
If you can suggest me something better that would be great.if you think I have not provided enough information please let me know, I will provide required information.
Update :I do not want to use native or Create query statements (until I don't have any other option).
May be something like this:
Pageable pageable = new PageRequest(0, 10);
Page<Project> list = projectRepository.findAll(spec, new pageable(int startIndex,int endIndex){
// here my logic.
});
If you have better options, you can suggest me that as well.
Thanks.
Your approach didn't work, because new PageRequest(0, 10); doens't do what you think. As stated in docs, the input arguments are page and size, not limit and offset.
As far as I know (and somebody correct me if I'm wrong), there is no "out of the box" support for what you need in default SrpingData repositories. But you can create custom implementation of Pagable, that will take limit/offset parameters. Here is basic example - Spring data Pageable and LIMIT/OFFSET
We can do this with Pagination and by setting the database table column name, value & row counts as below:
#Transactional(readOnly=true)
public List<String> queryEmployeeDetails(String columnName,String columnData, int startRecord, int endRecord) {
Query query = sessionFactory.getCurrentSession().createQuery(" from Employee emp where emp.col= :"+columnName);
query.setParameter(columnName, columnData);
query.setFirstResult(startRecord);
query.setMaxResults(endRecord);
List<String> list = (List<String>)query.list();
return list;
}
If I am understanding your problem correctly, you want your repository to allow user to
Provide criteria for query (through Specification)
Provide column to sort
Provide the range of result to retrieve.
If my understanding is correctly, then:
In order to achieve 1., you can make use of JpaSpecificationExecutor from Spring Data JPA, which allow you to pass in Specificiation for query.
Both 2 and 3 is achievable in JpaSpecificationExecutor by use of Pagable. Pageable allow you to provide the starting index, number of record, and sorting columns for your query. You will need to implement your range-based Pageable. PageRequest is a good reference on what you can implement (or you can extend it I believe).
So i got this working as one of the answer suggested ,i implemented my own Pageable and overrided getPagesize(),getOffset(),getSort() thats it.(In my case i did not need more)
public Range(int startIndex, int endIndex, String sortBy) {
this.startIndex = startIndex;
this.endIndex = endIndex;
this.sortBy = sortBy;
}
#Override
public int getPageSize() {
if (endIndex == 0)
return 0;
return endIndex - startIndex;
}
#Override
public int getOffset() {
// TODO Auto-generated method stub
return startIndex;
}
#Override
public Sort getSort() {
// TODO Auto-generated method stub
if (sortBy != null && !sortBy.equalsIgnoreCase(""))
return new Sort(Direction.ASC, sortBy);
else
return new Sort(Direction.ASC, "id");
}
where startIndex ,endIndex are starting and last index of record.
to access it :
repository.findAll(spec,new Range(0,20,"id");
There is no offset parameter you can simply pass. However there is a very simple solution for this:
int pageNumber = Math.floor(offset / limit) + ( offset % limit );
PageRequest pReq = PageRequest.of(pageNumber, limit);
The client just have to keep track on the offset instead of page number. By this I mean your controller would receive the offset instead of the page number.
Hope this helps!
I am having an issue with converting a string of id to an int when there are multiple strings passed in, the code shows the following:
for(int i = 0; i < Request.Params["service"].Length; i++)
{
int serviceID = int.Parse(Request.Params["service"]);
db.ServiceAssignments.Add(serviceAssignment);
serviceAssignment.locationID = locationID;
serviceAssignment.ServiceID = serviceID;
db.SaveChanges();
}
If you pass in one param, you get: int.Parse(Request.Params["Service"]); = 1, which then works and the database saves. however if you have the following you get:
'1,2' which three. What I want is 1 and then 2, not 1,2.
What is 1 and 2?
When you create anew location you get to select services for that location. The service id, in the case of this problem is 1 and 2. if I select one service then it saves and all is well. When I select two or more it doesnt work.
I though I could do:
Request.Params["Service"][i] because "Service" is an array after all. How ever this causes database problems and a whole other mess.
So what would you suggest I can do to make it save id 1 and id 2 when you select them for a location?
MVC 3 is quite powerful to figure out the binding, I don't know exactly what are you doing in the view that get the service Ids from user but I assume there is form there and if all Ids are int you can do like this and you don't need any conversion, or maybe you can use FormCollection. I don't think using Request in MVC 3 is a good idea, it does not really belong the whole MVC idea.
public void Add(int[] service)
{
foreach (var item in service)
{
int serviceID = item;
}
}
OR
public void Add(FormCollection frm)
{
foreach (var item in frm.AllKeys)
{
if (item.StartsWith("service"))
{
int serviceID = Int32.Parse(frm[item]);
}
}
}
anyway none of these are also MVC, these are should work but I recommend you to use Models in views and controllers
This will work. Just tested it:
string[] items = Request.Params["service"].Split(',');
for (int i = 0; i < items.Length; i++)
{
int serviceID = int.Parse(items[i]);
db.ServiceAssignments.Add(serviceAssignment);
serviceAssignment.locationID = locationID;
serviceAssignment.ServiceID = serviceID;
db.SaveChanges();
}
As a side note, I'd probably make two changes:
I'd use a foreach statement. No real difference; just less typing.
I'd put the SaveChanges() AFTER the for loop. This will make fewer calls to the database, but still accomplish the same thing.
i have an extension method which a person was really helpful to give me... it does an orderby on IQueryable ... but i wanted one to do a normal IQueryable (non generic)
Here is the code, The count and Skip and i think Take are missing .
public static IQueryable GetPage(this IQueryable query,
int page, int pageSize, out int count)
{
int skip = (int)((page - 1) * pageSize);
count = query.Count(); //COUNT DOESN'T EXIST
return query.Skip(skip).Take((int)pageSize); // NEITHER SKIP
}
Here is the and it works perfectly no errors.
public static IQueryable<T> GetPage<T>(this IQueryable<T> query,
int page, int pageSize, out int count)
{
int skip = (int)((page - 1) * pageSize);
count = query.Count();
return query.Skip(skip).Take((int)pageSize);
}
Any ideas how i can get around this? I don't want to change my return types as it works perfectly and i have another extension method called ToDataTable and it also functions on a non generic IQueryable ..
Is there a work around?
Thanks in advance
EDIT
I call it like so on an existing IQueryable
IQueryable<Client> gen = null;
IQueryable nongen = null;
var test = gen.GetPage(); //COMPILES!
var test 1 = non.GetPage(); // Doesn't compile because GETPAGE
// for non generic is broken as it has
// invalid methods like COUNT and SKIP etc.
I tried removing the GetPage non generic version but then my non Generic Iqueryable doesn't pickup the extension due to the fact its not a Iqueryable but only an IQueryable
Well, quite simply those methods aren't available for IQueryable. If you look at the Queryable methods you'll see they're almost all based on IQueryable<T>.
If your data source will really be an IQueryable<T> at execution time, and you just don't know what T is, then you could find that out with reflection... or in C# 4, just use dynamic typing:
public static IQueryable GetPage(this IQueryable query,
int page, int pageSize, out int count)
{
int skip = (int)((page - 1) * pageSize);
dynamic dynamicQuery = query;
count = Queryable.Count(dynamicQuery);
return Queryable.Take(Queryable.Skip(dynamicQuery, skip), pageSize);
}
The dynamic bit of the C# compiler will take care of working out T for you at execution time.
In general though, I'd encourage you to just try to use the generic form everywhere instead - it's likely to be significantly simpler.
The question is old but when someone needs the IQueryable extension methods then he/she should return IQueriable when the return type is anonymous.