Java学习笔记之——通过分页查询样例感受JDBC、Mybatis以及MybatisPlus(一)
该系列通过使用JDBC、Mybatis、MybatisPlus 三种方式分别实现分页查询,顺便更直观的感受后两者框架的便利性。
代码仓库:https://gitee.com/strivezhangp/spring-mysql-demo.git (该系列将在该仓库更新!!!)
分页查询就是将大量数据分成多个"页面"来显示,而不是一次性加载所有数据。就像看书一样,一页一页地翻阅,而不是把整本书的内容都摊开在面前。
【资源分享】
在学习数据库的时候,没有合适的数据可以参考MySQL官网提供的样例数据,如:https://dev.mysql.com/doc/index-other.html
(其中是一个Word的数据库,三张表格,包含了各个国家的一些城市,数据量中等,可以用来参考学习)
1 分页查询的两种实现方式
客户端分页(不推荐)
原理:一次性从数据库查询所有数据,然后在应用程序内存中进行分页
缺点:数据量大时性能极差,占用大量内存
示例:
SELECT * FROM city
→ 在Java中手动分页
服务端分页(推荐)
原理:数据库只返回当前页需要的数据
优点:性能好,节省内存和网络带宽
示例:
SELECT * FROM city LIMIT 0, 10
→ 只返回前10条数据
2 一个分页请求需要传递哪些数据
一般来说,常用的web请求,服务端拥有以下参数需要接收和返回。
private Integer pageNum; // 当前页码
private Integer pageSize; // 每页大小
private Long totalCount; // 总记录数
private Integer totalPages; // 总页数
private List<T> data; // 当前页数据列表
3 通过最基础的JDBC实现一个分页查询
3.1 JDBC建立数据库连接
JDBC驱动数据库的准备工作主要分为以下步骤实现:
1、 创建一个数据库工具类 DatabaseUtil 类
【下图是一个自建的工具类中部分方法,绿框中是静态的字段 包含了数据库信息; 红色框中则是封装了一些必备的方法】
2、 创建数据表中对应的实体类 City3、 在Dao层借助工具类 DatabaseUtil 类实现分页查询功能
4、 最后在 controller 中通过Spring Web接口调用 测试分页查询的结果
3.2 JDBC实现基本查询的代码实现
首先,借助JDBC实现一个简单的数据的查询 select * from city limit 50
。
通过这个查询的实现,了解 JDBC 对数据操作主要的步骤如下:
1、 连接数据库 connection(获取数据库连接)
2、 创建自己的SQL语句 此处使用了条件限制,查询前50条数据
3、 创建 Statement对象,对SQL语句进行预编译
4、 执行SQL语句,并返回到结果集 ResultSet 中
5、 从结果集 ResultSet 中获取查询信息,并返回
6、 关闭全部资源(数据库连接、结果集等)
/*** 查询城市数据列表*/
public List<City> listCity() {List<City> cityList = new ArrayList<>();// 连接数据库前相关信息的声明Connection connection = null; // 数据库连接的声明PreparedStatement statement = null; // 执行预编译的SQL语句 比较Statement 更安全ResultSet resultSet = null; // 结果集try {// 获取数据库连接connection = DatabaseUtil.getConnection();// 创建SQL语句String sql = "select * from city limit 50";// 创建执行SQL语句的Statement对象statement = connection.prepareStatement(sql);// 执行SQL语句resultSet = statement.executeQuery();// 遍历结果集while (resultSet.next()) {City city = new City();city.setId(resultSet.getInt("id"));city.setName(resultSet.getString("name"));city.setCountryCode(resultSet.getString("countrycode"));city.setDistrict(resultSet.getString("district"));city.setPopulation(resultSet.getInt("population"));cityList.add(city);}logger.info("-------------SQL语句:{}, 共查询数据 {} 行--------------", sql, cityList.size());} catch (SQLException e) {e.printStackTrace();} finally {// 关闭数据库连接DatabaseUtil.closeAll(connection, statement, resultSet);}return cityList;
}
3.3 JDBC实现简单的分页查询
3.3.1 分页查询的实现
首先,实现一个拥有分页效果的查询 通过采用了SQL语句中的
limit
条件限制,如下:limit 拥有两个数据,一个是起始的位置,一个是从起始位置开始往后的数据量
/*** 实现城市信息的分页查询* @param pageNum 页码* @param pageSize 每页的数据量*/
public List<City> listCityByPage(int pageNum, int pageSize) {List<City> cityList = new ArrayList<>();Connection connection = null;PreparedStatement statement = null;ResultSet resultSet = null;try {// 获取数据库连接connection = DatabaseUtil.getConnection();// 手动计算分页查询的起始位置 当前页码-1 乘以 每页的记录数int start = (pageNum - 1) * pageSize;// 拼接SQL语句 实现分页查询String sql = "select * from city limit ?, ?";statement = connection.prepareStatement(sql);statement.setInt(1, start); // 添加limit条件的起点位置statement.setInt(2, pageSize); // 添加limit条件的数据量// 获取结果resultSet = statement.executeQuery();// 遍历结果集while (resultSet.next()) {City city = new City();city.setId(resultSet.getInt("id"));city.setName(resultSet.getString("name"));city.setCountryCode(resultSet.getString("countrycode"));city.setDistrict(resultSet.getString("district"));city.setPopulation(resultSet.getInt("population"));cityList.add(city);}} catch (SQLException e) {throw new RuntimeException(e);}finally {DatabaseUtil.closeAll(connection, statement, resultSet);}return cityList;
}
3.3.2 获取每页的数据量
【思考】
只有上述的条件限制查询是不够的,因为分页查询应当返回当前表的 总数据量 用来计算 共多少页 数据 。
例如:共400页 / 第 10 页。因此,还需要两个方法来进一步实现。
- **获取整个标的总数据量 **
select count(*) from city
- 获取根据每页数据量要求能分为几个页面
所以,添加两个方法如下:
/*** 获取总的记录条数*/
public int getTotalCount() {Connection connection = null;PreparedStatement statement = null;ResultSet resultSet = null;int totalCount = 0;try {connection = DatabaseUtil.getConnection();String sql = "select count(*) from city";statement = connection.prepareStatement(sql);resultSet = statement.executeQuery();if (resultSet.next()) {totalCount = resultSet.getInt(1);}}catch (SQLException e) {throw new RuntimeException(e);}finally {DatabaseUtil.closeAll(connection, statement, resultSet);}return totalCount;
}/*** 获取总页数*/
public int getTotalPage(int pageSize) {int totalCount = getTotalCount();// 借助了Math.ceil()方法 达到向上取整的效果 如:3.2 取 4return (int)Math.ceil((double) totalCount / pageSize);
}
3.3.3 分页查询的最终实现
service层方法
/*** 分页查询城市数据*/
public Map<String, Object> listCityByPage(int pageNum, int pageSize) {// 查询当前页数据List<City> cityList = cityDao.listCityByPage(pageNum, pageSize);// 获取总页数和总数据流long totalCount = cityDao.getTotalCount();int totalPage = cityDao.getTotalPage(pageSize);// 构建返回的结果Map<String, Object> result = new HashMap<>();result.put("pageNum", pageNum);result.put("pageSize", pageSize);result.put("totalCount", totalCount);result.put("totalPage", totalPage);result.put("list", cityList);return result;
}
controller层调用
/*** 城市信息分页查询*/
@GetMapping("/getCityListByPage")
public Map<String, Object> getCityListByPage(@RequestParam Integer pageNum, @RequestParam Integer pageSize) {return cityService.listCityByPage(pageNum, pageSize);
}
3.3.4 结果展示
4 思考
关于JDBC这个样例,后续有以下的改进空间:
1、规范化返回接口的信息,可以专门封装一个 PageResult
实体类,用来查询并返回分页结果;
2、返回的分页查询结构考虑进一步实现序列化;
3、如何更灵活的实现一个带条件的分页查询?
5 改进
5.1 规范接口信息的 PageResult 实体类的封装
通过结合泛型,实现分页查询结果的封装,定义为一个新类,主要的优点如下:
- 标准化响应结构:保持所有分页接口返回格式统一
- 前端友好:提供完整的分页导航信息
- 易于扩展:方便后续添加其他分页相关字段
- 减少重复代码:避免在每个接口中重复定义分页字段
具体的实现很简单,见下代码:
public class PageResult<T> {private Integer pageNum;private Integer pageSize;private Long total;private Integer totalPages;private List<T> data; // 采用泛型来接受不定类型的数据// 构造方法以及getter与setter方法省略
}
5.1.1 结合分页结果实体类实现分页查询
5.2 补充JDBC动态条件分页查询
整体的思路如下:
city数据库中的字段有 Name、ContryCode、District以及Population,如何实现多条件查询?
如:即有可能根据Name查询,也有可能根据ContryCode进行查询,还有需要根据人口数量进行排序查询
【重点问题】
传统的JDBC方式的SQL语句 如何实现动态的拼接?
该问题的解决思路如下:
可以借助StringBuilder对SQL语句进行一个拼接,如何将参数判断那个字段为查询条件,进一步借助了map中的 containsKey
方法来对条件进行筛选。
/**
* 如:name=beijing 这个条件传入,将name放到map中,map中如果有这个字段,就进一步将 beijing 值放入paramslist中,等待最终的sql拼接。
**/// StringBuilder 动态构建SQL
StringBuilder sql = new StringBuilder("SELECT * FROM city WHERE 1=1");
List<Object> params = new ArrayList<>(); // List存放具体的参数值// map 动态存放条件查询的字段
Map<String, Object> conditions = new HashMap<>();
5.2.1 具体的代码实现
(1)Dao层添加代码
两个方法
- 查询符合条件的城市信息的方法(主要作用是拼接sql条件,实现查询)
- 统计符合条件的城市信息总条数(主要作用是方便返回符合条件的总记录数以及总页数的计算)
具体的代码见下:
/*** 带动态条件的分页查询*/
public List<City> getCitiesByPageWithConditions(int pageNum, int pageSize, Map<String, Object> conditions) {Connection connection = null;PreparedStatement preparedStatement = null;ResultSet resultSet = null;List<City> cities = new ArrayList<>();try {connection = DatabaseUtil.getConnection();int start = (pageNum - 1) * pageSize;// StringBuilder 动态构建SQLStringBuilder sql = new StringBuilder("SELECT * FROM city WHERE 1=1");List<Object> params = new ArrayList<>();// 判断是否有城市名字段作为条件,有的话补全like查询的sql语句,将其值放入sql占位符if (conditions.containsKey("nameLike")) {sql.append(" AND Name LIKE ?");params.add("%" + conditions.get("nameLike") + "%");}// 添加最小的人口字段作为条件 同上原理if (conditions.containsKey("minPopulation")) {sql.append(" AND Population >= ?");params.add(conditions.get("minPopulation"));}// 最终结果按照人口数量降序sql.append(" ORDER BY Population DESC LIMIT ?, ?");// 添加limit条件params.add(start);params.add(pageSize);preparedStatement = connection.prepareStatement(sql.toString());// 设置参数for (int i = 0; i < params.size(); i++) {preparedStatement.setObject(i + 1, params.get(i));}resultSet = preparedStatement.executeQuery();while (resultSet.next()) {City city = new City();city.setId(resultSet.getInt("ID"));city.setName(resultSet.getString("Name"));city.setCountryCode(resultSet.getString("CountryCode"));city.setDistrict(resultSet.getString("District"));city.setPopulation(resultSet.getInt("Population"));cities.add(city);}} catch (SQLException e) {e.printStackTrace();} finally {DatabaseUtil.closeAll(connection, preparedStatement, resultSet);}return cities;
}/*** 获取带条件的总记录数*/
public long getCountByConditions(Map<String, Object> conditions) {Connection connection = null;PreparedStatement preparedStatement = null;ResultSet resultSet = null;long count = 0;try {connection = DatabaseUtil.getConnection();StringBuilder sql = new StringBuilder("SELECT COUNT(*) FROM city WHERE 1=1");List<Object> params = new ArrayList<>();// 同样的条件逻辑if (conditions.containsKey("nameLike")) {sql.append(" AND Name LIKE ?");params.add("%" + conditions.get("nameLike") + "%");}// 添加最小的人口字段作为条件 同上原理if (conditions.containsKey("minPopulation")) {sql.append(" AND Population >= ?");params.add(conditions.get("minPopulation"));}// ... 其他条件preparedStatement = connection.prepareStatement(sql.toString());for (int i = 0; i < params.size(); i++) {preparedStatement.setObject(i + 1, params.get(i));}resultSet = preparedStatement.executeQuery();if (resultSet.next()) {count = resultSet.getLong(1);}} catch (SQLException e) {e.printStackTrace();} finally {DatabaseUtil.closeAll(connection, preparedStatement, resultSet);}return count;
}
(2)Service层添加代码
主要是调用dao层代码进一步规范化返回结果,条件分页查询
/*** 动态条件分页查询*/
public PageResult<City> getCitiesByPageWithConditions(int pageNum, int pageSize, Map<String, Object> conditions) {// 获取分页结果List<City> cities = cityDao.getCitiesByPageWithConditions(pageNum, pageSize, conditions);// 获取符合条件的总记录条数long totalCount = cityDao.getCountByConditions(conditions);// 获取总页数int totalPages = (int) Math.ceil((double) totalCount / pageSize);return new PageResult<>(pageNum, pageSize, totalCount, totalPages, cities);
}
(3)Controller层代码
通过收集传递的条件信息,拼装成map形式,进一步调用service层方法
/*** 带动态条件的分页查询*/
@GetMapping("/getCitiesListByPageWithConditions")
public PageResult<City> getCitiesListByPageWithConditions(@RequestParam Integer pageNum,@RequestParam Integer pageSize,@RequestParam(required = false) String name,@RequestParam(required = false) Integer minPopulation
) {// 构建查询条件的 mapMap<String, Object> conditions = new HashMap<>();if (name != null) conditions.put("nameLike" , name);if (minPopulation != null) conditions.put("minPopulation", minPopulation);return cityService.getCitiesByPageWithConditions(pageNum, pageSize, conditions);
}
5.2.2 图示演示整体思路
实现的基本步骤通过调试演示如下: