在單層 JSP 時代,分頁只是 LIMIT offset, size 的事。但引入 DAO / Service / Action 三層後,分頁資訊(總筆數、總頁數、當前頁、每頁筆數)必須跨層傳遞,稍有不慎就會產生「Service 層塞 SQL 參數」「Action 直接操作 Hibernate Session」等反模式。本文梳理一套乾淨、可複用的分頁協定,適用於 SSH 專案,也能輕鬆移植到 Spring Boot + MyBatis/JPA。
public class PageBean<T> implements Serializable {
private List<T> list; // 當頁資料
private int totalCount; // 總筆數
private int pageSize; // 每頁筆數
private int currentPage; // 當前頁碼 (1-based)
// 衍生屬性:總頁數、是否首尾頁、上下頁頁碼...
public int getTotalPages() { return (int) Math.ceil((double) totalCount / pageSize); }
public boolean hasPrev() { return currentPage > 1; }
public boolean hasNext() { return currentPage < getTotalPages(); }
// getter/setter 略
}
public interface MemberDao {
List<Member> findByPage(String hql, int offset, int pageSize);
long countAll(String hql);
}
@Repository
public class MemberDaoImpl extends HibernateDaoSupport implements MemberDao {
@Override
public List<Member> findByPage(String hql, int offset, int pageSize) {
return getHibernateTemplate().execute(session -> {
Query q = session.createQuery(hql);
q.setFirstResult(offset);
q.setMaxResults(pageSize);
return q.list();
});
}
@Override
public long countAll(String hql) {
// 將 select ... 轉 select count(*) 以避免全表掃描
String countHql = "select count(*) " + hql.substring(hql.indexOf("from"));
return getHibernateTemplate().execute(session ->
((Number) session.createQuery(countHql).uniqueResult()).longValue()
);
}
}
@Service
@Transactional(readOnly = true)
public class MemberServiceImpl implements MemberService {
@Autowired MemberDao memberDao;
@Override
public PageBean<Member> queryPage(int pageNo, int pageSize) {
String hql = "from Member order by id desc";
long total = memberDao.countAll(hql);
int offset = (pageNo - 1) * pageSize;
List<Member> rows = memberDao.findByPage(hql, offset, pageSize);
PageBean<Member> pb = new PageBean<>();
pb.setList(rows);
pb.setTotalCount(total);
pb.setPageSize(pageSize);
pb.setCurrentPage(pageNo);
return pb;
}
}
@Controller
public class ListMemberAction extends ActionSupport {
@Autowired MemberService memberService;
private int page = 1; // Struts2 自動注入 ?page=2
private PageBean<Member> pageBean; // 供 JSP 讀取
@Override
public String execute() {
pageBean = memberService.queryPage(page, 10);
return SUCCESS;
}
// getter/setter 略
}
<s:iterator value="pageBean.list" var="m">
<tr>
<td><s:property value="#m.name" /></td>
<td><s:property value="#m.dept" /></td>
<td>
<a href="edit.action?id=<s:property value='#m.id'/>">編輯</a>
<a href="delete.action?id=<s:property value='#m.id'/>" onclick="return confirm('確定刪除?')">刪除</a>
</td>
</tr>
</s:iterator>
<div class="pagination">
<s:if test="pageBean.hasPrev()">
<a href="list.action?page=1">首頁</a>
<a href="list.action?page=<s:property value='pageBean.currentPage-1'/>">上頁</a>
</s:if>
<s:else>首頁 上頁</s:else>
第 <s:property value="pageBean.currentPage"/> 頁 / 共 <s:property value="pageBean.totalPages"/> 頁
<s:if test="pageBean.hasNext()">
<a href="list.action?page=<s:property value='pageBean.currentPage+1'/>">下頁</a>
<a href="list.action?page=<s:property value='pageBean.totalPages'/>">尾頁</a>
</s:if>
<s:else>下頁 尾頁</s:else>
</div>
PageHelper.startPage(pageNo, pageSize) 自動攔截 SQL,回傳 PageInfo<T> 直接對應 PageBean。Page<Member> page = repo.findAll(PageRequest.of(pageNo-1, pageSize)) 一行搞定。@RestController 回傳 JSON,前端用 Vue/React + Ant Design Table + Pagination 元件。分頁的本質是「資料切片」+「元資料傳遞」。只要 DAO 只管切頁、Service 只管組裝 PageBean、Action/Controller 只管參數轉換,三層職責清晰,未來換框架也只需替換 DAO 實作,業務邏輯完全不動。