MyBatis 查询数据库入门

2,199 阅读21分钟

我正在参加「掘金·启航计划」

一、MyBatis 是什么?

MyBatis 是一款优秀的持久层框架,它支持自定义 SQL、存储过程以及高级映射。MyBatis 去除了几乎 所有的 JDBC 代码以及设置参数和获取结果集的工作。MyBatis 可以通过简单的 XML 或注解来配置和 映射原始类型、接口和 Java POJO(Plain Old Java Objects,普通老式 Java 对象)为数据库中的记 录。

简单来说 MyBatis 是更简单完成程序和数据库交互的工具,也就是更简单的操作和读取数据库工具。

针对 “MyBatis 是一款优秀的持久层框架” 进行分析和补充:

MyBatis也是一个 ORM (Object Relational Mapping,即对象关系映射)框架。

在面向对象编程语言中,将关系型数据库中的数据与对象建立起映射关系,进而自动的完成数据与对象的相互转换:

  1. 将输入数据(即传入对象)+ SQL 映射成原生 SQL。
  2. 将结果集映射为返回对象,即输出对象。\

ORM 把数据库映射为对象:

  • 数据库表(table) --> 类(class)
  • 记录(record,行数据) --> 对象(object)
  • 字段(field) --> 对象的属性(attribute)

一般的 ORM 框架,会将数据库模型的每张表都映射为一个 Java 类。

即,使用 MyBatis 可以像操作对象一样来操作数据库中的表,可以实现对象和数据库表之间的转换。


即:MyBatis 可以当作是一座 “桥梁”:

将数据库 和 程序,映射起来。

MySQL 和 MyBatis 是不一样的:

MySQL 提供了一个 数据存取(数据管理)的软件。

而 MyBatis 是一个 “中间桥梁”,用于连接程序和数据库,建立映射关系,进行 数据操作 的中间层(持久层)。

二、为什么要学习 MyBatis

对于后端开发来说,程序是由以下两个重要的部分组成的:

  1. 后端程序
  2. 数据库

而这两个重要的组成部分要通讯,就要依靠数据库连接工具,那数据库连接工具有哪些?比如之前我们 学习的 JDBC,还有今天我们将要介绍的 MyBatis,那已经有了 JDBC 了,为什么还要学习 MyBatis?

这是因为 JDBC 的操作太繁琐了,我们回顾一下 JDBC 的操作流程:

  1. 创建数据库连接池 DataSource
  2. 通过 DataSource获取数据库连接 Connection
  3. 编写要执行带 ? 占位符的 SQL 语句
  4. 通过 ConnectionSQL 创建操作命令对象 Statement  
  5. 替换占位符:指定要替换的数据库字段类型,占位符索引及要替换的值  
  6. 使用 Statement 执行 SQL 语句  
  7. 查询操作:返回结果集 ResultSet,更新操作:返回更新的数量
  8. 处理结果集
  9. 释放资源

JDBC 操作示例回顾

import com.mysql.jdbc.jdbc2.optional.MysqlDataSource;

import javax.sql.DataSource;
import java.sql.Connection;
import java.sql.PreparedStatement;
import java.sql.SQLException;
import java.util.Scanner;

public class TestJDBC {
    public static void main(String[] args) throws SQLException {
        //让用户手动输入数据到数据库中
        Scanner scanner = new Scanner(System.in);

        //1. 创建数据源
        DataSource dataSource = new MysqlDataSource();

        //设置数据库所在的地址
        ((MysqlDataSource)dataSource).setURL("jdbc:mysql://127.0.0.1:3306/java?characterEncoding=utf8&useSSL=false");
        //设置登录数据库的用户名
        ((MysqlDataSource)dataSource).setUser("root");
        //设置登录数据库的密码
        ((MysqlDataSource)dataSource).setPassword("1234");

        //2. 让代码和数据库服务器之间建立连接
        Connection connection = dataSource.getConnection();

        //3. 操作数据库,以插入数据为例
        //   关键所在:构造一个 SQL 语句
        //   在 JDBC 中构造 SQL 语句,不必带上 ;
        //   ; 只是在命令行中用来区分不同的语句,现在是直接在代码中操作
        String sql = "insert into JDBC values(1,'张三')";
        // 此处光是一个 String 类型的 sql 还不行,需要把这个 String 包装成一个 “语句对象”
        PreparedStatement preparedStatement = connection.prepareStatement(sql);

        //用户手动输入数据
        System.out.println("请输入 ID:");
        int id = scanner.nextInt();
        System.out.println("请输入 姓名:");
        String name = scanner.next();

        //? 相当于告诉 java程序,这两个字段的值 还不确定,此时就使用 ? 先占个位置
        // 再使用 PreparedStatement 的 setXXX 方法 进行替换,这里的setXXX方法很多,需要让这里的方法和数据库的列的类型匹配
        String sql2 = "insert into JDBC values(?,?)";
        PreparedStatement statement = connection.prepareStatement(sql2);
        //进行替换操作
        statement.setInt(1,id); //下标从 1 开始计算,把第一个 ? 替换成 id 这样的值
        statement.setString(2,name);
        System.out.println("statement: " + statement); //通过这个打印操作,可以看到拼装好之后的 SQL 长啥样

        //4. 执行 SQL
        //      SQL 里面如果是 insert, update, delete ,都使用 executeUpdate 方法,
        //      SQL 里面如果是 select,则使用 executeQuery 方法
        //      返回值就表示这个操作,影响到了几行,就相当于在控制台里输入 sql 之后,得到的数字
        int ret =  preparedStatement.executeUpdate();
        int ret2 = statement.executeUpdate();
        System.out.println(ret);
        System.out.println(ret2);

        //5. 此时 SQL 已经执行完毕,然后还需要释放资源
        preparedStatement.close();
        statement.close();
        connection.close();
    }
}

从上述代码和操作流程可以看出,对于 JDBC 来说,整个操作非常的繁琐,我们不但要拼接每一个参 数,而且还要按照模板代码的方式,一步步的操作数据库,并且在每次操作完,还要手动关闭连接等, 而所有的这些操作步骤都需要在每个方法中重复书写。于是我们就想,那有没有一种方法,可以更简单、更方便的操作数据库呢?

答案是肯定的,这就是我们要学习 MyBatis 的真正原因,它可以帮助我们更方便、更快速的操作数据库。

三、怎么学 MyBatis

MyBatis 学习只分为两部分:

  • 配置 MyBatis 开发环境。
  • 使用 MyBatis 模式和语法操作数据库。

1. 创建 MyBatis 项目

准备工作:创建数据库 和 数据表

-- 创建数据库
drop database if exists Mybatis;
create database Mybatis DEFAULT CHARACTER SET utf8;
-- 使⽤数据数据
use Mybatis;
-- 创建表[⽤户表]
drop table if exists userinfo;
create table userinfo(
id int primary key auto_increment,
username varchar(100) not null,
password varchar(32) not null,
photo varchar(500) default '',
createtime datetime default now(),
updatetime datetime default now(),
`state` int default 1
);
-- 创建⽂章表
drop table if exists articleinfo;
create table articleinfo(
id int primary key auto_increment,
title varchar(100) not null,
content text not null,
createtime datetime default now(),
updatetime datetime default now(),
uid int not null,
rcount int not null default 1,
`state` int default 1
);
-- 创建视频表
drop table if exists videoinfo;
create table videoinfo(
vid int primary key,
`title` varchar(250),
`url` varchar(1000),
createtime datetime default now(),
updatetime datetime default now(),
uid int
);
-- 添加⼀个⽤户信息
INSERT INTO `Mybatis`.`userinfo` (`id`, `username`, `password`, `photo`,
`createtime`, `updatetime`, `state`) VALUES
(1, 'admin', 'admin', '', '2021-12-06 17:10:48', '2021-12-06 17:10:48',
1);
-- ⽂章添加测试数据
insert into articleinfo(title,content,uid)
values('Java','Java正⽂',1);
-- 添加视频
insert into videoinfo(vid,title,url,uid) values(1,'java
title','http://www.baidu.com',1);

将上面的代码复制到本地的 MySQL。

1.1 添加 MyBatis 相关依赖

这里会涉及两个场景:

  1. 项目创建的时候,引入 MyBatis 相关依赖。
  2. 老项目添加 MyBatis。

1. 新建一个 MyBatis 项目

  1. 创建新项目:

\

  1. MyBatis 项目是建立在 Spring MVC 项目上的,因此,下面三个框架依赖必不可少:

\

  1. 添加 MyBatis 项目:

\

  1. 引入可驱动的数据库:

  1. 点击 finish即可完成。

2. 老项目中引入 MyBatis 相关依赖

使用 Edit Starter插件进行添加。

pom.xml文件中的 dependencies标签中,找到一个空行(人造一个也行),鼠标右击,选择 generate

下面为获取依赖的官方网址:

选择要添加的依赖:

\

\

1.2 配置数据库连接字符串

不要立即启动项目,否则就会报错:

\

  1. resources下创建三个.yml文件

(现在的学习还不会涉及到生产环境)

  1. 配置开发环境配置文件:

注意:关于 driver-class-name中所写的驱动名称

  • 驱动名称:我们使用的是 MySQL,因此我们填写的是 MySQL 的驱动名称。
  • 如果使用的 MySQL 是 5.x之前的版本,则用 com.mysql.jdbc.Driver
    如果版本大于 5.x,则用 com.mysql.cj.jdbc.Driver

\

  1. 在主配置文件中激活开发环境

\

此时启动项目后就不会报错了:

\

1.3 配置 MyBatis 保存的 xml 的目录

MyBatis 有两种操作方法:

  1. 使用 xml的形式。
  2. 注解。(MyBatis 3.1 版本之后提供)

但是,注解的方式并不好用,我们主要还是关注 xml形式的操作。

  1. 一般我们在 resources目录下,创建一个子目录,来存放 xml文件:

\

  1. 由于此配置是公共的,所以我们在主配置文件配置 MyBatis 的 xml 保存路径:

\

四、使用 MyBatis 的操作模式操作数据库

MyBatis 的操作模式

MyBatis 的操作模式,包含两个部分:

  1. Interface(接口,里面是方法的定义)
  2. xml 文件(对 方法 的实现,使用 SQL 语句)

在 Interface 接口中会加一个注解,这个注解是MyBatis里的注解 @Mapper,将普通的接口变为 MyBatis 里面的接口,将接口里面的方法映射进 xml文件。

\

MyBatis 查询:实现一个根据用户id来查询用户信息的操作

数据库中已经创建好的表:

\

1. 定义接口

现在根目录底下创建几个包:

  • model 与数据库交互
  • controller 与前端交互
  • server 决定调用哪些映射方法
  • mapper 操作数据,实现映射

\

为了能与数据进行交互,需要创建实体类,来存储在数据库中得到的结果。在 model目录底下创建实体类,(实体类名称最好与数据库的表名一致,这样更好处理):

在实体类里面的字段,也要和数据库中表里面的字段名称相同:

\

下面就可以在 mapper包底下写数据了:

注意:一定要在接口上加上 注解@Mapper。

在我们给接口加上 @Mapper 注解后,它就不是一个单纯的接口了。此时,UserMapper接口变成 MyBatis 的一部分了。这个 接口 里面的方法,就可以使用 xml去实现了。

但是 Java 的接口是依靠类来实现的。这确实和传统的开发是不一样的,传统开发直接写业务代码就行了。

现在是要写 SQL 了,SQL 和 业务代码,是两个不同的体系。

普通接口变成 MyBatis 接口,有什么好处呢?

它将接口的生命周期,交给容器来托管,它会实现 interface 和 xml 之间的关联关系。

\

下面来写 User Mapper接口里面方法的定义:

package com.example.MyBatisDemo.mapper;

import com.example.MyBatisDemo.model.UserInfo;
import org.apache.ibatis.annotations.Mapper;
import org.apache.ibatis.annotations.Param;

@Mapper // 变成 mybatis interface
public interface UserMapper {
    // 根据用户 id 查询用户
    public UserInfo getUserById(@Param("id")Integer id); // @Param 代表参数在xml中名字为 id

}

\

2. 创建 xml,实现接口里面定义的方法

xml 文件,不能随意创建。

因为我们在之前的配置文件中,指定了 xml 文件的保存路径,并且命名规则也指定了。

按照规则创建 xml 文件,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">
<!-- namespace 要设置成实现接口的具体包名加类名 -->
<mapper namespace="">

</mapper>

其中里面的 namespace设置成实现接口的位置:包名 + 接口名称:

\

接着,在 xml 文件中,实现 接口UserMapper里面的方法了:

查询标签里面的 resultType 是不能省略的,否则访问网页会报错:

\

  • select标签里面的内容你就按照 SQL 语句写就行了,此处我们写的是 select * from userinfo where id=#{id}

\

  • 关于接口UserMapper中的@Param:

如果不写这个注解,默认在 xml 文件中,获取方法参数的名称,就是方法参数原本的名称。

但是,在有一些 jdk 版本中,不写这个注解就会报错。所以为了避免麻烦,还是加上这个注解比较好。\

  • 这里面要注意一点,#{id}里面的 id 是 @Param("id")里的 id 名称。(换句话说,如果 @Param("userId"),你 SQL 里面要变为 #{userId}。\

  • 还有一点,SQL 语句末尾可以不要分号。

\

  • 关于 获取方法参数,使用 ${}#{}的区别,后面会详细讲。

运行结果展示

service包底下创建一个 UserService类。

UserService类里面写一个方法,去调用 接口UserMapper中的方法。

然后,在 controller包下,创建 UserContronller类,去调用 类UserService中的方法。

UserMapper 中方法

service 层

controller 层

\

启动项目,打开浏览器去访问:

\

\

开启 MyBatis 日志打印功能

一般在开发环境中开启。

目录结构:

# 开启 Mybatis SQL 打印
logging:
	level:
		com:
			example:
				MyBatisDemo: debug
mybatis:
	configuration:
		log-impl: org.apache.ibatis.logging.stdout.StdOutImpl

开启后可以在idea的控制台上看见日志:

当我们开启 MyBatis SQL 日志打印的时候,我们能清楚看到最终生成的SQL语句以及执行结果。

Spring Boot 单元测试简单使用

五、增、删、改 操作

接下来实现以下用户信息的增加、删除和修改操作,对应使用MyBatis 的标签如下:

  • <insert>标签:插入语句。
  • <update>标签:修改语句。
  • <delete>标签:删除语句。

@Transactional 注解

有一点要注意:在进行增、删、改的时候,如果不想污染数据库,可以在Test中加注解:@Transactional(表示在方法执行完之后回滚事务)

\

MyBatis 修改操作

  1. interface 里面添加修改方法的声明:

注意:@Param 建议要添加上,不然有些电脑上运行会报错,会提示找不到参数。

它和 xml 里面的参数是匹配的。

  1. 在 xml 中添加<update>标签和具体的执行 sql

名称要对应:

\

MyBatis 删除操作

  1. 在 interface 里面添加 删除 的代码声明

  1. 在 xml 中添加 <delete>标签和删除的 sql 编写

\

MyBatis 添加操作

  1. 在 interface 添加方法声明

  1. 在 xml 实现添加业务

\

注意:这里的参数传入的是一个对象啊,我们怎么才能获取到里面的属性呢?可以直接通过 #{属性名}去获取就行:

\

如果不想将影响的行数,作为返回值了。想要 插入成功的用户信息的ID 作为返回值!虽然实现的步骤没有发生变化,但是想要达到预期的效果,还需要添加些东西:

在 第二个步骤中(方法名称设为 addGetId):想要返回用户信息的ID,还需要添加两个属性:useGeneratedKeyskeyProperty

  • useGeneratedKeys: 是否自动生成主键(true 为是,false 为否)。这会令 MyBatis 使用 JDBC 的 getGeneratedKeys 方法来取出由数据 库内部生成的主键(比如:像 MySQL 和 SQL Server 这样的关系型数据库管理系统的自动递增字段),默认值:false
  • keyProperty: 返回的主键赋值到那个属性上,这个时候可以指定 id 属性了。指定能够唯一识别对象的属性,MyBatis 会使用 getGeneratedKeys 的返回值 或 insert 语句的 selectKey 子元素设置它的值,默认值:未设置(unset)。如果生成列不止一个,可以用逗号分隔多个属性名称。

注意:最好加一个属性 keyColumn 设置生成键值在表中的列名,在某些数据库(如 PostgreSQL)中,当主键列不是表中第一列的时候,是必须设置的。如果生成列不止一个,就可以用逗号分隔多个属性名称。

即,加入 keyColumn 的原因就是为了确保获取对应字段。

\

\

六、查询操作

单表查询

单表查询即为上面所介绍的:

\

参数占位符 #{} 和 ${}

1. 功能不同

  • #{} 预编译处理。
  • ${} 字符直接替换。

预编译处理 MyBatis 在处理 #{}时,会将 SQL 中的 #{}替换为 ?。使用 PreparedStatement 的 set 方法来赋值

直接替换 MyBatis 在处理 ${}时,把 ${}替换成变量的值。

可以在之前写过的查询代码中,演示下 #{}${}的区别:

测试类

使用 #{}占位符:

测试类运行结果:

使用 ${}占位符:

测试类运行结果:

\

${} 的问题

当参数为数值类型时(在不考虑安全问题的前提下),${}和 #{} 的执行效果都是一样的,然而当参数类型为 字符时,再使用 ${}就有问题了,如下代码所示:

<select id="getUserById" resultType="com.example.MyBatisDemo.model.UserInfo">
	select * from userinfo where username=${name}
</select>

以上程序执行时,生成的 SQL 语句如下:

这会导致程序报错,因为传递的参数是字符类型的,而在 SQL 语法中,如果是字符类型需要给值添加单引号,否则就会报错,而 ${}是直接替换的,不会自动添加单引号,所以执行就会报错。而使用 #{}采用的是 占位符 预执行的,所以不存在任何问题,它的实现代码如下:

<select id="getUserById" resultType="com.example.MyBatisDemo.model.UserInfo">
	select * from userinfo where username=#{name}
</select>

以上程序的最终生成的执行 SQL 如下:

2. 使用场景不同

虽然使用 #{} 的方式可以处理任意类型的参数,然而当传递的参数是一个 SQL 命令或 SQL 关键字时 #{} 就会出问题了。比如,当我们要根据价格从高到低(倒序)、或从低到高(正序)查询时,如下图所示:

此时我们要传递的排序的关键字,desc 倒序(价格从高到低)或者是 asc 正序(价格从低到高),此时我们使用${} 的实现代码如下:

<select id="getOrderList" resultType="com.example.MyBatisDemo.model.Goods">
    select * from goods order by price ${order}
</select>

以上代码生成的执行 SQL 和 运行结果如下:

但是,如果将代码中的 ${}改为 #{},那么程序就会报错,#{}的实现代码如下:

<select id="getOrderList" resultType="com.example.MyBatisDemo.model.Goods">
    select * from goods order by price #{order}
</select>

以上代码生成的执行 SQL 和 运行结果如下:

从上述的执行结果可以看出,当传递的是普通参数时,需要使用 #{} 的方式,而当传递的是 SQL 命令或 SQL 关键字时,需要使用 ${} 来对 SQL 中的参数进行直接替换并执行。


3. 安全性不同

${}#{}最主要的区别体现在安全方面,当使用 ${} 会出现安全问题,也就是 SQL 注入的问题,而使用 #{} 因为是预处理,所以不会出现安全问题, 下面通过登录功能来观察二者的区别:

3.1 使用 ${} 实现用户登录

信息:

UserMapper.xml 中的实现代码如下:

<!-- 用户登录 -->
<select id="login" resultType="com.example.MyBatisDemo.model.UserInfo">
    select * from userinfo where username='${username}' and password='${password}'
</select>

单元测试代码如下:

@Test
void login() {
    UserInfo userInfo = userMapper.login("admin","admin");
    System.out.println(userInfo);
}

以上代码生成的执行 SQL 和运行结果如下:

从结果可以看出,当我们传入了正确的用户名和密码时,能成功查询数据。但是,在我们使用 ${}时,当我们在不知道正确密码的情况下,使用 SQL 注入语句也能得到用户的私人信息,SQL 注入的实现代码如下:

@Test
void login() {
    List<UserInfo> userInfo = userMapper.login("admin","' or 1='1");
    System.out.println("用户信息:"+userInfo);
}

以上代码生成的执行 SQL 和 运行结果如下:

从上述结果可以看出,当使用 ${} 时,在不知道正确密码的情况下也能得到用户的私人数据,这就像一个小偷在没有你们家钥匙的情况下,也能轻松的打开你们家大门一样,这是何其恐怖的事情。那使用 #{} 有没有安全问题呢?接下来我们来测试一下。

3.2 使用 #{} 实现用户登录

首先将 UserMapper.xml 中的代码改成以下内容:

<select id="login" resultType="com.example.MyBatisDemo.model.UserInfo">
    select * from userinfo where username=#{username} and password=#{password}
</select>

接着我们使用上面的 SQL 注入来测试登录功能:

@Test
void login() {
    List<UserInfo> userInfo = userMapper.login("admin","' or 1='1");
    System.out.println("用户信息:"+userInfo);
}

最终生成的 SQL 和 执行结果如下:

从上述代码可以看出,使用 SQL 注入是无法攻破 #{} 的“大门”的,所以可以放心使用。


总结:

${}#{} 都是 MyBatis 中用来替换参数的,它们二者的区别主要体现在:

  1. 功能不同:${} 是直接替换,而 #{} 是预处理。
  2. 使用场景不同:普通参数使用 #{},如果传递的是 SQL 命令或 SQL 关键字,需要使用 ${},但在使用前一定要做好安全验证。
  3. 安全性不同:使用 ${} 存在安全问题,而 #{} 则不存在安全问题。

like 查询 - 特殊情况

在前面的问题中,由于关键字就那么几个,可以直接穷举,所以很容易在 Controller 层里面判断数据的正确性。

但是!模糊匹配的结果是不能穷举的!如果数据有几百万个,我们验证数据的正确性得累死!

下面演示:

like 使用 #{}报错:

<select id="getUserByName2" resultType="com.example.MyBatisDemo.model.UserInfo">
  select * from userinfo where username like '%#{username}%'
</select>

报错信息如下:

相当于:select * from userinfo where username like '%'username'%'

所以这里就不能使用 #{}了,可以使用 ${}进行直接的替换,就不会出现单双引号了:

<select id="getUserByName2" resultType="com.example.MyBatisDemo.model.UserInfo">
  select * from userinfo where username like '%${username}%'
</select>

测试类运行结果如下:

但是!使用了 ${},就需要在 Controller 里进行验证数据,这一点很重要!!

但是模糊匹配的结果,是海量的!我们不可能全部穷举出来。

\

此时就可以考虑 concat拼接方法:

可以考虑使用 mysql 的内置函数 concat()来处理,此函数的效果为:

实现代码如下:

<select id="getUserByName2" resultType="com.example.MyBatisDemo.model.UserInfo">
    select * from userinfo where username like concat('%',#{username},'%')
</select>

结果如下:

\

多表查询

前置知识

返回类型:resultType

这个我们很熟悉,这是用于指定 SQL 查询结果映射的对象类型。

但是这里有个细节!!

虽然我们只是使用了 id 属性,但是有一个问题:当我们指定了返回的类型之后,这个实体类(UserInfo)里面的属性必须和数据库中的字段是一致的!哪怕这个属性在 SQL 语句中没有被使用,也是一样的,必须相同!

在名字相同的情况下,运行测试类,看看情况:

\

如果我们将 UserInfo 中的 username 属性 改为 name:

此时再来运行测试类:

虽然测试通过了,但是 name 属性是为 null 的,而不是 admin。

出现这个问题的原因就是:数据库表中的 字段名称 和 实体类属性名称,不相同!!

因此无法完成匹配赋值!

除了修改 实体类属性名称,还有一个办法,使用下面介绍的 resultMap

返回字典映射:resultMap

resultMap 使用场景:

  1. 字段名称和程序中属性名不同的情况,可以使用 resultMap 配置映射:

下面的写法只适合 单表查询

  <resultMap id="BaseMap" type="com.example.MyBatisDemo.model.UserInfo">
      <!-- 主键映射 -->
      <id column="id" property="id"></id>
      <!-- 表示普通属性映射 -->
      <result column="username" property="name"></result>
  </resultMap>
  • resultMap里,为此 resultMap 命名为 idtype 为你要映射的类的地址
  • 普通属性映射里面,column 代表的是数据库中的字段名,property 代表的是程序中属性名。即 column 映射到 property。

此时就可把 resultType 删除,改用 resultMap了,其值为我们给 resultMap 起的别名 BaseMap:

<select id="getUserById" resultMap="BaseMap">
    select * from userinfo where id=${id}
</select>

此时再运行测试单元,得到结果:

\

  1. 一对一和多对多关系可以使用 resultMap 映射并查询数据。

多表查询:一对一关系

下面来模拟实现一对一的关系。文章和作者就是一对一的关系,因为一篇文章只能对应一个作者。

我们先创建文章表的实体类:由于 文章表 和 作者表 是一对一关系,所以可以在文章表的实体类中加入 UserInfo的属性:

package com.example.MyBatisDemo.model;

import lombok.Data;

import java.time.LocalDateTime;

@Data
public class ArticleInfo {
    private Integer id;
    private String title;
    private String content;
    private LocalDateTime createtime;
    private LocalDateTime updatetime;
    private Integer uid;
    private Integer rcount;
    private Integer state;
    private UserInfo userInfo;
}

先来观察下 articleInfo 表中的信息,方便后面写代码:

\

在 mapper 包中创建 MyBatis 接口,实现根据文章 id 查询到文章的详细信息:

@Mapper
public interface ArticleMapper {
    public List<ArticleInfo> getAll(@Param("id")Integer id);
}

创建一个新的 xml 用来编写 SQL:

<mapper namespace="com.example.MyBatisDemo.mapper.ArticleMapper">

    <select id="getAll" resultType="com.example.MyBatisDemo.model.ArticleInfo">
        select * from articleinfo where id=#{id}
    </select>

</mapper>

编写测试类:

@SpringBootTest
@Slf4j
class ArticleMapperTest {

    @Resource
    private ArticleMapper articleMapper;

    @Test
    void getAll() {
        List<ArticleInfo> articleInfo = articleMapper.getAll(1);
        log.info("作者信息: " + articleInfo);
    }
}

测试类执行结果:

咋一看上面的结果没啥问题,但是观察 属性 UserInfo 时却发现是空的:

为什么会出现上面的情况呢?

原因就是,实体类 ArticleInfo中有 UserInfo属性,但是 文章表中却没有这个字段:

此时我们就需要 resultMap了。

设置 resultMap

<mapper namespace="com.example.MyBatisDemo.mapper.ArticleMapper">
    <resultMap id="BaseMap" type="com.example.MyBatisDemo.model.ArticleInfo">
        
    </resultMap>
    <select id="getAll" resultMap="BaseMap">
        select * from articleinfo where id=#{id}
    </select>

</mapper>
  • BaseMap为此标签的别名,与其他 xml 中的 resultMap 的别名重复的话,不会有影响。

在 resultMap 中设置映射关系:将文章表中所有字段都映射出来:

<mapper namespace="com.example.MyBatisDemo.mapper.ArticleMapper">
    <resultMap id="BaseMap" type="com.example.MyBatisDemo.model.ArticleInfo">
        <id column="id" property="id"></id>
        <result column="title" property="title"></result>
        <result column="content" property="content"></result>
        <result column="createtime" property="createtime"></result>
        <result column="updatetime" property="updatetime"></result>
        <result column="uid" property="uid"></result>
        <result column="rcount" property="rcount"></result>
        <result column="state" property="state"></result>
    </resultMap>
    <select id="getAll" resultMap="BaseMap">
        select * from articleinfo where id=#{id}
    </select>

</mapper>

此时我们发现,resultMap 中还没有映射对象,这时候就需要用到标签 association

<mapper namespace="com.example.MyBatisDemo.mapper.ArticleMapper">
    <resultMap id="BaseMap" type="com.example.MyBatisDemo.model.ArticleInfo">
        <id column="id" property="id"></id>
        <result column="title" property="title"></result>
        <result column="content" property="content"></result>
        <result column="createtime" property="createtime"></result>
        <result column="updatetime" property="updatetime"></result>
        <result column="uid" property="uid"></result>
        <result column="rcount" property="rcount"></result>
        <result column="state" property="state"></result>
        <association property="userInfo"
                     resultMap="com.example.MyBatisDemo.mapper.UserMapper.BaseMap">
        </association>
    </resultMap>
    <select id="getAll" resultMap="BaseMap">
        select * from articleinfo where id=#{id}
    </select>

</mapper>

association标签的意思为:通过 resultMap 将目前的 resultMap 与另一个 resultMap 进行关联!就是通过 UserMapper 的 BaseMap 配置的字段信息,打包赋值给 userinfo。

将 SQL 语句改为多表的查询:

<mapper namespace="com.example.MyBatisDemo.mapper.ArticleMapper">
<select id="getAll" resultMap="BaseMap">
        select a.*,u.* from articleinfo a left join userinfo u on a.uid=u.id where a.id=#{id}
    </select>

</mapper>

但是如果 UserMapper 的 BaseMap 中只配置了部分字段,就会出现一些问题:

<mapper namespace="com.example.MyBatisDemo.mapper.UserMapper">
    <resultMap id="BaseMap" type="com.example.MyBatisDemo.model.UserInfo">
        <!-- 主键映射 -->
        <id column="id" property="id"></id>
        <!-- 表示普通属性映射 -->
        <result column="username" property="name"></result>
        
    </resultMap>

</mapper>

执行 ArticleMapperTest 测试单元,结果如下:

可以看到 UserInfo 里面只显示了在 UserMapper.xml 中映射的字段,其余没有映射的字段都是为空(status 默认为 0)

这里可以得出一个小结论:

  • 在本身 xml 文件中,是可以不用映射所有属性的信息,因为它是自己调用自己,所以不需要 resultMap 将属性全部映射,都能自动完成所有属性的映射。
  • 而想要在一个 resultMap 中调用另一个 resultMap 中的信息,只能是它映射了的信息,否则无法获取。

这时候我们将 UserMapper.xml 中的属性映射补全:

<mapper namespace="com.example.MyBatisDemo.mapper.UserMapper">
    <resultMap id="BaseMap" type="com.example.MyBatisDemo.model.UserInfo">
        <!-- 主键映射 -->
        <id column="id" property="id"></id>
        <!-- 表示普通属性映射 -->
        <result column="username" property="name"></result>
        <result column="password" property="password"></result>
        <result column="photo" property="photo"></result>
        <result column="createtime" property="createtime"></result>
        <result column="updatetime" property="updatetime"></result>
        <result column="state" property="state"></result>
    </resultMap>

</mapper>

测试类 ArticleMapperTest 运行结果:

可以看到UserInfo的所有字段都出来了。

但是上面获得的部分信息却是有误的:

但是有误的信息却和 ArticleInfo 中的信息相符合:

\

得出一个结论:

当一个字段在两张表中都存在时,默认读取的是 association 标签的 property 参数所在的数据表的字段值。

当前 property 是在 ArticleInfo 表中的:

下面来看下重名的字段:

下面来解决重名的问题。

解决 不同数据表中字段重名的情况

解决方法很简单,添加一个前缀,让 SQL 中的字段都带有 “身份标记”,这样可以避免重名的情况。

这种添加前缀的方式,是通过在 association中添加一个属性 columnPrefix,翻译成中文就是 属性前缀 的意思。

<association property="userInfo"
             resultMap="com.example.MyBatisDemo.mapper.UserMapper.BaseMap"
             columnPrefix="u_">
</association>

设置好前缀后,修改 SQL 语句:把那些字段名称重复的,前面加 u_前缀,并且我们获取 userinfo 中全部的字段信息:

<select id="getAll" resultMap="BaseMap">
    select a.*,u.id u_id,u.updatetime u_updatetime,u.createtime u_createtime,u.state u_state,
           u.username u_username,u.password u_password,u.photo u_photo
    from articleinfo a left join userinfo u on a.uid=u.id where a.id=#{id}
</select>

测试类 ArticleMapperTest 运行结果:

\

这才真正是实现了 一对一 关系的多表查询!!

多表查询:一对多关系

一对多关系,比如,一个用户可以是多篇文章的作者。

一对多关系,需要使用 <collection>标签,用法和 是一样的。

下面来演示:

首先将 用户表 的实体类进行处理:

@Data
public class UserInfo {
    private Integer id;
    private String name;
    private String password;
    private String photo;
    private String createtime;
    private String updatetime;
    private int state;
    private List<ArticleInfo> articleInfoList;
}

使用线性表的属性,来接收 多篇文章的信息。

在 UserMapper 中定义方法:

@Mapper
public interface UserMapper {
	// 根据用户 id 查询用户信息,和 所对应的文章信息
    public UserInfo getUserAndArticleById(@Param("id")Integer id);
}

\

在 UserMapper,xml 中映射属性:

<mapper namespace="com.example.MyBatisDemo.mapper.UserMapper">
    <resultMap id="BaseMap" type="com.example.MyBatisDemo.model.UserInfo">
        <!-- 主键映射 -->
        <id column="id" property="id"></id>
        <!-- 表示普通属性映射 -->
        <result column="username" property="name"></result>
        <result column="password" property="password"></result>
        <result column="photo" property="photo"></result>
        <result column="createtime" property="createtime"></result>
        <result column="updatetime" property="updatetime"></result>
        <result column="state" property="state"></result>
    </resultMap>
</mapper>

映射 中 articleList 对象,使用 collection标签:

<collection property="articleInfoList"
            resultMap="com.example.MyBatisDemo.mapper.ArticleMapper.BaseMap"
            columnPrefix="a_">
</collection>
  • property:需要映射的属性。
  • resultMap:映射对象。
  • columnPrefix:将文章表中 带有 a_前缀的字段值,打包映射到 articleList 对象中。

修改 SQL 语句:

<!-- 根据用户 id 查询用户信息,和 所对应的文章信息 -->
<select id="getUserAndArticleById" resultMap="BaseMap">
    select u.*,a.id a_id,a.title a_title,a.content a_content,a.createtime a_createtime,a.updatetime a_updatetime,a.uid a_uid,a.rcount a_rcount,a.state a_state
    from userinfo u left join articleinfo a on u.id=a.uid where u.id=#{id}
</select>

\

生成并编写测试单元:

@SpringBootTest
@Slf4j
class UserMapperTest {

    @Resource
    private UserMapper userMapper;

    @Test
    void getUserAndArticleById() {
        UserInfo userInfo = userMapper.getUserAndArticleById(1);
        log.info("用户信息:" + userInfo);
    }
}

运行结果:

\

至此一对多查询完结!

七、复杂情况:动态 SQL 使用

动态 sql 是Mybatis的强大特性之一,能够完成不同条件下不同的 sql 拼接。

可以参考官方文档:mybatis – MyBatis 3 | 动态 SQL

1. if 标签

在注册用户的时候,可能会有这样一个问题,如下图所示:

注册分为两种字段:必填字段和非必填字段,那如果在添加用户的时候有不确定的字段传入,程序应该 如何实现呢?

这个时候就需要使用动态标签 来判断了。

if 标签判断一个参数是否有值,如果没值,那么就会隐藏 if 中的 sql。

但是 必须有一个值是必传的。

语法:

<if test="username!=null">
	username=#{username}
</if>
  • 在 test 中,判断表达式是否为空。
  • 如果test 的结果为 true,那么拼接里面的 SQL 语句,加上 username=#{username}
  • 如果test 的结果为 false,那么 if 标签中的 sql 就会被忽略。
  • 这两个 username 是对应的。名字是 实体类中的属性名。

演示:

我们先给 userinfo 表中的 photo 设置一个默认值,默认为 default.png

\

在 UserMapper 中添加方法 add2:

@Mapper // 变成 mybatis interface
public interface UserMapper {

    // 添加用户,photo 是非必传参数
    public int add2(UserInfo userInfo);

}

\

在 UserMapper.xml 中编写 SQL:

<!-- 添加用户,photo 是非必传参数 -->
<insert id="add2">
    insert into userinfo(username,password
    <if test="photo!=null">
        ,photo
    </if>
    ) value(#{name},#{password}
        <if test="photo!=null">
            ,#{photo}
        </if>
        )
</insert>

test 里面的 photo 为对象的属性名。

编写测试类,当传入 photo 属性时:

@SpringBootTest
@Slf4j
class UserMapperTest {

    @Resource
    private UserMapper userMapper;

    @Test
    void add2() {
        UserInfo userInfo = new UserInfo();
        userInfo.setName("张三");
        userInfo.setPassword("123");
        userInfo.setPhoto("123.png");
        int result = userMapper.add2(userInfo);
        log.info("用户信息:"+userInfo);
    }
}

运行结果:

当不传入 photo 时:

2. trim 标签

最主要的作用:去除 SQL 语句前后多余的某个字符。

标签中有如下属性:

  • prefix:表示整个语句块,以prefix的值作为前缀
  • suffix:表示整个语句块,以suffix的值作为后缀
  • prefixOverrides:表示整个语句块要去除掉的前缀
  • suffixOverrides:表示整个语句块要去除掉的后缀

\

语法:

<trim prefix="(" suffix")" prefixOverrides="," suffixOverrides=",">
  <if test="xxx">
    ...
  </if>
  ...
</trim>
  • 基于 prefix 配置,开始部分加上(
  • 基于suffix 配置,结束部分加上 )
  • 多个 组织的语句都以 , 结尾,在最后拼接好的字符串还会以,结尾,会基于 suffixOverrides 配置去掉最后一个 ,

演示:

在 UserMapper 添加方法 add3.

编写 SQL 语句:

<mapper namespace="com.example.MyBatisDemo.mapper.UserMapper">

    <insert id="add3">
        insert into userinfo
        <trim prefix="(" suffix=")" suffixOverrides=",">
            <if test="name!=null">
                username,
            </if>
            <if test="password!=null">
                password,
            </if>
            <if test="photo!=null">
                photo,
            </if>
        </trim>
        values
        <trim prefix="(" suffix=")" suffixOverrides=",">
            <if test="name!=null">
                #{name},
            </if>
            <if test="password!=null">
                #{password},
            </if>
            <if test="photo!=null">
                #{photo},
            </if>
        </trim>
    </insert>

</mapper>

测试类:不传入 photo 时

@SpringBootTest
@Slf4j
class UserMapperTest {

    @Resource
    private UserMapper userMapper;

    @Test
    void add3() {
        UserInfo userInfo = new UserInfo();
        userInfo.setName("王五");
        userInfo.setPassword("123");
        //userInfo.setPhoto("123.png");
        int result = userMapper.add3(userInfo);
        log.info("用户信息:"+userInfo);
    }
}

运行结果:

当传入 photo 时:

\

3. where 标签

主要作用:实现查询中的 where 替换。它可以实现如果没有任何的查询条件,那么它可以隐藏查询中的 where sql,但如果存在查询条件,那么会生成 where 的 sql 查询,并且使用 where 标签可以自动的去除最前面一个 and 字符。

演示:

SQL 语句编写:

<!-- 根据 id 查询用户 -->
<select id="getUserById" resultMap="BaseMap">
  select * from userinfo
  <where>
    <if test="id!=null">
      id=#{id}
    </if>
  </where>
</select>

当传入id 时:

结果:

当不传入 id 时:

结果:

如果 里面的都为空了,则不会添加 where 语句。

可以去除最前一个 and:

但是无法去除最后一个 and:

以上标签也可以使用 <trim prefix="where" prefixOverrides="and"> 替换。

4. set 标签

根据传入的用户对象属性来更新用户数据,可以使用标签来指定动态内容。

可以去除最后一个,

语法:

update table_name
	<set>
    <if test="xxx">
      ...
    </if>
    ...
  </set>

演示:

<update id="updateById">
    update userinfo
    <set>
        <if test="name!=null">
            username=#{name},
        </if>
        <if test="password!=null">
            password=#{password},
        </if>
        <if test="photo!=null">
            photo=#{photo}
        </if>
    </set>
    where id=#{id}
</update>

测试:

@Test
void updateById() {
    UserInfo userInfo = new UserInfo();
    userInfo.setId(5);
    userInfo.setName("拉普拉斯");
    int result = userMapper.updateById(userInfo);
}

结果:

以上标签也可以使用 <trim prefix="set" suffixOverrides=","> 替换。

5. foreach 标签

对集合进行遍历时可以使用该标签。标签有如下属性:

  • collection:绑定方法参数中的集合,如 List,Set,Map或数组对象
  • item:遍历时的每一个对象
  • open:语句块开头的字符串
  • close:语句块结束的字符串
  • separator:每次遍历之间间隔的字符串

\

演示:

在 UserMapper 中添加方法 delIds

@Mapper // 变成 mybatis interface
public interface UserMapper {

    public int delIds(List<Integer> ids);
}

编写 sql 语句:

<mapper namespace="com.example.MyBatisDemo.mapper.UserMapper">
    <delete id="delIds">
        delete from userinfo where id in
        <foreach collection="ids" open="(" close=")" separator="," item="id">
            #{id}
        </foreach>
    </delete>

</mapper>

\

编写测试类:

@SpringBootTest
@Slf4j
class UserMapperTest {

    @Resource
    private UserMapper userMapper;

    @Test
    void delIds() {
        List<Integer> list = new ArrayList<>();
        list.add(5);
        list.add(6);
        list.add(7);
        list.add(8);
        list.add(9);
        int result = userMapper.delIds(list);
    }
}

执行结果:

删除前:

删除后:

\