MyBatis入门:快速掌握数据库操作技巧

一. 前言

在应用分层学习时,我们了解到web应永程序⼀般分为三层,即:Controller、Service、Dao. 之前的案例中,请求流程如下:浏览器发起请求,先请求Controller,Controller接收到请求之后,调用 Service进行业务逻辑处理,Service再调用Dao,但是Dao层的数据是Mock的,真实的数据应该从数据库 中读取.我们学习MySQL数据库时,已经学习了JDBC来操作数据库,但是JDBC操作太复杂了.

我们不但要拼接每⼀个参 数,⽽且还要按照模板代码的⽅式,⼀步步的操作数据库,并且在每次操作完,还要⼿动关闭连接 等,⽽所有的这些操作步骤都需要在每个⽅法中重复书写.那有没有⼀种⽅法,可以更简单、更⽅便的 操作数据库呢? 答案是肯定的,这就是我们要学习 MyBatis 的真正原因,它可以帮助我们更方便、更快速的操作数据库.

二. 什么是MyBatis?

MyBatis是⼀款优秀的 持久层框架,用于简化JDBC的开发。

MyBatis本是 Apache的⼀个开源项目iBatis,2010年这个项目由apache迁移到了google code,并 且改名为MyBatis。2013年11月迁移到Github.

更多介绍,可查看 MyBatis 中文官网 (https://mybatis.net.cn/

注意:我们下面所讲的MyBatis不是原生的MyBatis,而是MyBatis 官方专门给 SpringBoot 做的整合包: mybatis-spring-boot-starter

三. MyBatis 入门

Mybatis操作数据库的步骤:

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

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

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

  4. 测试

3.1 准备工作

3.1.2 创建springboot工程

创建springboot⼯程,并导⼊mybatis的起步依赖、mysql的驱动包

Mybatis 是⼀个持久层框架, 具体的数据存储和数据操作还是在MySQL中操作的, 所以需要添加 MySQL驱动

3.1.2 数据库表准备

sql 复制代码
-- 创建数据库
DROP DATABASE IF EXISTS mybatis_test;

CREATE DATABASE mybatis_test DEFAULT CHARACTER SET utf8mb4;

-- 使用数据数据
USE mybatis_test;

-- 创建表[用户表]
DROP TABLE IF EXISTS user_info;
CREATE TABLE `user_info` (
        `id` INT ( 11 ) NOT NULL AUTO_INCREMENT,
        `username` VARCHAR ( 127 ) NOT NULL,
        `password` VARCHAR ( 127 ) NOT NULL,
        `age` TINYINT ( 4 ) NOT NULL,
        `gender` TINYINT ( 4 ) DEFAULT '0' COMMENT '1-男 2-女 0-默认',
        `phone` VARCHAR ( 15 ) DEFAULT 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; 

-- 添加用户信息
INSERT INTO mybatis_test.user_info( username, `password`, age, gender, phone )
VALUES ( 'admin', 'admin', 18, 1, '18612340001' );
INSERT INTO mybatis_test.user_info( username, `password`, age, gender, phone )
VALUES ( 'zhangsan', 'zhangsan', 18, 1, '18612340002' );
INSERT INTO mybatis_test.user_info( username, `password`, age, gender, phone )
VALUES ( 'lisi', 'lisi', 18, 1, '18612340003' );
INSERT INTO mybatis_test.user_info( username, `password`, age, gender, phone )
VALUES ( 'wangwu', 'wangwu', 18, 1, '18612340004' );

在下面我们使用 Navicat 这个数据库图形化管理工具

这样我们就可以在数据库中得到一张表

3.1.3 创建对应的实体类 UserInfo

注意:数据库属性命名规范:小写+下划线。 Java属性命名规范:驼峰

在我们为数据库中的一个表设置对应的Java实体类时,如果数据库中属性名为小写,对应Java实体类属性名要完全对应上,如果数据库中属性名为小写+下划线,对应Java实体类属性名可以命名为驼峰,因为Mybatis有办法帮我们转换(后面会讲到)

java 复制代码
package com.example.Mybatis.entity;
import lombok.Data;
import java.util.Date;

@Data
public class UserInfo {
    private Integer id;
    private String username;
    private String password;
    private Integer age;
    private String phone;
    private Integer deleteFlag;
    private Date createTime;
    private Date updateTime;
}

注意:我们在为数据库表设计对应java实体类时,该类中每个属性都要有对应的set方法(我们这里是使用lombok工具中的@Data注解帮我们自动生成所有属性对应得set方法)

3.2 配置数据库连接字符串

Mybatis中要连接数据库,需要数据库相关参数配置

• MySQL驱动类

• 登录名

• 密码

• 数据库连接字符串

sql 复制代码
# 数据库连接配置
spring:
  datasource:
    url: jdbc:mysql://127.0.0.1:3306/mybatis_test?characterEncoding=utf8&useSSL=false
    username: root
    password: root
    driver-class-name: com.mysql.cj.jdbc.Driver

3.3 写持久层代码

注意:在之前讲到过,数据库数据层的操作一般是以dao为包名命名的,但是我们使用MyBatis进行对数据库的操作时,我们一般将包名命名为 mapper(而不用dao)

java 复制代码
package com.example.Mybatis.mapper;
import com.example.Mybatis.entity.UserInfo;
import org.apache.ibatis.annotations.Mapper;
import org.apache.ibatis.annotations.Select;
import java.util.List;

@Mapper
public interface UserInfoMapper {

    @Select("select * from user_info")//查询整个表
    List<UserInfo> selectAll();

}

注意:

  • Mybatis的持久层接口规范⼀般都叫XxxMapper
  • 我们设定的是接口而不是类,因为设计成接口就可以不用写方法的具体实现,Mybatis框架会自动生成其实体类对象
  • @Mapper注解:表示MyBatis中的Mapper接口,程序运行时,框架会自动动生成接口的实现类对象(代理对象),并给交Spring的IOC容器管理
  • @Select注解:代表的就是select查询,也就是注解对应方法的具体实现内容.

3.4 单元测试

在创建出来的SpringBoot工程中,在src下的test目录下,已经⾃动帮我们创建好了测试类,我们可以 直接使用这个测试类来进行测试.

java 复制代码
package com.example.Mybatis;

import com.example.Mybatis.entity.UserInfo;
import com.example.Mybatis.mapper.UserInfoMapper;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;

import java.util.List;

@SpringBootTest
class MybatisApplicationTests {
@Autowired
	UserInfoMapper userInfoMapper;
	@Test
	void contextLoads() {
		List<UserInfo> userInfoList=userInfoMapper.selectAll();
		System.out.println(userInfoList);
	}

}

@Test 注解是单元测试代码中的重要注解:

@Test

  1. 作用

用来标记一个普通方法为测试方法,让测试框架(如 JUnit、SpringBoot Test)可以自动识别并运行这个方法。我们直接点击 @Test 左边的三角标志 ,即可执行该方法(不用写main方法)

  1. 好处
  • 不用写 main 方法就能运行代码

  • 专门用来测试:Mapper、Service、Controller 等功能

  • 运行快、不启动整个项目、方便排查错误

@SpringBootTest 注解也是单元测试的重要注解:

@SpringBootTest 的作用:

  1. 启动了Spring的容器,Spring 容器 里面的Bean我们都可以直接注入使用(启动了才能注入)

  2. 让测试中可以使用 @Autowired 自动注入(Mapper、Service、Controller 都能注入)

  3. 提供和真实项目一样的运行环境,方便测试业务代码

  4. 点击@SpringBootTest 左边的三角标志,即可执行该类中的所有带有@Test注解的方法

测试类上添加了注解 @SpringBootTest,该测试类在运行时,就会自动加载Spring的运行环境. 我们通过@Autowired这个注解, 注入我们要测试的类, 就可以开始进行测试了。

使用Idea自动生成测试类

除此之外,也可以使用Idea自动生成测试类

即可生成:

注意:我们使用 IDEA 自动生成的测试类,是对应需要测试的那个业务类的。

以后凡是要测试这个类里的方法,统一都写在这个对应的测试类中。

四. MyBatis的基础操作

4.1 打印日志

在Mybatis当中我们可以借助日志,查看到sql语句的执行、执行传递的参数以及执行结果在配置文件中进行配置即可

java 复制代码
mybatis:
 configuration: # 配置打印 MyBatis⽇志 
   log-impl: org.apache.ibatis.logging.stdout.StdOutImpl

未加该配置前:(测试selectAll()方法)

加之后:(测试selectAll()方法)

通过MyBatis日志配置,我们就可以直观看到执行的 SQL 语句、参数、返回结果。(日志里打印的结果,就是数据库直接返回的原始数据,还没经过 MyBatis 封装成 Java 对象之前的样子。数据库里的表是什么样子,日志打印出来就是什么样子。)

==> 是"发出去"的 SQL 和参数

<== 是"收回来"的列名、行数据和总行数

  • Columns: :查询结果的列名列表。

  • Row: :查询返回的每一行原始数据。

  • Total: :返回的总行数。

4.2 参数传递

例子:

需求:从数据中的的user_info中查找 id=?的用户,这个id就是需要我们给它传递参数

解决方案:将方法中的参数,传给SQL语句使用 #{ } 的方式获取方法中的参数

方法编写:

java 复制代码
@Mapper
public interface UserInfoMapper {
    @Select("select * from user_info where id=#{id}")
    UserInfo selectById(Integer id);
   
}

测试方法编写:

java 复制代码
@SpringBootTest
class UserInfoMapperTest {
    @Autowired
    private UserInfoMapper userInfoMapper;

   
    @Test
    void selectById() {
        UserInfo userInfo=userInfoMapper.selectById(3);
        System.out.println(userInfo);
    }
}

当我们传入参数时,即id=3时,就会将参数中的id传入 SQL语句中的 #{id} (#{ } 就相当于一个占位符)

注意:

  • 当参数只有一个是方法的参数名可以和 #{ } 中的名称不一致,MyBatis也能识别,当有两个/多个参数时,两者就要完全一致,不然会报错
  • 也可以通过 @Param ,设置参数的别名,如果使用 @Param 设置别名, #{...} 里面的属性名必须和 @Param 设置的⼀样(@Param 的作用就是:把方法里的参数和别名绑定在一起。)
java 复制代码
@Mapper
public interface UserInfoMapper {
    @Select("select * from user_info where id=#{userId}")
    UserInfo selectById(@Param("userId") Integer id);//通过@Param起别名
    @Select("select * from user_info")
   List<UserInfo> selectAll();
}

//@Param("userId") Integer id 就是把 id 和 userId 绑定。
//之后在  @Select  的 SQL 里,就用 #{userId} 来对应这个别名,MyBatis 就知道要取 id 的值了。
  • @select 注解中的内容通过MyBatis处理后就是直接要交给数据库执行的原生SQL语句

执行测试方法:

4.3 增(Insert)

SQL语句:

sql 复制代码
insert into user_info (username, `password`, age, gender, phone) values 
("zhaoliu","zhaoliu",19,1,"18700001234")

把SQL中的常量替换为动态的参数

Mapper接口:

java 复制代码
@Mapper
public interface UserInfoMapper {

   List<UserInfo> selectAll();
    @Insert("insert into user_info (username, `password`, age, gender, phone)"+
            " values (#{username},#{password},#{age},#{gender},#{phone})")
    Integer insert(UserInfo userInfo);//当参数有多个时,可以设置为对象

}

测试代码:

java 复制代码
@SpringBootTest
class UserInfoMapperTest {
    @Autowired
    private UserInfoMapper userInfoMapper;

    @Test
    void insert() {
        UserInfo userInfo=new UserInfo();
        userInfo.setUsername("zhaoliu");
        userInfo.setPassword("zhaoliu");
        userInfo.setGender(2);
        userInfo.setAge(21);
        userInfo.setPhone("18612340005");

        Integer a=userInfoMapper.insert(userInfo);
        System.out.println("影响行数"+a);
        System.out.println(userInfo);
    }

执行结果:

补: 为什么我们在为数据库表设计对应java实体类时,为什么没有将这个实体类交给Spring管?

答: Mapper 中的每个方法,即便最终返回的数据结构相同,用于承载数据的实体类对象也是相互独立的。实体类之所以不需要交给 Spring 管理,是因为它仅作为数据载体,主要出现在方法的参数与返回值位置,只负责封装和传输数据,不包含任何业务行为与逻辑处理,因此无需由 Spring 容器创建、管理和依赖注入

如果我们为对象参数使用 @Param 起别名时,#{...} 需要使用 参数.属性 来获取,代码如下:

java 复制代码
@Mapper
public interface UserInfoMapper {

   List<UserInfo> selectAll();
    @Insert("insert into user_info (username, `password`, age, gender, phone)"+
            " values (#{UI.username},#{UI.password},#{UI.age},#{UI.gender},#{UI.phone})")
    Integer insert(@Param("UI") UserInfo userInfo);

}

从上面的代码可以清晰地看出,MyBatis 的工作流程是:

  1. 首先从我们传入的参数(即 UserInfo 对象)中,通过 UI.username 、 UI.password 等表达式提取出对应字段的值。

  2. 然后将这些提取到的数据,填充到预编译的 SQL 语句中,最终执行

返回主键

Insert 语句默认返回的是受影响的行数

但有些情况下,数据插⼊之后,还需要有后续的关联操作,需要获取到新插⼊数据的id。比如订单系统 当我们下完订单之后,需要通知物流系统,库存系统,结算系统等,这时候就需要拿到订单ID

如果想要拿到增id,需要在Mapper接的方法上添加⼀个Options的注解

java 复制代码
@Mapper
public interface UserInfoMapper {

@Options(useGeneratedKeys = true, keyProperty = "id")
@Insert("insert into user_info (username, `password`, age, gender, phone)"+
            " values (#{UI.username},#{UI.password},#{UI.age},#{UI.gender},#{UI.phone})")
    Integer insert(@Param("UI") UserInfo userInfo);

}

解释:

|------------------|------|---------------------------------|
| 参数名 | 取值 | 含义 |
| useGeneratedKeys | true | 开启"获取数据库自动生成主键"的功能(默认值为 false ) |
| keyProperty | "id" | 指定将生成的主键值,赋值给传入实体类对象的哪个属性 |

核心好处:无需额外执行 SELECT 语句获取自增 ID

在传统方式下,插入一条新记录后,若要获取数据库自动生成的自增主键 ID,必须再执行一次 SELECT LAST_INSERT_ID() 或类似的查询语句,这会增加一次数据库交互。

而通过 @Options(useGeneratedKeys = true, keyProperty = "id") ,MyBatis 会在插入操作的同一数据库连接和事务上下文中,自动将生成的主键值回填到传入的实体对象中。

注意:设置 useGeneratedKeys=true 之后,方法返回值依然是受影响的行数,自增id 会设置在上述 keyProperty 指定的属性中.

测试数据:

java 复制代码
@SpringBootTest
class UserInfoMapperTest {
    @Autowired
    private UserInfoMapper userInfoMapper;

    @Test
    void insert() {
        UserInfo userInfo=new UserInfo();
        userInfo.setUsername("zhaoliu");
        userInfo.setPassword("zhaoliu");
        userInfo.setGender(2);
        userInfo.setAge(21);
        userInfo.setPhone("18612340005");

        Integer count=userInfoMapper.insert(userInfo);
        System.out.println("影响行数:"+count+"自增id= "+userInfo.getId());

    }

运行结果:

4.4 删(Delete)

SQL语句:

sql 复制代码
delete from user_info where id=6

把SQL中的常量替换为动态的参数

Mapper接口:

java 复制代码
@Mapper
public interface UserInfoMapper {

 @Delete("delete from user_info where id=#{id}")
    Integer DeleteById(Integer id);
 }
}

注意:Delete操作返回值含义:表示本次操作成功影响到的数据库行数

4.5 改(Update)

SQL语句:

sql 复制代码
update user_info set username="zhaoliu" where id=5

把SQL中的常量替换为动态的参数

Mapper接口:

java 复制代码
@Mapper
public interface UserInfoMapper {

 @Update("update user_info set username=#{username} where id=#{id}")
    Integer update(UserInfo userInfo);

 }

测试数据:

java 复制代码
@SpringBootTest
class UserInfoMapperTest {
    @Autowired
    private UserInfoMapper userInfoMapper;

    @Test
    void update() {
        UserInfo userInfo=new UserInfo();
        userInfo.setUsername("aaa");
        userInfo.setId(5);
        Integer count=userInfoMapper.update(userInfo);
    }

4.6 查 (Select)

我们在上⾯查询时发现,有几个字段是没有赋值的, 只有Java对象属性和数据库字段⼀模⼀样时, 才会进行赋值 接下来我们多查询⼀些数据

java 复制代码
@Mapper
public interface UserInfoMapper {

@Select("select id, username, `password`, age, gender, phone, delete_flag,create_time, update_time from user_info")
 List<UserInfo> queryAllUser();

}

测试数据:

java 复制代码
@SpringBootTest
class UserInfoMapperTest {
    @Autowired
    private UserInfoMapper userInfoMapper;

    @Test
    void queryAllUser() {
        List<UserInfo> userInfos=userInfoMapper.queryAllUser();
        System.out.println(userInfos);

    }

运行结果:

从运行结果上可以看到,我们SQL语句中,查询了delete_flag, create_time, update_time,但是这几个 属性却没有赋值.

核心原因:

MyBatis 在进行结果映射时,默认采用的是严格的名称匹配规则:

数据库字段名: delete_flag 、 create_time 、 update_time (下划线命名法)

Java 对象属性名: deleteFlag 、 createTime 、 updateTime (驼峰命名法)

由于两者名称不完全一致,MyBatis 无法自动建立映射关系,因此这些字段的值无法被正确赋值到对象属性中,导致它们为 null 。

在执行 SELECT 操作时,MyBatis 的工作流程如下:

  1. 执行 SQL:首先执行 @Select 注解或 XML 中定义的 SQL 语句,从数据库中获取到结果集(ResultSet)。

  2. 结果映射:遍历结果集中的每一行数据,将其转换为对应的 Java 实体类对象。

  3. 名称匹配:在映射过程中,MyBatis 会严格按照名称匹配的规则,将数据库字段名与 Java 实体类的属性名进行一一对应。

  • 只有当两者的名称完全一致时,才会将数据库字段的值成功赋值给对应的实体类属性。

  • 如果名称不一致(例如数据库使用下划线命名 delete_flag ,而 Java 使用驼峰命名 deleteFlag ),MyBatis 无法自动建立映射关系,就会导致该属性值为 null ,即映射失败。

解决方案:

  • 法一:起别名
java 复制代码
@Select("select id, username, `password`, age, gender, phone, delete_flag as deleteFlag, +" +
        " create_time as createTime, update_time as updateTime from user_info")
        List<UserInfo> queryAllUser();

注:SQL语句太长时,使用加号 + 进行字符串拼接

运行结果:(这样就赋值成功了)

  • 法二:结果映射---使用 @Results 注解定义映射关系
java 复制代码
@Select("select id, username, `password`, age, gender, phone, delete_flag, " +
        " create_time, update_time from user_info")
@Results({
        @Result(column = "delete_flag",property = "deleteFlag"),
        @Result(column = "create_time",property = "createTime"),
        @Result(column = "update_time",property = "updateTime")
})
List<UserInfo> queryAllUser();

@Results 本质上就是一个注解容器,专门用来存放多个 @Result 注解。每一个@Result 注解就定义了一条映射关系,其中

如果其他SQL,也希望可以复用这个映射关系,可以给这个Results定义⼀个名称,使用 id 属性给该 Results 定义别名,使用 @ResultMap 注解来复用其他定义的 ResultMap :

java 复制代码
@Select("select id, username, `password`, age, gender, phone, delete_flag," +
        " create_time, update_time from user_info")
@Results(id="resultMap1",value = {
        @Result(column = "delete_flag",property = "deleteFlag"),
        @Result(column = "create_time",property = "createTime"),
        @Result(column = "update_time",property = "updateTime")
})
        List<UserInfo> queryAllUser();

@Select("select id, username, `password`, age, gender, phone, delete_flag, create_time, update_time " +
            "from user_info where id= #{userid} ")
@ResultMap(value = "resultMap1")//复用映射关系
    UserInfo queryById(@Param("userid") Integer id);
  • 法三:开启驼峰命名(推荐)

通常数据库列使用蛇形命名法进行命名(下划线分割各个单词), 而 Java 属性⼀般遵循驼峰命名法约定. 为了在这两种命名方式之间启用动映射,需要将 mapUnderscoreToCamelCase 设置为 true。

java 复制代码
mybatis:
  configuration:
     map-underscore-to-camel-case: true #配置驼峰⾃动转换 

Java代码不做任何处理

java 复制代码
@Select("select id, username, `password`, age, gender, phone, delete_flag," +
        " create_time, update_time from user_info")
        List<UserInfo> queryAllUser();

五. MyBatis XML配置文件

Mybatis的开发有两种方式:

  1. 注解(我们上面讲到的 MyBatis的基础操作)

  2. XML

上面学习了注解的方式,接下来我们学习XML的方式

MyBatis XML的方式需要以下两步:

1. 配置数据库连接字符串和MyBatis

2. 写持久层代码

5.1 配置连接字符串和MyBatis

此步骤需要进行两项设置,数据库连接字符串设置和 MyBatis 的 XML 文件配置。如果是application.yml文件, 配置内容如下:

python 复制代码
# 数据库连接配置 
spring:
  datasource:
    url: jdbc:mysql://127.0.0.1:3306/mybatis_test?characterEncoding=utf8&useSSL=false
    username: root
    password: root
    driver-class-name: com.mysql.cj.jdbc.Driver

# 配置 mybatis xml 的⽂件路径,在 resources/mapper 创建所有表的 xml ⽂件 
mybatis:
 mapper-locations: classpath:mapper/**Mapper.xml

注意:

  • classpath:表示 resources 目录
  • mapper目录和 **Mapper.xml文件都需要我们手动创建,IDEA 不会自动生成
  • **Mapper.xml:表示我们创建的MyBatis XML文件名需要以Mapper结尾,以xml为文件后缀

5.2 写持久层代码

持久层代码分两部分

  1. 方法定义 Interface

  2. 方法实现: XXX.xml(相当于我们把之前注解的那部分内容写到了MyBatis 的 XML文件中)

5.2.1 添加 mapper 接口

mapper 接口中我们只写方法定义,不写方法实现

java 复制代码
ackage com.example.Mybatis.mapper;
import com.example.Mybatis.entity.UserInfo;
import org.apache.ibatis.annotations.*;

import java.util.List;

@Mapper
public interface UserInfoXMlMapper {
    List<UserInfo> queryAllUser();
    
}

5.2.2 添加 UserInfoXMLMapper.xml

添加 MyBatis XML文件需要和我们配置 mybatis xml 的文件路径一致

数据持久成的实现,MyBatis 的固定xml格式:

XML 复制代码
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" 
"http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.example.demo.mapper.UserInfoMapper">
 
</mapper>

注意:我们要生成哪个mapper接口的MyBatis XML文件,在mapper文件的 namespace 必须写:你要绑定的那个 Mapper 接口的全类名(包名 + 类名)

查询所有用户的具体实现:

XML 复制代码
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" 
"http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.example.Mybatis.mapper.UserInfoXMlMapper">

 <select id="queryAllUser" resultType="com.example.Mybatis.entity.UserInfo">
 select username,`password`, age, gender, phone from user_info
 </select>

</mapper>

以下是对以上标签的说明:

标签:需要指定 namespace 属性,表示命名空间,值为对应 mapper接口的全限定名 包括全包名.类名。

• 查询标签:是用来执行数据库的查询操作的:

--id :是和Interface (接⼝)中定义的方法名称⼀样的,表示对接⼝的具体实现方法。

--resultType :是返回的数据类型,也就是开头我们定义的实体类.

5.2.3 单元测试

与使用注解进行的单元测试方式一致

java 复制代码
@SpringBootTest
class UserInfoXMlMapperTest {
@Autowired
UserInfoXMlMapper userInfoXMlMapper;
    @Test
    void queryAllUser() {
        List<UserInfo> userInfos=userInfoXMlMapper.queryAllUser();
        System.out.println(userInfos);
    }
}

5.3 增删改查操作

接下来,我们来实现⼀下⽤⼾的增加、删除和修改的操作.

5.3.1 增 (Insert)

UserInfoMapper接⼝:

java 复制代码
@Mapper
public interface UserInfoXMlMapper {
   
    Integer insertUser(UserInfo userInfo);

}

UserInfoMapper.xml实现:

XML 复制代码
 <insert id="insertUser">
        insert into userinfo (username, `password`, age, gender, phone) values (#
        {username}, #{password}, #{age},#{gender},#{phone})
 </insert>

如果使用@Param设置参数名称的话,使用方法和注解类似

UserInfoMapper接⼝:

java 复制代码
Integer insertUser(@Param("userInfo") UserInfo userInfo);

UserInfoMapper.xml实现:

XML 复制代码
<insert id="insertUser" useGeneratedKeys="true" keyProperty="id">

 insert into user_info (username, `password`, age, gender, phone) values
 (#{userInfo.username},#{userInfo.password},#{userInfo.age},#
{userInfo.gender},#{userInfo.phone})

</insert>

返回自增 id

接⼝定义不变, Mapper.xml 实现 设置useGeneratedKeys 和keyProperty属性(与使用@Options注解类似)

XML 复制代码
<insert id="insertUser" useGeneratedKeys="true" keyProperty="id">

 insert into user_info (username, `password`, age, gender, phone) values
 (#{userInfo.username},#{userInfo.password},#{userInfo.age},#
{userInfo.gender},#{userInfo.phone})

</insert>

5.3.2 删 (Delete)

UserInfoMapper接口:

java 复制代码
Integer deleteUser(Integer id);

UserInfoMapper.xml实现:

XML 复制代码
<delete id="deleteUser">
 delete from user_info where id = #{id}
</delete>

5.3.3 改 (Update)

UserInfoMapper接⼝:

java 复制代码
Integer updateUser(UserInfo userInfo);

UserInfoMapper.xml实现:

XML 复制代码
<update id="updateUser">
 update user_info set username=#{username} where id=#{id}
</update>

5.3.4 查 (Select)

同样的,使用XML 的方式进行查询,也存在数据封装的问题 。我们把SQL语句进⾏简单修改,查询更多的字段内容

XML 复制代码
<select id="queryAllUser" resultType="com.example.demo.model.UserInfo">

 select id, username,`password`, age, gender, phone, delete_flag, 
create_time, update_time from user_info

</select>

结果显示: deleteFlag, createTime, updateTime 也没有进行赋值. 解决办法和注解类似:

  1. 起别

  2. 结果映射

  3. 开启驼峰命名

其中1,3的解决办法和注解⼀样,不再多说,接下来看下xml如果来写结果映射

XML 复制代码
    <resultMap id="BaseMap" type="com.example.Mybatis.entity.UserInfo">
        <id column="id" property="id"></id>
        <result column="delete_flag" property="deleteFlag"></result>
        <result column="create_time" property="createTime"></result>
        <result column="update_time" property="updateTime"></result>
    </resultMap>
    <select id="queryAllUser" resultMap="BaseMap">
        select username,`password`, age, gender, phone from user_info
    </select>

六. 其它查询操作

6.1 多表查询

多表查询和单表查询类似,只是SQL不同而已

6.1.1 准备工作

上面建了⼀张用户表,我们再来建⼀张文章表,进行多表关联查询. 文章表的uid,对应用户表的id.

sql 复制代码
-- 创建⽂章表 
DROP TABLE IF EXISTS article_info;

CREATE TABLE articleinfo (
 id INT PRIMARY KEY auto_increment,
 title VARCHAR ( 100 ) NOT NULL,
 content TEXT NOT NULL,
 uid INT NOT NULL,
 delete_flag TINYINT ( 4 ) DEFAULT 0 COMMENT '0-正常, 1-删除',
 create_time DATETIME DEFAULT now(),
 update_time DATETIME DEFAULT now() 
) DEFAULT charset 'utf8mb4';
-- 插⼊测试数据 
INSERT INTO article_info ( title, content, uid ) VALUES ( 'Java', 'Java正⽂', 1 );

创建对应实体Java类

java 复制代码
@Data
public class ArticleInfo {
    private Integer id;
    private String title;
    private String content;
    private Integer uid;
    private Integer deleteFlag;
    private Date createTime;
    private Date updateTime;

}

6.1.2 数据查询

需求:根据uid查询作者的名称等相关信息

SQL:

sql 复制代码
SELECT ta.id,ta.title,ta.content,ta.uid,tb.username,tb.age,tb.gender 
FROM articleinfo ta LEFT JOIN user_info tb ON ta.uid = tb.id WHERE ta.id =1

补充实体类:

java 复制代码
@Data
public class ArticleInfo {
    private Integer id;
    private String title;
    private String content;
    private Integer uid;
    private Integer deleteFlag;
    private Date createTime;
    private Date updateTime;
    //用户相关信息
    private String username;
    private Integer age;
    private Integer gender;

}

接⼝定义:

java 复制代码
@Mapper
public interface ArticleInfoMapper {
    @Select("SELECT ta.id,ta.title,ta.content,ta.uid,tb.username,tb.age,tb.gender "+
            "FROM article_info ta LEFT JOIN user_info tb ON ta.uid = tb.id WHERE ta.id =#{id}")
    ArticleInfo queryUserByUid(Integer id);
}

6.2 #{ } 和 ${ }

MyBatis 参数赋值有两种方式,咱们前面使用了#{} 进行赋值,接下来我们看下二者的区别

6.2.1 #{ } 和 ${ } 使用

  • 先看Interger类型的参数

使用 # { }:

java 复制代码
@Mapper
public interface UserInfoMapper {
    List<UserInfo> queryAllUser();
    @Select("select * from user_info where id=#{id}")
    UserInfo selectById(Integer id);

}

我们输入的参数并没有在后面拼接,id的值是使用 ? 进行占位. 这种SQL我们称之为"预编译SQL"

使用 ${ }:

java 复制代码
@Mapper
public interface UserInfoMapper {
    List<UserInfo> queryAllUser();
    @Select("select * from user_info where id=${id}")
    UserInfo selectById(Integer id);

}

可以看到,这次的参数是直接拼接在SQL语句中了.

  • 接下来我们再看String类型的参数
java 复制代码
@Mapper
public interface UserInfoMapper {
    @Select("select username, `password`, age, gender, phone from user_info where username= #{name} ")
    UserInfo queryByName(String name);
    
}

观察我们打印的日志,结果正常返回

我们把 #{} 改成 ${} 再观察打印的日志:

java 复制代码
@Mapper
public interface UserInfoMapper {
    @Select("select username, `password`, age, gender, phone from user_info where username= ${name} ")
    UserInfo queryByName(String name);

}

可以看到,这次的参数依然是直接拼接在SQL语句中了,但是字符串作为参数时,需要添加引号 '' ,使用 ${} 不会拼接引号 '' ,导致程序报错.

修改代码如下:

java 复制代码
@Mapper
public interface UserInfoMapper {
    @Select("select username, `password`, age, gender, phone from user_info where username= '${name}' ")
    UserInfo queryByName(String name);

}

再次运行,结果正确返回

从上面两个例子可以看出:

#{} 使用的是预编译SQL,通过 ? 占位的放式, 提前对SQL进行编译,然后把参数填充到SQL语句 中.#{} 会根据参数类型, 自动拼接引号'' .

${} 会直接进行字符替换, ⼀起对SQL进行编译. 如果参数为字符串, 需要加上引号 '' 。

6.2.2 #{ } 和 ${ } 的区别

#{} 和 ${}的区别就是 预编译SQL 和 即时SQL 的区别.

  • 性能更高

绝⼤多数情况下,某⼀条SQL语句可能会被反复调用执行,或者每次执行的时候只有个别的值不同(比 如 select 的 where 子句值不同, update 的 set 子句值不同, insert 的 values 值不同).如果每次都需要 经过上面的语法解析,SQL优化、SQL编译等,则效率就明显不行了.

预编译SQL,编译⼀次之后会将编译后的SQL语句缓存起来,后面再次执行这条语句时,不会再次编译 (只是输入的参数不同),省去了解析优化等过程,以此来提⾼效率

  • 更安全(防止SQL注⼊)

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

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

先来看看SQL注⼊的例子:

java 复制代码
@Mapper
public interface UserInfoMapper {
    @Select("select username, `password`, age, gender, phone from user_info where username= '${name}' ")
    UserInfo queryByName(String name);

}

正常访问传参: admin

java 复制代码
    @Test
    void queryByName() {
        UserInfo userInfo=userInfoMapper.queryByName("admin");
        System.out.println(userInfo);
    }
}

运行结果:

SQL注⼊场景: 传参 ' or 1='1

结果依然被正确查询出来了,其中参数 or 被当做了SQL语句的⼀部分

可以看出来,查询的数据并不是自己想要的数据.所以用于查询的字段,尽量使用 #{} 预查询的方式

SQL注入(SQL Injection)

一、定义

SQL注入是Web安全中最常见、危害最大的漏洞之一。

指攻击者在用户可控的输入点(如表单、URL参数、Cookie等),恶意插入/拼接SQL语句片段,欺骗后端数据库执行非预期的SQL命令,从而非法操作数据。

本质原因:

用户输入未做过滤/校验,直接拼接到SQL语句中执行。

二、主要危害

  1. 数据泄露

窃取用户信息、密码、手机号、身份证、订单、商业数据等。

  1. 数据篡改

删库、删表、改密码、改权限、改订单金额/状态。

  1. 越权访问

绕过登录、查看他人数据、获取管理员权限。

  1. 服务器沦陷

部分数据库可执行系统命令、读写文件,进而拿下服务器权限。

  1. 业务瘫痪

删库、拖库、导致系统崩溃、业务停服,造成经济与信誉损失。​

  1. 法律风险

泄露用户隐私违反《网络安全法》《个人信息保护法》,企业需承担责任。

6.3 排序功能

从上面的例子中,可以得出结论: { }会有SQL注入的风险,所以我们尽量使用 #{ }完成查询 既然如此,是不是 { }就没有存在的必要性了呢?----当然不是.

接下来我们看下${}的使用场景:

SQL:

java 复制代码
select id, username, age, gender, phone, delete_flag, create_time, 
update_time from user_info order by id asc
  • ASC:升序(从小到大,默认可不写)

  • DESC:降序(从大到小)

ASC 和 DESC不能添加引号!!

由于ASC 和 DESC不能添加引号并且 #{} 会根据参数类型判断是否拼接引号 ''( 如果参数类型为String,就会加上引号.),所以这里就不可以使用 #{},而要使用 ${}

但是我们前面讲到过使用 ${} 会有SQL注入的风险,那么我们该如何预防呢-----限制用户输入,把参数设为枚举类型,只有枚举里允许的值才能传入。

6.4 like 查询

like 使用 #{ } 报错

java 复制代码
@Mapper
public interface UserInfoMapper {

    @Select("select id, username, age, gender, phone, delete_flag, create_time, update_time          from user_info where username like '%#{key}%' ")
    List<UserInfo> queryAllUserByLike(String key);

}

like 模糊查询

SQL语法:

sql 复制代码
SELECT * FROM 表
WHERE 字段 LIKE '匹配串';

解决办法: 使用 mysql 的内置函数 concat() 来处理,实现代码如下:

java 复制代码
@Select("select id, username, age, gender, phone, delete_flag, create_time, 
update_time from user_info where username like concat('%',#{key},'%')")
List<UserInfo> queryAllUserByLike(String key);

七. 数据库连接池

在上⾯Mybatis的讲解中, 我们使用了数据库连接池技术, 避免频繁的创建连接,销毁连接 下 我们来了解下数据库连接池(以前我们是 每次操作都新建连接 → 用完关闭​ ;现在用 连接池(DataSource):一开始就创建一批连接,循环复用,不频繁创建销毁。)

7.1 介绍

数据库连接池负责分配、管理和释放数据库连接,它允许应用程序重复使用⼀个现有的数据库连接, 而不是再重新建立⼀个.

没有使用数据库连接池的情况: 每次执行SQL语句, 要先创建⼀个新的连接对象, 然后执行SQL语句, SQL 语句执行完,再关闭连接对象释放资源.这种重复的创建连接,销毁连接比较消耗资源。

使用数据库连接池的情况: 程序启动时,会在数据库连接池中创建⼀定数量的Connection对象,当客户请求数据库连接池,会从数据库连接池中获取Connection对象,然后执行SQL,SQL语句执行完,再把 Connection归还给连接池.

优点:

  • 减少了网络开销
  • 资源重用
  • 提升了系统的性能

7.2 使用

常见的数据库连接池: • C3P0 • DBCP • Druid • Hikari

目前前较流行的是Hikari,Druid

其中 Hikari 是 SpringBoot默认使⽤的数据库连接池

如果我们想把默认的数据库连接池切换为Druid数据库连接池,只需要引⼊相关依赖即可

XML 复制代码
<dependency>
 <groupId>com.alibaba</groupId>
 <artifactId>druid-spring-boot-3-starter</artifactId>
 <version>1.2.21</version>
</dependency>

八. 总结:#{ } 和 ${ } 区别

  1. #{}:预编译处理, ${}:字符直接替换
  2. #{}可以防止SQL注入,${}存在SQL注⼊的风险,查询语句中,可以使用 #{},推荐使用#{}
  3. 但是⼀些场景,#{}不能完成,比如排序功能,表名,字段名作为参数时,这些情况需要使用${}
  4. 模糊查询虽然${}可以完成,但因为存在SQL注⼊的问题,所以通常使用mysql内置函数concat来完成

IDEA我还有一个工具:MyBatisX ,可以辅助我们更简洁,方便的使用MyBatis

相关推荐
Teable任意门互动2 小时前
中小企业进销存实战:Teable多维表格从零搭建高效库存管理系统
开发语言·数据库·excel·飞书·开源软件
Y001112362 小时前
Day7-MySQL-约束
数据库·sql·mysql
tuyanfei2 小时前
Spring 简介
java·后端·spring
ZhengEnCi2 小时前
J0A-JPA持久化技术专栏链接目录
java·数据库
baizhigangqw3 小时前
Spring Boot spring.factories文件详细说明
spring boot·后端·spring
爱吃土豆的马铃薯ㅤㅤㅤㅤㅤㅤㅤㅤㅤ3 小时前
DB-GPT 和 Dify 区别
数据库·gpt
Insist7533 小时前
kingbase数据库--指定备份集恢复
数据库
ZhengEnCi3 小时前
J6D-ACID到底是什么?
数据库
q***75183 小时前
MySQL Workbench菜单汉化为中文
android·数据库·mysql
2301_821700533 小时前
使用Scikit-learn进行机器学习模型评估
jvm·数据库·python