当前位置: 首页 > news >正文

MyBatis 查询数据库入门

一、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 拼接。

1. 标签

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

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

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

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. 标签

最主要的作用:去除 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 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. 标签

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

可以去除最后一个,

语法:

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. 标签

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

  • 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);
    }
}

执行结果:

删除前:

删除后:

\

相关文章:

  • LQ0026 修剪灌木【数学】
  • 重识Nginx - 02 手把手教你编译适合自己的nginx 1.22.0
  • Java泛型详解
  • opencv连通域标记 connectedComponentsWithStats()函数
  • 【C#在资源管理器中显示自定义文件格式的缩略图】
  • 【NLP】第2章 开始使用 Transformer 模型的架构
  • 电容的分类
  • MYBatis-Plus常用注解@TableName、@TableId、@TableField、@TableLogic
  • 沉睡者IT - 贡献者和律师的Web3指南:充分去中心化
  • unityEditor扩展开发onGUI添加右键菜单
  • 认识 fcntl 接口函数
  • a16z:呼吁SEC改革加密资产托管规则的建议
  • 通过虚拟代理服务器解决Axios跨域问题[Vue.js项目实践: 新冠自检系统]
  • 【JavaScript-动画原理】如何使用js进行动画效果的实现
  • Servlet对象的生命周期
  • [ 一起学React系列 -- 8 ] React中的文件上传
  • 【划重点】MySQL技术内幕:InnoDB存储引擎
  • Date型的使用
  • Django 博客开发教程 16 - 统计文章阅读量
  • Java编程基础24——递归练习
  • Java深入 - 深入理解Java集合
  • java正则表式的使用
  • log4j2输出到kafka
  • mysql中InnoDB引擎中页的概念
  • node-glob通配符
  • React-flux杂记
  • Redux 中间件分析
  • TCP拥塞控制
  • vue+element后台管理系统,从后端获取路由表,并正常渲染
  • 从PHP迁移至Golang - 基础篇
  • 技术胖1-4季视频复习— (看视频笔记)
  • 问题之ssh中Host key verification failed的解决
  • 项目管理碎碎念系列之一:干系人管理
  • 用 Swift 编写面向协议的视图
  • 智能合约开发环境搭建及Hello World合约
  • 昨天1024程序员节,我故意写了个死循环~
  • ## 临床数据 两两比较 加显著性boxplot加显著性
  • ###51单片机学习(2)-----如何通过C语言运用延时函数设计LED流水灯
  • #pragma once
  • (33)STM32——485实验笔记
  • (4)(4.6) Triducer
  • (层次遍历)104. 二叉树的最大深度
  • (二)正点原子I.MX6ULL u-boot移植
  • (过滤器)Filter和(监听器)listener
  • (全注解开发)学习Spring-MVC的第三天
  • (原創) 如何安裝Linux版本的Quartus II? (SOC) (Quartus II) (Linux) (RedHat) (VirtualBox)
  • (转)Android中使用ormlite实现持久化(一)--HelloOrmLite
  • (转)负载均衡,回话保持,cookie
  • .axf 转化 .bin文件 的方法
  • .NET Standard 的管理策略
  • .NET/C# 的字符串暂存池
  • .net中生成excel后调整宽度
  • @autowired注解作用_Spring Boot进阶教程——注解大全(建议收藏!)
  • @RestControllerAdvice异常统一处理类失效原因
  • @基于大模型的旅游路线推荐方案