本文是《轻量级 Java Web 框架架构设计》的系列博文。
之前做过一个 CRUD 示例(点击这里打开),但没有提供查询与分页功能,有些网友也提到了这个问题。今天我要做的就是,让这个示例更加全面,让它支持查询与分页。
思路反过来,我们先看页面:
这里提供了一个 Search 框,还有一个翻页控件。
要实现的目标是:使用 Ajax 效果实现查询与分页。
先看 Action 吧:
@Bean
public class ProductAction extends BaseAction {
...
@Request("post:/products")
public Result searchProducts(Map<String, String> formFieldMap) {
int pageNumber = CastUtil.castInt(formFieldMap.get("pageNumber"));
int pageSize = CastUtil.castInt(formFieldMap.get("pageSize"));
String queryString = formFieldMap.get("queryString");
Map<String, String> queryMap = WebUtil.createQueryMap(queryString);
Pager<Product> productPager = productService.searchProductPager(pageNumber, pageSize, queryMap);
return new Result(true).data(productPager);
}
}
为了实现查询,需要在 ProductAction 中增加一个用于查询的 searchProducts() 方法。
首先,该方法接收 POST 类型的 /products 请求,通过 Map<String, String> formFieldMap 参数,从前端 form 中获取相应的数据。包括:
- pageNumber:页面编号
- pageSize:每页条数
- queryString:查询字符串(格式:name1:value1;name2:value2;...)
然后,调用 WebUtil.createQueryMap() 方法,从 queryString 中创建 queryMap,也就是将 queryString 中的分号和冒号分隔成 Map 的形式并返回。
最后,调用 productService 的 searchProductPager() 方法,将相关参数传入其中,返回一个 Pager<Product> 对象,这个也就是封装的分页对象了。
Pager 类代码如下:
public class Pager<T> extends BaseBean {
private int pageNumber; // 页面编号
private int pageSize; // 每页条数
private int totalRecord; // 总记录数
private List<T> recordList; // 数据列表
public Pager(int pageNumber, int pageSize, int totalRecord, List<T> recordList) {
this.pageNumber = pageNumber;
this.pageSize = pageSize;
this.totalRecord = totalRecord;
this.recordList = recordList;
}
public int getTotalPage() {
return totalRecord % pageSize == 0 ? totalRecord / pageSize : totalRecord / pageSize + 1;
}
public int getPageNumber() {
return pageNumber;
}
public void setPageNumber(int pageNumber) {
this.pageNumber = pageNumber;
}
...
}
Pager 类封装了所有与分页相关的数据,并提供 getTotalPage() 方法,方便获取总页面数,以供前端显示。
返回的 Result 对象将被序列化为 JSON 对象,这是由 Smart Framework 提供的。返回的 JSON 数据大致是这样的:
{
success: true,
error: 0,
data: {
pageNumber: 1,
pageSize: 10,
totalRecord: 13,
recordList: [
{
id: 1,
productTypeId: 1,
productName: "iPhone 3gs",
productCode: "MP001",
price: 3500,
description: "iPhone 3gs 移动电话"
},
...
],
totalPage: 2
}
}
当 JS 通过 AJAX 拿到以上数据后,剩下的事情就是将这些数据进行解析并渲染到界面中。实现方式比较多,这里就不做深入了。总之,数据拿到了界面展现就容易了。
可能有人会问:翻页是需要做 SQL 查询的,这些代码写在哪里呢?
关于分页的 SQL 语句,是平台给封装好了,目前实现了 MySQL、Oracle、SQL Server 三种,对于其他数据库也非常容易扩展。当然,这些都由架构师或高级程序员负责扩展的,普通程序员只需要知道怎么用就行了。下面就是具体的用法。在 ProductServcie 接口中,添加了一个 searchProductPager() 方法,并完成实现类:
@Bean
public class ProductServiceImpl extends BaseService implements ProductService {
...
@Override
public Pager<Product> searchProductPager(int pageNumber, int pageSize, Map<String, String> queryMap) {
String where = "";
if (MapUtil.isNotEmpty(queryMap)) {
String productName = queryMap.get("productName");
if (StringUtil.isNotEmpty(productName)) {
where += "product_name like '%" + productName + "%'";
}
}
String sort = "id asc";
int totalRecord = DataSet.selectCount(Product.class, where);
List<Product> productList = DataSet.selectListForPager(pageNumber, pageSize, Product.class, where, sort);
return new Pager<Product>(pageNumber, pageSize, totalRecord, productList);
}
}
首先,从 queryMap 中获取相关的参数,这里只有一个 productName 参数,然后拼成 SQL 查询条件。
然后,定义排序方式。注意:在某些情况下,需要指定排序字段,尤其对于 Oracle 数据库而言。
随后,调用 DataSet.selectCount() 方法,获取 totalRecord(总记录数);调用 DataSet.selectListForPager() 方法获取 productList(分页列表)。
最后,将相关数据封装为 Pager<Product> 对象并返回。
以上是实现查询与分页要做的事情,是不是很简单,很 Smart 呢?欢迎您的评论!
补充(2013-09-19)
以上示例中只是对 product 表进行查询(单表查询),注意在返回的结果集中有一个 product_type_id 字段,直接将该字段的值显示在界面上,恐怕不太友好,有必要将其转换为 product_type 表的 product_type_name 字段并显示在界面上。这样必然会出现 product 表连接 product_type 表的一个连接查询或子查询,如果将 SQL 语句写在程序中或配置文件中当然是一种解决方案,但我并没有这样去做。
以下我的解决方案:
首先,需要对 ProductService 接口的以下方法进行了重构:
Pager<Product> searchProductPager(int pageNumber, int pageSize, Map<String, String> queryMap)
重构后的代码如下:
Pager<ProductBean> searchProductPager(int pageNumber, int pageSize, Map<String, String> formFieldMap);
这里将 Product 重构为 ProductBean,它是一个典型的 JavaBean。代码如下:
public class ProductBean extends BaseBean {
private Product product;
private ProductType productType;
public ProductBean(Product product, ProductType productType) {
this.product = product;
this.productType = productType;
}
...
}
可见,ProductBean 封装了 Product 与 ProductType 这两个实体。
然后,要做的就是如何初始化这个 ProductBean,并将其作为 List 放入 Pager 对象中,从而实现分页的效果。
需要对 searchProductPager() 方法的实现代码进行如下改动:
...
@Override
public Pager<ProductBean> searchProductPager(int pageNumber, int pageSize, Map<String, String> queryMap) {
String where = "";
if (MapUtil.isNotEmpty(queryMap)) {
String productName = queryMap.get("productName");
if (StringUtil.isNotEmpty(productName)) {
where += "product_name like '%" + productName + "%'";
}
}
String sort = "id asc";
int totalRecord = DataSet.selectCount(Product.class, where);
List<Product> productList = DataSet.selectListForPager(pageNumber, pageSize, Product.class, where, sort);
List<ProductBean> productBeanList = new ArrayList<ProductBean>();
Map<Long, ProductType> productTypeMap = DataSet.selectMap(ProductType.class, "");
for (Product product : productList) {
ProductType productType = productTypeMap.get(product.getProductTypeId());
if (productType != null) {
productBeanList.add(new ProductBean(product, productType));
}
}
return new Pager<ProductBean>(pageNumber, pageSize, totalRecord, productBeanList);
}
...
从 17 行代码开始是我做的修改。首先定义了一个 productBeanList,空的,下面的代码就是为它初始化。然后调用 DataSet.selectMap() 方法获取一个 productTypeMap,它是 ProductType 实体的主键 ID 与 ProductType 对象的之间建立的映射关系。随后遍历 productList,并初始化 productBeanList,在遍历的过程中,需要借助 productTypeMap,通过 ID 来获取 ProductType 对象。最后将初始化后的 productBeanList 放入 Pager 中。
在 DataSet 类中增加了 selectMap() 方法:
...
public static <T> Map<Long, T> selectMap(Class<T> cls, String condition, Object... params) {
Map<Long, T> map = new HashMap<Long, T>();
List<T> list = selectList(cls, condition, "", params);
for (T obj : list) {
Long id = CastUtil.castLong(ObjectUtil.getFieldValue(obj, "id"));
map.put(id, obj);
}
return map;
}
...
实际上还是调用 selectList() 方法,拿到一个 list,再遍历这个 list,去初始化 map,最后将 map 返回。需要注意的是,这里通过反射获取了实体的 id 字段。这种做法有个问题,实体必须有一个 long 型的 id 主键字段,如果它的名称或类型改了,运行时这行代码肯定会报错。需要在修改实体主键的时候,也顺便修改一下这一行代码。目前找不到更好的解决方案了。
现在返回的 JSON 是这样的:
{
success: true,
error: 0,
data: {
pageNumber: 1,
pageSize: 10,
totalRecord: 13,
recordList: [
{
"product": {
"id": 1,
"productTypeId": 1,
"productName": "iPhone 3gs",
"productCode": "MP001",
"price": 3500,
"description": "iPhone 3gs 移动电话"
},
"productType": {
"id": 1,
"productTypeName": "Mobile Phone",
"productTypeCode": "MP"
}
},
...
],
totalPage: 2
}
}
最后看看 HTML 因如何改动:
以前:
...
<tr data-id="{id}" data-name="${productName}">
<td>{productTypeId}</td>
<td>{productName}</td>
<td>{productCode}</td>
<td>{price}</td>
<td>{description}</td>
<td>
<a href="#" class="ext-product-edit">Edit</a>
<a href="#" class="ext-product-delete">Delete</a>
</td>
</tr>
...
现在:
...
<tr data-id="{product.id}" data-name="${product.productName}">
<td>{productType.productTypeName}</td>
<td>{product.productName}</td>
<td>{product.productCode}</td>
<td>{product.price}</td>
<td>{product.description}</td>
<td>
<a href="#" class="ext-product-edit">Edit</a>
<a href="#" class="ext-product-delete">Delete</a>
</td>
</tr>
...
这里使用了 HTML 模板技术来生成相关 HTML 代码。