جاوا و تکنولوژی های آن

java programming language

در این وبلاگ به بررسی نکات موجود در جاوا و تکنولوژی های آن می پردازیم

طبقه بندی موضوعی


در این بخش روی پیاده سازی Rest Pagination به کمک Spring DATA و Spring MVC می پردازیم


مسئله طراحی یک صفحه به عنوان Resource یا صفحه به عنوان نمایش دهنده اطلاعات Resource :


اولین سوالی که در هنگام طراحی pagination در معماری RESTful مطرح میشود این است که آیا یک صفحه را باید به عنوان یک Resource واقعی در نظر گرفت یا به عنوان ارائه دهنده نمایش اطلاعات Resource 

اگر Page را به عنوان Resource در نظر بگیریم مشکلات زیادی اعم از عدم توانایی در شناسایی Resource ها هنگام ارتباطات بوجود خواهد آمد و این واقعیت را میرساند که در لایه Persistence صفحات دیگر یک Entity نیستند بلکه یک ظرف برای نگهداری اطلاعات هستند و صفحه نمایش دهنده اطلاعات آن ظرف است 


سوال بعدی در طراحی این است که وجه افتراق یک صفحه را در کجای URL باید قرار دهیم ؟ در Path یا در URI query 

in the URI path: /foo/page/1
the URI query: /foo?page=1

*این نکته را در ذهن بسپارید که یک page یک Resource نیست پس روش URI path برای مشخص کردن شماره صفحه اصولی نیست و از URI query استفاده خواهیم کرد 



طراحی Controller :


@GetMapping(params = { "page", "size" })
public List<Foo> findPaginated(@RequestParam("page") int page, 
  @RequestParam("size") int size, UriComponentsBuilder uriBuilder,
  HttpServletResponse response) {
    Page<Foo> resultPage = service.findPaginated(page, size);
    if (page > resultPage.getTotalPages()) {
        throw new MyResourceNotFoundException();
    }
    eventPublisher.publishEvent(new PaginatedResultsRetrievedEvent<Foo>(
      Foo.class, uriBuilder, response, page, resultPage.getTotalPages(), size));
 
    return resultPage.getContent();
}


همانطور که میبینید به کمک RequestParam@ مقدار پارامتر های size و page در می آوریم ولی یک راه دیگر استفاده از ابجکت Pageable است که مقدار page , size و sort را اتوماتیک به ما می دهد علاوه بر این entity ابجکت PagingAndSortingRepository میتواند از ابجکت Pageable به عنوان یک پارامتر ورودی استفاده کند

همچنین در ورودی متد کنترلر ما دو پارامتر دیگر را تزریق کرده ایم یکی HttpServletResponse و دیگری UriComponentsBuilder که کمک میکند اطلاعات بیشتری را بدست بیاوریم ولی اگر داشتن اطلاعات بیشتر از اهداف طراحی API شما نیست میتوانید بسادگی آنها را حذف نماید



قابلیت کشف یا HATEOS در REST Pagination :


یکی از شرایط طراحی Pagination قابلیت next و prev برای رفتن به صفحات قبلی و بعدی همچنین first و last برای رفتن به صفحه اول و آخرین صفحه در صفحه جاری است برای این منظور ما از هدر Link استفاده کنیم 

لینک هر صفحه یکی دیگر از مسایلی است که باید به آن پرداخت زیرا هر بار که کلاینت آن لینک را وارد میکند و مربوط به صفحه خاصی میشد باید آن اطلاعات قابل دسترس باشد این مسئله توسط Event قابل حل است بدین صورت که PaginatedResultsRetrievedEvent یک Event است که در لایه کنترلر میتوانیم یک Listener برای آن در نظر بگیریم و Listener در هر بار درخواست چک میکند که آیا میتوانیم صفحات Next و Prev و first و last را داشته باشیم یا نه و اگر این صفحات وجود داشت آنرا در هدر Link برای ارسال Response اضافه میکند


حال بیاییم قدم به قدم آنرا بررسی کنیم :


ابجکت UriComponentsBuilder تنها در بر گیرنده اطلاعات اولیه URL مانند Host , Port و ContextPath است بنابراین نیاز است بخش های دیگری به آن اضافه شود :


void addLinkHeaderOnPagedResourceRetrieval(
 UriComponentsBuilder uriBuilder, HttpServletResponse response,
 Class clazz, int page, int totalPages, int size ){
 
   String resourceName = clazz.getSimpleName().toString().toLowerCase();
   uriBuilder.path( "/admin/" + resourceName );
 
    // ...
    
}


سپس برای مشخص کردن هر لینک از StringJoiner استفاده میکنیم :


StringJoiner linkHeader = new StringJoiner(", ");
if (hasNextPage(page, totalPages)){
    String uriForNextPage = constructNextPageUri(uriBuilder, page, size);
    linkHeader.add(createLinkHeader(uriForNextPage, "next"));
}


در کد بالا متد constructNextPageUri دارای بدنه زیر است :


String constructNextPageUri(UriComponentsBuilder uriBuilder, int page, int size) {
    return uriBuilder.replaceQueryParam(PAGE, page + 1)
      .replaceQueryParam("size", size)
      .build()
      .encode()
      .toUriString();
}


و در نهایت اطلاعات را به هدر Link اضافه میکنیم :


response.addHeader("Link", linkHeader.toString());



تست :


برای تست از کتابخانه REST-assured استفاده شده است 

@Test
public void whenResourcesAreRetrievedPaged_then200IsReceived(){
    Response response = RestAssured.get(paths.getFooURL() + "?page=0&size=2");
 
    assertThat(response.getStatusCode(), is(200));
}
@Test
public void whenPageOfResourcesAreRetrievedOutOfBounds_then404IsReceived(){
    String url = getFooURL() + "?page=" + randomNumeric(5) + "&size=2";
    Response response = RestAssured.get.get(url);
 
    assertThat(response.getStatusCode(), is(404));
}
@Test
public void givenResourcesExist_whenFirstPageIsRetrieved_thenPageContainsResources(){
   createResource();
   Response response = RestAssured.get(paths.getFooURL() + "?page=0&size=2");
 
   assertFalse(response.body().as(List.class).isEmpty());
}



تست قابلیت کشف ( Discoverability ) در Pagination :


@Test
public void whenFirstPageOfResourcesAreRetrieved_thenSecondPageIsNext(){
   Response response = RestAssured.get(getFooURL()+"?page=0&size=2");
 
   String uriToNextPage = extractURIByRel(response.getHeader("Link"), "next");
   assertEquals(getFooURL()+"?page=1&size=2", uriToNextPage);
}
@Test
public void whenFirstPageOfResourcesAreRetrieved_thenNoPreviousPage(){
   Response response = RestAssured.get(getFooURL()+"?page=0&size=2");
 
   String uriToPrevPage = extractURIByRel(response.getHeader("Link"), "prev");
   assertNull(uriToPrevPage );
}
@Test
public void whenSecondPageOfResourcesAreRetrieved_thenFirstPageIsPrevious(){
   Response response = RestAssured.get(getFooURL()+"?page=1&size=2");
 
   String uriToPrevPage = extractURIByRel(response.getHeader("Link"), "prev");
   assertEquals(getFooURL()+"?page=0&size=2", uriToPrevPage);
}
@Test
public void whenLastPageOfResourcesIsRetrieved_thenNoNextPageIsDiscoverable(){
   Response first = RestAssured.get(getFooURL()+"?page=0&size=2");
   String uriToLastPage = extractURIByRel(first.getHeader("Link"), "last");
 
   Response response = RestAssured.get(uriToLastPage);
 
   String uriToNextPage = extractURIByRel(response.getHeader("Link"), "next");
   assertNull(uriToNextPage);
}



بدست آوردن لیست منابع در Pagination :


کلاینت ها میتوانند قابلیت دریافت لیست تمامی Resource ها را داشته باشند و یا نسبت موقعیت قرار گرفته درخواست کنند اگر کلاینت به لیست Resource ها طی یک درخواست دسترسی نداشته باشد میتواند چند انتخاب داشته باشد که یکی از آنها استفاده از کد وضعیت 404 و هدر Link برای هدایت به صفحه اول میباشد :


Link=<http://localhost:8080/rest/api/admin/foo?page=0&size=2>; rel=”first”, 
<http://localhost:8080/rest/api/admin/foo?page=103&size=2>; rel=”last”


انتخاب دیگر استفاده از کد وضعیت 303 و هدایت کردن کاربر به صفحه اول است اما اگر بخواهیم احتیاط کنیم میتوانیم از کد وضعیت 405 (Method not Allowed) برای درخواست های GET استفاده کرد 



روش Range Header برای Pagination:


یکی دیگر از روش های متفاوت برای Pagination استفاده از هدر های Range, Content-Range, If-Range, Accept-Ranges همراه به کد وضعیت 206 (Partial Content) و 413 (Request Entity Too Large) و 416 (Requested Range Not Satisfiable)



REST Pagination همراه Spring Data :


در Spring Data اگر ما بخواهیم داده یک Page را از مجموعه داده کامل صفحات که داریم استخراج کنیم میتوانیم از روش Pageable repository استفاده کنیم زیرا این روش همیشه داده های یک Page را برمیگرداند و نتایج بر اساس شماره و سایز Page بصورت مرتب شده دریافت خواهد شد 

Spring Data REST به طور خودکار پارامتر های page, size, sort و ... را از URL تشخیص میدهد

برای استفاده از روشهای صفحه بندی از هر مخزنی تنها نیاز است از کلاس PagingAndSortingRepository ارث بری کنیم :

public interface SubjectRepository extends PagingAndSortingRepository<Subject, Long>{}


اگر ما درخواستی به آدرس http://localhost:8080/subjects داشته باشیم Spring به طور خودکار پارامتر های page, size , sort را اضافه خواهد کرد :

"_links" : {
  "self" : {
    "href" : "http://localhost:8080/subjects{?page,size,sort}",
    "templated" : true
  }
}


به طور پیش فرض اندازه page روی 20 تنظیم شده است که ما میتوانیم آنرا تغییر دهیم مثلا http://localhost:8080/subjects?page=10


اگر ما بخواهیم مکانیزم صفحه بندی را بصورت دلخواه در repository API داشته باشیم  لازم است که یک ابجکت Pageable به عنوان پارامتر ارسال کنیم و ابجکت برگشتی هم از نوع Page باشد :


@RestResource(path = "nameContains")
public Page<Subject> findByNameContaining(@Param("name") String name, Pageable p);


هرگاه روی endpoint ایجاد شده مثلا تحت آدرس search/ یک endpoint دیگر اضافه کنیم به طور خودکار آنرا بصورت Pagination ارائه خواهد داد :


"findByNameContaining" : {
  "href" : "http://localhost:8080/subjects/search/nameContains{?name,page,size,sort}",
  "templated" : true
}


تمامی API هایی که PagingAndSortingRepository را پیاده سازی کنند یک ابجکت از نوع Page را برگشت خواهند داد و اگر ما بخواهیم لیستی از Page را برگشت بدهیم متد ()getContent لیستی از رکورد ها در یک جواب توسط Spring Data REST API برگشت داده خواهد شد




تبدیل کردن یک List به Page :


فرض کنید ابجکتی از Pageable به عنوان ورودی داریم اما اطلاعاتی که ما در اختیار داریم به جای PagingAndSortingRepository در یک List قرار گرفته است در این حالت نیاز داریم که اطلاعات List را تبدیل کنیم مثلا فرض کنید که این List توسط جوابی از سرویس SOAP دریافت شده است :

List<Foo> list = getListOfFooFromSoapService();


حال برای تبدیل این List به Page ما نیاز داریم که ایندکس اول از List که در ابجکت Pageable مشخص شده است را بدست بیاوریم، ممکن است ایندکس اول مورد نیاز در Pageable همان ایندکس اول List نباشد و offset داشته باشد :


int start = (int) pageable.getOffset();


همینطور آخرین ایندکس را هم بدست می آوریم :


int end = (int) ((start + pageable.getPageSize()) > fooList.size() ? fooList.size()
  : (start + pageable.getPageSize()));


و با داشتن ایندکس ابتدایی و انتهایی ابجکت های بین این دو عدد را به Page تبدیل میکنیم :


Page<Foo> page 
  = new PageImpl<Foo>(fooList.subList(start, end), pageable, fooList.size());


* توجه داشته باشید که اگر نیاز بود عملیات مرتب سازی sort هم داشته باشیم این کار باید در حالت List انجام و بعد عمل تبدیل انجام شود 







نظرات  (۰)

هیچ نظری هنوز ثبت نشده است

ارسال نظر

ارسال نظر آزاد است، اما اگر قبلا در بیان ثبت نام کرده اید می توانید ابتدا وارد شوید.
شما میتوانید از این تگهای html استفاده کنید:
<b> یا <strong>، <em> یا <i>، <u>، <strike> یا <s>، <sup>، <sub>، <blockquote>، <code>، <pre>، <hr>، <br>، <p>، <a href="" title="">، <span style="">، <div align="">
تجدید کد امنیتی