过滤查询&分页查询
摘要:
快速搭建一个只显示列表功能的JavaWeb项目。
因为前面我们已经学习了DAO的设计思想以及规范,Servlet技术,JSP技术,EL技术,JSTL技术,Web的三层架构,MyBatis的入门使用。所以我们现在利用前面学习过的技术来完成一个CRUD的开发。
开发工具: IDEA
开发环境: Tomcat9+JDK1.8+MySQL5.7
使用技术: JSP+Servlet+MyBatis+EL+JSTL
项目需求:
完成商品信息的CRUD。
功能展示:
商品列表:
1.png准备项目中需要用到的表
DROP TABLE IF EXISTS `product`;
CREATE TABLE `product` (
`id` bigint(11) NOT NULL AUTO_INCREMENT,
`productName` varchar(50) DEFAULT NULL,
`dir_id` bigint(11) DEFAULT NULL,
`salePrice` decimal(10,2) DEFAULT NULL,
`supplier` varchar(50) DEFAULT NULL,
`brand` varchar(50) DEFAULT NULL,
`cutoff` decimal(2,2) DEFAULT NULL,
`costPrice` decimal(10,2) DEFAULT NULL,
PRIMARY KEY (`id`),
KEY `dir_id` (`dir_id`)
) ENGINE=InnoDB AUTO_INCREMENT=37 DEFAULT CHARSET=utf8;
开发步骤
- 1 . 创建JavaWeb项目。
-
2 . 搭建项目的目录结构。
我们的项目是基于三层架构开发的。 表现层(web): 包:cn.wolfcode.crud.web.servlet 包下的类: ProductServlet 业务层(service): 包:cn.wolfcode.crud.service 包下面的接口:IProductService 包:cn.wolfcode.crud.service.impl 包下的类:ProductServiceImpl 包:cn.wolfcode.crud.test 包下的类:ProductServiceImplTest 持久层(DAO): 包:cn.wolfcode.crud.dao 包下的接口:IProductDAO 包:cn.wolfcode.crud.dao.impl 包下的类:ProductDAOImpl 包:cn.wolfcode.crud.domain 包下的类:Product 包:cn.wolfcode.crud.util 包下的类:MyBatisUtil,StringUtils 包:cn.wolfcode.crud.mapper 包下的映射文件:ProductMapper.xml
-
3 . 导入需要jar和相关的资源文件的配置。
日志相关的jar包:
log4j-1.2.17.jar
log4j-api-2.3.jar
slf4j-api-1.7.25.jar
slf4j-log4j12-1.7.25.jar
lombok插件的jar包:
lombok.jar
mybatis的jar包:
mybatis-3.4.5.jar
数据库的驱动jar包:
mysql-connector-java-5.1.40-bin.jar
JSTL相关的jar包:
taglibs-standard-impl-1.2.5.jar
taglibs-standard-spec-1.2.5.jar
导入jar的方式 如图:
![7.png](https://img.haomeiwen.com/i3636960/3b26cd6008dd8acd.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240)
resources文件夹下面的相关的配置文件:
数据库的配置文件:db.properties
日志文件:log4j.properties
MyBatis的主配置文件:mybatis-cfg.xml
resources文件夹的创建及设置:
![8.png](https://img.haomeiwen.com/i3636960/8f6239a9da3f7220.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240)
- 4 . 实现DAO层的逻辑代码。
IProductDAO:
public interface IProductDAO {
/**
* 获取所有商品数据
* @return 包含所有商品的集合
*/
List<Product> list();
}
ProductDAOImpl:
public class ProductDAOImpl implements IProductDAO {
@Override
public List<Product> list() {
SqlSession session = MyBatisUtil.openSession();
List<Product> list = session.selectList("cn.wolfcode.crud.mapper.ProductMapper.list");
session.close();
return list;
}
}
ProductMapper.xml:
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE mapper
PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
"http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="cn.wolfcode.crud.mapper.ProductMapper">
<resultMap type="Product" id="BaseMapper">
<result column="dir_id" property="dirId"/>
</resultMap>
<select id="list" resultMap="BaseMapper">
SELECT id,productName,dir_id,salePrice,supplier,brand,cutoff,costPrice
FROM product
</select>
</mapper>
- 5 . 编写对应的工具类。
MyBatisUtil:
public class MyBatisUtil {
private MyBatisUtil(){}
private static SqlSessionFactory fac;
static{
try {
fac = new SqlSessionFactoryBuilder().build(Resources.getResourceAsStream("mybatis-cfg.xml"));
} catch (IOException e) {
e.printStackTrace();
}
}
public static SqlSession openSession(){
return fac.openSession();
}
}
StringUtils:
public final class StringUtils {
private StringUtils() {
}
public static boolean hasLength(String str) {
return str != null && str.trim().length() != 0;
}
}
- 6 . 实现service层的逻辑代码。
IProductService:
public interface IProductService {
/**
* 获取所有商品信息
* @return 包含商品信息的list集合
*/
List<Product> list();
}
ProductServiceImpl:
public class ProductServiceImpl implements IProductService {
private IProductDAO dao = new ProductDAOImpl();
@Override
public List<Product> list() {
return dao.list();
}
}
- 7 . 编写MyBatis的主配置文件等相关的配置文件。
mybatis-cfg.xml:
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE configuration
PUBLIC "-//mybatis.org//DTD Config 3.0//EN"
"http://mybatis.org/dtd/mybatis-3-config.dtd">
<configuration>
<!-- 加载db.properties -->
<properties resource="db.properties"/>
<typeAliases>
<package name="cn.wolfcode.crud.domain"/>
</typeAliases>
<environments default="dev">
<environment id="dev">
<transactionManager type="JDBC"/>
<dataSource type="POOLED">
<property name="driver" value="${driverClassName}"/>
<property name="url" value="${url}"/>
<property name="username" value="${username}"/>
<property name="password" value="${password}"/>
</dataSource>
</environment>
</environments>
<!-- 加载映射文件 -->
<mappers>
<mapper resource="cn/wolfcode/crud/mapper/ProductMapper.xml"/>
</mappers>
</configuration>
db.properties:
driverClassName=com.mysql.jdbc.Driver
url=jdbc:mysql:///wolf
username=root
password=root123
log4j.properties:
# Global logging configuration
log4j.rootLogger=ERROR, stdout
# MyBatis logging configuration...
log4j.logger.cn.wolfcode.crud.mapper=TRACE
# Console output...
log4j.appender.stdout=org.apache.log4j.ConsoleAppender
log4j.appender.stdout.layout=org.apache.log4j.PatternLayout
log4j.appender.stdout.layout.ConversionPattern=%5p [%t] - %m%n
- 8 . 编写测试类,测试service层。
如果测试service层没有什么问题,那么说明DAO层也没有什么问题。
ProductServiceImplTest:
public class ProductServiceImplTest {
private IProductService service = new ProductServiceImpl();
@Test
public void testList() {
List<Product> list = service.list();
for (Product product : list) {
System.out.println(product);
}
}
}
- 9 . 搭建界面
相关的界面是不应该直接被通过浏览器进行访问的,所以应该放在WEB-INF文件夹下面。
在WEB-INF文件夹下面定义一个文件夹views,用来管理jsp页面。编写(列表(list.jsp)
list.jsp:
<%@ page contentType="text/html;charset=UTF-8" language="java"%>
<%@taglib prefix="c" uri="http://java.sun.com/jsp/jstl/core"%>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Title</title>
<meta name="viewport" content="width=device-width, initial-scale=1">
</head>
<body>
<table border="1" cellpadding="5" cellspacing="0" width="70%">
<tr bgcolor="gray">
<th>编号</th>
<th>商品名称</th>
<th>分类编号</th>
<th>零售价</th>
<th>供应商</th>
<th>品牌</th>
<th>折扣</th>
<th>成本价</th>
</tr>
<c:forEach items="${list}" var="p" varStatus="vs">
<tr ${vs.count % 2 ==0 ? "bgcolor='gray'":""} >
<td>${p.id}</td>
<td>${p.productName}</td>
<td>${p.dirId}</td>
<td>${p.salePrice}</td>
<td>${p.supplier}</td>
<td>${p.brand}</td>
<td>${p.cutoff}</td>
<td>${p.costPrice}</td>
</tr>
</c:forEach>
</table>
</body>
</html>
- 10 . 编写Servlet,完成CRUD的操作。
ProductServlet:
@WebServlet("/product")
public class ProductServlet extends HttpServlet {
private static final long serialVersionUID = 1L;
private IProductService service = new ProductServiceImpl();
@Override
protected void service(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
req.setCharacterEncoding("UTF-8");
list(req, resp);
}
protected void list(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
List<Product> list = service.list();
req.setAttribute("list", list);
req.getRequestDispatcher("/WEB-INF/views/product/list.jsp").forward(req, resp);
}
}
最终的项目的目录结构:
9.png 10.png分页查询
课前引导为何要对查询的结果进行分页显示呢?
在开发过程中,经常做的一件事,也是最基本的事,就是从数据库中查询数据,然后在客户端显示出来。当数据少时,可以在一个页面内显示完成。然而,如果查询记录是几百条、上千条呢?直接一个页面显示完全的话,页面需要滚动多久才能结束?
11.png比如上图中,百度一下java关键字搜索,发现总共查询了42,300,000条记录,如果一次性显示的话,存在很多问题,第一用户需要滚动很久才能滚动结束,站在用户的角度来说,这样体验不佳。第二如果全部从数据库中把数据检索出来,也需要时间,那么这个检索的时间,对于用户来说,就只能等待了,站在用户的角度来说,这样体验不佳。第三如果数据量非常大,全部检索出来放哪里,如果放内存,因为数量庞大也可能造成内存溢出。而百度采用分页显示,一页显示15条记录,这样用户可以自行翻阅,记录少,清晰显示。如图:
12.png所以综上所述,我们应该在开发中采用分页的做法,来处理查询的数据。
分页的设计
我们已经了解了分页的必要性,那么如何来设计分页呢?大致上把分页的设计分为两大类:
- 1 . 逻辑分页(假分页/内存分页):
一次性把数据库某张表中所有数据都查询出来,并存放在一个List集合中,每次翻页的时候只需要从内存中去获取指定的条数即可.
优点:减少了客户端和服务器交互的次数,客户端进行数据缓存,提高了系统交互性。
缺点:增加了第一次交互的负荷,如果数据过多,可能造成内存溢出情况.
-
2 . 物理分页(真分页/数据库分页).
比如在MySQL中通过LIMIT关键字进行分页。每次翻页都从数据库中去查询指定的条数.
优点:每次从数据库返回较少数据,当次交互的负荷较轻。
缺点:每次切页时都访问数据库,增加了数据库访问并发性。
那我们今天学习的是物理分页。
分页的原理和参数的分析
常见的分页效果有很多种,那么我们今天要学习的分页效果长什么样子呢? 如图:
13.png通过上图我们可以直观的看到分页条的信息。其实就是几个a标签,比如分页条中包含了【首页】【上页】【下页】【末页】等。
其中:
数字3 是代表当前页。
数字7 代表总页数(其实总页也是末页,我们说数据总共7页,那么最后一页也是7)。
数字100 代表总条数。
数字15 代表一页上显示的条数总数。
通过上图我们还看到数字 3 和 15 是可以有用户操作的。也就是用户可以控制当前页为第几页,还可以控制每页显示多少条数据。
通过上图我们还得知在这个页面中,总共有两大部分内容组成,一个是当前页的结果集数据。一个是分页条信息。那么我们可以给它们起一个英文名字来代表它们。如:
需要查询数据库才能获取的数据:
表中的数据总条数: rows
当前页的结果集数据: data
(rows 、 data)两个数据的获取,需要发送两条SQL语句。
第一条SQL:查询符合条件的结果总数(rows)
SELECT COUNT(*) FROM 表名 [WHERE 条件]
第二条SQL:查询符合条件的结果集(data)
SELECT * FROM 表名 [WHERE 条件] LIMIT beginIndex ,pageSize;
beginIndex: 从哪一个索引的数据开始截取(从0开始)
pageSize: 每页多少条数据
需要用户传入的数据:
当前页(跳转到第几页): currentPage
每页显示多少条数据: pageSize
(currentPage 、 pageSize)两个数据是有用户控制的。它们的获取,需要用户通过客户端传入进来。
需要程序员计算的数据:
上一页: prevPage
下一页: nextPage
总页数(末页): totalPage
(prevPage 、nextPage 、totalPage)三个数据需要借助于前面的数据进行计算来得出。
比如:
上一页(prevPage)的计算:
上一页(prevPage)要依赖当前页(currentPage)的值,在程序中我们不能直接使用prevPage=currentPage-1;公式算出上一页(prevPage),我们要考虑程序的健壮性,开发的实际情况,如果当前页(currentPage)的值为1呢,还直接用上面的公式,那么上一页(prevPage)的值就变成了0了,实际情况中当前页(currentPage)的值不可能为0. 所以如果当前页(currentPage)的值为1,那么上一页(prevPage)的值还让它为1即可。在java中,我们学过三目录运算符,可以使用三目录运算符来简化计算。
综上所述,使用三目录运算符得出上一页的计算公式如下:
prevPage = currentPage-1 >= 1 ? currentPage-1 : 1
总页数(末页)(totalPage)的计算:
总页数(末页)(totalPage)要依赖当前表的总条数(rows)和 每页显示的条数(pageSize),在程序中我们不能直接使用 totalPage = rows / pageSize;公式算出总页数(totalPage),为何呢?
比如在生活中,几个人去酒店开房,酒店的一个房间住两个人,如果四个人去开房,那么需要开两间房,这个都好理解,那么如果三个人去开房,同样也需要开两间房,因为开一间房不够用,这时我们是按照: 人数除以一个房间能住的人数,如果整除,那么除数正是需要开的房间数,如果不能整除,需要开的房间数就是除数再加上1。
在我们开发中,也同样有类似上述案例的情况。
比如:表中的数据的总条数(totalPage)为 6条 ,如果我们规定每页显示 3条,那么需要两页显示完整。
如果我们规定每页显示 5条,那么还是需要两页才能显示完整。计算方式就是 使用总条数(rows) 除以 每页显示的数量(pageSize) 如果整除,那么除数就是总页数(totalPage),如果不能整除,那么总页数(totalPage)就是除数再加上1. 和我们去酒店开房的例子的计算方式是一样的。那么在java中可以使用符号“%”来判断是否能整除。同样也借助于三目运算来简化计算。
综上所述,使用三目录运算符得出总页数(totalPage)的计算公式如下:
totalPage = rows % pageSize == 0 ? rows / pageSize : rows / pageSize + 1
下一页(nextPage)的计算:
下一页(nextPage)的计算要依赖当前页(currentPage)和总页数/末页(totalPage)。
我们直接使用当前页(currentPage)加1,不就算出下一页(nextPage)了吗?为何还要依赖总页数呢?
在程序中我们不能直接使用nextPage = currentPage + 1;公式算出下一页(nextPage),我们要考虑程序的健壮性,开发的实际情况,如果当前页(currentPage)的值为 totalPage 呢,还直接用上面的公式,那么下一页(nextPage)的值就变成了(totalPage + 1)了,实际情况中下一页(nextPage)的值为(totalPage + 1).就越界了。所以应该判断当前页(currentPage)的值,如果已经是末页(totalPage)的值了,那么下一页(nextPage)的值就等于末页(totalPage)的值,如果还没到末页(totalPage)到值,就使用当前页(currentPage)的值加1作为下一页, 在java中,我们学过三目录运算符,可以使用三目录运算符来简化计算。
综上所述,使用三目录运算符得出下一页(nextPage)的计算公式如下:
nextPage = currentPage + 1 <= totalPage ? currentPage + 1 : totalPage
注意:这个公式中要依赖总页数(totalPage),所以要先计算总页数(totalPage)。
封装PageResult对象
前面我们分析了上面的图中的显示的列表的信息元素。在页面中显示7项内容,那么也就是需要我们在Servlet中需要往Request域对象中存入7项内容 如:
14.png通过看图,我们已经感觉到了代码太low,脑子里立刻想到了java的封装思想。所以这个问题的解决方案:把多个需要共享的数据,封装到一个对象:那么就只需要把数据封装到该对象,再共享该对象即可。并且在封装的时候,考虑到这个类以后变得很通用,可以把结果集中存的对象类型设置为泛型或者问号,这样就做到了代码的复用性 .如:
15.png那么这个PageResult对象应该如何设计呢?
在这里设计的数据的总条数的类型为int,不合适,应该为Long类型,为了讲课方面,设计成int类型,避免了以后转型的麻烦。
该对象是对上面的7个参数进行的一个封装。其中两参数(rows,data)来源于数据库,两个参数(currentPage,pageSize)来源于用户输入,三个参数(prevPage,totalPage,nextPage)是通过计算算出。
所以有四个参数需要赋值,那么赋值的方式有两种,一种是通过构造器,一种是通过setter方法,在这里我们可以通过构造器来统一给四个参数赋值。
三个参数(prevPage,totalPage,nextPage)的计算要依赖前面的四个参数(rows,data,currentPage,pageSize)所以应该放在赋值以后,才可以进行计算。在计算之前,有一种情况我们需要考虑到。如果查询到的结果的数量小于或者等于每页规定显示的数据的数量(pageSize),那么就没有必要再进行计算这三个参数(prevPage,totalPage,nextPage)的值了。
所以在PageResult对象的构造器中先给四个参数(rows,data,currentPage,pageSize)赋值,然后判断查询结果的总条数(rows)是否小于或者等于每页显示的数量(pageSize)。如果不满足条件,再进行计算。如:
PageResult:
@Getter
public class PageResult {
// 查询数据库
private int rows; // 表中的数据总条数
private List<?> data; // 当前页的结果集数据
// 用户传入
private int currentPage = 1;// 当前页 默认值为1
private int pageSize = 5;// 每页显示多少条数据 默认值为5
// 通过计算算出
private int prevPage;// 上一页
private int totalPage;// 总页数
private int nextPage;// 下一页
public PageResult(int currentPage , int pageSize,int rows,List<?> data){
this.currentPage = currentPage;
this.pageSize = pageSize;
this.rows = rows;
this.data = data;
// 比较查询的结果的数量决定是否还计算
if(rows<=pageSize){
prevPage = 1;
totalPage = 1;
nextPage = 1;
return;
}
// 计算
prevPage = currentPage - 1 >= 1 ? currentPage - 1 : 1;
totalPage = rows % pageSize == 0 ? rows / pageSize : rows / pageSize + 1;
nextPage = currentPage + 1 < totalPage ? currentPage + 1 : totalPage;
}
}
PageResult对象的使用
PageResult 对象已经设计好了,那么在哪个地方使用呢?也就是说我们项目是遵守的三层架构的模式开发的。
那么如果数据的封装放在表现层(Web)合适吗?不合适,因为我们说在Servlet中指做三件事(1.接收表单提交的参数。2.调用业务逻辑层处理逻辑。3.跳转到相应的页面),所以在Servlet中不处理复杂的业务逻辑。
那么如果数据的封装放在持久层(DAO)合适吗?不合适,因为持久层只负责和数据库打交道,根据相应的业务逻辑,对数据库中的数据进行增删改查操作。
所以通过分析,应该放在业务逻辑层(Service),最合适。所以在service方法中设计一个方法,供Servlet调用。
方法的设计思路:
说到方法的设计,首先要确定需求,方法的编写的目的是完成某一个需求的。 再者搞清楚如何定义一个方法,方法的组成部分有哪些。我们根据需求确定,方法的组成部分该怎么写。
实际应用:
需求:
定义一个query方法,根据指定的当前页(currentPage)和 每页显示的数量(pageSize),查询相应的商品信息,并把信息封装到PageResult对象中。
需求分析:
通过上面的需求,我们从中获取了和定义方法相关的信息。确定了方法的名称 query,传入两个参数,在这里我们可以把这两个参数封装到一个对象中(QueryObject).方法的返回值是我们的封装对象(PageResult)。因为该方法是供Servlet调用的,所以方法的修饰符应该为public修饰。所以方法的设计如下:
PageResult query(QueryObject qo);
所以使用PageResult对象的具体流程如下:
16.pngQueryObject对象的设计
在前面的需求中,要根据分页获取对应的数据。在SQL中分页的关键字limit后面需要两个参数。第一个是从哪一个索引的数据开始获取,第二个参数是每页显示多少条数据。所以需要传入两个参数。并且给定一个默认值。但是MyBatis提供的操作方法传入执行SQL任务的参数对象只能是一个对象,注意只能是一个。就是目前学到技术,还不能解决传入两个参数的情况,所以我们直接想到的方法就是封装,把两个参数封装到对象中,这样就是传一个对象作为参数,就满足条件了。如:
@Getter
@Setter
public class QueryObject {
// 当前页 默认显示第一页
private int currentPage = 1;
// 每页显示的数量 默认一页显示5条数据
private int pageSize = 5;
}
但是这样设计还有点不完美的地方,就是分页关键字limit后面跟的第一个参数是需要计算的得出的,那么这个计算的结果应该作为属性,传到DAO中,所以我们可以定义一个方法,返回值就是limit关键字的第一个参数,我们之前讲过JavaBean规范,一个类的属性是看它有没有规范的Setter或者Getter方法,只要满足任何一个,就称为属性。所以我们在定义这个方法的时候应该符合Getter方法的规范。计算需要当前页的数据,所以方法的传入参数为当前页,所以方法的定义如下:
// 返回索引 通过计算公式计算出从哪一个索引的数据开始获取
public int getStartIndex()
{
return (currentPage - 1) * pageSize;
}
上面的这个方法符合JavaBean的规范,所以startIndex是QueryObject类的一个属性。代表分页中limit的第一个参数的值。
DAO中方法的设计
service层中的query方法设计好了,接下来应该实现query方法。实现query方法,返回结果PageResult对象,在PageResult对象中,我们知道有两个数据是需要查询数据库才能获取的,那么和数据库打交道,需要操作DAO层。所以我们要先来设计在DAO层中应该提供什么样的方法供service层调用。
在前面提到PageResult对象中两个参数(rows,data),需要访问数据库,所以需要设计两个方法,返回两个值。方法的设计思路,前面我们已经说过,这里不再赘述。
第一个需求是返回表的数据的总条数,所以有返回值,并且为int类型,需要供service层调用,方法的修饰符为public。如果以后需要获取指定条件的数据的总条数,为了扩展性,方法的输入参数给一个QueryObject类型。所以最终方法为:
int queryForCount(QueryObject qo);
第二个需求是返回指定哪一页对应的商品信息,所以有返回值,并且返回的是一个集合,集合中存的是商品信息对象。需要供service层调用,方法的修饰符为public。因为获取指定页数的数据,需要传入两个参数,作为limit的两个参数,前面我们封装过一个QueryObject对象,所以最终的方法为:
List<Product> queryForList(QueryObject qo);
DAO中方法的实现
在DAO层中对应的接口中增加两个方法。在对应的实现类中,进行实现。在映射文件中增加相应的标签。注意:在sql映射文件中的返回值的类型,接收参数变量的要求。代码如下:
IProductDAO:
/**
* 查询总条数
* @param qo 查询的条件
* @return 数据的总条数
*/
int queryForCount(QueryObject qo);
/**
* 查询符合条件的数据
* @param qo 查询的条件
* @return 指定条件的数据集合
*/
List<Product> queryForList(QueryObject qo);
ProductDAOImpl:
@Override
public int queryForCount(QueryObject qo)
{
SqlSession session = MyBatisUtil.openSession();
int rows = session.selectOne("cn.wolfcode.crud.mapper.ProductMapper.queryForCount",qo);
session.close();
return rows ;
}
@Override
public List<Product> queryForList(QueryObject qo)
{
SqlSession session = MyBatisUtil.openSession();
List<Product> list = session.selectList("cn.wolfcode.crud.mapper.ProductMapper.queryForList",qo);
session.close();
return list;
}
ProductMapper.xml:
<select id="queryForCount" resultType="int">
SELECT count(*)
FROM product
</select>
<select id="queryForList" resultMap="BaseMapper">
SELECT id,productName,dir_id,salePrice,supplier,brand,cutoff,costPrice
FROM product
LIMIT #{startIndex},#{pageSize}
</select>
Service中query方法的实现
DAO层对应的部分已经实现了,那么接下来就是把之前的query方法进行实现。 如:
@Override
public PageResult query(QueryObject qo) {
// 获取总条数
int rows = dao.queryForCount(qo);
// 根据指定的条件获取对应的商品信息
List<Product> list = dao.queryForList(qo);
// 创建一个PageResult对象,把需要的参数通过构造器传入
PageResult pageResult =
new PageResult(qo.getCurrentPage(), qo.getPageSize(), rows, list);
// 返回PageResult对象
return pageResult;
}
方法是实现了,逻辑没有问题,但是有些地方需要优化一下,比如,我们思考一下,如果查询rows的值为0呢,意思就是根据条件没有查询到结果,总条数等于0,那么我们还有必要再去调用queryForList方法,查询指定条件的数据吗?完全没有这个必要,所以我们应该先判断一下rows的值,如果rows的值为0,我们就不再调用queryForList方法,发送一条查询数据的SQL语句。这样也减少了服务器的访问次数。所以方法的改良为:
@Override
public PageResult query(QueryObject qo) {
// 获取总条数
int rows = dao.queryForCount(qo);
if (rows ==0){
return new PageResult(1,qo.getPageSize(),0, Collections.emptyList());
}
// 根据指定的条件获取对应的商品信息
List<Product> list = dao.queryForList(qo);
// 创建一个PageResult对象返回,把需要的参数通过构造器传入
return new PageResult(qo.getCurrentPage(), qo.getPageSize(), rows, list);
}
我们还可以再优化一下,比如,当rows=0时,PageResult的构造器中有些参数就是常量(当前页,总条数,返回的商品信息的集合),所以我们可以在PageResult类中提供一个构造器方法的重载方法。目的是尽量减少程序猿使用过于麻烦。所以改良如下:
PageResult:
public PageResult(int pageSize){
this(1,pageSize,0, Collections.emptyList());
}
@Override
public PageResult query(QueryObject qo) {
// 获取总条数
int rows = dao.queryForCount(qo);
if (rows ==0){
return new PageResult( qo.getPageSize());
}
// 根据指定的条件获取对应的商品信息
List<Product> list = dao.queryForList(qo);
// 创建一个PageResult对象返回,把需要的参数通过构造器传入
return new PageResult(qo.getCurrentPage(), qo.getPageSize(), rows, list);
}
在控制台测试分页
在测试ProductServiceImplTest类中 ,增加一个测试方法,来测试一下分页的逻辑。
ProductServiceImplTest:
// 测试分页
@Test
public void testQuery(){
QueryObject qo = new QueryObject();
// 获取第二页
qo.setCurrentPage(1);
// 设置每页显示3条数据
qo.setPageSize(3);
// 调用servic中的query方法
PageResult pageResult = service.query(qo);
// 获取相应的商品信息的结果集
List<Product> list = (List<Product>) pageResult.getData();
// 获取对应的上一页
int prevPage = pageResult.getPrevPage();
System.out.println("prevPage = " + prevPage);
// 获取对应的下一页
int nextPage = pageResult.getNextPage();
System.out.println("nextPage = " + nextPage);
// 使用lambda表达式来遍历商品信息中的商品名称
list.stream().map(x->x.getProductName()).forEach(System.out::println);
}
控制台输出结果;
17.pngJSP页面的修改
为了方便讲解我们的分页所以把页面上新增,编辑和删除功能暂时去掉。在前面分页的原理分析中,分页其实就是几个a标签。在JSP页面增加几个分页的超链接。超链接跳转的地址应该还是列表页面。在地址后面追加一个当前页的参数过去。如果用户点击首页,把当前页(currentPage)的值设置为1,依此类推。
18.png完整代码如下:
list.jsp:
<body>
<table border="1" cellpadding="5" cellspacing="0" width="70%">
<tr bgcolor="gray">
<th>编号</th>
<th>商品名称</th>
<th>分类编号</th>
<th>零售价</th>
<th>供应商</th>
<th>品牌</th>
<th>折扣</th>
<th>成本价</th>
</tr>
<c:forEach items="${pageResult.data}" var="p" varStatus="vs">
<tr ${vs.count % 2 ==0 ? "bgcolor='gray'":""} >
<td>${p.id}</td>
<td>${p.productName}</td>
<td>${p.dirId}</td>
<td>${p.salePrice}</td>
<td>${p.supplier}</td>
<td>${p.brand}</td>
<td>${p.cutoff}</td>
<td>${p.costPrice}</td>
</tr>
</c:forEach>
<tr>
<td colspan="8" style="text-align: center">
<a href="/product?currentPage=1">首页</a>
<a href="/product?currentPage=${pageResult.prevPage}">上一页</a>
<a href="/product?currentPage=${pageResult.nextPage}">下一页</a>
<a href="/product?currentPage=${pageResult.totalPage}">末页</a>
当前第${pageResult.currentPage}/${pageResult.totalPage}页
一共${pageResult.rows}条数据
</td>
</tr>
</table>
</body>
Servlet的修改
修改servlet中修改list方法。在list方法中接收页面上传过来的当前页(currentPage)的值,把值封装到QueryObject 对象中,调用业务逻辑,获取对应的PageResult对象,然后存入Request域对象中。代码如下:
protected void list(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
// 定义一个QueryObject对象
QueryObject qo = new QueryObject();
// 获取页面上提交的参数
String currentPage = req.getParameter("currentPage");
// 判断传入的参数是否满足条件
if (StringUtils.hasLength(currentPage)) {
// 设置当前页
qo.setCurrentPage(Integer.valueOf(currentPage));
}
// 调用service中的query方法,返回PageResult对象
PageResult pageResult = service.query(qo);
// 通过setAttribute方法存入到request域对象中
req.setAttribute("pageResult", pageResult);
// 请求转发到商品列表界面(list.jsp)
req.getRequestDispatcher("/WEB-INF/views/product/list.jsp").forward(req, resp);
}
运行结果:
19.png常见问题:如果刚开始翻页操作成功,但是翻几页之后就不能翻页了,只能重启Tomcat才可以翻页,但是操作几次又不能翻页了。
解决问题:在DAO中没有关闭SqlSession对象的资源。
过滤查询
课前引导为何要有过滤查询功能呢?
在我们日常的使用软件过程中,过滤查询是我们必不可少的功能。什么是过滤查询呢?其实就是添加多个where条件来查询数据。可以动态的拼接不同的条件,来根据条件过滤信息。比如我们常用的QQ:
20.png上图中,我想添加一个网友聊天,那么我要找的网友的条件需要满足所在地为广州,故乡为中国,性别为女,年龄为18-22岁之间,并且在线的网友,这些条件之间的关系是并列的关系,组装在一起是作为条件拼接在SQL的where关键字后面的。就是过滤查询。
比如:
21.png上图中,我们打开淘宝,想买一款手机,但是搜索出来的手机信息很多,那么我们每个人的购买的需求重点是不同的,所以我们应该根据我们的需求,我们的条件,搜索符合我们需求的手机商品,再从符合的手机中,挑选我们最终需要的。 比如我只要品牌为华为的,尺寸为5.5寸的,那么我就可以点击上面的条件,这样下面就会列出全部都是华为的,尺寸为5.5寸的手机商品。
通过上面的两个常用的例子,体会到了,什么是过滤查询?使用过滤查询,帮助我们缩小了查找的范围,给我们查找商品进行了精准的查找,节省了时间,体会到了,过滤查询功能的必要性.
过滤查询的原理
输入条件点击查询按钮。把用户选择的条件提交给Servlet,本质上就是一个表单的提交。在Servlet中接收页面上提交过来的参数,把它们作为查询的条件拼接在SQL语句的后面。注意这里的条件拼接是动态的,意思是,每个用户过滤的条件不同,提交到Servlet中的参数也是不同的。那么在拼接SQL的时候应该是动态的。如图:
22.png通过上图我们可以了解到,解决用户输入不同的条件的情况,只有三个条件的过滤查询,都有很多组合的情况。寻找解决方案:
方案一:根据每一种组合,编写一条SQL语句,并对应写一个方法。
如果不同的情况,就对应一个SQL语句,那么需要写很多条SQL,每一条SQL都编写一个对应的查询方法。那么如果过滤的条件很多,那要写很多方法,所以这种方案是不可取的。加剧了开发的工作量。
方案二:使用String的工具类,进行动态的拼接SQL
使用StringBuffer或者StringBuilder工具类,来动态的添加条件。这种方案,可以写一个方法,在方法中判断每一个条件是否存在,如果条件存在,就拼接,不存在就不拼接,使用这种方案,比方案一,确实有所改善,但是也有不足的地方。就是动态拼接,要注意很多细节,比如 AND 连接符的编写,一定要注意带上空格,如果空格没有处理好,就会出现字符串连在一起的情况,如:
select * from product where salePrice>=200ANDsalePrice<=500
这时候SQL的语法就存在问题了。
还有就是关键字where关键字的添加问题,如果固定写在SQL中,就会出现where和and关键字一起使用如:
select * from product where and salePrice>=200
这时候SQL的语法就存在问题了。
如果有过滤条件,再添加,如果没有不添加,这里是不好控制的。
所以可以使用
where 1=1
固定在SQL中,这样不用关心后面的动态的条件有没有,都会有一个永远成立的条件拼接在里面,这样就不用关心何时添加where 关键字了。如:
select * from product where 1=1 and salePrice>=200
但是使用WHERE 1=1 会影响性能,因为不能使用索引来查询了.并且这种方案会导致我们放弃掉MyBatis的使用,所以综合考虑这种方案也不好。
方案三:使用MyBatis给我们提供的动态SQL
什么是MyBatis的动态SQL?
动态 SQL 元素和 JSTL 或基于类似 XML 的文本处理器相似。使用一些标签可以完成条件的动态拼接功能。
如果你有使用 JDBC 或其它类似框架的经验,你就能体会到根据不同条件拼接 SQL 语句的痛苦。例如拼接时要确保不能忘记添加必要的空格,还要注意去掉列表最后一个列名的逗号。利用动态 SQL 这一特性可以彻底摆脱这种痛苦。很好的解决了方案二中拼接麻烦的问题。
MyBatis提供的动态SQL的标签
MyBatis中提供了很多标签,那么今天我们只需要学习两个标签if,where,就可以完成过滤查询中多条件的动态拼接。
if标签
动态 SQL 通常要做的事情是根据条件包含 where 子句的一部分。如:
<select id="get">
SELECT * FROM BLOG
WHERE 1=1
<if test="title != null and title != '' ">
AND title = #{title}
</if>
</select>
代码中对字段title,使用了if标签,test为判断条件,test=“boolean表达式”,如果表达式返回结果为true,就拼接标签体中的条件,如果为false,就不拼接。
注意:代码中,条件的内容title字段必须有对应的getter方法,符合JavaBean规范。不然会出错。
如果有多个条件需要处理,只需要继续追加if标签即可。达到了动态拼接的目的。
where 标签
在上面的代码中,where关键字不能单独使用,后面还是拼接了一个永远成立的条件。因为如果用户没有输入条件,那么SQL语句单独出现一个where关键字,就又是SQL错误了。 如:
select * from product where
但是使用WHERE 1=1 会影响性能,因为不能使用索引来查询了.MyBatis 给我们提供一个where标签,并且能处理动态拼接where关键字,where 元素只会在至少有一个子元素的条件返回 SQL 子句的情况下才去插入“WHERE”子句。而且,若语句的开头为“AND”或“OR”,where 元素也会将它们去除。
如:
<select id="get">
SELECT * FROM BLOG
<where>
<if test="title != null and title != '' ">
AND title = #{title}
</if>
</where>
</select>
如果title有值,拼接出的SQL为:
SELECT * FROM BLOG WHERE title = #{title}
如果title没有值,拼接出来的SQL为:
SELECT * FROM BLOG
list.jsp 的修改
修改list.jsp页面,在body标签中新增一个form表单,表单的action的地址是商品列表,method为post,表单中提供三个标签(关键字,最低售价,最高售价)。并提供相应的name,当用户点击查询按钮的时候,把相应的条件的值提交给servlet中。如:
list.jsp:
<form action="/product" method="post">
关键字: <input type="text" name="keyword" />
最低售格:<input type="text" name="minPrice" />
最高售格:<input type="text" name="maxPrice" />
<input type="submit" value="查询">
</form>
封装ProductQueryObject对象
表单上提交过来的参数如果很多,我们在Servelt中接收到以后如果也一个一个到传入给业务层,就太过于麻烦了,我们应该利用java的封装特性,对参数进行封装。所以定义一个对象ProductQueryObject。查询条件作为类的属性,属性名称我们可以保持和list.jsp页面中的条件表单中的name的值一致,方便维护。
我们需要把这些条件传入业务层,但是现在已经有一个封装的对象了(QueryObject)作为参数传入业务层了,那如何处理这个新增的条件对象呢?,如果采用修改的方式,或者采用新增传入参数的方式,都不太妥当,为何呢?我们思考一下,如果还有很多其他的功能模块,每个页面都有不同的搜索条件,如果采用修改的方式,那么需要把条件都写在QueryObject类中,修改起来比较麻烦,并且条件没有归类,区分,以后维护起来也困难,如果采用新增传入参数的方式,那一个页面需要封装一个条件对象,作为参数,传入的参数也会越来越多,日后不好维护,所以我们可以采用java的继承思想,把条件封装到类中,并继承QueryObject,这样service层,和DAO层的代码不用改,只需要修改servlet和sql的映射文件即可。
在封装条件的时候,条件如果是数字,应当使用相应的包装类类型,为何呢?如果不使用包装类,那么属性的默认值初始值为0,有时候有这个初始值是不合理的,比如年龄。使用包装类型,那么初始值就是null了。
在servlet里对参数进行封装,在sql的映射文件中获取。实际上传输的条件的真实类型是子类。如:
@Getter
@Setter
public class ProductQueryObject extends QueryObject {
private String keyword; // 关键字
private Double minPrice;// 商品的最低售价
private Double maxPrice;// 商品的最高售价
}
Servlet的修改
在方法中接收表单提交过来的过滤条件,把过滤条件封装到ProductQueryObject对象中。把ProductQueryObject作为参数传入query方法中,调用业务层中的query方法,获取PageResult对象,并使用Request域对象保存PageResult对象,然后使用请求转发跳转到列表页面。
代码如下:
protected void list(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
// 定义一个ProductQueryObject对象
ProductQueryObject qo = new ProductQueryObject();
// 接收表单提交过来的过滤条件
String keyword = req.getParameter("keyword");
String minPrice = req.getParameter("minPrice");
String maxPrice = req.getParameter("maxPrice");
String currentPage = req.getParameter("currentPage");
// 把接收的过滤条件封装到ProductQueryObject对象中
qo.setKeyword(keyword);
if(StringUtils.hasLength(minPrice)){
qo.setMinPrice(Double.valueOf(minPrice));
}
if(StringUtils.hasLength(maxPrice)){
qo.setMaxPrice(Double.valueOf(maxPrice));
}
if(StringUtils.hasLength(currentPage)){
qo.setCurrentPage(Integer.valueOf(currentPage));
}
// 调用service中的query方法,返回PageResult对象
PageResult pageResult = service.query(qo);
// 通过setAttribute方法存入到request域对象中
req.setAttribute("pageResult", pageResult);
// 请求转发到商品列表界面(list.jsp)
req.getRequestDispatcher("/WEB-INF/views/product/list.jsp").forward(req, resp);
}
Sql映射文件的修改
在SQL的映射文件中,找到和分页相关的两个SQL,给他们增加条件。
使用where,if 标签,动态拼接表单提交过来的过滤条件。如:
<select id="queryForCount" resultType="int">
SELECT count(*)
FROM product
<where>
<if test="keyword != null and keyword != ''">
AND (productName like concat('%',#{keyword},'%') OR brand like concat('%',#{keyword},'%') )
</if>
<if test="minPrice != null">
AND salePrice >= #{minPrice}
</if>
<if test="maxPrice != null">
AND salePrice <=#{maxPrice}
</if>
</where>
</select>
<select id="queryForList" resultMap="BaseMapper">
SELECT id,productName,dir_id,salePrice,supplier,brand,cutoff,costPrice
FROM product
<where>
<if test="keyword != null and keyword != ''">
AND (productName like concat('%',#{keyword},'%') OR brand like concat('%',#{keyword},'%') )
</if>
<if test="minPrice != null">
AND salePrice >= #{minPrice}
</if>
<if test="maxPrice != null">
AND salePrice <=#{maxPrice}
</if>
</where>
LIMIT #{startIndex},#{pageSize}
</select>
代码优化---过滤条件回显
通过上面的代码测试功能上是没有问题的,能进行过滤查询,但是发现,点击查询按钮以后,查询的条件就不见了。不见的原因,也很好理解,就是点击查询,提交表单,重新发送一次请求,页面是重新请求回来的,所以过滤的条件不见了。如:
23.png过滤条件不见了,这样是不友好的,如果过滤条件过多,用户可能忘记了自己刚刚过滤的条件是什么,只有通过过滤条件回显,来告知用户刚刚筛选的条件是什么。
那么理解了过滤条件回显的必要性,那么如何让过滤条件回显呢?
过滤条件回显很简单,列表数据是怎么显示出来的,过滤条件采用同样的方式即可,把之前用户条件的过滤条件的值存入到Request域对象中即可。如:
// 把过滤条件存入到Request域对象中
req.setAttribute("qo",qo);
然后在JSP页面上通过EL表达式从域对象中获取,通过value属性来设置显示内容。如:
<form action="/product" method="post">
关键字: <input type="text" name="keyword" value="${qo.keyword}" />
最低售格:<input type="text" name="minPrice" value="${qo.minPrice}" />
最高售格:<input type="text" name="maxPrice" value="${qo.maxPrice}" />
<input type="submit" value="查询">
</form>
测试效果:
24.png代码优化---SQL映射文件的优化
在SQL的映射文件中,发现两条SQL的过滤条件是固定的。我们应该考虑进行抽取,不然呢?如果需要修改过滤条件的话,就需要修改两份,那如果重复的多了,就会导致维护起来困难。如:
25.png使用sql标签进行抽取以后,就变得很好维护了。如:
26.png在上述优化中我们把where条件进行了抽取。在判断关键字的时候,发现既要判断不能为null,也要判断不能为空字符串。这样也过于麻烦,我们可以在该属性的getter方法中,进行优化处理一下,把空字符串的情况转化成null。如:
@Getter
@Setter
public class ProductQueryObject extends QueryObject {
private String keyword; // 关键字
private Double minPrice;// 商品的最低售价
private Double maxPrice;// 商品的最高售价
public String getKeyword(){
return StringUtils.hasLength(keyword) ? keyword : null;
}
}
这样在SQL映射文件中,只需要判断字符串不等于null,就行了,就省去了判断keyword为空字符串的麻烦。如:
<sql id="condition">
<where>
<if test="keyword != null ">
AND (productName like concat('%',#{keyword},'%')
OR brand like concat('%',#{keyword},'%') )
</if>
<if test="minPrice != null">
AND salePrice >= #{minPrice}
</if>
<if test="maxPrice != null">
AND salePrice <=#{maxPrice}
</if>
</where>
</sql>
代码优化--- 翻页数据丢失的问题
我们先通过高级查询,查询数据,然后再对查询的结果进行翻页,发现测试的结果和我们理想的结果不符合。如:
27.png
为何是这样的结果呢?
就是因为提交的过滤条件是post请求,而分页条的信息都是get请求。所以点击分页信息,是没有把过滤条件提交给后台。
那么如何点击分页的时候把过滤条件一起提交给后台呢?
我们可以把点击分页信息,屏蔽超链接,添加点击事件。通过点击事件计算出当前页码(currentPage),在之前的过滤查询表单中再增加一个隐藏控件,在js方法中,把接收的当前页码(currentPage),设置给隐藏的控件。 这样再提交表单,就会把当前页的数据提交给后台。如:
list.jsp:
在head标签中添加:
<script>
function goPage(pageNo)
{
// 给过滤条件的表单中的隐藏控件设置值
document.getElementById("currentPage").value = pageNo;
// 使用JS提交表单
document.forms[0].submit();
}
</script>
在过滤条件的表单中添加:
<form action="/product" method="post">
<input type="hidden" name="currentPage" id="currentPage"/>
关键字: <input type="text" name="keyword" value="${qo.keyword}" />
最低售格:<input type="text" name="minPrice" value="${qo.minPrice}" />
最高售格:<input type="text" name="maxPrice" value="${qo.maxPrice}" />
<input type="submit" value="查询">
</form>
修改分页条的信息:
<tr>
<td colspan="8" style="text-align: center">
<a href="#" onclick="goPage(1);">首页</a>
<a href="#" onclick="goPage(${pageResult.prevPage})">上一页</a>
<a href="#" onclick="goPage(${pageResult.nextPage})">下一页</a>
<a href="#" onclick="goPage(${pageResult.totalPage})">末页</a>
当前第${pageResult.currentPage}/${pageResult.totalPage}页
共${pageResult.rows}条数据
</td>
</tr>
JSP页面增加跳转到指定页功能(了解)
需求:
增加一个入口,可以让用户输入指定的页码,点击按钮,跳转到指定的页面。
当页数很多的时候,切换页码只能通过上一页和下一页按钮进行一页一页的切换,过于麻烦,所以跳转到指定的页码这个功能,在这样的情况下,就显得很有必要了。
需求流程图:
需求分析:
通过看图分析,用户输入需要跳转的页码,然后点击GO按钮,发送请求到后台,所以我们应该提供一个输入框,一个按钮控件。并需要给GO按钮添加一个点击事件。
那么接下来我们要处理的问题就是如何把输入框中的页码传给后台?
方案一:把form表单的范围扩大,让form表单包含整个table标签,我们使用一个普通的按钮控件 type为button类型,增加点击方法,在方法中获取用户输入的数据,然后设置给里面的隐藏控件。这样提交再提交表单,就把用户输入的当前页的值传给了后台。
问题:
这种方案可以解决此问题,只是有点麻烦。隐藏的控件在这里显得多此一举。
思考:
试想一下,输入框中输入的值是什么?其实就是我们的当前页的值,为何不利用一下呢?它也是我们表单中的一个控件,这个时候之前编写的隐藏的控件就没有意义了,添加按钮,通过JS,再提交表单,这也是没有必要的,直接给type 设置成submit,是一个提交表单的按钮,不就行了吗?
方案二: 把form表单的范围扩大,让form表单包含整个table标签,把输入框的控件的name设置成和隐藏控件的name一样,把隐藏控件删除掉,增加一个提交按钮控件,type设置成submit。
代码实现:
list.jsp:
<tr>
<td colspan="8" style="text-align: center">
<a href="#" onclick="goPage(1);">首页</a>
<a href="#" onclick="goPage(${pageResult.prevPage})">上一页</a>
<a href="#" onclick="goPage(${pageResult.nextPage})">下一页</a>
<a href="#" onclick="goPage(${pageResult.totalPage})">末页</a>
当前第${pageResult.currentPage}/${pageResult.totalPage}页
共${pageResult.rows}条数据
跳转到<input type="number" name="currentPage" id="currentPage" value="${pageResult.currentPage}"
style="width: 40px;"> 页 <input type="submit" value="GO" >
</td>
</tr>
JSP页面增加设置每页的显示数量的功能(了解)
需求:
我们每个人用的屏幕尺寸不同,所以每页显示多少条数据,每个人的需要不同,可能屏幕尺寸大的,想每页显示的数据多些,屏幕尺寸小的,想每页显示的数据少点。每页显示多少条数据,我们可以给定一些数据,给用户去选择。
需求流程图:
29.png需求分析:
看图分析:当用户选择完要显示的数据以后,列表立刻根据用户选择显示的数据,重新刷新列表。所以这里应该使用一个下拉选,我们提前设置一些数据,让用户去选择,用一监听改变的事件,监听用户选择的数据,可以使用onchange事件来监听。监听到用户选择了每页显示多少条以后,就重新发送请求给后台,把pageSize发送给后台。重新加载数据。
注意:重新加载数据,要把当前页重置为1.所以应当在JS函数中传入参数1.这样就可以修改当前页码的值。
代码实现:
在list.jsp增加:每页显示的数据
每页显示
<select name="pageSize" onchange="goPage(1);">
<option ${pageResult.pageSize == 3 ? "selected":""}>3</option>
<option ${pageResult.pageSize == 5 ? "selected":""}>5</option>
<option ${pageResult.pageSize == 10 ? "selected":""}>10</option>
<option ${pageResult.pageSize == 15 ? "selected":""}>15</option>
</select>
条数据
在Servlet中增加接收每页显示多少条数据并封装到条件对象中:
// 定义一个ProductQueryObject对象
ProductQueryObject qo = new ProductQueryObject();
// 接收表单提交过来的过滤条件
String pageSize = req.getParameter("pageSize");
// 把接收的过滤条件封装到ProductQueryObject对象中
if(StringUtils.hasLength(pageSize))
{
qo.setPageSize(Integer.valueOf(pageSize));
}
优化点击高级查询和分页混合使用的情况
问题:
我们先通过测试把问题给抛出来 :
第一步:输入查询条件关键字为”罗技“,点击查询按钮,输出结果。如:
30.png第二步:跳转到第8页。如:
31.png第三步:增加搜索条件,设置最高价格为300.点击查询按钮,输出结果。如:
32.png通过测试发现,在分页状态条中显示的信息是错乱的。
问题原因:
通过查看提交的sql语句中发现。当前页的字段没有提交过去。只提交了过滤查询相关的查询条件。如:
33.png
所以导致了分页条信息显示混乱。
解决办法:
把查询按钮改成普通的按钮,点击查询按钮通过JS的方式提交表单,同时把当前页重置为1.如:
<button type="button" onclick="goPage(1)">查询</button>
运行效果图:
好了在这里,我们就完成了高级查询和分页查询。完整代码如下:
list.jsp:
<%@ page contentType="text/html;charset=UTF-8" language="java"%>
<%@taglib prefix="c" uri="http://java.sun.com/jsp/jstl/core"%>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Title</title>
<meta name="viewport" content="width=device-width, initial-scale=1">
<script>
function goPage(pageNo) {
// 给过滤条件的表单中的隐藏控件设置值
document.getElementById("currentPage").value = pageNo;
// 使用JS提交表单
document.forms[0].submit();
}
</script>
</head>
<body>
<form action="/product" method="post">
关键字: <input type="text" name="keyword" value="${qo.keyword}" />
最低售格:<input type="text" name="minPrice" value="${qo.minPrice}" />
最高售格:<input type="text" name="maxPrice" value="${qo.maxPrice}" />
<button type="button" onclick="goPage(1)">查询</button>
<table border="1" cellpadding="5" cellspacing="0" width="70%">
<tr bgcolor="gray">
<th>编号</th>
<th>商品名称</th>
<th>分类编号</th>
<th>零售价</th>
<th>供应商</th>
<th>品牌</th>
<th>折扣</th>
<th>成本价</th>
</tr>
<c:forEach items="${pageResult.data}" var="p" varStatus="vs">
<tr ${vs.count % 2 ==0 ? "bgcolor='gray'":""} >
<td>${p.id}</td>
<td>${p.productName}</td>
<td>${p.dirId}</td>
<td>${p.salePrice}</td>
<td>${p.supplier}</td>
<td>${p.brand}</td>
<td>${p.cutoff}</td>
<td>${p.costPrice}</td>
</tr>
</c:forEach>
<tr>
<td colspan="8" style="text-align: center">
<a href="#" onclick="goPage(1);">首页</a>
<a href="#" onclick="goPage(${pageResult.prevPage})">上一页</a>
<a href="#" onclick="goPage(${pageResult.nextPage})">下一页</a>
<a href="#" onclick="goPage(${pageResult.totalPage})">末页</a>
当前第${pageResult.currentPage}/${pageResult.totalPage}页
共${pageResult.rows}条数据
跳转到<input type="number" name="currentPage" id="currentPage" value="${pageResult.currentPage}"
style="width: 40px;"> 页 <input type="submit" value="GO" >
每页显示
<select name="pageSize" onchange="goPage(1);">
<option ${pageResult.pageSize == 3 ? "selected":""}>3</option>
<option ${pageResult.pageSize == 5 ? "selected":""}>5</option>
<option ${pageResult.pageSize == 10 ? "selected":""}>10</option>
<option ${pageResult.pageSize == 15 ? "selected":""}>15</option>
</select>
条数据
</td>
</tr>
</table>
</form>
</body>
</html>
ProductServlet:
@WebServlet("/product")
public class ProductServlet extends HttpServlet {
private static final long serialVersionUID = 1L;
private IProductService service = new ProductServiceImpl();
@Override
protected void service(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
// 设置post的请求的乱码
req.setCharacterEncoding("UTF-8");
list(req, resp);
}
protected void list(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
// 定义一个ProductQueryObject对象
ProductQueryObject qo = new ProductQueryObject();
// 接收表单提交过来的过滤条件
String keyword = req.getParameter("keyword");
String minPrice = req.getParameter("minPrice");
String maxPrice = req.getParameter("maxPrice");
String currentPage = req.getParameter("currentPage");
String pageSize = req.getParameter("pageSize");
// 把接收的过滤条件封装到ProductQueryObject对象中
qo.setKeyword(keyword);
if (StringUtils.hasLength(minPrice)) {
qo.setMinPrice(Double.valueOf(minPrice));
}
if (StringUtils.hasLength(maxPrice)) {
qo.setMaxPrice(Double.valueOf(maxPrice));
}
if (StringUtils.hasLength(currentPage)) {
qo.setCurrentPage(Integer.valueOf(currentPage));
}
if (StringUtils.hasLength(pageSize)) {
qo.setPageSize(Integer.valueOf(pageSize));
}
// 调用service中的query方法,返回PageResult对象
PageResult pageResult = service.query(qo);
// 通过setAttribute方法存入到request域对象中
req.setAttribute("pageResult", pageResult);
// 把过滤条件存入到Request域对象中
req.setAttribute("qo", qo);
// 请求转发到商品列表界面(list.jsp)
req.getRequestDispatcher("/WEB-INF/views/product/list.jsp").forward(req, resp);
}
}
IProductService:
public interface IProductService {
/**
* 获取所有商品信息
* @return 包含商品信息的list集合
*/
List<Product> list();
/**
* 处理分页信息
* @return 封装指定商品信息的PageResult对象
*/
PageResult query(QueryObject qo);
}
ProductServiceImpl:
public class ProductServiceImpl implements IProductService {
private IProductDAO dao = new ProductDAOImpl();
@Override
public List<Product> list() {
return dao.list();
}
@Override
public PageResult query(QueryObject qo) {
// 获取总条数
int rows = dao.queryForCount(qo);
if (rows ==0){
return new PageResult( qo.getPageSize());
}
// 根据指定的条件获取对应的商品信息
List<Product> list = dao.queryForList(qo);
// 创建一个PageResult对象返回,把需要的参数通过构造器传入
return new PageResult(qo.getCurrentPage(), qo.getPageSize(), rows, list);
}
}
IProductDAO:
public interface IProductDAO {
/**
* 获取所有商品数据
* @return 包含所有商品的集合
*/
List<Product> list();
/**
* 查询总条数
* @param qo 查询的条件
* @return 数据的总条数
*/
int queryForCount(QueryObject qo);
/**
* 查询符合条件的数据
* @param qo 查询的条件
* @return 指定条件的数据集合
*/
List<Product> queryForList(QueryObject qo);
}
ProductDAOImpl:
public class ProductDAOImpl implements IProductDAO {
@Override
public List<Product> list() {
SqlSession session = MyBatisUtil.openSession();
List<Product> list = session.selectList("cn.wolfcode.crud.mapper.ProductMapper.list");
session.close();
return list;
}
@Override
public int queryForCount(QueryObject qo) {
SqlSession session = MyBatisUtil.openSession();
int rows = session.selectOne("cn.wolfcode.crud.mapper.ProductMapper.queryForCount", qo);
session.close();
return rows;
}
@Override
public List<Product> queryForList(QueryObject qo) {
SqlSession session = MyBatisUtil.openSession();
List<Product> list = session.selectList("cn.wolfcode.crud.mapper.ProductMapper.queryForList", qo);
session.close();
return list;
}
}
Product:
@Getter@Setter@ToString
@NoArgsConstructor
@AllArgsConstructor
public class Product {
private Long id;
private String productName;
private Long dirId;
private Double salePrice;
private String supplier;
private String brand;
private Double cutoff;
private Double costPrice;
}
PageResult:
@Getter
public class PageResult {
// 查询数据库
private int rows; // 表中的数据总条数
private List<?> data; // 当前页的结果集数据
// 用户传入
private int currentPage = 1;// 当前页 默认值为1
private int pageSize = 5;// 每页显示多少条数据 默认值为5
// 通过计算算出
private int prevPage;// 上一页
private int totalPage;// 总页数
private int nextPage;// 下一页
public PageResult(int currentPage , int pageSize,int rows,List<?> data){
this.currentPage = currentPage;
this.pageSize = pageSize;
this.rows = rows;
this.data = data;
// 比较查询的结果的数量决定是否还计算
if(rows<=pageSize){
prevPage = 1;
totalPage = 1;
nextPage = 1;
return;
}
// 计算
prevPage = currentPage - 1 >= 1 ? currentPage - 1 : 1;
totalPage = rows % pageSize == 0 ? rows / pageSize : rows / pageSize + 1;
nextPage = currentPage + 1 < totalPage ? currentPage + 1 : totalPage;
}
public PageResult(int pageSize){
this(1,pageSize,0, Collections.emptyList());
}
}
ProductQueryObject:
@Getter
@Setter
public class ProductQueryObject extends QueryObject {
private String keyword; // 关键字
private Double minPrice;// 商品的最低售价
private Double maxPrice;// 商品的最高售价
public String getKeyword(){
return StringUtils.hasLength(keyword) ? keyword : null;
}
}
QueryObject:
@Getter
@Setter
public class QueryObject {
// 当前页 默认显示第一页
private int currentPage = 1;
// 每页显示的数量 默认一页显示5条数据
private int pageSize = 3;
// 返回索引 通过计算公式计算出从哪一个索引的数据开始获取
public int getStartIndex(){
return (currentPage - 1) * pageSize;
}
}
ProductMapper.xml:
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE mapper
PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
"http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="cn.wolfcode.crud.mapper.ProductMapper">
<resultMap type="Product" id="BaseMapper">
<result column="dir_id" property="dirId"/>
</resultMap>
<select id="list" resultMap="BaseMapper">
SELECT id,productName,dir_id,salePrice,supplier,brand,cutoff,costPrice
FROM product
</select>
<sql id="condition">
<where>
<if test="keyword != null ">
AND (productName like concat('%',#{keyword},'%')
OR brand like concat('%',#{keyword},'%') )
</if>
<if test="minPrice != null">
AND salePrice >= #{minPrice}
</if>
<if test="maxPrice != null">
AND salePrice <=#{maxPrice}
</if>
</where>
</sql>
<select id="queryForCount" resultType="int">
SELECT count(*)
FROM product
<include refid="condition"/>
</select>
<select id="queryForList" resultMap="BaseMapper">
SELECT id,productName,dir_id,salePrice,
supplier,brand,cutoff,costPrice
FROM product
<include refid="condition"/>
LIMIT #{startIndex},#{pageSize}
</select>
</mapper>
MyBatisUtil:
public class MyBatisUtil {
private MyBatisUtil(){}
private static SqlSessionFactory fac;
static{
try {
fac = new SqlSessionFactoryBuilder().build(Resources.getResourceAsStream("mybatis-cfg.xml"));
} catch (IOException e) {
e.printStackTrace();
}
}
public static SqlSession openSession(){
return fac.openSession();
}
}
StringUtils:
public final class StringUtils {
private StringUtils() {
}
public static boolean hasLength(String str) {
return str != null && str.trim().length() != 0;
}
}
网友评论