首页 > 其他分享 >Mybatis

Mybatis

时间:2024-08-04 10:55:18浏览次数:7  
标签:语句 name public emp SQL Mybatis id

Mybatis入门1

前言

在前面我们学习MySQL数据库时,都是利用图形化客户端工具(如:idea、datagrip),来操作数据库的。

在客户端工具中,编写增删改查的SQL语句,发给MySQL数据库管理系统,由数据库管理系统执行SQL语句并返回执行结果。

增删改操作:返回受影响行数

查询操作:返回结果集(查询的结果)

我们做为后端程序开发人员,通常会使用Java程序来完成对数据库的操作。Java程序操作数据库,现在主流的方式是:Mybatis。

什么是MyBatis?

  • MyBatis是一款优秀的 持久层 框架,用于简化JDBC的开发。
  • MyBatis本是 Apache的一个开源项目iBatis,2010年这个项目由apache迁移到了google code,并且改名为MyBatis 。2013年11月迁移到Github。
  • 官网:https://mybatis.org/mybatis-3/zh_CN/index.html

在上面我们提到了两个词:一个是持久层,另一个是框架。

  • 持久层:指的是就是数据访问层(dao),是用来操作数据库的。

image-20220901114951631

  • 框架:是一个半成品软件,是一套可重用的、通用的、软件基础代码模型。在框架的基础上进行软件开发更加高效、规范、通用、可拓展。

Mybatis课程安排:

  • Mybatis入门

  • Mybatis基础增删改查

  • Mybatis动态SQL

接下来,我们就通过一个入门程序,让大家快速感受一下通过Mybatis如何来操作数据库。

快速入门

需求:使用Mybatis查询所有用户数据。

入门程序分析

以前我们是在图形化客户端工具中编写SQL查询代码,发送给数据库执行,数据库执行后返回操作结果。

image-20221209155704203

图形化工具会把数据库执行的查询结果,使用表格的形式展现出来

image-20220901121116813

现在使用Mybatis操作数据库,就是在Mybatis中编写SQL查询代码,发送给数据库执行,数据库执行后返回结果。

image-20221209155904370

Mybatis会把数据库执行的查询结果,使用实体类封装起来(一行记录对应一个实体类对象)

image-20221209161623051

Mybatis操作数据库的步骤:

  1. 准备工作(创建springboot工程、数据库表user、实体类User)

  2. 引入Mybatis的相关依赖,配置Mybatis(数据库连接信息)

  3. 编写SQL语句(注解/XML)

入门程序实现

准备工作

创建springboot工程

创建springboot工程,并导入 mybatis的起步依赖、mysql的驱动包。

image-20221209162827242

image-20221209163123443

项目工程创建完成后,自动在pom.xml文件中,导入Mybatis依赖和MySQL驱动依赖

<!-- 仅供参考:只粘贴了pom.xml中部分内容 -->
<dependencies>
        <!-- mybatis起步依赖 -->
        <dependency>
            <groupId>org.mybatis.spring.boot</groupId>
            <artifactId>mybatis-spring-boot-starter</artifactId>
            <version>2.3.0</version>
        </dependency>

        <!-- mysql驱动包依赖 -->
        <dependency>
            <groupId>com.mysql</groupId>
            <artifactId>mysql-connector-j</artifactId>
            <scope>runtime</scope>
        </dependency>
        
        <!-- spring单元测试 (集成了junit) -->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-test</artifactId>
            <scope>test</scope>
        </dependency>
</dependencies>
数据准备

创建用户表user,并创建对应的实体类User。

  • 用户表:
-- 用户表
create table user(
    id int unsigned primary key auto_increment comment 'ID',
    name varchar(100) comment '姓名',
    age tinyint unsigned comment '年龄',
    gender tinyint unsigned comment '性别, 1:男, 2:女',
    phone varchar(11) comment '手机号'
) comment '用户表';

-- 测试数据
insert into user(id, name, age, gender, phone) VALUES (null,'白眉鹰王',55,'1','18800000000');
insert into user(id, name, age, gender, phone) VALUES (null,'金毛狮王',45,'1','18800000001');
insert into user(id, name, age, gender, phone) VALUES (null,'青翼蝠王',38,'1','18800000002');
insert into user(id, name, age, gender, phone) VALUES (null,'紫衫龙王',42,'2','18800000003');
insert into user(id, name, age, gender, phone) VALUES (null,'光明左使',37,'1','18800000004');
insert into user(id, name, age, gender, phone) VALUES (null,'光明右使',48,'1','18800000005');

image-20220901121116813

  • 实体类

    • 实体类的属性名与表中的字段名一一对应。
public class User {
    private Integer id;   //id(主键)
    private String name;  //姓名
    private Short age;    //年龄
    private Short gender; //性别
    private String phone; //手机号
    
    //省略GET, SET方法
}

image-20221209170354143

配置Mybatis

在之前使用图形化客户端工具,连接MySQL数据库时,需要配置:

image-20221209172527630

连接数据库的四大参数:

  • MySQL驱动类
  • 登录名
  • 密码
  • 数据库连接字符串

基于上述分析,在Mybatis中要连接数据库,同样也需要以上4个参数配置。

在springboot项目中,可以编写application.properties文件,配置数据库连接信息。我们要连接数据库,就需要配置数据库连接的基本信息,包括:driver-class-name、url 、username,password。

在入门程序中,大家可以直接这么配置,后面会介绍什么是驱动。

application.properties:

#驱动类名称
spring.datasource.driver-class-name=com.mysql.cj.jdbc.Driver
#数据库连接的url
spring.datasource.url=jdbc:mysql://localhost:3306/mybatis
#连接数据库的用户名
spring.datasource.username=root
#连接数据库的密码
spring.datasource.password=123456

上述的配置,可以直接复制过去,不要敲错了。 全部都是 spring.datasource.xxxx 开头。

编写SQL语句

在创建出来的springboot工程中,在引导类所在包下,在创建一个包 mapper。在mapper包下创建一个接口 UserMapper ,这是一个持久层接口(Mybatis的持久层接口规范一般都叫 XxxMapper)。

image-20221209175843651

UserMapper:

import com.itheima.pojo.User;
import org.apache.ibatis.annotations.Mapper;
import org.apache.ibatis.annotations.Select;
import java.util.List;

@Mapper
public interface UserMapper {
    
    //查询所有用户数据
    @Select("select id, name, age, gender, phone from user")
    public List<User> list();
    
}

@Mapper注解:表示是mybatis中的Mapper接口

  • 程序运行时:框架会自动生成接口的实现类对象(代理对象),并给交Spring的IOC容器管理

@Select注解:代表的就是select查询,用于书写select查询语句

单元测试

在创建出来的SpringBoot工程中,在src下的test目录下,已经自动帮我们创建好了测试类 ,并且在测试类上已经添加了注解 @SpringBootTest,代表该测试类已经与SpringBoot整合。

该测试类在运行时,会自动通过引导类加载Spring的环境(IOC容器)。我们要测试那个bean对象,就可以直接通过@Autowired注解直接将其注入进行,然后就可以测试了。

测试类代码如下:

@SpringBootTest
public class MybatisQuickstartApplicationTests {
	
    @Autowired
    private UserMapper userMapper;
	
    @Test
    public void testList(){
        List<User> userList = userMapper.list();
        for (User user : userList) {
            System.out.println(user);
        }
    }

}

运行结果:

User{id=1, name='白眉鹰王', age=55, gender=1, phone='18800000000'}
User{id=2, name='金毛狮王', age=45, gender=1, phone='18800000001'}
User{id=3, name='青翼蝠王', age=38, gender=1, phone='18800000002'}
User{id=4, name='紫衫龙王', age=42, gender=2, phone='18800000003'}
User{id=5, name='光明左使', age=37, gender=1, phone='18800000004'}
User{id=6, name='光明右使', age=48, gender=1, phone='18800000005'}

解决SQL警告与提示

默认我们在UserMapper接口上加的@Select注解中编写SQL语句是没有提示的。 如果想让idea给我们提示对应的SQL语句,我们需要在IDEA中配置与MySQL数据库的链接。

默认我们在UserMapper接口上的@Select注解中编写SQL语句是没有提示的。如果想让idea给出提示,可以做如下配置:

image-20221210143348119

配置完成之后,发现SQL语句中的关键字有提示了,但还存在不识别表名(列名)的情况:

image-20221210143934318

产生原因:Idea和数据库没有建立连接,不识别表信息

解决方案:在Idea中配置MySQL数据库连接

image-20221210144139792

在配置的时候指定连接那个数据库,如上图所示连接的就是mybatis数据库。

JDBC

概述

JDBC:(Java DataBase Connectivity),就是使用Java语言操作关系型数据库的一套API。

image-20231130113441567

本质:

  • sun公司官方定义的一套操作所有关系型数据库的规范,即接口。

  • 各个数据库厂商去实现这套接口,提供数据库驱动jar包。

  • 我们可以使用这套接口(JDBC)编程,真正执行的代码是驱动jar包中的实现类。

快速入门

需求:通过JDBC程序,执行update语法,更新用户表中的数据

步骤:

  • 准备工作:
    • Connection
  • 执行SQL语句
  • 释放资源

演示:

  1. 准备数据库 web,及数据库表 user

    create database web;
    use web;
    
    create table user(
        id int unsigned primary key auto_increment comment 'ID,主键',
        username varchar(20) comment '用户名',
        password varchar(32) comment '密码',
        name varchar(10) comment '姓名',
        age tinyint unsigned comment '年龄'
    ) comment '用户表';
    
    insert into user(id, username, password, name, age) values (1, 'daqiao', '123456', '大乔', 22),
                                                               (2, 'xiaoqiao', '123456', '小乔', 18),
                                                               (3, 'diaochan', '123456', '貂蝉', 24),
                                                               (4, 'lvbu', '123456', '吕布', 28),
                                                               (5, 'zhaoyun', '12345678', '赵云', 27);
    
  2. 创建一个普通的maven项目(非springboot项目),引入mysql的驱动、junit依赖

    <dependency>
        <groupId>mysql</groupId>
        <artifactId>mysql-connector-java</artifactId>
        <version>8.0.33</version>
    </dependency>
    
    <dependency>
        <groupId>org.junit.jupiter</groupId>
        <artifactId>junit-jupiter</artifactId>
        <version>5.9.3</version>
        <scope>test</scope>
    </dependency>
    
  3. src/test 目录下创建一个包 com.itheima.test,并在其中创建一个测试类 JDBCTest,编写入门程序 ,具体如下:

    @Test
    public void testUpdate() throws Exception {
        //1.准备工作
        //1.1 注册驱动
        Class.forName("com.mysql.cj.jdbc.Driver");
        
        //1.2 获取连接
        String url = "jdbc:mysql://localhost:3306/web";
        Connection connection = DriverManager.getConnection(url, "root", "1234");
        
        //1.3 获取SQL语句执行对象
        Statement statement = connection.createStatement();
    
        //2.执行SQL
        statement.executeUpdate("update user set password = '1234567890' where id = 1");
    
        //3.释放资源
        statement.close();
        connection.close();
    }
    

在上述的步骤中,注册驱动、获取链接Connection、获取SQL语句执行对象 Statement 、释放资源 这几步都是固定步骤,使用JDBC程序操作数据库时,这几步都需要做的。

  1. 运行单元测试,查看数据库中数据的执行结果。

API详解

DriverManager

作用:

  • 注册驱动
  • 获取数据库链接 Connection
image-20231130140042611

注册驱动

而通过上述的代码,大家可以看到注册驱动,我们是通过 Class.forName("com.mysql.cj.jdbc.Driver") 来注册的,看似和DriverManager没什么联系。其实不然,Class.forName("com.mysql.cj.jdbc.Driver") 只是将 Driver 这个类加载到JVM中。 而在 Driver 这个类中定义了静态代码块,内容如下:

image-20231130140632731

Driver 这个类被加载时,就会自动执行静态代码块中的代码,然后就完成了注册驱动的操作。

备注:

而其实啊,Class.forName("com.mysql.cj.jdbc.Driver") 这句代码呢,是可以省略的,省略了,也可以正常的完成驱动的注册。 原因是因为在MySQL的驱动jar包中,在 META-INF/services 目录中提供了一个文件 java.sql.Driver ,在这个文件中编写了一行内容,就是驱动类的全类名 :

image-20231130145345407

当在程序需要用到这个类时,java会自动加载这个类,这个类一加载 就会自动执行静态代码块中的内容,就完成了注册驱动的操作 ,而java中的这个机制称之为 SPI

SPI机制:Service Provider Interface,JDK内置的一种服务提供发现机制,可以轻松的扩展你得程序(切换实现),实现接口与实现类之间的解耦。

获取链接

DriverManager.getConnection(url, username, password);

  • url: 数据库连接url

    • 语法:jdbc:mysql://ip地址(域名):端口号/数据库名?参数键值对1&参数键值对2

    • 说明:如果连接的是本机的默认端口的mysql,url可以简写为:jdbc:mysql:///数据库名?参数键值对…

  • user:用户名

  • password:密码

Connection & Statement

  • Connection的作用:获取执行SQL的对象
    • 执行普通SQL对象的Statement:connection.createStatement();
    • 执行预编译SQL对象PreparedStatement:connection.preparedStatement(); 【后面单独讲解】

  • Statement的作用:执行SQL
    • 执行DDL、DML语句:executeUpdate(sql); 如果是执行DML语句完毕,返回值int代表 DML 语句影响的函数 。
    • 执行SQL语句:executeQuery(sql); 返回值为ResultSet,里面封装了查询结果。

ResultSet

image-20231130153101800

  • 需求:请根据 用户名 与 密码 查询用户的信息,来模拟用户登录操作。

  • 实现如下:

    @Test
    public void testQuery() throws Exception {
        //1. 注册驱动
        Class.forName("com.mysql.cj.jdbc.Driver");
    
        //2. 获取链接
        Connection connection = DriverManager.getConnection("jdbc:mysql://localhost:3306/web01", "root", "root@1234");
    
        //3. 获取数据库执行对象 Statement
        Statement statement = connection.createStatement();
    
        //4. 执行SQL
        ResultSet resultSet = statement.executeQuery("select * from user where username = 'lvbu' and password = '123456'");
    
        //5. 获取结果
        while (resultSet.next()){
            int id = resultSet.getInt("id");
            String username = resultSet.getString("username");
            String password = resultSet.getString("password");
            String name = resultSet.getString("name");
            int age = resultSet.getInt("age");
            System.out.println(new User(id,username,password,name,age));
        }
    
        //6. 释放资源
        statement.close();
        connection.close();
    }
    
  • ResultSet(结果集对象):封装了DQL查询语句查询的结果。

    • next():将光标从当前位置向前移动一行,并判断当前行是否为有效行,返回值为boolean。

      • true:有效行,当前行有数据

      • false:无效行,当前行没有数据

    • getXxx(…):获取数据,可以根据列的编号获取(编号从1开始),也可以根据列名获取(推荐)。

而上述的单元测试中,我们在SQL语句中,将将 用户名 和密码的值都写死了,而这两个值应该是动态的,是将来页面传递到服务端的。 那么,我们可以基于前面所讲解的JUnit中的参数化测试进行单元测试,代码改造如下:

    /**
     * 根据用户名和密码查询用户的基本信息 - 参数化测试
     */
    @ParameterizedTest
    @CsvSource({"lvbu,123456", "xiaoqiao,123456"})
    public void testQuery2(String _username , String _password) throws Exception {
        //1. 注册驱动
        Class.forName("com.mysql.cj.jdbc.Driver");

        //2. 获取链接
        Connection connection = DriverManager.getConnection("jdbc:mysql://localhost:3306/web01", "root", "root@1234");

        //3. 获取数据库执行对象 Statement
        Statement statement = connection.createStatement();

        //4. 执行SQL
        ResultSet resultSet = statement.executeQuery("select * from user where username = '"+_username+"' and password = '"+_password+"'");

        //5. 获取结果
        while (resultSet.next()){
            int id = resultSet.getInt("id");
            String username = resultSet.getString("username");
            String password = resultSet.getString("password");
            String name = resultSet.getString("name");
            int age = resultSet.getInt("age");
            System.out.println(new User(id,username,password,name,age));
        }

        //6. 释放资源
        statement.close();
        connection.close();
    }

如果在测试时,需要传递一组参数,可以使用 @CsvSource 注解。

DriverManager(类):数据库驱动管理类。

  • 作用:

    1. 注册驱动

    2. 创建java代码和数据库之间的连接,即获取Connection对象

Connection(接口):建立数据库连接的对象

  • 作用:用于建立java程序和数据库之间的连接

Statement(接口): 数据库操作对象(执行SQL语句的对象)。

  • 作用:用于向数据库发送sql语句

ResultSet(接口):结果集对象(一张虚拟表)

  • 作用:sql查询语句的执行结果会封装在ResultSet中

通过上述代码,我们看到直接基于JDBC程序来操作数据库,代码实现非常繁琐,所以在项目开发中,我们很少使用。 在项目开发中,通常会使用Mybatis这类的高级技术来操作数据库,从而简化数据库操作、提高开发效率。

PreparedStatement

  • 作用:预编译SQL语句并执行,可以防止SQL注入问题。

  • SQL注入:通过控制输入来修改事先定义好的SQL语句,以达到执行代码对服务器进行攻击的方法。

image-20231130161328967

SQL注入演示

1). 打开课程资料中的文件夹 资料/02. SQL注入演示,运行其中的jar包 sql_Injection_demo-0.0.1-SNAPSHOT.jar,进入该目录后,执行命令:

java -jar sql_Injection_demo-0.0.1-SNAPSHOT.jar

image-20231130162011127

2). 打开浏览器访问 http://localhost:9090/ ,必须登录后才能访问到系统。我们先测试正常的用户名和密码

image-20231130162105251

image-20231130162112128

3). 接下来,我们再来测试一下错误的用户名和密码 。

image-20231130162212300

我们看到,如果用户名密码错误,是不能进入到系统中进行访问的,会提示 用户名和密码错误

4). 那接下来,我们就要演示一下SQL注入现象,我们可以通过控制表单输入,来修改事先定义好的SQL语句的含义。 从而来攻击服务器。

image-20231130162504025

点击登录后,我们看到居然可以成功进入到系统中。

image-20231130162527078

为什么会出现这种现象呢?

在进行登录操作时,怎么样才算登录成功呢? 如果我们查询到了数据,就说明用户名密码是对的。 如果没有查询到数据,就说明用户名或密码错误。

而出现上述现象,原因就是因为,我们我们编写的SQL语句是基于字符串进行拼接的 。 我们输入的用户名无所谓,比如:shfhsjfhja ,而密码呢,就是我们精心设计的,如:' or '1' = '1

那最终拼接的SQL语句,如下所示:

image-20231130163110907

我们知道,or 连接的条件,是或的关系,两者满足其一就可以。 所以,虽然用户名密码输入错误,也是可以查询返回结果的。

SQL注入解决

而我们这小节,所讲解的 PreparedStatement 就可以解决SQL注入的问题。那么接下来,我们就来演示一下 PreparedStatement 的基本使用:

/**
 * 根据用户名和密码查询用户的基本信息 - PreparedStatement
 */
@ParameterizedTest
@CsvSource({"lvbu,123456"})
public void testQuery3(String _username , String _password) throws Exception {
    //1. 注册驱动
    Class.forName("com.mysql.cj.jdbc.Driver");

    //2. 获取链接
    Connection connection = DriverManager.getConnection("jdbc:mysql://localhost:3306/web01", "root", "root@1234");

    //3. 获取数据库执行对象 Statement
    PreparedStatement preparedStatement = connection.prepareStatement("select * from user where username = ? and password = ?");
    preparedStatement.setString(1, _username);
    preparedStatement.setString(2, _password);

    //4. 执行SQL
    ResultSet resultSet = preparedStatement.executeQuery();

    //5. 获取结果
    while (resultSet.next()){
        int id = resultSet.getInt("id");
        String username = resultSet.getString("username");
        String password = resultSet.getString("password");
        String name = resultSet.getString("name");
        int age = resultSet.getInt("age");
        System.out.println(new User(id,username,password,name,age));
    }

    //6. 释放资源
    preparedStatement.close();
    connection.close();
}

select * from user where username = ? and password = ? 这种SQL呢,我们称之为预编译SQL 。那么基于这种预编译SQL呢,是可以解决SQL注入问题的 。

1). 打开课程资料中的文件夹 资料/02. SQL注入演示,运行其中的jar包 sql_prepared_demo-0.0.1-SNAPSHOT.jar,进入该目录后,执行命令:

java -jar sql_prepared_demo-0.0.1-SNAPSHOT.jar

image-20231130164512424

2). 打开浏览器访问 http://localhost:9090/ ,必须登录后才能访问到系统 。我们先测试正常的用户名和密码

image-20231130164551148

image-20231130164603041

3). 那接下来,我们就要演示一下是否可以基于上述的密码 ' or '1' = '1,来完成SQL注入 。

image-20231130164758504

通过控制台,可以看到输入的SQL语句,是预编译SQL语句。

image-20231130165019265

而在预编译SQL语句中,当我们执行的时候,会把整个' or '1'='1作为一个完整的参数,赋值给第2个问号(' or '1'='1进行了转义,只当做字符串使用)

那么此时再查询时,就查询不到对应的数据了,登录失败。

注意:在以后的项目开发中,我们使用的基本全部都是预编译SQL语句。

预编译SQL的优势:

  • 安全(防止SQL注入)

  • 性能更高

    image-20240522210618282

原始的JDBC问题分析

原始的JDBC程序,存在以下几点问题:

  1. 数据库链接的四要素(驱动、链接、用户名、密码)全部硬编码在java代码中
  2. 查询结果的解析及封装非常繁琐
  3. 每一次查询数据库都需要获取连接,操作完毕后释放连接, 资源浪费, 性能降低

image-20221210153407998

技术对比

分析了JDBC的缺点之后,我们再来看一下在mybatis中,是如何解决这些问题的:

  1. 数据库连接四要素(驱动、链接、用户名、密码),都配置在springboot默认的配置文件 application.properties中

  2. 查询结果的解析及封装,由mybatis自动完成映射封装,我们无需关注

  3. 在mybatis中使用了数据库连接池技术,从而避免了频繁的创建连接、销毁连接而带来的资源浪费。

image-20221210154324151

使用SpringBoot+Mybatis的方式操作数据库,能够提升开发效率、降低资源浪费

而对于Mybatis来说,我们在开发持久层程序操作数据库时,需要重点关注以下两个方面:

  1. application.properties

    #驱动类名称
    spring.datasource.driver-class-name=com.mysql.cj.jdbc.Driver
    #数据库连接的url
    spring.datasource.url=jdbc:mysql://localhost:3306/mybatis
    #连接数据库的用户名
    spring.datasource.username=root
    #连接数据库的密码
    spring.datasource.password=1234
    
  2. Mapper接口(编写SQL语句)

    @Mapper
    public interface UserMapper {
        @Select("select id, name, age, gender, phone from user")
        public List<User> list();
    }
    

数据库连接池

在前面我们所讲解的mybatis中,使用了数据库连接池技术,避免频繁的创建连接、销毁连接而带来的资源浪费。

下面我们就具体的了解下数据库连接池。

介绍

image-20221210160341852

没有使用数据库连接池:

  • 客户端执行SQL语句:要先创建一个新的连接对象,然后执行SQL语句,SQL语句执行后又需要关闭连接对象从而释放资源,每次执行SQL时都需要创建连接、销毁链接,这种频繁的重复创建销毁的过程是比较耗费计算机的性能。

image-20221210161016314

数据库连接池是个容器,负责分配、管理数据库连接(Connection)

  • 程序在启动时,会在数据库连接池(容器)中,创建一定数量的Connection对象

允许应用程序重复使用一个现有的数据库连接,而不是再重新建立一个

  • 客户端在执行SQL时,先从连接池中获取一个Connection对象,然后在执行SQL语句,SQL语句执行完之后,释放Connection时就会把Connection对象归还给连接池(Connection对象可以复用)

释放空闲时间超过最大空闲时间的连接,来避免因为没有释放连接而引起的数据库连接遗漏

  • 客户端获取到Connection对象了,但是Connection对象并没有去访问数据库(处于空闲),数据库连接池发现Connection对象的空闲时间 > 连接池中预设的最大空闲时间,此时数据库连接池就会自动释放掉这个连接对象

数据库连接池的好处:

  1. 资源重用
  2. 提升系统响应速度
  3. 避免数据库连接遗漏

产品

要怎么样实现数据库连接池呢?

  • 官方(sun)提供了数据库连接池标准(javax.sql.DataSource接口)

    • 功能:获取连接

      public Connection getConnection() throws SQLException;
      
    • 第三方组织必须按照DataSource接口实现

常见的数据库连接池:

  • C3P0
  • DBCP
  • Druid
  • Hikari (springboot默认)

现在使用更多的是:Hikari、Druid (性能更优越)

  • Hikari(追光者) [默认的连接池]

image-20220901144923251

  • Druid(德鲁伊)

    • Druid连接池是阿里巴巴开源的数据库连接池项目

    • 功能强大,性能优秀,是Java语言最好的数据库连接池之一

如果我们想把默认的数据库连接池切换为Druid数据库连接池,只需要完成以下两步操作即可:

参考官方地址:https://github.com/alibaba/druid/tree/master/druid-spring-boot-starter

  1. 在pom.xml文件中引入依赖
<dependency>
    <!-- Druid连接池依赖 -->
    <groupId>com.alibaba</groupId>
    <artifactId>druid-spring-boot-starter</artifactId>
    <version>1.2.8</version>
</dependency>
  1. 在application.properties中引入数据库连接配置

方式1:

spring.datasource.druid.driver-class-name=com.mysql.cj.jdbc.Driver
spring.datasource.druid.url=jdbc:mysql://localhost:3306/mybatis
spring.datasource.druid.username=root
spring.datasource.druid.password=1234

方式2:

spring.datasource.driver-class-name=com.mysql.cj.jdbc.Driver
spring.datasource.url=jdbc:mysql://localhost:3306/mybatis
spring.datasource.username=root
spring.datasource.password=1234

druid连接池使用步骤

步骤:
下载jar包 druid-1.x.x.jar
定义配置文件
properties文件
名称任意
driverClassName=com.mysql.cj.jdbc.Driver

指定数据库

url=xxx

指定用户名和密码

username=root
password=root

设置连接数和等待时间

initialSize=10
minIdle=5
maxActive=20
maxWait=5000

  加载配置文件,Properties
  获取数据库连接对象:通过工厂类来获取
	DruidDataSourceFactory.createDataSource(prop);
  获取连接:getConnection
创建声明
	getConnection
执行查询
	createStatement

案例:

package com.itheima;

import com.alibaba.druid.pool.DruidDataSourceFactory;

import javax.sql.DataSource;
import java.io.InputStream;
import java.sql.Connection;
import java.sql.ResultSet;
import java.sql.Statement;
import java.util.Properties;

/**
 * druid连接池工具类
 */
public class DruidDataSource {

    public static void main(String[] args) {
        createConnection();
    }

    public static void createConnection() {
        // 1、加载连接数据库配置信息的properties文件
        Properties properties = new Properties();
        InputStream inputStream = DruidDataSource.class.getClassLoader().getResourceAsStream("druid.properties");
        try {
            properties.load(inputStream);
            properties.forEach((k, v) -> System.out.println(k + ":" + v));
            // 2、获取druid连接池对象并创建连接
            DataSource dataSource = DruidDataSourceFactory.createDataSource(properties);
            Connection connection = dataSource.getConnection();
            Statement statement = connection.createStatement();
            int i = statement.executeUpdate("update student set age = 19 where id in (1,2,3)");
            System.out.println(i);

            ResultSet resultSet = statement.executeQuery("select name,age from student");
            while (resultSet.next()) {
                String name = resultSet.getString("name");
                int age = resultSet.getInt("age");
                System.out.println(name + ":" + age);
            }
        } catch (Exception e) {
            e.printStackTrace();
        }
    }
}

lombok

介绍

Lombok是一个实用的Java类库,可以通过简单的注解来简化和消除一些必须有但显得很臃肿的Java代码。

image-20221210164641266

通过注解的形式自动生成构造器、getter/setter、equals、hashcode、toString等方法,并可以自动化生成日志变量,简化java开发、提高效率。

注解 作用
@Getter/@Setter 为所有的属性提供get/set方法
@ToString 会给类自动生成易阅读的 toString 方法
@EqualsAndHashCode 根据类所拥有的非静态字段自动重写 equals 方法和 hashCode 方法
@Data 提供了更综合的生成代码功能(@Getter + @Setter + @ToString + @EqualsAndHashCode)
@NoArgsConstructor 为实体类生成无参的构造器方法
@AllArgsConstructor 为实体类生成除了static修饰的字段之外带有各参数的构造器方法。

使用

第1步:在pom.xml文件中引入依赖

<!-- 在springboot的父工程中,已经集成了lombok并指定了版本号,故当前引入依赖时不需要指定version -->
<dependency>
    <groupId>org.projectlombok</groupId>
    <artifactId>lombok</artifactId>
</dependency>

第2步:在实体类上添加注解

import lombok.Data;

@Data
public class User {
    private Integer id;
    private String name;
    private Short age;
    private Short gender;
    private String phone;
}

在实体类上添加了@Data注解,那么这个类在编译时期,就会生成getter/setter、equals、hashcode、toString等方法。

image-20221210170733921

说明:@Data注解中不包含全参构造方法,通常在实体类上,还会添加上:全参构造、无参构造

import lombok.Data;

@Data //getter方法、setter方法、toString方法、hashCode方法、equals方法
@NoArgsConstructor //无参构造
@AllArgsConstructor//全参构造
public class User {
    private Integer id;
    private String name;
    private Short age;
    private Short gender;
    private String phone;
}

Lombok的注意事项:

  • Lombok会在编译时,会自动生成对应的java代码
  • 在使用lombok时,还需要安装一个lombok的插件(新版本的IDEA中自带)

image-20221210165506359



Mybatis基础操作 2

学习完mybatis入门后,我们继续学习mybatis基础操作。

需求

需求说明:

  • 根据资料中提供的《tlias智能学习辅助系统》页面原型及需求,完成员工管理的需求开发。

image-20221210180155700

image-20221210180343288

image-20221210180515206

通过分析以上的页面原型和需求,我们确定了功能列表:

  1. 查询

    • 根据主键ID查询
    • 条件查询
  2. 新增

  3. 更新

  4. 删除

    • 根据主键ID删除
    • 根据主键ID批量删除

准备

实施前的准备工作:

  1. 准备数据库表
  2. 创建一个新的springboot工程,选择引入对应的起步依赖(mybatis、mysql驱动、lombok)
  3. application.properties中引入数据库连接信息
  4. 创建对应的实体类 Emp(实体类属性采用驼峰命名)
  5. 准备Mapper接口 EmpMapper

准备数据库表

-- 部门管理
create table dept
(
    id          int unsigned primary key auto_increment comment '主键ID',
    name        varchar(10) not null unique comment '部门名称',
    create_time datetime    not null comment '创建时间',
    update_time datetime    not null comment '修改时间'
) comment '部门表';
-- 部门表测试数据
insert into dept (id, name, create_time, update_time)
values (1, '学工部', now(), now()),
       (2, '教研部', now(), now()),
       (3, '咨询部', now(), now()),
       (4, '就业部', now(), now()),
       (5, '人事部', now(), now());


-- 员工管理
create table emp
(
    id          int unsigned primary key auto_increment comment 'ID',
    username    varchar(20)      not null unique comment '用户名',
    password    varchar(32) default '123456' comment '密码',
    name        varchar(10)      not null comment '姓名',
    gender      tinyint unsigned not null comment '性别, 说明: 1 男, 2 女',
    image       varchar(300) comment '图像',
    job         tinyint unsigned comment '职位, 说明: 1 班主任,2 讲师, 3 学工主管, 4 教研主管, 5 咨询师',
    entrydate   date comment '入职时间',
    dept_id     int unsigned comment '部门ID',
    create_time datetime         not null comment '创建时间',
    update_time datetime         not null comment '修改时间'
) comment '员工表';
-- 员工表测试数据
INSERT INTO emp (id, username, password, name, gender, image, job, entrydate, dept_id, create_time, update_time)
VALUES 
(1, 'jinyong', '123456', '金庸', 1, '1.jpg', 4, '2000-01-01', 2, now(), now()),
(2, 'zhangwuji', '123456', '张无忌', 1, '2.jpg', 2, '2015-01-01', 2, now(), now()),
(3, 'yangxiao', '123456', '杨逍', 1, '3.jpg', 2, '2008-05-01', 2, now(), now()),
(4, 'weiyixiao', '123456', '韦一笑', 1, '4.jpg', 2, '2007-01-01', 2, now(), now()),
(5, 'changyuchun', '123456', '常遇春', 1, '5.jpg', 2, '2012-12-05', 2, now(), now()),
(6, 'xiaozhao', '123456', '小昭', 2, '6.jpg', 3, '2013-09-05', 1, now(), now()),
(7, 'jixiaofu', '123456', '纪晓芙', 2, '7.jpg', 1, '2005-08-01', 1, now(), now()),
(8, 'zhouzhiruo', '123456', '周芷若', 2, '8.jpg', 1, '2014-11-09', 1, now(), now()),
(9, 'dingminjun', '123456', '丁敏君', 2, '9.jpg', 1, '2011-03-11', 1, now(), now()),
(10, 'zhaomin', '123456', '赵敏', 2, '10.jpg', 1, '2013-09-05', 1, now(), now()),
(11, 'luzhangke', '123456', '鹿杖客', 1, '11.jpg', 5, '2007-02-01', 3, now(), now()),
(12, 'hebiweng', '123456', '鹤笔翁', 1, '12.jpg', 5, '2008-08-18', 3, now(), now()),
(13, 'fangdongbai', '123456', '方东白', 1, '13.jpg', 5, '2012-11-01', 3, now(), now()),
(14, 'zhangsanfeng', '123456', '张三丰', 1, '14.jpg', 2, '2002-08-01', 2, now(), now()),
(15, 'yulianzhou', '123456', '俞莲舟', 1, '15.jpg', 2, '2011-05-01', 2, now(), now()),
(16, 'songyuanqiao', '123456', '宋远桥', 1, '16.jpg', 2, '2010-01-01', 2, now(), now()),
(17, 'chenyouliang', '123456', '陈友谅', 1, '17.jpg', NULL, '2015-03-21', NULL, now(), now());

创建一个新的springboot工程,选择引入对应的起步依赖(mybatis、mysql驱动、lombok)

image-20221210182008131

application.properties中引入数据库连接信息

提示:可以把之前项目中已有的配置信息复制过来即可

#驱动类名称
spring.datasource.driver-class-name=com.mysql.cj.jdbc.Driver
#数据库连接的url
spring.datasource.url=jdbc:mysql://localhost:3306/mybatis
#连接数据库的用户名
spring.datasource.username=root
#连接数据库的密码
spring.datasource.password=1234

创建对应的实体类Emp(实体类属性采用驼峰命名)

@Data
@NoArgsConstructor
@AllArgsConstructor
public class Emp {
    private Integer id;
    private String username;
    private String password;
    private String name;
    private Short gender;
    private String image;
    private Short job;
    private LocalDate entrydate;     //LocalDate类型对应数据表中的date类型
    private Integer deptId;
    private LocalDateTime createTime;//LocalDateTime类型对应数据表中的datetime类型
    private LocalDateTime updateTime;
}

准备Mapper接口:EmpMapper

/*@Mapper注解:表示当前接口为mybatis中的Mapper接口
  程序运行时会自动创建接口的实现类对象(代理对象),并交给Spring的IOC容器管理
*/
@Mapper
public interface EmpMapper {

}

完成以上操作后,项目工程结构目录如下:

image-20221210182500817

删除

功能实现

页面原型:

image-20221210183336095

当我们点击后面的"删除"按钮时,前端页面会给服务端传递一个参数,也就是该行数据的ID。 我们接收到ID后,根据ID删除数据即可。

功能:根据主键删除数据

  • SQL语句
-- 删除id=17的数据
delete from emp where id = 17;

Mybatis框架让程序员更关注于SQL语句

  • 接口方法
@Mapper
public interface EmpMapper {
    
    //@Delete("delete from emp where id = 17")
    //public void delete();
    //以上delete操作的SQL语句中的id值写成固定的17,就表示只能删除id=17的用户数据
    //SQL语句中的id值不能写成固定数值,需要变为动态的数值
    //解决方案:在delete方法中添加一个参数(用户id),将方法中的参数,传给SQL语句
    
    /**
     * 根据id删除数据
     * @param id    用户id
     */
    @Delete("delete from emp where id = #{id}")//使用#{key}方式获取方法中的参数值
    public void delete(Integer id);
    
}

@Delete注解:用于编写delete操作的SQL语句

如果mapper接口方法形参只有一个普通类型的参数,#{…} 里面的属性名可以随便写,如:#{id}、#{value}。但是建议保持名字一致。

  • 测试
    • 在单元测试类中通过@Autowired注解注入EmpMapper类型对象
@SpringBootTest
class SpringbootMybatisCrudApplicationTests {
    @Autowired //从Spring的IOC容器中,获取类型是EmpMapper的对象并注入
    private EmpMapper empMapper;

    @Test
    public void testDel(){
        //调用删除方法
        empMapper.delete(16);
    }

}

删除操作是有返回值的,返回值代表的是删除操作影响的记录数

@Mapper
public interface EmpMapper {
    
    //@Delete("delete from emp where id = 17")
    //public void delete();
    //以上delete操作的SQL语句中的id值写成固定的17,就表示只能删除id=17的用户数据
    //SQL语句中的id值不能写成固定数值,需要变为动态的数值
    //解决方案:在delete方法中添加一个参数(用户id),将方法中的参数,传给SQL语句
    
    /**
     * 根据id删除数据
     * @param id    用户id
     */
    @Delete("delete from emp where id = #{id}")//使用#{key}方式获取方法中的参数值
    public int delete(Integer id);
}
@SpringBootTest
class SpringbootMybatisCrudApplicationTests {
    @Autowired //从Spring的IOC容器中,获取类型是EmpMapper的对象并注入
    private EmpMapper empMapper;

    @Test
    public void testDel(){
        //调用删除方法
        int i = empMapper.delete(16);
    }
}

配置SQL提示

默认我们在UserMapper接口上加的 @Select 注解中编写SQL语句是没有提示的。 如果想让idea给我们提示对应的SQL语句,我们需要在IDEA中配置与MySQL数据库的链接。

默认我们在UserMapper接口上的 @Select 注解中编写SQL语句是没有提示的。如果想让idea给出提示,可以做如下配置:

image-20231130174149531

配置完成之后,发现SQL语句中的关键字有提示了,但还存在不识别表名(列名)的情况:

image-20231130174210007

产生原因:Idea和数据库没有建立连接,不识别表信息

解决方案:在Idea中配置MySQL数据库连接

image-20231130174355957

在配置的时候指定连接那个数据库,如上图所示连接的就是mybatis数据库(自己的数据库名是什么就指定什么)。

注意:该配置的目的,仅仅是为了在编写SQL语句时,有语法提示(写错了会报错),不会影响运行,即使不配置也是可以的。

日志输入

在Mybatis当中我们可以借助日志,查看到sql语句的执行、执行传递的参数以及执行结果。具体操作如下:

  1. 打开application.properties文件

  2. 开启mybatis的日志,并指定输出到控制台

#指定mybatis输出日志的位置, 输出控制台
mybatis.configuration.log-impl=org.apache.ibatis.logging.stdout.StdOutImpl

开启日志之后,我们再次运行单元测试,可以看到在控制台中,输出了以下的SQL语句信息:

image-20220901164225644

但是我们发现输出的SQL语句:delete from emp where id = ?,我们输入的参数16并没有在后面拼接,id的值是使用?进行占位。那这种SQL语句我们称为预编译SQL。

预编译SQL

介绍

预编译SQL有两个优势:

  1. 性能更高
  2. 更安全(防止SQL注入)

image-20221210202222206

性能更高:预编译SQL,编译一次之后会将编译后的SQL语句缓存起来,后面再次执行这条语句时,不会再次编译。(只是输入的参数不同)

更安全(防止SQL注入):将敏感字进行转义,保障SQL的安全性。

SQL注入

SQL注入:是通过操作输入的数据来修改事先定义好的SQL语句,以达到执行代码对服务器进行攻击的方法。

由于没有对用户输入进行充分检查,而SQL又是拼接而成,在用户输入参数时,在参数中添加一些SQL关键字,达到改变SQL运行结果的目的,也可以完成恶意攻击。

测试1:使用资料中提供的程序,来验证SQL注入问题

image-20221210205419634

第1步:进入到DOS

image-20221211124744203

image-20221211124840720

第2步:执行以下命令,启动程序

#启动存在SQL注入的程序
java -jar sql_Injection_demo-0.0.1-SNAPSHOT.jar 

image-20221210211605231

第3步:打开浏览器输入http://localhost:9090/login.html

image-20221210212406527

发现竟然能够登录成功:

image-20221210212511915

以上操作为什么能够登录成功呢?

  • 由于没有对用户输入内容进行充分检查,而SQL又是字符串拼接方式而成,在用户输入参数时,在参数中添加一些SQL关键字,达到改变SQL运行结果的目的,从而完成恶意攻击。

image-20221210213311518

image-20221210214431228

用户在页面提交数据的时候人为的添加一些特殊字符,使得sql语句的结构发生了变化,最终可以在没有用户名或者密码的情况下进行登录。

测试2:使用资料中提供的程序,来验证SQL注入问题

第1步:进入到DOS

第2步:执行以下命令,启动程序:

#启动解决了SQL注入的程序
java -jar sql_prepared_demo-0.0.1-SNAPSHOT.jar

第3步:打开浏览器输入http://localhost:9090/login.html

image-20221210212406527

发现无法登录:

image-20221211125751981

以上操作SQL语句的执行:

image-20221211130011973

把整个' or '1'='1作为一个完整的参数,赋值给第2个问号(' or '1'='1进行了转义,只当做字符串使用)

参数占位符 #与$

在mybatis的mapper接口中,我们定义SQL语句,参数占位符可以使用 #{...}${...},那具体什么区别呢?

符号 说明 场景 优缺点
# 执行时,会将#{…}替换为?,生成预编译SQL,并自动设置参数值 参数值传递 安全、性能高 (推荐)
$ 拼接SQL。直接将参数拼接在SQL语句中,存在SQL注入问题 表名、字段名动态设置时使用 不安全、性能低

1). 如果我们在定义SQL语句的时候,使用的是 #{...}

image-20231130203428766

我们看到,最终生成的SQL语句是预编译的SQL语句。

2). 如果我们在定义SQL语句的时候,使用的是 ${...}

image-20231130203742136

新增

功能:新增员工信息

image-20221211134239610

基本新增

员工表结构:

image-20221211134746319

SQL语句:

insert into emp(username, name, gender, image, job, entrydate, dept_id, create_time, update_time) values ('songyuanqiao','宋远桥',1,'1.jpg',2,'2012-10-09',2,'2022-10-01 10:00:00','2022-10-01 10:00:00');

接口方法:

@Mapper
public interface EmpMapper {

    @Insert("insert into emp(username, name, gender, image, job, entrydate, dept_id, create_time, update_time) values (#{username}, #{name}, #{gender}, #{image}, #{job}, #{entrydate}, #{deptId}, #{createTime}, #{updateTime})")
    public void insert(Emp emp);

}

说明:#{...} 里面写的名称是对象的属性名

测试类:

import com.itheima.mapper.EmpMapper;
import com.itheima.pojo.Emp;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import java.time.LocalDate;
import java.time.LocalDateTime;

@SpringBootTest
class SpringbootMybatisCrudApplicationTests {
    @Autowired
    private EmpMapper empMapper;

    @Test
    public void testInsert(){
        //创建员工对象
        Emp emp = new Emp();
        emp.setUsername("tom");
        emp.setName("汤姆");
        emp.setImage("1.jpg");
        emp.setGender((short)1);
        emp.setJob((short)1);
        emp.setEntrydate(LocalDate.of(2000,1,1));
        emp.setCreateTime(LocalDateTime.now());
        emp.setUpdateTime(LocalDateTime.now());
        emp.setDeptId(1);
        //调用添加方法
        empMapper.insert(emp);
    }
}

日志输出:

image-20221211140222240

主键返回

概念:在数据添加成功后,需要获取插入数据库数据的主键。

如:添加套餐数据时,还需要维护套餐菜品关系表数据。

image-20221211150353385

业务场景:在前面讲解到的苍穹外卖菜品与套餐模块的表结构,菜品与套餐是多对多的关系,一个套餐对应多个菜品。既然是多对多的关系,是不是有一张套餐菜品中间表来维护它们之间的关系。

image-20221212093655389

在添加套餐的时候,我们需要在界面当中来录入套餐的基本信息,还需要来录入套餐与菜品的关联信息。这些信息录入完毕之后,我们一点保存,就需要将套餐的信息以及套餐与菜品的关联信息都需要保存到数据库当中。其实具体的过程包括两步,首先第一步先需要将套餐的基本信息保存了,接下来第二步再来保存套餐与菜品的关联信息。套餐与菜品的关联信息就是往中间表当中来插入数据,来维护它们之间的关系。而中间表当中有两个外键字段,一个是菜品的ID,就是当前菜品的ID,还有一个就是套餐的ID,而这个套餐的 ID 指的就是此次我所添加的套餐的ID,所以我们在第一步保存完套餐的基本信息之后,就需要将套餐的主键值返回来供第二步进行使用。这个时候就需要用到主键返回功能。

那要如何实现在插入数据之后返回所插入行的主键值呢?

方式一

  • 默认情况下,执行插入操作时,是不会主键值返回的。如果我们想要拿到主键值,需要在Mapper接口中的方法上添加一个Options注解,并在注解中指定属性useGeneratedKeys=true和keyProperty="实体类属性名"

主键返回代码实现:

@Mapper
public interface EmpMapper {
    
    //会自动将生成的主键值,赋值给emp对象的id属性
    @Options(useGeneratedKeys = true,keyProperty = "id")
    @Insert("insert into emp(username, name, gender, image, job, entrydate, dept_id, create_time, update_time) values (#{username}, #{name}, #{gender}, #{image}, #{job}, #{entrydate}, #{deptId}, #{createTime}, #{updateTime})")
    public void insert(Emp emp);

}

测试:

@SpringBootTest
class SpringbootMybatisCrudApplicationTests {
    @Autowired
    private EmpMapper empMapper;

    @Test
    public void testInsert(){
        //创建员工对象
        Emp emp = new Emp();
        emp.setUsername("jack");
        emp.setName("杰克");
        emp.setImage("1.jpg");
        emp.setGender((short)1);
        emp.setJob((short)1);
        emp.setEntrydate(LocalDate.of(2000,1,1));
        emp.setCreateTime(LocalDateTime.now());
        emp.setUpdateTime(LocalDateTime.now());
        emp.setDeptId(1);
        //调用添加方法
        empMapper.insert(emp);

        System.out.println(emp.getId());
    }
}

方式二

<insert id="insertDept">
        <selectKey resultType="java.lang.Integer" keyProperty="d.id" order="AFTER">
            select last_insert_id()
        </selectKey>
        insert into dept (name, create_time, update_time)
        values (#{d.name}, #{d.createTime}, #{d.updateTime})
    </insert>

方式三

<insert id="insertDept" useGeneratedKeys = true keyProperty = "d.id">
        insert into dept (name, create_time, update_time)
        values (#{d.name}, #{d.createTime}, #{d.updateTime})
    </insert>

更新

功能:修改员工信息

image-20221212095605863

点击"编辑"按钮后,会查询所在行记录的员工信息,并把员工信息回显在修改员工的窗体上(下个知识点学习)

在修改员工的窗体上,可以修改的员工数据:用户名、员工姓名、性别、图像、职位、入职日期、归属部门

思考:在修改员工数据时,要以什么做为条件呢?

答案:员工id

SQL语句:

update emp set username = 'linghushaoxia', name = '令狐少侠', gender = 1 , image = '1.jpg' , job = 2, entrydate = '2012-01-01', dept_id = 2, update_time = '2022-10-01 12:12:12' where id = 18;

接口方法:

@Mapper
public interface EmpMapper {
    /**
     * 根据id修改员工信息
     * @param emp
     */
    @Update("update emp set username=#{username}, name=#{name}, gender=#{gender}, image=#{image}, job=#{job}, entrydate=#{entrydate}, dept_id=#{deptId}, update_time=#{updateTime} where id=#{id}")
    public void update(Emp emp);
    
}

测试类:

@SpringBootTest
class SpringbootMybatisCrudApplicationTests {
    @Autowired
    private EmpMapper empMapper;

    @Test
    public void testUpdate(){
        //要修改的员工信息
        Emp emp = new Emp();
        emp.setId(23);
        emp.setUsername("songdaxia");
        emp.setPassword(null);
        emp.setName("老宋");
        emp.setImage("2.jpg");
        emp.setGender((short)1);
        emp.setJob((short)2);
        emp.setEntrydate(LocalDate.of(2012,1,1));
        emp.setCreateTime(null);
        emp.setUpdateTime(LocalDateTime.now());
        emp.setDeptId(2);
        //调用方法,修改员工数据
        empMapper.update(emp);
    }
}

查询

根据ID查询

在员工管理的页面中,当我们进行更新数据时,会点击 “编辑” 按钮,然后此时会发送一个请求到服务端,会根据Id查询该员工信息,并将员工数据回显在页面上。

image-20221212101331292

SQL语句:

select id, username, password, name, gender, image, job, entrydate, dept_id, create_time, update_time from emp;

接口方法:

@Mapper
public interface EmpMapper {
    @Select("select id, username, password, name, gender, image, job, entrydate, dept_id, create_time, update_time from emp where id=#{id}")
    public Emp getById(Integer id);
}

测试类:

@SpringBootTest
class SpringbootMybatisCrudApplicationTests {
    @Autowired
    private EmpMapper empMapper;

    @Test
    public void testGetById(){
        Emp emp = empMapper.getById(1);
        System.out.println(emp);
    }
}

执行结果:

image-20221212103004961

而在测试的过程中,我们会发现有几个字段(deptId、createTime、updateTime)是没有数据值的

数据封装

我们看到查询返回的结果中大部分字段是有值的,但是deptId,createTime,updateTime这几个字段是没有值的,而数据库中是有对应的字段值的,这是为什么呢?

image-20221212103124490

原因如下:

  • 实体类属性名和数据库表查询返回的字段名一致,mybatis会自动封装。
  • 如果实体类属性名和数据库表查询返回的字段名不一致,不能自动封装。

解决方案:

  1. 起别名
  2. 结果映射
  3. 开启驼峰命名

起别名:在SQL语句中,对不一样的列名起别名,别名和实体类属性名一样

@Select("select id, username, password, name, gender, image, job, entrydate, " +
        "dept_id AS deptId, create_time AS createTime, update_time AS updateTime " +
        "from emp " +
        "where id=#{id}")
public Emp getById(Integer id);

再次执行测试类:

image-20221212111027396

手动结果映射:通过 @Results及@Result 进行手动结果映射

@Results({@Result(column = "dept_id", property = "deptId"),
          @Result(column = "create_time", property = "createTime"),
          @Result(column = "update_time", property = "updateTime")})
@Select("select id, username, password, name, gender, image, job, entrydate, dept_id, create_time, update_time from emp where id=#{id}")
public Emp getById(Integer id);

@Results源代码:

@Documented
@Retention(RetentionPolicy.RUNTIME)
@Target({ElementType.METHOD})
public @interface Results {
String id() default "";

Result[] value() default {};  //Result类型的数组
}

@Result源代码:

@Documented
@Retention(RetentionPolicy.RUNTIME)
@Target({ElementType.METHOD})
@Repeatable(Results.class)
public @interface Result {
boolean id() default false;//表示当前列是否为主键(true:是主键)

String column() default "";//指定表中字段名

String property() default "";//指定类中属性名

Class<?> javaType() default void.class;

JdbcType jdbcType() default JdbcType.UNDEFINED;

Class<? extends TypeHandler> typeHandler() default UnknownTypeHandler.class;

One one() default @One;

Many many() default @Many;
}

开启驼峰命名(推荐):如果字段名与属性名符合驼峰命名规则,mybatis会自动通过驼峰命名规则映射

驼峰命名规则: abc_xyz => abcXyz

  • 表中字段名:abc_xyz
  • 类中属性名:abcXyz
# 在application.properties中添加:
mybatis.configuration.map-underscore-to-camel-case=true

要使用驼峰命名前提是 实体类的属性 与 数据库表中的字段名严格遵守驼峰命名。

条件查询

在员工管理的列表页面中,我们需要根据条件查询员工信息,查询条件包括:姓名、性别、入职时间。

image-20221212113422924

通过页面原型以及需求描述我们要实现的查询:

  • 姓名:要求支持模糊匹配
  • 性别:要求精确匹配
  • 入职时间:要求进行范围查询
  • 根据最后修改时间进行降序排序

SQL语句:

select id, username, password, name, gender, image, job, entrydate, dept_id, create_time, update_time 
from emp 
where name like '%张%' 
      and gender = 1 
      and entrydate between '2010-01-01' and '2020-01-01 ' 
order by update_time desc;

接口方法:

  • 方式一
@Mapper
public interface EmpMapper {
    @Select("select * from emp " +
            "where name like '%${name}%' " +
            "and gender = #{gender} " +
            "and entrydate between #{begin} and #{end} " +
            "order by update_time desc")
    public List<Emp> list(String name, Short gender, LocalDate begin, LocalDate end);
}

image-20221212115149151

以上方式注意事项:

  1. 方法中的形参名和SQL语句中的参数占位符名保持一致

  2. 模糊查询使用${...}进行字符串拼接,这种方式呢,由于是字符串拼接,并不是预编译的形式,所以效率不高、且存在sql注入风险。

  • 方式二(解决SQL注入风险)
    • 使用MySQL提供的字符串拼接函数:concat('%' , '关键字' , '%')
@Mapper
public interface EmpMapper {

    @Select("select * from emp " +
            "where name like concat('%',#{name},'%') " +
            "and gender = #{gender} " +
            "and entrydate between #{begin} and #{end} " +
            "order by update_time desc")
    public List<Emp> list(String name, Short gender, LocalDate begin, LocalDate end);

}

执行结果:生成的SQL都是预编译的SQL语句(性能高、安全)

image-20221212120006242

测试方案:

@Test
    public void testList(){
        List<Emp> list = empMapper.list("张",(short) 1,LocalDate.of(2010,1,1),LocalDate.of(2020,1,1));
        System.out.println(list);
    }

参数名说明

在上面我们所编写的条件查询功能中,我们需要保证接口中方法的形参名和SQL语句中的参数占位符名相同。

当方法中的形参名和SQL语句中的占位符参数名不相同时,就会出现以下问题:

image-20221212150611796

参数名在不同的SpringBoot版本中,处理方案还不同:

  • 在springBoot的2.x版本(保证参数名一致)

image-20221212151156273

springBoot的父工程对compiler编译插件进行了默认的参数parameters配置,使得在编译时,会在生成的字节码文件中保留原方法形参的名称,所以#{…}里面可以直接通过形参名获取对应的值

image-20221212151411154

  • 在springBoot的1.x版本/单独使用mybatis(使用@Param注解来指定SQL语句中的参数名)

image-20221212151628715

在编译时,生成的字节码文件当中,不会保留Mapper接口中方法的形参名称,而是使用var1、var2、...这样的形参名字,此时要获取参数值时,就要通过@Param注解来指定SQL语句中的参数名

image-20221212151736274

部门列表查询

功能实现

需求

查询数据库表中的所有部门数据,展示在页面上。

image-20231130190642478

实现

  1. 准备数据库表 dept 及 实体类 Dept

    -- 部门管理
    create table dept(
        id int unsigned primary key auto_increment comment '主键ID',
        name varchar(10) not null unique comment '部门名称',
        create_time datetime  comment '创建时间',
        update_time datetime  comment '修改时间'
    ) comment '部门表';
    
    INSERT INTO `dept` VALUES (1,'学工部','2023-09-25 09:47:40','2023-09-25 09:47:40'),
    						(2,'教研部','2023-09-25 09:47:40','2023-09-25 09:47:40'),
    						(3,'咨询部','2023-09-25 09:47:40','2023-09-25 09:47:40'),
    						(4,'就业部','2023-09-25 09:47:40','2023-09-25 09:47:40'),
    						(5,'人事部','2023-09-25 09:47:40','2023-09-25 09:47:40'),
    						(6,'行政部','2023-09-27 14:00:00','2023-09-27 14:00:00'),
    						(7,'综合部','2023-09-25 14:44:19','2023-09-25 14:44:19');
    
    import lombok.AllArgsConstructor;
    import lombok.Data;
    import lombok.NoArgsConstructor;
    import java.time.LocalDateTime;
    
    @Data
    @NoArgsConstructor
    @AllArgsConstructor
    public class Dept {
        private Integer id;
        private String name;
        private LocalDateTime createTime;
        private LocalDateTime updateTime;
    }
    
    
  2. 在项目中引入Mybatis的起步依赖,mysql的驱动包

    <dependency>
        <groupId>org.mybatis.spring.boot</groupId>
        <artifactId>mybatis-spring-boot-starter</artifactId>
        <version>3.0.3</version>
    </dependency>
    
    <dependency>
        <groupId>com.mysql</groupId>
        <artifactId>mysql-connector-j</artifactId>
        <scope>runtime</scope>
    </dependency>
    
  3. 在项目的application.properties 中引入Mybatis的配置信息 (数据库连接、日志输出)

    #数据库连接信息
    spring.datasource.url=jdbc:mysql://localhost:3306/web01
    spring.datasource.driver-class-name=com.mysql.cj.jdbc.Driver
    spring.datasource.username=root
    spring.datasource.password=root@1234
    
    #mybatis 日志
    mybatis.configuration.log-impl=org.apache.ibatis.logging.stdout.StdOutImpl
    
  4. 定义mapper包,并且定义DeptMapper接口,并声明接口方法。

    import com.itheima.pojo.Dept;
    import org.apache.ibatis.annotations.Mapper;
    import org.apache.ibatis.annotations.Select;
    import java.util.List;
    
    @Mapper
    public interface DeptMapper {
    
        @Select("select * from dept")
        public List<Dept> findAll();
    
    }
    
  5. 改造之前编写的dao、service的代码,在service实现中注入mapper接口

    • dao层的代码不需要了(备份之后,可以删除)

    • service层的代码,需要注入Mapper接口,调用mapper接口方法查询数据库中的数据

      @Service
      public class DeptServiceImpl implements DeptService {
         
          @Autowired
          private DeptMapper deptMapper;
      
          @Override
          public List<Dept> queryDeptList() {
              return deptMapper.findAll();
          }
      }
      
  6. 启动服务,打开apifox进行测试

    image-20231130192402337

    image-20231130192424671

​ 经过测试,我们发现,创建时间 createTime,修改时间 updateTime 属性并未成功封装。 接下来,我们就需要来处理数据封装问题。

数据封装

我们看到查询返回的结果中大部分字段是有值的,但是createTime,updateTime这两个字段是没有值的,而数据库中是有对应的字段值的,这是为什么呢?

image-20231130192556070

原因如下:

  • 实体类属性名和数据库表查询返回的字段名一致,mybatis会自动封装。
  • 如果实体类属性名和数据库表查询返回的字段名不一致,不能自动封装。

解决方案:

  1. 起别名
  2. 结果映射
  3. 开启驼峰命名

1. 起别名:在SQL语句中,对不一样的列名起别名,别名和实体类属性名一样

@Select("select id, name, create_time createTime, update_time updateTime from dept")
public List<Dept> findAll();

2. 手动结果映射:通过 @Results及@Result 进行手动结果映射

@Results({@Result(column = "create_time", property = "createTime"),
          @Result(column = "update_time", property = "updateTime")})
@Select("select id, name, create_time, update_time from dept")
public List<Dept> findAll();

@Results源代码:

@Documented
@Retention(RetentionPolicy.RUNTIME)
@Target({ElementType.METHOD})
public @interface Results {
	String id() default "";
	Result[] value() default {};  //Result类型的数组
}

@Result源代码:

@Documented
@Retention(RetentionPolicy.RUNTIME)
@Target({ElementType.METHOD})
@Repeatable(Results.class)
public @interface Result {
	boolean id() default false;//表示当前列是否为主键(true:是主键)
	String column() default "";//指定表中字段名
	String property() default "";//指定类中属性名
}

3. 开启驼峰命名(推荐):如果字段名与属性名符合驼峰命名规则,mybatis会自动通过驼峰命名规则映射

驼峰命名规则: abc_xyz => abcXyz

  • 表中字段名:abc_xyz
  • 类中属性名:abcXyz
# 在application.properties中添加:
mybatis.configuration.map-underscore-to-camel-case=true

要使用驼峰命名前提是 实体类的属性 与 数据库表中的字段名严格遵守驼峰命名。

Mybatis的XML配置文件

Mybatis的开发有两种方式:

  1. 注解
  2. XML

XML配置文件规范

使用Mybatis的注解方式,主要是来完成一些简单的增删改查功能。如果需要实现复杂的SQL功能,建议使用XML来配置映射语句,也就是将SQL语句写在XML配置文件中。

在Mybatis中使用XML映射文件方式开发,需要符合一定的规范:

  1. XML映射文件的名称与Mapper接口名称一致,并且将XML映射文件和Mapper接口放置在相同包下(同包同名)

  2. XML映射文件的namespace属性为Mapper接口全限定名一致

  3. XML映射文件中sql语句的id与Mapper接口中的方法名一致,并保持返回类型一致。

image-20221212153529732

<select>标签:就是用于编写select查询语句的。

  • resultType属性,指的是查询返回的单条记录所封装的类型。

XML配置文件实现

第1步:创建XML映射文件

image-20221212154908306

image-20221212155304635

image-20221212155544404

第2步:编写XML映射文件

xml映射文件中的dtd约束,直接从mybatis官网复制即可

<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE mapper
  PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
  "https://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="">
 
</mapper>

配置:XML映射文件的namespace属性为Mapper接口全限定名

image-20221212160316644

<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE mapper
        PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
        "https://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.itheima.mapper.EmpMapper">

</mapper>

配置:XML映射文件中sql语句的id与Mapper接口中的方法名一致,并保持返回类型一致

image-20221212163528787

<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE mapper
        PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
        "https://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.itheima.mapper.EmpMapper">

    <!--查询操作-->
    <select id="list" resultType="com.itheima.pojo.Emp">
        select * from emp
        where name like concat('%',#{name},'%')
              and gender = #{gender}
              and entrydate between #{begin} and #{end}
        order by update_time desc
    </select>
</mapper>

运行测试类,执行结果:

image-20221212163719534

MybatisX的使用

MybatisX是一款基于IDEA的快速开发Mybatis的插件,为效率而生。

MybatisX的安装:

image-20221213120923252

可以通过MybatisX快速定位:

image-20221213121521406

MybatisX的使用在后续学习中会继续分享

学习了Mybatis中XML配置文件的开发方式了,大家可能会存在一个疑问:到底是使用注解方式开发还是使用XML方式开发?

官方说明:https://mybatis.net.cn/getting-started.html

image-20220901173948645

结论:使用Mybatis的注解,主要是来完成一些简单的增删改查功能。如果需要实现复杂的SQL功能,建议使用XML来配置映射语句。

Mybatis动态SQL

什么是动态SQL

在页面原型中,列表上方的条件是动态的,是可以不传递的,也可以只传递其中的1个或者2个或者全部。

image-20220901173203491

而在我们刚才编写的SQL语句中,我们会看到,我们将三个条件直接写死了。 如果页面只传递了参数姓名name 字段,其他两个字段 性别 和 入职时间没有传递,那么这两个参数的值就是null。

此时,执行的SQL语句为:

image-20220901173431554

这个查询结果是不正确的。正确的做法应该是:传递了参数,再组装这个查询条件;如果没有传递参数,就不应该组装这个查询条件。

比如:如果姓名输入了"张", 对应的SQL为:

select *  from emp where name like '%张%' order by update_time desc;

如果姓名输入了"张",,性别选择了"男",则对应的SQL为:

select *  from emp where name like '%张%' and gender = 1 order by update_time desc;

SQL语句会随着用户的输入或外部条件的变化而变化,我们称为:动态SQL

image-20221213122623278

在Mybatis中提供了很多实现动态SQL的标签,我们学习Mybatis中的动态SQL就是掌握这些动态SQL标签。

动态SQL-if

<if>:用于判断条件是否成立。使用test属性进行条件判断,如果条件为true,则拼接SQL。

<if test="条件表达式">
   要拼接的sql语句
</if>

接下来,我们就通过<if>标签来改造之前条件查询的案例。

条件查询

示例:把SQL语句改造为动态SQL方式

  • 原有的SQL语句
<select id="list" resultType="com.itheima.pojo.Emp">
        select * from emp
        where name like concat('%',#{name},'%')
              and gender = #{gender}
              and entrydate between #{begin} and #{end}
        order by update_time desc
</select>
  • 动态SQL语句
<select id="list" resultType="com.itheima.pojo.Emp">
        select * from emp
        where
    
             <if test="name != null">
                 name like concat('%',#{name},'%')
             </if>
             <if test="gender != null">
                 and gender = #{gender}
             </if>
             <if test="begin != null and end != null">
                 and entrydate between #{begin} and #{end}
             </if>
    
        order by update_time desc
</select>

测试方法:

@Test
public void testList(){
    //性别数据为null、开始时间和结束时间也为null
    List<Emp> list = empMapper.list("张", null, null, null);
    for(Emp emp : list){
        System.out.println(emp);
    }
}

执行的SQL语句:

image-20221213140353285

下面呢,我们修改测试方法中的代码,再次进行测试,观察执行情况:

@Test
public void testList(){
    //姓名为null
    List<Emp> list = empMapper.list(null, (short)1, null, null);
    for(Emp emp : list){
        System.out.println(emp);
    }
}

执行结果:

image-20221213141139015

image-20221213141253355

再次修改测试方法中的代码,再次进行测试:

@Test
public void testList(){
    //传递的数据全部为null
    List<Emp> list = empMapper.list(null, null, null, null);
    for(Emp emp : list){
        System.out.println(emp);
    }
}

执行的SQL语句:

image-20221213143854434

以上问题的解决方案:使用<where>标签代替SQL语句中的where关键字

  • <where>只会在子元素有内容的情况下才插入where子句,而且会自动去除子句的开头的AND或OR
<select id="list" resultType="com.itheima.pojo.Emp">
        select * from emp
        <where>
             <!-- if做为where标签的子元素 -->
             <if test="name != null">
                 and name like concat('%',#{name},'%')
             </if>
             <if test="gender != null">
                 and gender = #{gender}
             </if>
             <if test="begin != null and end != null">
                 and entrydate between #{begin} and #{end}
             </if>
        </where>
        order by update_time desc
</select>

测试方法:

@Test
public void testList(){
    //只有性别
    List<Emp> list = empMapper.list(null, (short)1, null, null);
    for(Emp emp : list){
        System.out.println(emp);
    }
}

执行的SQL语句:

image-20221213141909455

更新员工

案例:完善更新员工功能,修改为动态更新员工数据信息

  • 动态更新员工信息,如果更新时传递有值,则更新;如果更新时没有传递值,则不更新
  • 解决方案:动态SQL

修改Mapper接口:

@Mapper
public interface EmpMapper {
    //删除@Update注解编写的SQL语句
    //update操作的SQL语句编写在Mapper映射文件中
    public void update(Emp emp);
}

修改Mapper映射文件:

<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE mapper
        PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
        "https://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.itheima.mapper.EmpMapper">

    <!--更新操作-->
    <update id="update">
        update emp
        set
            <if test="username != null">
                username=#{username},
            </if>
            <if test="name != null">
                name=#{name},
            </if>
            <if test="gender != null">
                gender=#{gender},
            </if>
            <if test="image != null">
                image=#{image},
            </if>
            <if test="job != null">
                job=#{job},
            </if>
            <if test="entrydate != null">
                entrydate=#{entrydate},
            </if>
            <if test="deptId != null">
                dept_id=#{deptId},
            </if>
            <if test="updateTime != null">
                update_time=#{updateTime}
            </if>
        where id=#{id}
    </update>

</mapper>

测试方法:

@Test
public void testUpdate2(){
        //要修改的员工信息
        Emp emp = new Emp();
        emp.setId(20);
        emp.setUsername("Tom111");
        emp.setName("汤姆111");

        emp.setUpdateTime(LocalDateTime.now());

        //调用方法,修改员工数据
        empMapper.update(emp);
}

执行的SQL语句:

image-20221213152533851

再次修改测试方法,观察SQL语句执行情况:

@Test
public void testUpdate2(){
        //要修改的员工信息
        Emp emp = new Emp();
        emp.setId(20);
        emp.setUsername("Tom222");
      
        //调用方法,修改员工数据
        empMapper.update(emp);
}

执行的SQL语句:

image-20221213152850322

以上问题的解决方案:使用<set>标签代替SQL语句中的set关键字

  • <set>:动态的在SQL语句中插入set关键字,并会删掉额外的逗号。(用于update语句中)
<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE mapper
        PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
        "https://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.itheima.mapper.EmpMapper">

    <!--更新操作-->
    <update id="update">
        update emp
        <!-- 使用set标签,代替update语句中的set关键字 -->
        <set>
            <if test="username != null">
                username=#{username},
            </if>
            <if test="name != null">
                name=#{name},
            </if>
            <if test="gender != null">
                gender=#{gender},
            </if>
            <if test="image != null">
                image=#{image},
            </if>
            <if test="job != null">
                job=#{job},
            </if>
            <if test="entrydate != null">
                entrydate=#{entrydate},
            </if>
            <if test="deptId != null">
                dept_id=#{deptId},
            </if>
            <if test="updateTime != null">
                update_time=#{updateTime}
            </if>
        </set>
        where id=#{id}
    </update>
</mapper>

再次执行测试方法,执行的SQL语句:

image-20221213153329553

小结

  • <if>

    • 用于判断条件是否成立,如果条件为true,则拼接SQL

    • 形式:

      <if test="name != null"> … </if>
      
  • <where>

    • where元素只会在子元素有内容的情况下才插入where子句,而且会自动去除子句的开头的AND或OR
  • <set>

    • 动态地在行首插入 SET 关键字,并会删掉额外的逗号。(用在update语句中)

动态SQL-foreach

案例:员工删除功能(既支持删除单条记录,又支持批量删除)

image-20220901181751004

SQL语句:

delete from emp where id in (1,2,3);

Mapper接口:

@Mapper
public interface EmpMapper {
    //批量删除
    public void deleteByIds(List<Integer> ids);
}

XML映射文件:

  • 使用<foreach>遍历deleteByIds方法中传递的参数ids集合
<foreach collection="集合名称" item="集合遍历出来的元素/项" separator="每一次遍历使用的分隔符" 
         open="遍历开始前拼接的片段" close="遍历结束后拼接的片段">
</foreach>
<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE mapper
        PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
        "https://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.itheima.mapper.EmpMapper">
    <!--删除操作-->
    <delete id="deleteByIds">
        delete from emp where id in
        <foreach collection="ids" item="id" separator="," open="(" close=")">
            #{id}
        </foreach>
    </delete>
</mapper> 

image-20221213165710141

执行的SQL语句:

image-20221213164957636

测试方法:

// 批量删除员工 -> 13,14,15
    @Test
    public void testDeleteByIds(){
        List<Integer> ids = Arrays.asList(13, 14, 15);
        empMapper.deleteByIds(ids);
    }

动态SQL-sql&include

问题分析:

  • 在xml映射文件中配置的SQL,有时可能会存在很多重复的片段,此时就会存在很多冗余的代码

我们可以对重复的代码片段进行抽取,将其通过<sql>标签封装到一个SQL片段,然后再通过<include>标签进行引用。

  • <sql>:定义可重用的SQL片段

  • <include>:通过属性refid,指定包含的SQL片段

image-20221213171244796

SQL片段: 抽取重复的代码

<sql id="commonSelect">
 	select id, username, password, name, gender, image, job, entrydate, dept_id, create_time, update_time from emp
</sql>

然后通过<include> 标签在原来抽取的地方进行引用。操作如下:

<select id="list" resultType="com.itheima.pojo.Emp">
    <include refid="commonSelect"/>
    <where>
        <if test="name != null">
            name like concat('%',#{name},'%')
        </if>
        <if test="gender != null">
            and gender = #{gender}
        </if>
        <if test="begin != null and end != null">
            and entrydate between #{begin} and #{end}
        </if>
    </where>
    order by update_time desc
</select>

if

使用动态 SQL 最常见情景是根据条件包含 where 子句的一部分。比如:

<select id="findActiveBlogWithTitleLike"
     resultType="Blog">
  SELECT * FROM BLOG
  WHERE state = ‘ACTIVE’
  <if test="title != null">
    AND title like #{title}
  </if>
</select>

这条语句提供了可选的查找文本功能。如果不传入 “title”,那么所有处于 “ACTIVE” 状态的 BLOG 都会返回;如果传入了 “title” 参数,那么就会对 “title” 一列进行模糊查找并返回对应的 BLOG 结果(细心的读者可能会发现,“title” 的参数值需要包含查找掩码或通配符字符)。

如果希望通过 “title” 和 “author” 两个参数进行可选搜索该怎么办呢?首先,我想先将语句名称修改成更名副其实的名称;接下来,只需要加入另一个条件即可。

<select id="findActiveBlogLike"
     resultType="Blog">
  SELECT * FROM BLOG WHERE state = ‘ACTIVE’
  <if test="title != null">
    AND title like #{title}
  </if>
  <if test="author != null and author.name != null">
    AND author_name like #{author.name}
  </if>
</select>

choose、when、otherwise

有时候,我们不想使用所有的条件,而只是想从多个条件中选择一个使用。针对这种情况,MyBatis 提供了 choose 元素,它有点像 Java 中的 switch 语句。

还是上面的例子,但是策略变为:传入了 “title” 就按 “title” 查找,传入了 “author” 就按 “author” 查找的情形。若两者都没有传入,就返回标记为 featured 的 BLOG(这可能是管理员认为,与其返回大量的无意义随机 Blog,还不如返回一些由管理员挑选的 Blog)。

<select id="findActiveBlogLike"
     resultType="Blog">
  SELECT * FROM BLOG WHERE state = ‘ACTIVE’
  <choose>
    <when test="title != null">
      AND title like #{title}
    </when>
    <when test="author != null and author.name != null">
      AND author_name like #{author.name}
    </when>
    <otherwise>
      AND featured = 1
    </otherwise>
  </choose>
</select>

trim、where、set

前面几个例子已经合宜地解决了一个臭名昭著的动态 SQL 问题。现在回到之前的 “if” 示例,这次我们将 “state = ‘ACTIVE’” 设置成动态条件,看看会发生什么。

<select id="findActiveBlogLike"
     resultType="Blog">
  SELECT * FROM BLOG
  WHERE
  <if test="state != null">
    state = #{state}
  </if>
  <if test="title != null">
    AND title like #{title}
  </if>
  <if test="author != null and author.name != null">
    AND author_name like #{author.name}
  </if>
</select>

如果没有匹配的条件会怎么样?最终这条 SQL 会变成这样:

SELECT * FROM BLOG
WHERE

这会导致查询失败。如果匹配的只是第二个条件又会怎样?这条 SQL 会是这样:

SELECT * FROM BLOG
WHERE
AND title like ‘someTitle’

这个查询也会失败。这个问题不能简单地用条件元素来解决。这个问题是如此的难以解决,以至于解决过的人不会再想碰到这种问题。

MyBatis 有一个简单且适合大多数场景的解决办法。而在其他场景中,可以对其进行自定义以符合需求。而这,只需要一处简单的改动:

<select id="findActiveBlogLike"
     resultType="Blog">
  SELECT * FROM BLOG
  <where>
    <if test="state != null">
         state = #{state}
    </if>
    <if test="title != null">
        AND title like #{title}
    </if>
    <if test="author != null and author.name != null">
        AND author_name like #{author.name}
    </if>
  </where>
</select>

where 元素只会在子元素返回任何内容的情况下才插入 “WHERE” 子句。而且,若子句的开头为 “AND” 或 “OR”,where 元素也会将它们去除。

如果 where 元素与你期望的不太一样,你也可以通过自定义 trim 元素来定制 where 元素的功能。比如,和 where 元素等价的自定义 trim 元素为:

<trim prefix="WHERE" prefixOverrides="AND |OR ">
  ...
</trim>

prefixOverrides 属性会忽略通过管道符分隔的文本序列(注意此例中的空格是必要的)。上述例子会移除所有 prefixOverrides 属性中指定的内容,并且插入 prefix 属性中指定的内容。

用于动态更新语句的类似解决方案叫做 setset 元素可以用于动态包含需要更新的列,忽略其它不更新的列。比如:

<update id="updateAuthorIfNecessary">
  update Author
    <set>
      <if test="username != null">username=#{username},</if>
      <if test="password != null">password=#{password},</if>
      <if test="email != null">email=#{email},</if>
      <if test="bio != null">bio=#{bio}</if>
    </set>
  where id=#{id}
</update>

这个例子中,set 元素会动态地在行首插入 SET 关键字,并会删掉额外的逗号(这些逗号是在使用条件语句给列赋值时引入的)。

来看看与 set 元素等价的自定义 trim 元素吧:

<trim prefix="SET" suffixOverrides=",">
  ...
</trim>

注意,我们覆盖了后缀值设置,并且自定义了前缀值。

foreach

动态 SQL 的另一个常见使用场景是对集合进行遍历(尤其是在构建 IN 条件语句的时候)。比如:

<select id="selectPostIn" resultType="domain.blog.Post">
  SELECT *
  FROM POST P
  WHERE ID in
  <foreach item="item" index="index" collection="list"
      open="(" separator="," close=")">
        #{item}
  </foreach>
</select>

foreach 元素的功能非常强大,它允许你指定一个集合,声明可以在元素体内使用的集合项(item)和索引(index)变量。它也允许你指定开头与结尾的字符串以及集合项迭代之间的分隔符。这个元素也不会错误地添加多余的分隔符,看它多智能!

提示 你可以将任何可迭代对象(如 List、Set 等)、Map 对象或者数组对象作为集合参数传递给 foreach。当使用可迭代对象或者数组时,index 是当前迭代的序号,item 的值是本次迭代获取到的元素。当使用 Map 对象(或者 Map.Entry 对象的集合)时,index 是键,item 是值。

selectKey

这里有一个简单(也很傻)的示例,它可以生成一个随机 ID(不建议实际使用,这里只是为了展示 MyBatis 处理问题的灵活性和宽容度):

<insert id="insertAuthor">
  <selectKey keyProperty="id" resultType="int" order="BEFORE">
    select CAST(RANDOM()*1000000 as INTEGER) a from SYSIBM.SYSDUMMY1
  </selectKey>
  insert into Author
    (id, username, password, email,bio, favourite_section)
  values
    (#{id}, #{username}, #{password}, #{email}, #{bio}, #{favouriteSection,jdbcType=VARCHAR})
</insert>

在上面的示例中,首先会运行 selectKey 元素中的语句,并设置 Author 的 id,然后才会调用插入语句。这样就实现了数据库自动生成主键类似的行为,同时保持了 Java 代码的简洁。

SelectKey需要注意order属性,像Mysql一类支持自动增长类型的数据库中,order需要设置为after才会取到正确的值。

selectKey 元素描述如下:

<selectKey
  keyProperty="id"
  resultType="int"
  order="BEFORE"
  statementType="PREPARED">
属性 描述
keyProperty selectKey 语句结果应该被设置到的目标属性。如果生成列不止一个,可以用逗号分隔多个属性名称。
keyColumn 返回结果集中生成列属性的列名。如果生成列不止一个,可以用逗号分隔多个属性名称。
resultType 结果的类型。通常 MyBatis 可以推断出来,但是为了更加准确,写上也不会有什么问题。MyBatis 允许将任何简单类型用作主键的类型,包括字符串。如果生成列不止一个,则可以使用包含期望属性的 Object 或 Map。
order 可以设置为 BEFOREAFTER。如果设置为 BEFORE,那么它首先会生成主键,设置 keyProperty 再执行插入语句。如果设置为 AFTER,那么先执行插入语句,然后是 selectKey 中的语句 - 这和 Oracle 数据库的行为相似,在插入语句内部可能有嵌入索引调用。
statementType 和前面一样,MyBatis 支持 STATEMENTPREPAREDCALLABLE 类型的映射语句,分别代表 Statement, PreparedStatementCallableStatement 类型。

mybatis的标签主要可以用来获取自增主键id的值并进行设置,SELECT LAST_INSERT_ID() 该sql的作用返回最近一次插入的id通常用来配合标签来使用 ,但要注意假如用insert同时插入多条sql,其只能返回插入的第一条记录的自增主键id因此是不支持批量插入获取主键值的

#mapper.xml
  <insert id="insert">
     <selectKey keyProperty="userId" keyColumn="user_id" order="AFTER" resultType="integer">
       select last_insert_id()
     </selectKey>
     insert into user(username, password, nickname)
     values(#{username}, #{password}, #{nickname})
  </insert>

trim

搭配where使用

<select id="selectUserByUsernameAndSex" resultType="user" parameterType="com.ys.po.User">
        select * from user
        <!-- <where>
            <if test="username != null">
               username=#{username}
            </if>
             
            <if test="username != null">
               and sex=#{sex}
            </if>
        </where>  -->
        <trim prefix="where" prefixOverrides="and | or">
            <if test="username != null">
               and username=#{username}
            </if>
            <if test="sex != null">
               and sex=#{sex}
            </if>
        </trim>
    </select>

搭配set使用

<!-- 根据 id 更新 user 表的数据 -->
    <update id="updateUserById" parameterType="com.ys.po.User">
        update user u
            <!-- <set>
                <if test="username != null and username != ''">
                    u.username = #{username},
                </if>
                <if test="sex != null and sex != ''">
                    u.sex = #{sex}
                </if>
            </set> -->
            <trim prefix="set" suffixOverrides=",">
                <if test="username != null and username != ''">
                    u.username = #{username},
                </if>
                <if test="sex != null and sex != ''">
                    u.sex = #{sex},
                </if>
            </trim>
         
         where id=#{id}
    </update>

动态插入

<insert id="add">
        insert  into student
        <trim prefix="(" suffix=")" suffixOverrides=",">
            <if test="sname != null">sname,</if>
            <if test="spwd != null">spwd,</if>
            <if test="sex != null">sex,</if>
            <if test="phone != null">phone,</if>
        </trim>

        <trim prefix="values (" suffix=")"  suffixOverrides=",">
            <if test="sname != null">#{sname},</if>
            <if test="spwd != null">#{spwd},</if>
            <if test="sex != null">#{sex},</if>
            <if test="phone != null">#{phone}</if>
        </trim>

    </insert>

resultMap

查询回显

2.1.1 接口文档

根据ID查询员工数据

  • 基本信息

    请求路径:/emps/{id}
    
    请求方式:GET
    
    接口描述:该接口用于根据主键ID查询员工的信息
    
  • 请求参数

    参数格式:路径参数

    参数说明:

    参数名 类型 是否必须 备注
    id number 必须 员工ID

    请求参数样例:

    /emps/1
    
  • 响应数据

    参数格式:application/json

    参数说明:

    名称 类型 是否必须 备注
    code number 必须 响应码, 1 成功 , 0 失败
    msg string 非必须 提示信息
    data object 必须 返回的数据
    |- id number 非必须 id
    |- username string 非必须 用户名
    |- name string 非必须 姓名
    |- password string 非必须 密码
    |- entryDate string 非必须 入职日期
    |- gender number 非必须 性别 , 1 男 ; 2 女
    |- image string 非必须 图像
    |- job number 非必须 职位, 说明: 1 班主任,2 讲师, 3 学工主管, 4 教研主管, 5 咨询师
    |- salary number 非必须 薪资
    |- deptId number 非必须 部门id
    |- createTime string 非必须 创建时间
    |- updateTime string 非必须 更新时间
    |- exprList object[] 非必须 工作经历列表
    |- id number 非必须 ID
    |- company string 非必须 所在公司
    |- job string 非必须 职位
    |- begin string 非必须 开始时间
    |- end string 非必须 结束时间
    |- empId number 非必须 员工ID

    响应数据样例:

    {
      "code": 1,
      "msg": "success",
      "data": {
        "id": 2,
        "username": "zhangwuji",
        "password": "123456",
        "name": "张无忌",
        "gender": 1,
        "image": "https://web-framework.oss-cn-hangzhou.aliyuncs.com/2022-09-02-00-27-53B.jpg",
        "job": 2,
        "salary": 8000,
        "entryDate": "2015-01-01",
        "deptId": 2,
        "createTime": "2022-09-01T23:06:30",
        "updateTime": "2022-09-02T00:29:04",
        "exprList": [
          {
            "id": 1,
            "begin": "2012-07-01",
            "end": "2019-03-03"
            "company": "百度科技股份有限公司"
            "job": "java开发",
            "empId": 2
          },
          {
            "id": 2,
            "begin": "2019-3-15",
            "end": "2023-03-01"
            "company": "阿里巴巴科技股份有限公司"
            "job": "架构师",
            "empId": 2
          }
        ]
      }
    }
    

2.1.2 实现思路

在查询回显时,既需要查询出员工的基本信息,又需要查询出该员工的工作经历信息。

image-20231206150751994

我们可以先通过一条SQL语句,查询出指定员工的基本信息,及其员工的工作经历信息。SQL如下:

select e.*,
       ee.id ee_id,
       ee.begin ee_begin,
       ee.end ee_end,
       ee.company ee_company,
       ee.job ee_job
from emp e left join emp_expr ee on e.id = ee.emp_id where e.id = 39;

具体的实现思路如下:

image-20231206150841795

2.1.3 代码实现

1). EmpController 添加 getInfo 用来根据ID查询员工数据,用于页面回显

/**
 * 查询回显
 */
@GetMapping("/{id}")
public Result getInfo(@PathVariable Integer id){
    log.info("根据id查询员工的详细信息");
    Emp emp  = empService.getInfo(id);
    return Result.success(emp);
}

2). EmpService 接口中增加 getInfo 方法

/**
 * 根据ID查询员工的详细信息
 */
Emp getInfo(Integer id);

3). EmpServiceImpl 实现类中实现 getInfo 方法

@Override
public Emp getInfo(Integer id) {
    return empMapper.getById(id);
}

4). EmpMapper 接口中增加 getById 方法

/**
 * 根据ID查询员工详细信息
 */
Emp getById(Integer id);

5). EmpMapper.xml 配置文件中定义对应的SQL

<!--自定义结果集ResultMap-->
<resultMap id="empResultMap" type="com.itheima.pojo.Emp">
    <id column="id" property="id" />
    <result column="username" property="username" />
    <result column="password" property="password" />
    <result column="name" property="name" />
    <result column="gender" property="gender" />
    <result column="phone" property="phone" />
    <result column="job" property="job" />
    <result column="salary" property="salary" />
    <result column="image" property="image" />
    <result column="entry_date" property="entryDate" />
    <result column="dept_id" property="deptId" />
    <result column="create_time" property="createTime" />
    <result column="update_time" property="updateTime" />

    <!--封装exprList-->
    <collection property="exprList" ofType="com.itheima.pojo.EmpExpr">
        <id column="ee_id" property="id"/>
        <result column="ee_company" property="company"/>
        <result column="ee_job" property="job"/>
        <result column="ee_begin" property="begin"/>
        <result column="ee_end" property="end"/>
        <result column="ee_empid" property="empId"/>
    </collection>
</resultMap>

<!--根据ID查询员工的详细信息-->
<select id="getById" resultMap="empResultMap">
    select e.*,
        ee.id ee_id,
        ee.emp_id ee_empid,
        ee.begin ee_begin,
        ee.end ee_end,
        ee.company ee_company,
        ee.job ee_job
    from emp e left join emp_expr ee on e.id = ee.emp_id
    where e.id = #{id}
</select>

在这种一对多的查询中,我们要想成功的封装的结果,需要手动的基于 <resultMap> 来进行封装结果。

  • Mybatis中封装查询结果,什么时候用 resultType,什么时候用resultMap ?
    • 如果查询返回的字段名与实体的属性名可以直接对应上,用resultType 。
    • 如果查询返回的字段名与实体的属性名对应不上,或实体属性比较复杂,可以通过resultMap手动封装 。

分页查询

原始分页

需求分析

上述我们在Mapper接口中定义了接口方法,完成了查询所有员工及其部门名称的功能,是将数据库中所有的数据查询出来了。 试想如果数据库中的数据有很多(假设有几千几万条)的时候,将数据全部展示出来肯定不现实,那如何解决这个问题呢?

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

而在员工管理的需求中,就要求我们进行分页查询,展示出对应的数据。 具体的页面原型如下:

image-20231203153042186

要想从数据库中进行分页查询,我们要使用LIMIT关键字,格式为:limit 开始索引 每页显示的条数

1). 查询第1页数据的SQL语句是:

select * from emp  limit 0,10;

2). 查询第2页数据的SQL语句是:

select * from emp  limit 10,10;

3). 查询第3页的数据的SQL语句是:

select * from emp  limit 20,10;

观察以上SQL语句,发现: 开始索引一直在改变 , 每页显示条数是固定的

开始索引的计算公式: 开始索引 = (当前页码 - 1) * 每页显示条数

我们继续基于页面原型,继续分析,得出以下结论:

  1. 前端在请求服务端时,传递的参数
    • 当前页码 page
    • 每页显示条数 pageSize
  2. 后端需要响应什么数据给前端
    • 所查询到的数据列表(存储到List 集合中)
    • 总记录数
image-20231203153321353

后台给前端返回的数据包含:List集合(数据列表)、total(总记录数)

而这两部分我们通常封装到PageBean对象中,并将该对象转换为json格式的数据响应回给浏览器。

@Data
@NoArgsConstructor
@AllArgsConstructor
public class PageBean {
	private Long total; //总记录数
	private List rows; //当前页数据列表
}
接口文档

员工列表查询

  • 基本信息

    请求路径:/emps
    
    请求方式:GET
    
    接口描述:该接口用于员工列表数据的条件分页查询
    
  • 请求参数

    参数格式:queryString

    参数说明:

    参数名称 是否必须 示例 备注
    name 姓名
    gender 1 性别 , 1 男 , 2 女
    begin 2010-01-01 范围匹配的开始时间(入职日期)
    end 2020-01-01 范围匹配的结束时间(入职日期)
    page 1 分页查询的页码,如果未指定,默认为1
    pageSize 10 分页查询的每页记录数,如果未指定,默认为10

    请求数据样例:

    /emps?name=张&gender=1&begin=2007-09-01&end=2022-09-01&page=1&pageSize=10
    
  • 响应数据

    参数格式:application/json

    参数说明:

    名称 类型 是否必须 备注
    code number 必须 响应码, 1 成功 , 0 失败
    msg string 非必须 提示信息
    data object 必须 返回的数据
    |- total number 必须 总记录数
    |- rows object [] 必须 数据列表
    |- id number 非必须 id
    |- username string 非必须 用户名
    |- name string 非必须 姓名
    |- gender number 非必须 性别 , 1 男 ; 2 女
    |- image string 非必须 图像
    |- job number 非必须 职位, 说明: 1 班主任,2 讲师, 3 学工主管, 4 教研主管, 5 咨询师
    |- salary number 非必须 薪资
    |- entryDate string 非必须 入职日期
    |- deptId number 非必须 部门id
    |- deptName string 非必须 部门名称
    |- updateTime string 非必须 更新时间

    响应数据样例:

    {
      "code": 1,
      "msg": "success",
      "data": {
        "total": 2,
        "rows": [
           {
            "id": 1,
            "username": "jinyong",
            "password": "123456",
            "name": "金庸",
            "gender": 1,
            "image": "https://web-framework.oss-cn-hangzhou.aliyuncs.com/2022-09-02-00-27-53B.jpg",
            "job": 2,
            "salary": 8000,
            "entryDate": "2015-01-01",
            "deptId": 2,
            "deptName": "教研部",
            "createTime": "2022-09-01T23:06:30",
            "updateTime": "2022-09-02T00:29:04"
          },
          {
            "id": 2,
            "username": "zhangwuji",
            "password": "123456",
            "name": "张无忌",
            "gender": 1,
            "image": "https://web-framework.oss-cn-hangzhou.aliyuncs.com/2022-09-02-00-27-53B.jpg",
            "job": 2,
            "salary": 6000,
            "entryDate": "2015-01-01",
            "deptId": 2,
            "deptName": "教研部",
            "createTime": "2022-09-01T23:06:30",
            "updateTime": "2022-09-02T00:29:04"
          }
        ]
      }
    }
    

​ 目前我们只考虑分页查询,先不考虑查询条件,而上述的接口文档中,与分页查询相关的参数就两个,一个是page,一个是pageSize。

思路分析
image-20231203153628217
代码实现

通过查看接口文档:员工列表查询

请求路径:/emps

请求方式:GET

请求参数:跟随在请求路径后的参数字符串。 例:/emps?page=1&pageSize=10

响应数据:json格式

1). EmpController

@Slf4j
@RequestMapping("/emps")
@RestController
public class EmpController {

    @Autowired
    private EmpService empService;

    @GetMapping
    public Result page(@RequestParam(defaultValue = "1") Integer page ,
                       @RequestParam(defaultValue = "10") Integer pageSize){
        log.info("查询员工信息, page={}, pageSize={}", page, pageSize);
        PageBean pageBean = empService.page(page, pageSize);
        return Result.success(pageBean);
    }

}

@RequestParam(defaultValue="默认值") //设置请求参数默认值

2). EmpService

public interface EmpService {
    /**
     * 分页查询
     * @param page 页码
     * @param pageSize 每页记录数
     */
    PageBean page(Integer page, Integer pageSize);
}

3). EmpServiceImpl

@Service
public class EmpServiceImpl implements EmpService {

    @Autowired
    private EmpMapper empMapper;

    @Override
    public PageBean page(Integer page, Integer pageSize) {
        //1. 获取总记录数
        Long total = empMapper.count();

        //2. 获取结果列表
        Integer start = (page - 1) * pageSize;
        List<Emp> empList = empMapper.list(start, pageSize);

        //3. 封装结果
        return new PageBean(total, empList);
    }
}

4). EmpMapper

@Mapper
public interface EmpMapper {

    /**
     * 查询总记录数
     */
    @Select("select count(*) from emp e left join dept d on e.dept_id = d.id ")
    public Long count();

    /**
     * 查询所有的员工及其对应的部门名称
     */
    @Select("select e.*, d.name deptName from emp as e left join dept as d on e.dept_id = d.id limit #{start}, #{pageSize}")
    public List<Emp> list(Integer start , Integer pageSize);

}
功能测试

功能开发完成后,重新启动项目,使用Apifox,发起GET请求:

image-20231203154952901

前后端联调

打开浏览器,测试后端功能接口:

image-20231203155012318

点击下面的页码,可以正常的查询出对应的数据 。

分页插件

介绍

前面我们已经完了基础的分页查询,大家会发现:分页查询功能编写起来比较繁琐。 而分页查询的功能是非常常见的,我们查询员工信息需要分页查询,将来在做其他项目时,查询用户信息、订单信息、商品信息等等都是需要进行分页查询的。

而分页查询的思路、步骤是比较固定的。 在Mapper接口中定义两个方法执行两条不同的SQL语句:

  1. 查询总记录数
  2. 指定页码的数据列表

在Service当中,调用Mapper接口的两个方法,分别获取:总记录数、查询结果列表,然后在将获取的数据结果封装到PageBean对象中。

大家思考下:在未来开发其他项目,只要涉及到分页查询功能(例:订单、用户、支付、商品),都必须按照以上操作完成功能开发

结论:原始方式的分页查询,存在着"步骤固定"、"代码频繁"的问题

解决方案:可以使用一些现成的分页插件完成。对于Mybatis来讲现在最主流的就是PageHelper

PageHelper是第三方提供的Mybatis框架中的一款功能强大、方便易用的分页插件,支持任何形式的单标、多表的分页查询。

官网:https://pagehelper.github.io/

那接下来,我们可以对比一下,使用PageHelper分页插件进行分页 与 原始方式进行分页代码实现的上的差别。

image-20231203155640765
  • Mapper接口层:
    • 原始的分页查询功能中,我们需要在Mapper接口中定义两条SQL语句。
    • PageHelper实现分页查询之后,只需要编写一条SQL语句,而且不需要考虑分页操作,就是一条正常的查询语句。
  • Service层:
    • 需要根据页码、每页展示记录数,手动的计算起始索引。
    • 无需手动计算起始索引,直接告诉PageHelper需要查询那一页的数据,每页展示多少条记录即可。
代码实现

当使用了PageHelper分页插件进行分页,就无需再Mapper中进行手动分页了。 在Mapper中我们只需要进行正常的列表查询即可。在Service层中,调用Mapper的方法之前设置分页参数,在调用Mapper方法执行查询之后,解析分页结果,并将结果封装到PageBean对象中返回。

1、在pom.xml引入依赖

<!--分页插件PageHelper-->
<dependency>
    <groupId>com.github.pagehelper</groupId>
    <artifactId>pagehelper-spring-boot-starter</artifactId>
    <version>1.4.7</version>
</dependency>

2、EmpMapper

/**
 * 查询所有的员工及其对应的部门名称
 */
@Select("select e.*, d.name deptName from emp as e left join dept as d on e.dept_id = d.id")
public List<Emp> list();

3、EmpServiceImpl

@Override
public PageBean page(Integer page, Integer pageSize) {
    //1. 设置分页参数
    PageHelper.startPage(page,pageSize);

    //2. 执行查询
    List<Emp> empList = empMapper.list();
    Page<Emp> p = (Page<Emp>) empList;

    //3. 封装结果
    return new PageBean(p.getTotal(), p.getResult());
}
测试

功能开发完成后,我们重启项目工程,打开Apifox,发起GET请求,访问 :http://localhost:8080/emps?page=1&pageSize=5

image-20231203160448506

我们可以看到数据可以正常查询返回,是可以正常实现分页查询的。

实现机制

我们打开Idea的控制台,可以看到在进行分页查询时,输出的SQL语句。

image-20231203160633448

我们看到执行了两条SQL语句,而这两条SQL语句,其实是从我们在Mapper接口中定义的SQL演变而来的。

  • 第一条SQL语句,用来查询总记录数。

    image-20231203160953717

​ 其实就是将我们编写的SQL语句进行的改造增强,将查询返回的字段列表替换成了 count(0) 来统计总记录数。

  • 第二条SQL语句,用来进行分页查询,查询指定页码对应 的数据列表。

    image-20231203161202077

​ 其实就是将我们编写的SQL语句进行的改造增强,在SQL语句之后拼接上了limit进行分页查询,而由于测试时查询的是第一页,起始索引是0,所以简写为limit ?。

​ 而PageHelper在进行分页查询时,会执行上述两条SQL语句,并将查询到的总记录数,与数据列表封装到了 Page<Emp> 对象中,我们再获取查询结果时,只需要调用Page对象的方法就可以获取。

注意:

1. PageHelper实现分页查询时,SQL语句的结尾一定一定一定不要加分号(

标签:语句,name,public,emp,SQL,Mybatis,id
From: https://www.cnblogs.com/kk-koala/p/18341531

相关文章

  • ] Failed to execute goal org.mybatis.generator:mybatis-generator-maven-plugin Ca
    [ERROR]Failedtoexecutegoalorg.mybatis.generator:mybatis-generator-maven-plugin:1.3.7:generate(default-cli)onprojectcom-zhianchen-pgsql:Executiondefault-cliofgoalorg.mybatis.generator:mybatis-generator-maven-plugin:1.3.7:generatefailed:Cann......
  • 【技术精粹】LambdaQueryWrapper实战指南:MyBatis-Plus从入门到精通(上:入门、基础用法
    文章目录JavaMyBatis-PlusLambdaQueryWrapper深入理解与实战应用前言第一部分:MyBatis-Plus入门1.1MyBatis-Plus是什么?1.2快速上手第二部分:LambdaQueryWrapper基础2.1LambdaQueryWrapper概览2.2LambdaQueryWrapper使用入门第三部分:LambdaQueryWrapper高级......
  • 三十七、MyBatis-Plus(3)
    ......
  • MyBatis代码生成器:SpringBoot 引入MybatisGenerator
    1.引入插件<plugin><groupId>org.mybatis.generator</groupId><artifactId>mybatis-generator-maven-plugin</artifactId><version>1.3.5</version>......
  • mybatis-plus 自定义sql拼接 的方式进行Wrapper条件查询 实现了分页 多表查询
    法一:用单表的思想(可以跳过,直接看法二)方法:先收集公共字段到集合中,再批量查询,然后封装起来缺点:相较于法二,代码量大,多访问了一次数据库service层:@Service@RequiredArgsConstructorpublicclassProductServiceImplextendsServiceImpl<ProductMapper,Product>implements......
  • Mybatis-Plus源码-自增主键填充到实体
     #mybatis-3.5.10+mybatisPlus-3.5.2#mybatis-plus自增id策略下何时填充id到实体com.baomidou.mybatisplus.extension.service.impl.ServiceImpl#saveOrUpdatecom.baomidou.mybatisplus.extension.service.IService#savecom.baomidou.mybatisplus.core.override.M......
  • mybatis配置自动填充时间拦截器
    1.Annotation:①FillOnInsertimportjava.lang.annotation.ElementType;importjava.lang.annotation.Retention;importjava.lang.annotation.RetentionPolicy;importjava.lang.annotation.Target;@Retention(RetentionPolicy.RUNTIME)@Target......
  • Mybatis进阶提升-(一)Mybatis入门
    前言Mybatis是Java项目开发使用率非常高的一款持久层框架,它支持自定义SQL、存储过程以及高级映射。MyBatis免除了几乎所有的JDBC代码以及设置参数和获取结果集的工作。MyBatis可以通过简单的XML或注解来配置和映射原始类型、接口和JavaPOJO(PlainOldJavaObjects,普......
  • 结合拦截器描述mybatis启动流程
    简介mybatis的启动入口一般有两个,在结合spring框架后由spring整合包下的SqlSessionFactoryBean启动如果没有整合spring,则有XMLConfigBuilder启动这两个启动入口都会初始化Configuration对象,该对象是mybatis配置文件的对象形式,我们实现的mybatis拦截器在此时被装载到configurati......
  • 在Mybatis中不对select做任何改变,将物理删除的逻辑迁移到逻辑删除
    假设现在有一个已经完成或者已经到中期的项目,但是忘记做逻辑删除了,更改的办法如下第一步,在项目中添加如下的拦截器importcom.baomidou.mybatisplus.core.toolkit.PluginUtils;importcom.baomidou.mybatisplus.extension.plugins.inner.DataPermissionInterceptor;importlom......