スポンサーサイト

上記の広告は1ヶ月以上更新のないブログに表示されています。
新しい記事を書く事で広告が消せます。

Spring MVC ページング機能を実装する(コントローラ編)

ページング機能の実装にあたり、今回はコントローラを中心に説明していきたいと思う。

なお、ボリュームが多いため、記事を3回に分けて掲載している。
 ①DB編
  ⇒Spring MVC ページング機能を実装する(DBアクセス編)
 ②コントローラ編
  ⇒本記事
 ③JSP編
  ⇒次回掲載予定

◎動作検証にあたっての各バージョンは以下の通り
  • SpringFramework 3.2.8.RELEASE
  • Java 1.7
  • Tomcat 7.0


イメージを掴むため、ページング機能実装後の画面を掲載しておく。

検索一覧(完成イメージ)
ページング_1


1.BookSearchForm.java

package jp.co.sample.book.form;

import java.io.Serializable;

public class BookSearchForm implements Serializable {

private static final long serialVersionUID = 1L;

//検索条件(タイトル)
private String bookName ="";

//ページ番号
private int pageNo;

//getter・setter省略
}

検索条件を格納するBookSearchFormクラスであるが、今回、pageNoを追加している。なお、BookSearchFormクラスはセッションスコープで保持している。セッションに関しては過去の記事を参考にしてほしい。

■セッションに関する過去の記事
Spring MVC @SessionAttributesアノテーションとログアウト時のセッション破棄


2.BookPageBean.java

package jp.co.sample.book.bean;

import java.io.Serializable;
import java.util.List;
import jp.co.sample.book.entity.BookEntity;

public class BookPageBean implements Serializable {

private static final long serialVersionUID = 1L;

//全ページ数
private int totalPages;

//現在のページ数
private int currentPage;

//全レコード数
private long totalRecords;

//開始レコード番号
private long startRecords;

//終了レコード番号
private long endRecords;

//1ページ分のBook情報
private List<BookEntity> books;

//次ページが存在するか
private boolean hasNextPage;

//前ページが存在するか
private boolean hasPreviousPage;

//getter・setter省略
}

JSPでページング情報を表示するために今回用意したJavaBeanクラスとなる。完成イメージのページング機能部分と見比べてもらうと、ある程度対応関係が掴めると思う。


3.BookController.java(関連箇所のみ掲載)

package jp.co.sample.book.controller;

import java.util.List;
import jp.co.sample.book.bean.BookPageBean;
import jp.co.sample.book.entity.BookEntity;
import jp.co.sample.book.form.BookSearchForm;
import jp.co.sample.book.service.BookService;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.data.domain.Page;
import org.springframework.data.domain.PageRequest;
import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.validation.BindingResult;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.SessionAttributes;
//他のimport文は省略

@Controller
@RequestMapping("/book")
@SessionAttributes("bookSearchForm")
public class BookController {

private static Logger logger = LoggerFactory.getLogger(BookController.class);

@Value("${upload.fileSize}")
private int uploadFileSize;

@Value("${page.records}")
private int pageRecords;

@Autowired
protected BookService<BookEntity> bookService;

@RequestMapping(method = RequestMethod.GET)
public String index() {
logger.info("index start");
//return "redirect:/book/list";
return "redirect:/book/list/page/1";
}

//listメソッドは使わなくなるため削除
//@RequestMapping(value = "/list", method = RequestMethod.GET)
//public String list(BookSearchForm form, Model model) {
// logger.info("list start");
// List<BookEntity> entites = bookService.findAll();
// model.addAttribute("books", entites);
// return "list";
//}

//searchメソッドはforwardするだけの処理に変更する
//@RequestMapping(value = "/list/search", method = RequestMethod.GET)
//public String search(BookSearchForm form, Model model) {
// logger.info("search start");
// List<BookEntity> entites = bookService.findByNameLike(form.getBookName());
// model.addAttribute("books", entites);
// return "list";
//}

//画面の検索ボタンが押下された場合
@RequestMapping(value = "/list/search",params="bookName", method = RequestMethod.GET)
public String search() {
logger.info("search start");
return "forward:/book/list/page/1";
}

//新規登録・編集画面から「一覧へ戻る」リンクボタンが押下された場合
@RequestMapping(value = "/list/search", method = RequestMethod.GET)
public String backToList(BookSearchForm form) {
logger.info("backToList start");
return "forward:/book/list/page/" + form.getPageNo();
}

//listメソッドに代わるページングに必要な情報を設定するメソッド
@RequestMapping(value="/list/page/{pageNo}", method = RequestMethod.GET)
public String pageList(BookSearchForm bookSearchForm,
BindingResult result,Model model) {

logger.info("pageList start");
int pageNo = bookSearchForm.getPageNo();
if (pageNo < 1) {
result.reject("error.pageNo","Page number error");
return "pageList";
}

PageRequest pageRequest = new PageRequest(pageNo-1, pageRecords);
Page<BookEntity> page = bookService.findPageByNameLike(pageRequest,bookSearchForm.getBookName());

if (page.getNumberOfElements() == 0) {
result.reject("error.pageRecords","Search error");
return "pageList";
}

BookPageBean bookPageBean = new BookPageBean();
bookPageBean.setTotalPages(page.getTotalPages());
bookPageBean.setCurrentPage(page.getNumber() + 1);
bookPageBean.setTotalRecords(page.getTotalElements());
bookPageBean.setBooks(page.getContent());
bookPageBean.setHasNextPage(page.hasNextPage());
bookPageBean.setHasPreviousPage(page.hasPreviousPage());

//表示レコード数
if (pageNo == 1) {
bookPageBean.setStartRecords(1);
bookPageBean.setEndRecords(page.getNumberOfElements() < pageRecords ? page.getNumberOfElements() : pageRecords);
} else if(pageNo == page.getTotalPages()) {
bookPageBean.setStartRecords((pageNo-1)*pageRecords+1);
bookPageBean.setEndRecords(page.getNumberOfElements() < pageRecords ? (pageNo-1)*pageRecords + page.getNumberOfElements() : pageNo*pageRecords);
} else {
bookPageBean.setStartRecords((pageNo-1)*pageRecords+1 );
bookPageBean.setEndRecords(pageNo*pageRecords);
}

model.addAttribute("bookPage", bookPageBean);

return "pageList";
}
//他のメソッドは省略
}

ページング機能の実装により、新規メソッドの追加だけではなく、既存のメソッドにもいくつか手を加えている。

まず、35行目の「@Value("${page.records}")」は@Valueアノテーションを使って、1ページあたりの表示レコード件数をプロパティファイルから取得するようにしている。プロパティファイルからの値取得については、過去の記事を参考にしてほしい。

■プロパティファイルに関する過去の記事
Spring MVC プロパティファイルを使用する

42行目のindexメソッドがアプリケーション起動後の最初に呼ばれるメソッドとなる。今までは、リダイレクトでlistメソッドにて全レコードを一覧表示させていたが、45行目「return "redirect:/book/list/page/1";」として、常に1ページ目を表示するよう変更している。

68行目のsearchメソッドと75行目のbackToListメソッドは、今回の実装に頭を悩ませた箇所となる。編集画面から検索一覧画面に戻る際に、編集画面を呼び出す前と同じ状態にするため、セッションに検索条件とページ番号を保持するようにした。ここまでは良かったのだが、searchメソッドを実行した場合は1ページ目を表示する必要があり、一方、戻る場合は1ページ目とは限らず、処理内容に差異が生じた。仮にURLで「/list/search/{pageNo}」のようにした場合、JSP側での「一覧へ戻る」は修正量が多く、保守性もイマイチ。検討の結果、もともと1メソッドだったのを2つに分けることにした。ただし、URLは同じにしたいので、67行目の「params="bookName"」を付与して、検索ボタンが押下された場合と「一覧へ戻る」が判断できるようにしてみた。処理は、それぞれforwardで転送しているだけとなる。

今回新規に追加したpageListメソッドの説明に入る。

81行目の「value="/list/page/{pageNo}"」であるが、メソッドの引数に@RequestParamアノテーションを使って、ページ番号を受け取ろうかと考えていた。しかし、URLのページ番号に0や100など存在しない値を直に設定された場合のエラーメッセージの実装で、「form:errors」タグを使った方が簡単に表示できると判断して、BookSearchFormクラスにページ番号を持たせることにした。ここで疑問だったのが、「テキストボックスなどからの入力項目でない値が、@RequestParamアノテーションを使わなくても、自動でマッピングしてくれるのか」という点であったが、きっとできるはずと思い、BookSearchFormクラスに同じフィールド名「pageNo」を用意して試したところ、値を取得できた。ちなみにpageNo1など異なるフィールド名にした場合は取得できなかった。(int型のため0となる。もう少し補足すると、フィールド名がpage1であっても、getter/setterがgetPageNo/setPageNoであれば取得できた。ただ一般的には、pageNo1の場合、getPageNo1/setPageNo1となるため、異なるフィールド名は取得できないでもよいかと思う。)

87行目は、先にも記載したが、URLに直接1未満の存在しないページ番号が設定された場合のエラー処理となる。また、88行目の「result.reject」は、過去の記事で「rejectValue」を紹介しているが、.rejectValueがフィールド名と紐づけしたい場合に使用するのに対して、rejectは特に結びつきがない場合に使用する。

■rejectValueに関する過去の記事
Spring MVC ファイルアップロードを実装する[入力チェック編]

92行目のPageRequestクラスであるが、こちらはAbstractPageRequestクラスを継承しており、さらにAbstractPageRequestクラスはPageableインターフェースを実装している。したがって、このPageRequestオブジェクトを生成して、93行目のfindPageByNameLikeメソッドの第1引数に渡してあげればよい。コンストラクタの引数はページ番号と1ページあたりの表示レコード数となるが、ページ番号は0開始のため、-1する必要がある。

95行目の「page.getNumberOfElements() == 0」は該当ページにレコードが取得できなかった場合、エラーとしている。また、URLに例えばページ番号100など直接設定された場合も、このエラーとなるようにしている。

100行目以降は、BookPageBeanオブジェクトに値を設定している処理が続く。

106行目のsetHasPreviousPageメソッドまでは、93行目で取得したpageオブジェクトの値を使って詰め替えている。

109行目からのif文は、pageオブジェクトには何レコード目かの情報をもっていないため、「先頭ページ」「最終ページ」「それ以外」の場合で値を取得している。page.getNumberOfElements()で該当ページのレコード数を取得している。

120行目「model.addAttribute("bookPage", bookPageBean);」で、pageList.jsp側は${bookPage.currentPage}のようにして、参照できるようになる。pageList.jspの詳細については次回説明する。


4.application.properties

upload.fileSize=60000
page.records=2

1ページあたりの表示レコード件数「page.records」は、変更しやすいよう定数ではなく、プロパティファイルに持たせている。

5.messages_ja.properties(関連箇所のみ掲載)

error.pageNo=ページ番号が存在しません
error.pageRecords=レコードが見つかりませんでした

今回追加したエラーメッセージをプロパティファイルに登録する。掲載はしないが、英語用のプロパティファイル(messages_en.properties)にもエラーメッセージを登録しておく。


6.エラーメッセージの確認
①検索結果0件の場合
ページング_8

②URLで直接0ページ指定の場合
ページング_6

③URLで直接100ページ指定の場合
ページング_7


コントローラの説明が随分長くなってしまったが、今回はここまで。次回に続く。


■過去のSpring関連記事
Spring関連記事 Index

スポンサーサイト

コメントの投稿

非公開コメント

承認待ちコメント

このコメントは管理者の承認待ちです
プロフィール

bookmount8

Author:bookmount8
システムエンジニア。サーバーサイドでjavaを扱うことが多い。最近は、ミドルやフロント周りも関心あり。

最新記事
カテゴリ
検索フォーム
最新コメント
月別アーカイブ
これまでの訪問者数
ブロとも申請フォーム

この人とブロともになる

RSSリンクの表示
上記広告は1ヶ月以上更新のないブログに表示されています。新しい記事を書くことで広告を消せます。