美文网首页
第16讲. 过滤查询&分页查询

第16讲. 过滤查询&分页查询

作者: 祥祺 | 来源:发表于2020-04-11 21:01 被阅读0次

过滤查询&分页查询

摘要:

快速搭建一个只显示列表功能的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项目。
5.png 6.png
  • 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.png

QueryObject对象的设计

在前面的需求中,要根据分页获取对应的数据。在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.png

JSP页面的修改

为了方便讲解我们的分页所以把页面上新增,编辑和删除功能暂时去掉。在前面分页的原理分析中,分页其实就是几个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> &nbsp;
                  <a href="/product?currentPage=${pageResult.prevPage}">上一页</a>&nbsp;
                  <a href="/product?currentPage=${pageResult.nextPage}">下一页</a>&nbsp;
                  <a href="/product?currentPage=${pageResult.totalPage}">末页</a>&nbsp;
                   当前第${pageResult.currentPage}/${pageResult.totalPage}页&nbsp;
                  一共${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 &lt;=#{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 &lt;=#{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 &lt;=#{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> &nbsp;
       <a href="#" onclick="goPage(${pageResult.prevPage})">上一页</a>&nbsp;
       <a href="#" onclick="goPage(${pageResult.nextPage})">下一页</a>&nbsp;
       <a href="#" onclick="goPage(${pageResult.totalPage})">末页</a>&nbsp;
       当前第${pageResult.currentPage}/${pageResult.totalPage}页&nbsp;
       共${pageResult.rows}条数据&nbsp;
   </td>
</tr>
  

JSP页面增加跳转到指定页功能(了解)

需求:
增加一个入口,可以让用户输入指定的页码,点击按钮,跳转到指定的页面。

当页数很多的时候,切换页码只能通过上一页和下一页按钮进行一页一页的切换,过于麻烦,所以跳转到指定的页码这个功能,在这样的情况下,就显得很有必要了。

需求流程图:

28.png

需求分析:

通过看图分析,用户输入需要跳转的页码,然后点击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> &nbsp;
       <a href="#" onclick="goPage(${pageResult.prevPage})">上一页</a>&nbsp;
      <a href="#" onclick="goPage(${pageResult.nextPage})">下一页</a>&nbsp;
       <a href="#" onclick="goPage(${pageResult.totalPage})">末页</a>&nbsp;
       当前第${pageResult.currentPage}/${pageResult.totalPage}页&nbsp;
       共${pageResult.rows}条数据&nbsp;

      跳转到<input type="number" name="currentPage" id="currentPage" value="${pageResult.currentPage}"
               style="width: 40px;"> 页 <input type="submit" value="GO" >&nbsp;
    </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>

运行效果图:

34.png

好了在这里,我们就完成了高级查询和分页查询。完整代码如下:

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> &nbsp;
                  <a href="#" onclick="goPage(${pageResult.prevPage})">上一页</a>&nbsp;
                  <a href="#" onclick="goPage(${pageResult.nextPage})">下一页</a>&nbsp;
                  <a href="#" onclick="goPage(${pageResult.totalPage})">末页</a>&nbsp;
                   当前第${pageResult.currentPage}/${pageResult.totalPage}页&nbsp;
                   共${pageResult.rows}条数据&nbsp;

                跳转到<input type="number" name="currentPage" id="currentPage" value="${pageResult.currentPage}"
                style="width: 40px;"> 页 <input type="submit" value="GO" >&nbsp;

                每页显示
                <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 &lt;=#{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;
    }
}

相关文章

  • 第16讲. 过滤查询&分页查询

    过滤查询&分页查询 摘要: 快速搭建一个只显示列表功能的JavaWeb项目。 因为前面我们已经学习了DAO的设计思...

  • 高级查询

    分页和过滤查询 流程图 : 前面我们已经讲过分页查询了 , 接下来我们讲讲 过滤查询 . 思路 : 从后台开始写 ...

  • 2020-03-01

    第7章 商品搜索 学习目标 根据搜索关键字查询 条件筛选 规格过滤 价格区间搜索 分页查询 排序查询 高亮查询 1...

  • 基本操作

    一、介绍 二、增删改查 三、排序查询 四、分页查询 五、bool查询 六、按条件过滤查询 七、短语检索 八、高亮检索

  • MySQL学习-数据查询语言(DQL)六之分页查询和联合查询(u

    九丶分页查询 9-1丶分页查询基本概念 9-2丶分页查询案例 案例1:查询前五条员工信息 案例2:查询第11条——...

  • hibernate中的查询

    HQL 查询所有 条件查询 分页查询 Criteria 查询所有 条件查询 分页查询 查询总记录 原生SQL

  • 分页查询实践

    练习使用分页查询 每页记录个数为6,请查询出第2页的数据(第7行至第12行) SQL SERVER分页查询 思路:...

  • elasticsearch 查询语句

    elasticsearch 查询语句 1、简单查询 不带参数、分页、排序查询 带分页、字段筛选查询 2、复杂查询 ...

  • 每日一博 | 如何跳过es分页这个坑?

    1、分页查询 1.1、 正常分页查询代码如下 假设现在你要查询第100页的10条数据,但是对于es来说,from=...

  • 避免HBase PageFilter踩坑,这几点你必须要清楚

    有这样一个场景,在HBase中需要分页查询,同时根据某一列的值进行过滤。 不同于RDBMS天然支持分页查询,HBa...

网友评论

      本文标题:第16讲. 过滤查询&分页查询

      本文链接:https://www.haomeiwen.com/subject/bfbgrqtx.html