利用 MyBatis 操作数据库完善案例

目录

留言墙

数据准备

[引入 MyBatis 和 MySQL 驱动依赖](#引入 MyBatis 和 MySQL 驱动依赖)

[配置 MySQL](#配置 MySQL)

后端代码

测试代码

图书管理系统

数据库设计

[引入 MyBatis 和 MySQL 驱动依赖](#引入 MyBatis 和 MySQL 驱动依赖)

配置数据库和日志

[model 创建](#model 创建)

用户登录

约定前后端交互接口:

实现服务器代码

测试

添加图书

约定前后端交互接口

实现服务器代码

调整前端代码

测试

图书列表

需求分析

翻页请求对象

翻页列表结果类

约定前后端交互接口

实现服务层代码

测试

实现客户端代码

分页信息

修改后端代码

测试

修改图书

约定前后端交互接口

实现服务器代码

测试后端代码

实现客户端代码

测试

删除图书

约定前后端交互接口

实现服务端代码

调整前端代码

测试

批量删除

约定前后端交互接口

实现服务段代码

测试

客户端代码

测试

强制登录

实现思路分析

实现服务器代码

修改客户端代码

测试

完!


留言墙

前面留言墙的实现中,我们把数据存储在内存中,当重启服务端的时候,数据就会消失。现在可以借助 MyBatis 将数据持久化存储。

数据准备

创建数据表:

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;

补充: SQL 语句中 ON UPDATE now() 表示:当数据发生更新操作的时候,自动把该列的值设置为 now()

引入 MyBatis 和 MySQL 驱动依赖

修改 pom 文件

或者使用插件Edit Starters 来引入依赖

配置 MySQL

后端代码

Model:对应数据库字段

根据个人习惯,从 controller -> service -> mapper 层

或者直接 mapper -> service -> controller

此处笔者习惯为第一种,感觉思路会更加通畅~~

接到请求后,controller 层的代码,将对应路径信息用 @RequestMapping 注释配置完毕,@Autowired 引入 service 层对象,创建方法 getList 和 publish,分别调用 service 层中的方法。

但我们此时并未创建 service 层的代码,如果第一次编写,idea 会自动爆红提示,自动生成 service 层的代码和对应的方法,在 service 层继续导入 mapper 层对象,使用 mapper 层的方法

mapper:

测试代码

运行程序,打开 http://127.0.0.1:8080/messagewall.html 符合预期~

图书管理系统

在前面的实现中,我们只完成了用户登录和图书列表,且数据都是 mock 的。接下来,我们对这个案例进行完善~

数据库设计

数据库设计是依据业务需求来设计的,如何设计出优秀的数据库表,与经验有很大关系。

数据库表通常分为两种:实体表和关系表。

创建数据库 book_test,库中有两张表,用户表和图书表

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.50, '北京十月文艺出版社');
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.80, '民主与建设出版社');

引入 MyBatis 和 MySQL 驱动依赖

配置数据库和日志

model 创建

根据数据库字段,在 Java 中创建对应的 model

用户登录

约定前后端交互接口:

浏览器给服务发送 /user/login 这样的 HTTP 请求。服务器给浏览器返回一个 Boolean 类型的数据。

实现服务器代码

**控制层:**从数据库中,根据名称查询用户,如果成功查到,且密码一致,则认为登录成功。

**业务层:**创建 UserService

**数据层:**创建 UserInfoMapper

测试

部署程序:

测试后端,使用 URL:http://127.0.0.1:8080/user/login?name=admin&password=admin

前端一起测试:

添加图书

约定前后端交互接口

实现服务器代码

控制层:

在 BookController 补充代码:

BookService 中补充代码:

数据层:

创建 BookInfoMapper

调整前端代码

book_add.html 中,js 已经提前留出了提交的函数:

我们补全 add() 方法即可~

补充:提交整个表单的数据:$("#addBook").serialize()

提交的内容格式:bookName=图书1&author=作者1&count=23&price=34&publish=出版社1&status=1

被 from 标签的所有输入表单(input,select)内容都会被提交~

测试

符合预期

图书列表

我们在添加图书成功后,正常应该是跳转到图书列表页面,但并没有显示我们刚才添加的图书信息,接下来我们实现图书列表。

需求分析

我们之前的留言墙案例,是将数据库中所有的数据查询出来并展示到页面上。但,如果数据库中的数据有很多很多的时候,将数据一下子全部展示出来肯定不可能,该如何解决这个问题?

==》

可以使用分页来解决这个问题。每次只展示一页的数据,比如:一页展示 10 条数据,如果要看其他数据,则通过点击页码来进行查询~

分页时候,数据如下格式展示:

第一页:显示 1 - 10 条的数据

第二页:显示 11 - 20 条的数据

第三页:显示 21 - 30 条的数据

以此类推~~

要想实现这个功能,可以在数据库中进行分页查询,使用 LIMIT 关键字。

我们先伪造一下数据,方便我们观察:

sql 复制代码
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');

基于前端的页面,继续分析得出如下结论:

  1. 前端在发起查询请求时候,需要向服务端传递的参数:

currentPage 当前页面 // 默认值为 1

pageSize 每页显示的条数 // 默认值为 10

  1. 后端响应时,需要相应给前端的数据:

records 所查询到的数据列表(存储在 List 集合中)

total 总记录数(用于告诉前端显示多少页),显示页数为:(total + pageSize - 1)/ pageSize

翻页请求和响应部分,我们通常封装在两个对象中

翻页请求对象

翻页列表结果类

返回结果中,可以使用泛型来定义记录的类型

约定前后端交互接口

交互接口中约定,浏览器给服务器发送一个 /book/getListByPage HTTP 请求,通过 currentPage 参数告诉服务器,当前请求为第几页的数据,后端根据请求参数,返回对应页的数据(还需要返回总共数据的个数 total,便于告诉前端显示多少页)

实现服务层代码

控制层:

完善 BookController 类代码:

业务层:

BookService:

  1. 翻页信息需要返回数据的总数和列表信息,需要查询两次数据库

  2. 图书状态:图书状态和数据库中存储的 status 有一定的关系。对应的关系就在 service 层进行设置~

但是,如果后续状态码发生变化,则需要修改项目中所有涉及的代码。这种情况,通常就要采用枚举类来处理映射关系。

java 复制代码
package com.zzz.bookdemo.enums;

public enum BookStatus {
    DELETE(0,"无效"),
    NORMAL(1,"可借阅"),
    FORBIDDEN(2,"不可借阅");

    private Integer code;
    private String name;

    BookStatus (int code, String name) {
        this.code = code;
        this.name = name;
    }
    public static BookStatus getNameByCode(Integer code) {
        switch (code) {
            case 0 : return DELETE;
            case 1 : return NORMAL;
            case 2 : return FORBIDDEN;
        }
        return null;
    }

    public Integer getCode() {
        return code;
    }

    public void setCode(Integer code) {
        this.code = code;
    }

    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }
}

通过静态方法 getNameByCode 方法,通过 code 来获取对应的枚举,以获取该枚举对应的中文名称。如果后续有状态变更,只需要修改该枚举类即可~

此时 BookService 中的代码,可以继续修改 :

当调用 BookStatus.getNameByCode(book.getStatus()) 的时候,会返回对应的枚举实例,再通过 .getNam() 方法,获取到中文描述,最终设置到 book 对象的 statusCN 属性中。

数据层:

翻页查询 SQL 语句为:

sql 复制代码
select * from book_info where status != 0 order by id desc limit #{offset}, #{pageSize}

其中 offset 在 PageRequest 类中已经赋值

BookInfoMapper:

图书列表按照 id 降序排列

测试

启动程序,访问后端程序:

http://127.0.0.1:8080/book/getListByPage 返回 1 - 10 条记录(按 id 降序)

http://127.0.0.1:8080/book/getListByPage?currentPage=2 返回 11 - 20 条记录

实现客户端代码

定义:

访问第一页图书的前端的 url 为:http://127.0.0.1:8080/book_list.html?currentPage=1

访问第二页列表的 url 为:http://127.0.0.1:8080/book_list.html?currentPage=2

浏览器访问 book_list.html,就去请求后端,将后端返回的数据显示在页面上,调用后端请求:

/book/getListByPage?currentPage=1

在前端 book_list.html 的代码中,将 /book/getList 改为 /book/getListByPage

此时,url 中,还未设置 currentPage 参数

我们可以直接使用 location.search 从 url 中获取参数信息即可~

location.search:获取 url 的查询字符串(包含问号)

如:http://127.0.0.1:8080/book_list.html?currentPage=1

location.search:?currentPage=1

则,还需要将上述的 url 改为:"/book/getListByPage" + location.search

分页信息

分页插件:该案例中,分页代码中使用了一个分页组件

文档介绍:jqPaginator分页组件

使用的时候,我们按照文档说明,将代码复制粘贴进来即可。提供的前端代码已经包括了,这里简单介绍使用:

其中,onPageChange 回调函数,表示当换页时触发(包括初始化第一页的时候),会传入两个参数。

  1. 目标页的页面:Number 类型

  2. 触发类型:可能的值:init 初始化,change 点击分页

图书列表信息加载之后,还需分页信息,同步加载。

分页组件还需要一些信息:totalCounts:总记录数,pageSize:每页的个数,visiblePages:可视页数,currentPage:当前页面。

这些信息,pageSize 和 visiblePages 前端直接设置即可。totalCounts 后端已经提供,currentPage 也可以从参数中得到,但太复杂了,可以直接由后端返回。

修改后端代码

为了避免后续还需要其他请求处的信息,我们直接在 PageResult 中添加 PageRequest 属性

BookService.java

后端返回数据后,我们加载页面信息,把分页代码挪到 getBookList 方法中

javascript 复制代码
            getBookList();
            function getBookList() {
                $.ajax({
                    type: "get",
                    url: "/book/getListByPage" + location.search,
                    success: function (result) {
                        console.log(result);
                        if (result != null) {
                            var finalHtml = "";
                            for (var book of result.records) {
                                finalHtml += '<tr>';
                                finalHtml += '<td><input type="checkbox" name="selectBook" value="' + book.id + '" id="selectBook" class="book-select"></td>';
                                finalHtml += '<td>' + book.id + '</td>';
                                finalHtml += '<td>' + book.bookName + '</td>';
                                finalHtml += '<td>' + book.author + '</td>';
                                finalHtml += '<td>' + book.count + '</td>';
                                finalHtml += '<td>' + book.price + '</td>';
                                finalHtml += '<td>' + book.publish + '</td>';
                                finalHtml += '<td>' + book.statusCN + '</td>';
                                finalHtml += '<td><div class="op">';
                                finalHtml += '<a href="book_update.html?bookId=' + book.id + '">修改</a>';
                                finalHtml += '<a href="javascript:void(0)" onclick="deleteBook(\'' + book.id + '\')">删除</a>';
                                finalHtml += '</div></td>';
                                finalHtml += "</tr>";
                            }

                            $("tbody").html(finalHtml);

                            //翻页信息
                            $("#pageContainer").jqPaginator({
                                totalCounts: result.total, //总记录数
                                pageSize: 10,    //每页的个数
                                visiblePages: 5, //可视页数
                                currentPage: result.pageRequest.currentPage,  // 根据后端返回的 pageRequest 对象的 currentPage 设置当前页面
                                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>',

                                //页面初始化和页码点击时都会执行
                                // page 为目标页码 tyoe 为触发类型 --> 分为 init 初始化 和 change 分页
                                onPageChange: function (page, type) {
                                    if (type != 'init') {
                                        location.href="book_list.html?currentPage=" + page;
                                    }
                                }
                            });
                        }
                    }
                });
            }

还要完善页面点击代码:

当点击页码的时候,跳转到页面:book_list.html?currentPage=?

注意,这里对 currentPage 的属性也要进行设置,否则会出现小 bug,在翻页时,尽管会正确显示数据,但是页面按钮不会发生变化~

测试

符合预期~

修改图书

约定前后端交互接口

进入修改页面,需要显示当前图书的信息

我们根据图书的 ID,获取当前图书的信息

点击修改按钮,修改图书信息

约定,浏览器给服务发送一个 /book/updateBook 这样的 HTTP 请求,form 表单的形式来提交数据

服务器返回处理结果,返回空字符串,否则返回失败信息。

实现服务器代码

控制层:

BookController.java:

业务层:

BookService:

数据层:

根据图书 ID,查询图书信息

在更新的逻辑中,传递了那些值,就更新那些值,所以需要使用动态 SQL 的方式来实现。我们使用 XML 的方式来实现拼接动态 SQL 语句。

在 mapper 中定义接口:

配置 XML 路径:

创建 BookInfoMapper.xml 文件

BookInfoMapper.xml:

测试后端代码

利用 postman 测试后端代码,均符合预期

实现客户端代码

客户端代码,在列表页时,就已经补充了 【修改】 的连接

点击【修改】连接的时候,就会自动跳转到 http://127.0.0.1:8080/book_update.html?bookId=25 (25 对应图书的 id)

进入修改图书页面时,需要先从后端拿到当前图书的信息,显示在页面上

补全修改图书的方法:

我们修改图书信息的时候,是根据图书的 ID 来进行修改的,所以在前端传递的参数中,应该包含图书 ID。

有两种方式:

  1. 获取 url 中参数的值。(但我们需要对 url 进行拆分)

  2. 在 form 表单中,再增加一个隐藏输入框,存数图书 ID,随着 $("#updateBook").serialize() 一起提交到后端

页面加载的时候,给该 hidden 框赋值

测试

运行程序,符合预期~

删除图书

约定前后端交互接口

逻辑删除:

逻辑删除也称为软删除,假删除,即并不真正删除数据,而是在某行数据上增加类型为 is_delete 的删除表示,一般使用 UPDATE 语句

物理删除:

物理删除也成为硬删除,从数据库表中删除某一行或某一集合的数据,一般使用 DELETE 语句

删除图书的两种实现方式:

逻辑删除:

update book_info set status=0 where id = 1

物理删除:

delete from book_info where id = 1

通常情况下,我们采取逻辑删除的方式,当然也可以采用[物理删除 + 归档] 的方式

物理删除 + 归档:

创建一个与原表差不多结构的归档表。,记录删除时间。

我们此处使用逻辑删除:

即怡然使用的是更新逻辑,但为了代码可读性,我们仍然将接口命名为 delete

实现服务端代码

BookController:

service:

mapper:

调整前端代码

测试

符合预期

批量删除

批量删除,即批量修改数据

约定前后端交互接口

点击批量删除的按钮后,我们只需要把复选框中的图书 ID 发送给后端即可~

多个 id 我们使用 List 的形式来传递参数

实现服务段代码

BookController:

BookService:

BookInfoMapper:

批量删除也需要使用到动态 SQL 语句,使用 xml 来进行实现。

测试

利用 postman 发送请求测试后端代码

符合预期

客户端代码

完善 batchDelete 函数的逻辑

测试

符合预期

强制登录

我们虽然实现了用户登录功能,但是,当用户不登录,仍然可以直接进入 book_list 页面操作图书。

所以我们需要进行强制登录。

即,如果用户未登录就访问图书列表或者添加图书等页面,就强制跳转到登录页面。

实现思路分析

用户登录时,我们已经把登录用户的信息存储在了 Session 中,那就可以通过 Session 中的信息来判断用户是否登录。

  1. 如果 Session 中可以获取到登录用户的信息,说明用户已经登录,可以对图书进行操作

  2. 如果 Session 中无法获取到登录用户的信息,则说明用户未登录,则应该跳转到登录页面

我们现在的图书列表接口返回的内容如下:

这个结果,前端是无法确认用户是否登录的,并且当后端返回数据为空时,前端也无法确认是后端无数据,还是后端出现错误了。

应该再增加一个属性,来告知后端的状态以及后端出错的原因:

当我们添加了两个字段后,我们的图书的增加,修改,修改接口都需要跟着修改。

不妨对所有后端返回的数据进行一个封装:

status 表示后端业务处理的状态码,也可以使用枚举来表示:

对 Result 类修改,添加一些常用方法

java 复制代码
@Data
public class Result<T> {
    private ResultStatus status;
    private String errorMessage;
    private T data; // data 表示之前接口(pageResult)返回的数据

    /**
     * 业务执行成功时候返回的方法
     * @param data
     * @return
     * @param <T>
     */
    public static <T> Result success(T data) {
        Result result = new Result();
        result.setStatus(ResultStatus.SUCCESS);
        result.setErrorMessage("");
        result.setData(data);
        
        return  result;
    }
    
    /**
     * 业务执行失败时候返回的方法
     * @param msg
     * @return
     * @param <T>
     */
    public static <T> Result fail(String msg) {
        Result result = new Result();
        result.setStatus(ResultStatus.FAIL);
        result.setErrorMessage(msg);
        result.setData("");
        
        return result;
    }

    /**
     * 业务执行失败时返回的方法
     * @return
     */
    public static Result unlogin() {
        Result result = new Result();
        result.setStatus(ResultStatus.UNLOGIN);
        result.setErrorMessage("用户未登录");
        result.setData(null);

        return result;
    }
}

实现服务器代码

修改图书列表接口,进行登录校验

当我们对常量 session 中的 key 进行修改的时候,就需要修改所有使用这个 key 的地方。出于高内聚低耦合的思想,我们常把常量集中在一个类中。

创建类:Constants

修改之前使用的 session_user_key

图书列表接口

登录接口

此时后端返回的数据格式如下:

修改客户端代码

由于后端接口发生变化,所以我们的前端接口也需要变化。

测试

符合预期

完!

相关推荐
Z_z在努力3 小时前
【MySQL 高阶】MySQL 架构与存储引擎全面详解
数据库·mysql·架构
全栈工程师修炼指南3 小时前
DBA | MySQL 数据库基础查询语句学习实践笔记
数据库·笔记·学习·mysql·dba
zandy10113 小时前
衡石HQL深度解析:如何用类SQL语法实现跨源数据的高效联邦查询?
数据库·数据仓库·sql·hql·数据湖仓一体
野犬寒鸦3 小时前
今日面试之项目拷打:锁与事务的深度解析
java·服务器·数据库·后端
阿沁QWQ4 小时前
使用c语言连接数据库
数据库
奥尔特星云大使4 小时前
mysql重置管理员密码
linux·运维·数据库·mysql·centos
Lbwnb丶4 小时前
p6spy 打印完整sql
java·数据库·sql
奥尔特星云大使4 小时前
MySQL多实例管理
linux·运维·数据库·mysql·dba·mysql多实例
ヾChen4 小时前
初识MySQL
数据库·物联网·学习·mysql