一、动态 SQL
动态 SQL 就是使用一系列标签,根据不同的条件,拼接出不同的 SQL 语句。
1、<if> 标签
(1)问题场景
插入一条用户信息,其中 id(自动自增)、username、password、age 是必填项,而 gender 是可填项 。如果把 SQL 语句写死(写全):
html
<insert id="insert" useGeneratedKeys="true" keyProperty="id">
INSERT INTO user_info (username, password, age, gender) VALUES (#{username}, #{password}, #{age}, #{gender})
</insert>

SQL 语句中有 gender 字段,但未填写值,插入的便是 Integer 类型的默认值 Null:


正常情况,SQL 语句中没有 gender 字段时,gender 有默认值 0:


因此我们希望实现,当某些可选项未填入值时,SQL 插入语句中不要拼接该字段,让该字段按表的设计填写默认值。
(2)使用标签
<if> 标签:对字段进行选择性拼接。
html
<insert id="insertByCondition">
INSERT INTO user_info (username, password, age,
<if test="gender!=null">
gender,
</if>
<if test="deleteFlag!=null">
delete_flag
</if>
) VALUES (#{username}, #{password}, #{age},
<if test="gender!=null">
#{gender},
</if>
<if test="deleteFlag!=null">
#{deleteFlag}
</if>
)
</insert>


(3)注解的方式(不推荐)
注解方式写复杂、动态 SQL 语句很麻烦(没有补全提示),不推荐用。用**<script></script>** 把上面的 xml 语句的内容括起来即可。
java
@Insert("<script>" +
"INSERT INTO user_info (username, password, age," +
"<if test=\"gender!=null\">" +
"gender," +
"</if>" +
"<if test=\"deleteFlag!=null\">" +
"delete_flag" +
"</if>" +
") VALUES (#{username}, #{password}, #{age}," +
"<if test=\"gender!=null\">" +
"#{gender}," +
"</if>" +
"<if test=\"deleteFlag!=null\">" +
"#{deleteFlag}" +
"</if>" +
")" +
"</script>")
Integer insertByCondition(UserInfo userInfo);
2、<trim> 标签
(1)问题场景
当 gender 和 delete_flag 字段都不填写时,age 字段后就会有多余的逗号,导致 SQL 语句错误:


我们需要 trim 标签去掉错误 SQL 语句后多余的逗号。
(2)使用标签

去掉末尾多余的逗号;添加前后包裹字段的括号(这样外面就不用写了,看着更好看)。
html
<insert id="insertByCondition">
INSERT INTO user_info
<trim prefix="(" suffix=")" suffixOverrides=",">
username, password, age,
<if test="gender!=null">
gender,
</if>
<if test="deleteFlag!=null">
delete_flag
</if>
</trim>
VALUES
<trim prefix="(" suffix=")" suffixOverrides=",">
#{username}, #{password}, #{age},
<if test="gender!=null">
#{gender},
</if>
<if test="deleteFlag!=null">
#{deleteFlag}
</if>
</trim>
</insert>

3、<where> 标签
(1)问题场景
查询语句中,有多个 where 条件。若没有填写第一个条件,那么需要去掉开头多余的 and;若没有写所有条件,那么就需要去掉末尾多余的 where:
java
<select id="selectByCondition" resultType="com.edu.mybatisdemo.demos.model.UserInfo">
<trim suffixOverrides="where">
select * from user_info where
<trim prefixOverrides="and">
<if test="age!=null">
age = #{age}
</if>
<if test="gender!=null">
and gender = #{gender}
</if>
<if test="deleteFlag!=null">
and delete_flag = #{deleteFlag}
</if>
</trim>
</trim>
</select>
或者在 where 后加上条件 1=1,每个字段前都有 and。即使没有填写任何条件,SQL 语句也正确:
html
<select id="selectByCondition" resultType="com.edu.mybatisdemo.demos.model.UserInfo">
select * from user_info where 1=1
<if test="age!=null">
and age = #{age}
</if>
<if test="gender!=null">
and gender = #{gender}
</if>
<if test="deleteFlag!=null">
and delete_flag = #{deleteFlag}
</if>
</select>
第一种方法太复杂,第二种方法不规范有点歪门邪道,我们有更好的写法 <where> 标签。
(2)使用标签
<where> 标签会自动去掉多余的 and ;有查询条件时,自动在开头添加 where:
java
<select id="selectByCondition" resultType="com.edu.mybatisdemo.demos.model.UserInfo">
select * from user_info
<where>
<if test="age!=null">
and age = #{age}
</if>
<if test="gender!=null">
and gender = #{gender}
</if>
<if test="deleteFlag!=null">
and delete_flag = #{deleteFlag}
</if>
</where>
</select>
4、<set> 标签
更新语句,需要添加 set,去掉多余的逗号:
java
<update id="updateByCondition">
UPDATE user_info
set
<trim suffixOverrides=",">
<if test="username!=null">
username = #{username},
</if>
<if test="password!=null">
password = #{password},
</if>
<if test="age!=null">
age = #{age},
</if>
<if test="gender!=null">
gender = #{gender}
</if>
</trim>
where id = #{id}
</update>
更简洁的写法:<set> 标签,自动添加 set,去掉多余逗号。
java
<update id="updateByCondition">
UPDATE user_info
<set>
<if test="username!=null">
username = #{username},
</if>
<if test="password!=null">
password = #{password},
</if>
<if test="age!=null">
age = #{age},
</if>
<if test="gender!=null">
gender = #{gender}
</if>
</set>
where id = #{id}
</update>
5、<foreach> 标签
对输入参数中的集合进行遍历时,使用的标签:
- collection:输入的集合类型参数名。
- item:集合参数中的每个对象。
- open:开头字符串。
- close:结尾字符串。
- separator:每次循环间隔的对象。
html
<delete id="deleteByCondition">
DELETE FROM user_info where id in
<foreach collection="ids" item="id" open="(" separator="," close=")">
#{id}
</foreach>
</delete>


6、<sql>、<include> 标签
为了重用 xml 文件中的 sql 代码片段,特别是 select 时查询很多字段,若添加了新的字段,那么所有的 select 语句都需要修改。如果使用 <sql> 标签定义重用片段 ,<include> 标签引用重用片段,就可以只在 <sql> 标签里修改语句了。
html
<sql id="columnList">
id, username, password, age, gender, delete_flag, create_time, update_time
</sql>
<select id="selectByCondition" resultType="com.edu.mybatisdemo.demos.model.UserInfo">
select <include refid="columnList"/> from user_info
<where>
<if test="age!=null">
and age = #{age}
</if>
<if test="gender!=null">
and gender = #{gender}
</if>
<if test="deleteFlag!=null">
and delete_flag = #{deleteFlag}
</if>
</where>
</select>
二、综合练习
1、表白墙
之前是把前端传来的数据保存到后端内存中,一旦后端程序重启,所有数据都没了。现在继续实现数据库操作,实现持久存储。
(1)数据库添加信息表
sql
DROP TABLE IF EXISTS message_info;
CREATE TABLE `message_info` (
`id` INT ( 11 ) NOT NULL AUTO_INCREMENT,
`from` VARCHAR ( 127 ) NOT NULL,
`to` VARCHAR ( 127 ) NOT NULL,
`message` VARCHAR ( 256 ) NOT NULL,
`delete_flag` TINYINT ( 4 ) DEFAULT 0 COMMENT '0-正常, 1-删除',
`create_time` DATETIME DEFAULT now(),
`update_time` DATETIME DEFAULT now() ON UPDATE now(),
PRIMARY KEY ( `id` )
) ENGINE = INNODB DEFAULT CHARSET = utf8mb4;
- ON UPDATE now():每次更新数据都会调用 now()。
(2)配置依赖和配置文件
MyBatis 框架、MySQL 驱动依赖:
html
<dependency>
<groupId>org.mybatis.spring.boot</groupId>
<artifactId>mybatis-spring-boot-starter</artifactId>
<version>3.0.0</version>
</dependency>
<dependency>
<groupId>com.mysql</groupId>
<artifactId>mysql-connector-j</artifactId>
<scope>runtime</scope>
</dependency>
配置 MySQL 数据库连接、MyBatis 日志和蛇形自动转换:
html
# 端口号
server:
port: 8080
# 数据库配置
spring:
datasource:
url: jdbc:mysql://127.0.0.1:3306/mybatis_test?characterEncoding=utf8&useSSL=false&allowPublicKeyRetrieval=true
username: root
password: "123456"
driver-class-name: com.mysql.jdbc.Driver
# MyBatis 配置
mybatis:
configuration:
log-impl: org.apache.ibatis.logging.stdout.StdOutImpl # 日志
map-underscore-to-camel-case: true # 配置驼峰⾃动转换
(3)补充实体类
数据库数据很重要,常用逻辑删除,所有表都应有删除标志、创建时间、更改时间 3 个字段。
java
package com.edu.springbootdemo.entity;
import lombok.Data;
import java.util.Date;
@Data
public class MessageInfo {
private Integer id;
private String from;
private String to;
private String message;
private Integer deleteFlag;
private Date createTime;
private Date updateTime;
}
(3)后端代码
Controller:改为从 Service 获取列表、写入数据。
java
package com.edu.springbootdemo.controller;
import com.edu.springbootdemo.entity.MessageInfo;
import com.edu.springbootdemo.service.MessageService;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.util.StringUtils;
import org.springframework.web.bind.annotation.*;
import java.util.List;
@Slf4j
@RestController
@RequestMapping("/message")
public class MessageController {
// private List<MessageInfo> messageList = new ArrayList<>();
@Autowired
private MessageService messageService;
@GetMapping("/getList")
public List<MessageInfo> getMessages() {
return messageService.getMessageList();
}
@PostMapping("/addMessage")
public Boolean addMessage(@RequestBody MessageInfo messageInfo) {
log.info("接收到参数:" + messageInfo);
// 参数校验
if(!StringUtils.hasLength(messageInfo.getFrom())
|| !StringUtils.hasLength(messageInfo.getTo())
|| !StringUtils.hasLength(messageInfo.getMessage())) {
log.error("参数不规范");
return false;
}
// 保存到数据库
Integer result = messageService.insertMessage(messageInfo);
return result == 1;
}
}
Service:改成从 Mapper select 列表和 insert 数据。
java
package com.edu.springbootdemo.service;
import com.edu.springbootdemo.entity.MessageInfo;
import com.edu.springbootdemo.mapper.MessageMapper;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import java.util.List;
@Service
public class MessageService {
@Autowired
private MessageMapper messageMapper;
public List<MessageInfo> getMessageList() {
return messageMapper.selectMessageList();
}
public Integer insertMessage(MessageInfo messageInfo) {
return messageMapper.insertMessage(messageInfo);
}
}
Mapper:编写 SQL 语句,访问数据库。
java
package com.edu.springbootdemo.mapper;
import com.edu.springbootdemo.entity.MessageInfo;
import org.apache.ibatis.annotations.Insert;
import org.apache.ibatis.annotations.Mapper;
import org.apache.ibatis.annotations.Select;
import java.util.List;
@Mapper
public interface MessageMapper {
@Select("SELECT `from`, `to`, message FROM message_info where delete_flag = 0")
List<MessageInfo> selectMessageList();
@Insert("INSERT INTO message_info (`from`, `to`, message) VALUES (#{from}, #{to}, #{message})")
Integer insertMessage(MessageInfo messageInfo);
}
(4)测试
从前端添加数据,数据库表添加成功:

重启后端,前端获取数据库数据成功:


2、图书管理系统
2.1、创建数据库表并插入数据
sql
-- 创建数据库
DROP DATABASE IF EXISTS book_test;
CREATE DATABASE book_test DEFAULT CHARACTER SET utf8mb4;
-- 用户表
DROP TABLE IF EXISTS user_info;
CREATE TABLE user_info (
`id` INT NOT NULL AUTO_INCREMENT,
`user_name` VARCHAR ( 128 ) NOT NULL,
`password` VARCHAR ( 128 ) NOT NULL,
`delete_flag` TINYINT ( 4 ) NULL DEFAULT 0,
`create_time` DATETIME DEFAULT now(),
`update_time` DATETIME DEFAULT now() ON UPDATE now(),
PRIMARY KEY ( `id` ),
UNIQUE INDEX `user_name_UNIQUE` ( `user_name` ASC )) ENGINE = INNODB DEFAULT CHARACTER
SET = utf8mb4 COMMENT = '用户表';
-- 图书表
DROP TABLE IF EXISTS book_info;
CREATE TABLE `book_info` (
`id` INT ( 11 ) NOT NULL AUTO_INCREMENT,
`book_name` VARCHAR ( 127 ) NOT NULL,
`author` VARCHAR ( 127 ) NOT NULL,
`count` INT ( 11 ) NOT NULL,
`price` DECIMAL (7,2 ) NOT NULL,
`publish` VARCHAR ( 256 ) NOT NULL,
`status` TINYINT ( 4 ) DEFAULT 1 COMMENT '0-无效, 1-正常, 2-不允许借阅',
`create_time` DATETIME DEFAULT now(),
`update_time` DATETIME DEFAULT now() ON UPDATE now(),
PRIMARY KEY ( `id` )
) ENGINE = INNODB DEFAULT CHARSET = utf8mb4;
-- 初始化数据
INSERT INTO user_info ( user_name, PASSWORD ) VALUES ( "admin", "admin" );
INSERT INTO user_info ( user_name, PASSWORD ) VALUES ( "zhangsan", "123456" );
-- 初始化图书数据
INSERT INTO `book_info` (book_name,author,count, price, publish) VALUES ('活着', '余华', 29, 22.00, '北京文艺出版社');
INSERT INTO `book_info` (book_name,author,count, price, publish) VALUES ('平凡的世界', '路遥', 5, 98.56, '北京十月文艺出版社');
INSERT INTO `book_info` (book_name,author,count, price, publish) VALUES ('三体', '刘慈欣', 9, 102.67, '重庆出版社');
INSERT INTO `book_info` (book_name,author,count, price, publish) VALUES ('金字塔原理', '麦肯锡', 16, 178.00, '民主与建设出版社');
2.4、创建实体类
java
package com.edu.springbookdemo.entity;
import java.util.Date;
public class UserInfo {
private Integer id;
private String userName;
private String password;
private Integer deleteFlag;
private Date createTime;
private Date updateTime;
}
java
package com.edu.springbookdemo.entity;
import lombok.Data;
import java.math.BigDecimal;
import java.util.Date;
@Data
public class BookInfo {
private Integer id;
private String bookName;
private String author;
private Integer count;
// 不要用 float、double 类型,因为精度问题
// 推荐使用 BigDecimal 类型;或者 long 类型,将单位换算成分,例如 1.1 元 = 110 分
private BigDecimal price;
private String publish;
private Integer status; // 0-不可借阅,1-可借阅
private String statusCN;
private Date createTime;
private Date updateTime;
}
2.5、功能实现
(1)用户登录
改成从数据库找到匹配用户信息,并校验密码。
表现层:
java
package com.edu.springbookdemo.controller;
import com.edu.springbookdemo.constant.Constants;
import com.edu.springbookdemo.service.UserService;
import jakarta.servlet.http.HttpSession;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.web.servlet.server.Session;
import org.springframework.util.StringUtils;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
@RestController
@RequestMapping("/user")
public class UserController {
@Autowired
private UserService userService;
@RequestMapping("/login")
public Boolean login(String username, String password, HttpSession session) {
if (!StringUtils.hasLength(username) || !StringUtils.hasLength(password)) {
return false;
}
// if ("admin".equals(username) && "admin".equals(password)) {
// return true;
// }
Boolean result = userService.checkPassword(username, password);
if (result) {
// 登陆成功,创建会话,让用户信息跟会话中的所有操作绑定
// 自定义常量类,利于重复利用
session.setAttribute(Constants.SESSION_USER_NAME, username);
return true;
}
return false;
}
}
业务逻辑层:
java
package com.edu.springbookdemo.service;
import com.edu.springbookdemo.mapper.UserMapper;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
@Slf4j
@Service
public class UserService {
@Autowired
private UserMapper userMapper;
public Boolean checkPassword(String username, String password) {
String passwordInDB = userMapper.selectPasswordByUsername(username);
if (passwordInDB == null) {
log.error("用户不存在: " + username);
return false;
}
return password.equals(passwordInDB);
}
}
持久层:
java
package com.edu.springbookdemo.mapper;
import org.apache.ibatis.annotations.Mapper;
import org.apache.ibatis.annotations.Select;
@Mapper
public interface UserMapper {
@Select("SELECT password FROM user_info WHERE user_name = #{username} and delete_flag = 0")
String selectPasswordByUsername(String username);
}
(2)图书显示(分页功能)
添加翻页功能。
插入更多数据:
java
INSERT INTO `book_info` ( book_name, author, count, price, publish )
VALUES
( '图书2', '作者2', 29, 22.00, '出版社2' ),( '图书3', '作者2', 29, 22.00, '出版社3' ),
( '图书4', '作者2', 29, 22.00, '出版社1' ),( '图书5', '作者2', 29, 22.00, '出版社1' ),
( '图书6', '作者2', 29, 22.00, '出版社1' ),( '图书7', '作者2', 29, 22.00, '出版社1' ),
( '图书8', '作者2', 29, 22.00, '出版社1'),('图书9', '作者2', 29, 22.00, '出版社1' ),
('图书10', '作者2', 29, 22.00, '出版社1'),('图书11', '作者2', 29, 22.00, '出版社1'),
('图书12', '作者2', 29, 22.00, '出版社1'),('图书13', '作者2', 29, 22.00, '出版社1'),
('图书14', '作者2', 29, 22.00, '出版社1'),('图书15', '作者2', 29, 22.00, '出版社1'),
('图书16', '作者2', 29, 22.00, '出版社1'),('图书17', '作者2', 29, 22.00, '出版社1'),
('图书18', '作者2', 29, 22.00, '出版社1'),('图书19', '作者2', 29, 22.00, '出版社1'),
('图书20', '作者2', 29, 22.00, '出版社1'),('图书21', '作者2', 29, 22.00, '出版社1');
- 前端提供页码 currentPage(默认1),后端计算 offset 和 limit(默认10)
- 计算公式:offset = (currentPage - 1) * limit
- SQL 语句:select * from book_info limit offset, limit
java
请求:
/book/getListByPage?page=1
参数:
无
响应:
data:
Content-Type: application/json
{
total: ,
records: [{
id: ,
bookNmae: ,
author: ,
count: ,
price: ,
publish: ,
statusCN:
},
......
]
}
- 请求类:page(从前端传来)、limit(默认 10) 用于计算 offset。
- 响应类:total(总记录数,提供给前端计算一共有多少页)、records(该页的所有记录,类型是 List<BookInfo>,为了通用性改为泛型 List<T>,翻页业务不限于图书系统)、 page(前端传来 page,后端又返回同样的 page,并不奇怪。前端用 location.search 获取 url 中的查询参数,得到的是 "?page=页数",想获取页数还得对该字符串进行分割,比较麻烦。后端做好人,直接给前端返回 page)。
java
package com.edu.springbookdemo.entity;
import lombok.Data;
@Data
public class PageRequest {
private Integer page = 1;
private Integer limit = 10;
// 必须有,Mapper 中 SQL 绑定参数时需要
// 参数绑定时,先访问对应的 getter 方法,如果没有 getter 方法,则访问对应的 field
private String offset;
public Integer getOffset() {
return (page - 1) * limit;
}
}
java
package com.edu.springbookdemo.entity;
import lombok.Data;
import java.util.List;
@Data
public class PageResult<T> {
private Integer total;
private List<T> records;
private Integer page;
}
- 为了避免状态码转换为字符串的 if-else 逻辑在扩展新的状态码时,需要在每一处用到的地方修改逻辑,我们将转换逻辑以及码与字符串的对应关系封装成枚举类。这样只需要修改封装的类即可。这个逻辑可以用 if-else 实现,也可以用 map 实现(hashmap,当状态码较少时,直接映射,更高效)。
java
package com.edu.springbookdemo.enums;
import lombok.Getter;
import java.util.HashMap;
import java.util.Map;
@Getter
public enum BookStatus {
DELETED(0, "无效"),
NORMAL(1, "可借阅"),
FORBBIDEN(2, "禁止借阅");
private int code;
private String desc;
private static final Map<Integer, String> map = new HashMap<>();
// 不写在构造函数、或实例代码块中,避免每构建一个枚举常量对象时都执行一次
// 静态代码块只在类被加载时执行一次
static {
for (BookStatus status : BookStatus.values()) {
map.put(status.code, status.desc);
}
}
BookStatus(int code, String desc) {
this.code = code;
this.desc = desc;
}
public static String getStatusCNByCode(int code) {
return map.get(code);
}
}
- 对图书的每种操作页面,只有登陆了才能访问,没登陆就跳转到登录页面:如果 session 保存了用户信息,则登陆了;否则未登录。
- 对于每种操作,都需要处理登录、未登录的情况。如果分操作去处理:图书显示操作(1. 登录了:返回的 pageRequest 中 count 有值。2. 未登录:返回的结果为 null)、根据 id 查询图书信息(1. 登陆了且查询到了:返回属性值不为空的 BookInfo。2. 登陆了没查询到:返回一个 不为 null 但属性值为空的对象。3. 没登陆:返回 null)等操作。每个操作都需要定制化处理,这不仅让后端设计起来很麻烦,也让调用接口的前端使用起来很麻烦。
- 因此,将返回结果统一封装(code 状态码, msg 信息, data 真实数据),把成功登陆后的计算结果放到 data 中,按 code 区分成功、用户未登录、后端出错等状态。
- 因为 code 是可枚举的,因此可以封装成枚举类。
- 所有操作都需要上述的统一处理,非常相似且繁琐。为了简化编程、专注于业务逻辑,后续会学习 MyBatis Generator 插件自动生成数据库表对应的实体类 (其他功能不好用)、Mybatis-Plus 框架封装了 CRUD 操作 ,无需再手动编写 SQL 语句、Spring Boot 统一功能处理。
java
package com.edu.springbookdemo.enums;
import lombok.Getter;
@Getter
public enum ResultCode {
SUCCESS(200, "操作成功"),
UNLOGIN(0, "用户未登录"),
FAILED(-1, "操作失败");
private int code;
private String msg;
ResultCode(int code, String message) {
this.code = code;
this.msg = message;
}
}
java
package com.edu.springbookdemo.entity;
import com.edu.springbookdemo.enums.ResultCode;
import lombok.Data;
@Data
public class Result<T> {
Integer code;
String msg;
// 不使用 Object 类型,节省强制转换动作
T data;
public static <T> Result<T> unLogin() {
Result<T> result = new Result<>();
result.setCode(ResultCode.UNLOGIN.getCode());
result.setMsg(ResultCode.UNLOGIN.getMsg());
return result;
}
public static <T> Result<T> success(T data) {
Result<T> result = new Result<>();
result.setCode(ResultCode.SUCCESS.getCode());
result.setMsg(ResultCode.SUCCESS.getMsg());
result.setData(data);
return result;
}
public static <T> Result<T> fail(String msg) {
Result<T> result = new Result<>();
result.setCode(ResultCode.FAILED.getCode());
result.setMsg(msg);
return result;
}
public static <T> Result<T> fail(Integer code, String msg) {
Result<T> result = new Result<>();
result.setCode(code);
result.setMsg(msg);
return result;
}
}
表现层:
java
@GetMapping("/getListByPage")
public Result<PageResult<BookInfo>> getBookListByPage(PageRequest pageRequest, HttpSession session) {
// 判断是否登录
if (session.getAttribute(Constants.SESSION_USER_NAME) == null) {
return Result.unLogin();
}
log.info("获取图书列表,请求参数:{}", pageRequest);
PageResult<BookInfo> result = bookService.getBookListByPage(pageRequest);
return Result.success(result);
}
业务处理层:
java
public PageResult<BookInfo> getBookListByPage(PageRequest pageRequest) {
Integer total = bookMapper.countAll();
List<BookInfo> records = bookMapper.selectByPage(pageRequest);
// 转换状态码为字符串信息
for (BookInfo book : records) {
book.setStatusCN(BookStatus.getStatusCNByCode(book.getStatus()));
}
PageResult<BookInfo> pageResult = new PageResult<>();
pageResult.setTotal(total);
pageResult.setRecords(records);
pageResult.setPage(pageRequest.getPage());
return pageResult;
}
持久层:
java
@Select("SELECT count(1) FROM book_info WHERE status = 1")
Integer countAll();
@Select("SELECT * FROM book_info WHERE status = 1 LIMIT #{offset},#{limit}")
List<BookInfo> selectByPage(PageRequest pageRequest);
前端JS:
参考资料:jqPaginator分页组件
java
getBookList();
function getBookList() {
$.ajax({
url: "/book/getListByPage" + location.search,
type: "get",
success: function (result) {
// 未登录跳转到登录页面
if(result.code === 0) {
location.href = "/login.html";
}
// TODO 处理异常
let bookList = result.data.records;
let html = "";
for (let book of bookList) {
html += "<tr><td><input type=\"checkbox\" name=\"selectBook\" value=" + book.id + " id=\"selectBook\" class=\"book-select\"></td>";
html += "<td>" + book.id + "</td>";
html += "<td>" + book.bookName + "</td>";
html += "<td>" + book.author + "</td>";
html += "<td>" + book.count + "</td>";
html += "<td>" + book.price + "</td>";
html += "<td>" + book.publish + "</td>";
html += "<td>" + book.statusCN + "</td>";
html += "<td><div class=\"op\"><a href=\"book_update.html?bookId=1\">修改</a><a href=\"javascript:void(0)\" onclick=\"deleteBook(1)\">删除</a>";
html += "</div></td></tr>";
}
$("tbody").append(html);
//翻页信息
$("#pageContainer").jqPaginator({
totalCounts: result.data.total, //总记录数
pageSize: 10, //每页的个数
visiblePages: 5, //可视页数
currentPage: result.data.page, //当前页码
first: '<li class="page-item"><a class="page-link">首页</a></li>',
prev: '<li class="page-item"><a class="page-link" href="javascript:void(0);">上一页<\/a><\/li>',
next: '<li class="page-item"><a class="page-link" href="javascript:void(0);">下一页<\/a><\/li>',
last: '<li class="page-item"><a class="page-link" href="javascript:void(0);">最后一页<\/a><\/li>',
page: '<li class="page-item"><a class="page-link" href="javascript:void(0);">{{page}}<\/a><\/li>',
//页面初始化和页码点击时都会执行,为了避免初始化时又跳转到第一页又初始化,导致死循环
// 加判断,点击页码时才跳转
onPageChange: function (page, type) {
console.log("第"+page+"页, 类型:"+type);
if (type === "change") {
location.href = "/book_list.html?page=" + page;
}
}
});
}
});
}
(3)添加图书

接口设计:
java
请求:
/book/addBook
参数:
bookName=图书1&author=作者1&count=23&price=36&publish=出版社1&status=1
响应:(失败信息, 成功时返回空字符串)
data:
""
表现层:
java
@PostMapping("/addBook")
public Result<String> addBook(BookInfo bookInfo, HttpSession session) {
log.info("添加图书,请求参数:{}", bookInfo);
// 判断是否登录
if (session.getAttribute(Constants.SESSION_USER_NAME) == null) {
return Result.unLogin();
}
// 校验参数
if (!StringUtils.hasLength(bookInfo.getBookName())
|| !StringUtils.hasLength(bookInfo.getAuthor())
|| bookInfo.getCount() == null
|| bookInfo.getPrice() == null
|| !StringUtils.hasLength(bookInfo.getPublish())) {
return Result.fail("参数不合法");
}
// 添加图书
try {
Integer result = bookService.addBook(bookInfo);
return result == 1 ? Result.success("") : Result.fail("添加失败");
} catch (Exception e) {
log.error("添加失败", e);
return Result.fail("添加失败");
}
}
业务层:
java
public Integer addBook(BookInfo bookInfo) {
return bookMapper.insertBook(bookInfo);
}
持久层:
java
@Insert("insert into book_info (book_name, author, count, price, publish, `status`) " +
"value (#{bookName}, #{author}, #{count}, #{price}, #{publish}, #{status})")
Integer insertBook(BookInfo bookInfo);
前端JS:
java
function add() {
$.ajax({
url: "/book/addBook",
type: "post",
data: $("#addBook").serialize(),
success: function (result) {
if (result.code === 0) {
alert(result.msg);
location.href = "/login.html";
}
if (result.code === -1) {
alert(result.msg);
}
if (result.code === 200 || result.data === "") {
alert("添加成功");
location.href = "/book_list.html";
}
}
});
}
接口测试:




(4)修改图书


接口设计:
java
按图书 id 显示信息
请求:
/book/queryBookById?bookId=25
参数:
无
响应:
{
"id": 25,
"bookName": "图书21",
"author": "作者2",
"count": 999,
"price": 222.00,
"publish": "出版社1",
"status": 2,
"statusCN": null, // 前端下拉菜单设置对应中文
"createTime": "2023-09-04T04:01:27.000+00:00",
"updateTime": "2023-09-05T03:37:03.000+00:00"
}
提交修改
请求:
/book/updateBook
参数:
{
"id": 1,
"bookName": "图书1",
"author": "作者2",
"count": 999,
"price": 222.00,
"publish": "出版社1",
"status": 1,
}
响应:(失败信息, 成功时返回空字符串 )
""
表现层:
java
@GetMapping("/queryBookById")
public Result<BookInfo> queryBookById(Integer bookId, HttpSession session) {
log.info("查询图书,请求参数:{}", bookId);
// 判断是否登录
if (session.getAttribute(Constants.SESSION_USER_NAME) == null) {
return Result.unLogin();
}
// 校验参数
if (bookId == null || bookId <= 0) {
return Result.fail("参数不合法");
}
// 查询图书
try {
BookInfo bookInfo = bookService.queryBookById(bookId);
return Result.success(bookInfo);
} catch (Exception e) {
log.error("查询失败", e);
return Result.fail("查询失败");
}
}
@PostMapping("/updateBook")
public Result<String> updateBook(BookInfo bookInfo, HttpSession session) {
log.info("更新图书,请求参数:{}", bookInfo);
// 判断是否登录
if (session.getAttribute(Constants.SESSION_USER_NAME) == null) {
return Result.unLogin();
}
// 校验参数,除了 bookId,其它参数可以为空,表示没有修改
if (bookInfo.getId() == null || bookInfo.getId() <= 0) {
return Result.fail("参数不合法");
}
// 更新图书
try {
Integer result = bookService.updateBook(bookInfo);
return result == 1 ? Result.success("") : Result.fail("更新失败");
} catch (Exception e) {
log.error("更新失败", e);
return Result.fail("更新失败");
}
}
业务层:
java
public BookInfo queryBookById(Integer bookId) {
return bookMapper.selectById(bookId);
}
public Integer updateBook(BookInfo bookInfo) {
return bookMapper.updateBook(bookInfo);
}
持久层:
java
@Select("SELECT * FROM book_info WHERE id = #{bookId}")
BookInfo selectById(Integer bookId);
Integer updateBook(BookInfo bookInfo);
xml:
XML
<update id="updateBook">
UPDATE book_info
<set>
<if test="bookName!= null">
book_name = #{bookName},
</if>
<if test="author!= null">
author = #{author},
</if>
<if test="count!= null">
`count` = #{count},
</if>
<if test="price!= null">
price = #{price},
</if>
<if test="publish!= null">
publish = #{publish},
</if>
<if test="status != null">
status = #{status}
</if>
</set>
WHERE id = #{id}
</update>
前端JS:
html
getBookInfo();
function getBookInfo() {
$.ajax({
type: "get",
// 修改页面 url 提供了 bookId 参数,因此需要在 url 中获取 bookId 值
url: "/book/queryBookById" + location.search,
success: function (result) {
if (result.code === 200 || result.data === "") {
let bookInfo = result.data;
$("#bookId").val(bookInfo.id);
$("#bookName").val(bookInfo.bookName);
$("#bookAuthor").val(bookInfo.author);
$("#bookStock").val(bookInfo.count);
$("#bookPrice").val(bookInfo.price);
$("#bookPublisher").val(bookInfo.publish);
$("#bookStatus").val(bookInfo.status);
} else if (result.code === 0) {
alert(result.msg);
location.href = "login.html";
} else {
alert(result.msg);
}
}
});
}
function update() {
$.ajax({
url: "/book/updateBook",
type: "post",
data: $("#updateBook").serialize(),
success: function (result) {
if (result.code === 200 || result.data === "") {
alert("修改成功");
location.href = "book_list.html";
} else if (result.code === 0) {
alert(result.msg);
location.href = "login.html";
} else {
alert(result.msg);
}
}
});
}
接口测试:



(5)删除图书
通常是逻辑删除,调用 update 的接口。
接口定义:
html
请求:
/book/deleteBook?bookId=1(调用 /book/updateBook)
参数:
无
响应:(失败信息,成功时返回空字符串)
data:
""
表现层:
java
@GetMapping("/deleteBook")
public Result<String> deleteBook(Integer bookId, HttpSession session) {
log.info("删除图书,请求参数:{}", bookId);
// 判断是否登录
if (session.getAttribute(Constants.SESSION_USER_NAME) == null) {
return Result.unLogin();
}
// 校验参数
if (bookId == null || bookId <= 0) {
return Result.fail("参数不合法");
}
// 删除图书
try {
Integer result = bookService.deteteBook(bookId);
return result == 1 ? Result.success("") : Result.fail("删除失败");
} catch (Exception e) {
log.error("删除失败", e);
return Result.fail("删除失败");
}
}
业务层:
java
public Integer deteteBook(Integer bookId) {
BookInfo bookInfo = new BookInfo();
bookInfo.setId(bookId);
bookInfo.setStatus(BookStatus.DELETED.getCode());
return bookMapper.updateBook(bookInfo);
}
前端JS:
java
function deleteBook(id) {
var isDelete = confirm("确认删除?");
if (isDelete) {
let page = location.search
$.ajax({
url: "/book/deleteBook?bookId=" + id,
type: "get",
success: function (result) {
// 未登录跳转到登录页面
if(result.code === 0) {
alert(result.msg);
location.href = "/login.html";
} else if (result.code === 200 || result.data === "") {
alert("删除成功");
location.href = "book_list.html" + page;
} else {
alert("删除失败");
}
}
});
}
}
(6)批量删除
批量修改数据。
接口定义:
java
请求:
/book/batchDeleteBook?ids=ids
参数:
无
响应:(失败信息,成功时返回空字符串)
data:
""
表现层:
java
// 集合类参数接收,需要用@RequestParam注解绑定
@GetMapping("deleteBatchBook")
public Result<String> deleteBatchBook(@RequestParam List<Integer> ids, HttpSession session) {
log.info("批量删除图书,请求参数:{}", ids);
// 判断是否登录
if (session.getAttribute(Constants.SESSION_USER_NAME) == null) {
return Result.unLogin();
}
// 校验参数
if (ids == null || ids.isEmpty()) {
return Result.fail("参数不合法");
}
// 批量删除图书
try {
Integer result = bookService.deleteBatchBook(ids);
return result == ids.size() ? Result.success("") : Result.fail("删除失败");
} catch (Exception e) {
log.error("批量删除失败", e);
return Result.fail("批量删除失败");
}
}
业务层:
java
public Integer deleteBatchBook(List<Integer> ids) {
return bookMapper.updateBatchBook(ids);
}
持久层:
java
Integer updateBatchBook(@Param("ids") List<Integer> ids);
xml:
java
<update id="updateBatchBook">
UPDATE book_info SET status = 0 WHERE id IN
<foreach collection="ids" item="id" open="(" close=")" separator=",">
#{id}
</foreach>
</update>
前端JS:
java
function batchDelete() {
var isDelete = confirm("确认批量删除?");
if (isDelete) {
//获取复选框的id
var ids = [];
$("input:checkbox[name='selectBook']:checked").each(function () {
ids.push($(this).val());
});
$.ajax({
url: "/book/deleteBatchBook?ids=" + ids,
type: "get",
success: function (result) {
// 未登录跳转到登录页面
if(result.code === 0) {
alert(result.msg);
location.href = "/login.html";
} else if (result.code === 200 || result.data === "") {
alert("批量删除成功");
location.href = "book_list.html";
} else {
alert("批量删除失败");
}
}
});
}
}
接口测试:


