【JavaEE】(18) MyBatis 进阶

一、动态 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("批量删除失败");
                            }
                        }
                    });
                }
            }

接口测试:

相关推荐
渣哥2 分钟前
很多人分不清!Java 运行时异常和编译时异常的真正区别
java
weixin_lynhgworld21 分钟前
打造绿色生活新方式——旧物二手回收小程序系统开发之路
java·小程序·生活
振鹏Dong33 分钟前
Spring事务管理机制深度解析:从JDBC基础到Spring高级实现
java·后端·spring
信码由缰39 分钟前
Java包装类:你需要掌握的核心要点
java
小凯 ོ1 小时前
实战原型模式案例
java·后端·设计模式·原型模式
Dcs1 小时前
性能提升超100%!PostgreSQL主键选择大变革:UUIDv7能否终结v4的统治?
java
渣哥2 小时前
没有 Optional,Java 程序员每天都在和 NullPointerException 打架
java
智_永无止境2 小时前
Java集合操作:Apache Commons Collections4启示录
java·apache
悟能不能悟2 小时前
java去图片水印的方法
java·人工智能·python