Mybatis 的多种标签使用以及 Spring 框架单元测试

一. Spring 内置的 JUnit 框架

在讲解 Mybatis 的标签之前, 要先介绍一下另一个 Java 的好帮手 Spring 框架内置的 JUnit 测试框架. 为什么要在 Mybatis 学习之前了解 JUnit 呢 ? 很大一部分原因不仅仅是因为单元测试是写完项目后开发人员自己需要做的, 更重要的是当前阶段学习中, 利用 JUnit 可以更简单的构造数据来帮我们学习 Mybatis 的用法.

可以想象一下, 如果不用 JUnit 我们要怎么去测这个 Mybatis 的标签呢 ? 当我们写好了 SQL 语句过后, 让 Interface 接口暴露出去, 让 service 去调用 Interface 然后再用 controller 去调用 service 一样可以完成, 然后通过访问路由方法, 一样是可以测的. 如果你得功能是在登陆页面之后的某个功能, 你是需要每次去不断授权登陆的才能看, 这将是非常麻烦的.

但是, 当你使用 Spring 内置的 JUnit 框架进行单元测试, 它将替你解决这些问题, 可以跳过授权, 帮你模拟数据等等, 更简单的让你测试你得代码, 堪称一大神器 ! ! !

二. Mybatis 基本标签使用

Mybatis 既然是操作数据库的, 哪最基本的功能肯定是 CURD 即我们所谓的增删改查操作( 和前面的 CURD 不是一一对应, 感兴趣可以自己去看看英文单词 ). 那么同样是增删改查, Mybatis 里和我们直接操作 MySQL 这样的数据库有什么不一样呢 ? 下面就一起来看看

1. 标签最少是需要两个属性的, 一是 id - 接口方法名, 二是返回类型 ( XML 文件中的配置可以查看我上一篇文章复制即可, 是固定的 )

<select id="getUserById" resultType="com.example.demo.entity.UserEntity" >
    // 此处方法名就是我在 Interface 中定义的查询方法
  	// 返回类型因为查询的是一个用户, 因此返回的就是该用户
</select>

接着就是构造 SQL 语句了

<select id="getUserById" resultType="com.example.demo.entity.UserEntity" >
    select * from userinfo where id=${id}
</select>

1.3.1. 遵循标准分层调用

  1. 建立 service 层调用 mybatis 的 Interface 接口,并提供方法供外部调用
@Service // 托管给 Spring 框架
public class UserService {
    // 如果不托管给 Spring, UserMapper 是无法注入进来的
    @Autowired
    private UserMapper userMapper;

    public UserEntity getUserById(Integer id) {
        return userMapper.getUserById(id);
    }
}
  1. 建立 controller 层调用 service 层的外部调用方法
@RestController
@RequestMapping("/user")
public class UserController {

    @Autowired
    private UserService userService;

    @RequestMapping("/getId")
    public UserEntity getUserById() {
      	// 此处需要自己手动传入 id
        return userService.getUserById(1);
    }
}
  1. 访问 controller 里的路由方法检验是否正确

先来看看我之前的数据库中的 userinf 表有什么数据,
Mybatis 的多种标签使用以及 Spring 框架单元测试_第1张图片

我们上面传入的是 id = 1, 看看能否返回这个正确的用户实体对象, 结果上看是拿取正确的Mybatis 的多种标签使用以及 Spring 框架单元测试_第2张图片

在看看拿取 id = 2 这个用户, 一样是可以正确拿取的
Mybatis 的多种标签使用以及 Spring 框架单元测试_第3张图片
除了我们去构建 mybatis 的两个组件外, 剩下的都是在去为测试这个 SQL 语句是否正确做铺垫, 过程很麻烦, 既然刚刚说到 JUnit 可以很好地解决这个问题, 那我们就来用用看有多神, 被大家说是神器 !

1.4 JUnit 单元测试

Spring 中是内置了 JUnit 的, 只要你创建的是 Spring 项目, 我们是可以在依赖库中看到的, 现在直接去使用它就可以了.
Mybatis 的多种标签使用以及 Spring 框架单元测试_第4张图片

1.4.1. 生成测试方法

在你需要测试的类下面右键选择 Generate
Mybatis 的多种标签使用以及 Spring 框架单元测试_第5张图片

接着选择跳出来的 Test
Mybatis 的多种标签使用以及 Spring 框架单元测试_第6张图片

Test 里面的这些设置, 只需要选择我们要测试的方法就行, 其他的最好是默认的, 这样可以一目了然的找到我们的测试方法在哪里, 名称是什么等待
Mybatis 的多种标签使用以及 Spring 框架单元测试_第7张图片

选择好后, 就会来到当前界面
Mybatis 的多种标签使用以及 Spring 框架单元测试_第8张图片

最重要的是添加@SpringBootTest 注解, 这个注解时非常重要的, 因为咱们的 UserMapper 是托管给 Spring Boot 框架的, 如果不加这个注解, 该测试方法就不是运行在 Spring Boot 环境下, 无法使用属性注入等方法进行注入属性

添加测试方法的具体实现, 我们测试的是 UserMapper 的接口方法, 因此此处注入 UserMapper

@SpringBootTest 
// 表明接下来我当前类所有的测试方法将运行 Spring Boot 环境上的            
// 如果没有这个注解, 是没有 IoC 的, 是没办法注入的, 无法使用注入对象
class UserMapperTest {

    @Autowired
    private UserMapper userMapper;

    @Test
    void getUserById() {
        UserEntity user = userMapper.getUserById(2);
        System.out.println(user);
    }
}

运行测试方法, 如果点的是方法坐标的一个箭头, 就是测试运行当前方法, 如果点的是类前面的两个箭头, 运行的就是当前这个类里的所有测试方法

image.png

绿色打钩显示正确运行, 并且我们也在控制台看到了结果获取到了数据库中用户表里 id 为 1 的那个用户. 相比之下, 比起我们遵循标准分层去调用测试是不是简单的太多了, 只需要生成测试方法就可以了. 不愧为神器 ! ! ! 太香啦~~

1.5 resultMap 字典映射

上面的 标签中, 我们提及了两个属性是必须要写的, 一个是 id 属性, 另一个就是返回类型 resultType. 对于 resultType 时, 我们强调的是一定要和数据库中的字段一致. 那么当数据库中的字段和我们定义的实体类属性不一致时, 我们该怎么让它兼容呢 ? 而其中一种方式就是使用 resultMap 字典映射

看下面这个业务场景 : 根据用户名和密码正确登陆后查询当前用户

// 根据用户名和密码正确登陆后查询当前用户
UserEntity login(UserEntity user);

但是此时我的密码 UserEntity 实体类的密码属性已经更改为了pwd 而不是和数据库中对应的 password 了. 在来执行之前的测试方法还能行吗 ?

@Test
void login() {
    String username = "admin";
    String password = "admin";
    // 构建对象传入
    UserEntity inputUser = new UserEntity();
    inputUser.setUsername(username); // 设置 UserEntity 用户实体类的账号和密码
    inputUser.setPwd(password);
    UserEntity user = userMapper.login(inputUser); // 接受查询到的结果
    System.out.println(user);
}

运行测试方法后发现可以正确拿到该用户, 但是密码却丢失了 ! 账号密码是正确的但是返回对象中无法获取到数据库里的对应的密码字段, 这边是因为我们使用 resultType 时返回的实体类的字段必须要和数据库中字段一致的原因. 否则就会拿不到对应的内容.
image.png

但是在开发中, 往往需求是很多的, 如果要求我们的 UserEntity 实体类的字段中密码就用 pwd 表示, 而数据库中又只能是 password 该怎么办呢 ? 这就需要前面说到的 resultMap 字典映射了.

字典映射标签中, 有两个必不可少的属性要设置, **同样是 id 但这里表示的是这个字典映射的名称, 而 type 属性表示的是我们需要将数据库映射到那个实体类.

字典映射标里面的 标签中需要设置两个属性, 一个是 property 也就是设置自增主键在实体类中的名称的. 而 column 表示对应到数据库中的那个字段
字典序标签里面的 标签表示设置字段映射. 什么意思呢 ?

也就是说, 想要把数据库中的某张表字段和我们的实体类关联起来就是通过这个标签来设置的

例如 : 设置

<resultMap id="MapDemo" type="com.example.demo.entity.UserEntity">
    <id property="id" column="id"></id>
  	// 需要哪些字段就映射那些就好
    <result property="username" column="username"></result>
    <result property="pwd" column="password"></result>
</resultMap>

这时候我们在去刚刚的 XML 中修改 resultType 为 resultMap 看看能否成功映射

<select id="login" resultType="com.example.demo.entity.UserEntity">
        select id, username, password as pwd, state from userinfo where username=#{username} and password=#{pwd}
    </select>

再次执行单元测试方法, 这时候 pwd 就成功被映射了, 能够对应到数据库里 userInfo 表中的 password 了.
Mybatis 的多种标签使用以及 Spring 框架单元测试_第9张图片

但是通常我们还是使用 resultType 更多一些, 但 resultMap 也需要掌握, 避免出现类似情况时而没有办法操作.

1.6 MySQL 中字段起别名

除了 resultMap 能解决这个问题以外, 我们又不想麻烦的设置 resultMap 字典映射, 那么我们就可以在 SQL 语句中起别名来达到同样的目的

<select id="login" resultType="com.example.demo.entity.UserEntity">
        select id, username, password as pwd, state from userinfo where username=#{username} and password=#{pwd}
    </select>

执行单元测试方法可以看到, 起别名的方式也是可以解决实体类字段名和数据库字段名不相同的问题了.
Mybatis 的多种标签使用以及 Spring 框架单元测试_第10张图片

2. 参数替换的两种方式

2.1 ${ } 直接替换

刚刚说到, @Parma 注解后, 为 XML 里的 SQL 语句提供了参数, 而这里我们用的是 ${ } 的形式来提供参数, 这种方式叫做直接赋值的形式

<select id="getUserById" resultType="com.example.demo.entity.UserEntity" >
    select * from userinfo where id=${id}
</select>

哪什么是直接替换 ? 为了更好地看清 SQL 的语句执行, 需要配置一下 Mybatis 的执行 SQL 和日志文件

mybatis:
  configuration: # 配置打印 MyBatis 执行的 SQL
    log-impl: org.apache.ibatis.logging.stdout.StdOutImpl

# 配置打印 MyBatis 执行的 SQL 日志
logging:
  level: # 只设置我们写的代码下的日志
    com:
      example:
        demo: debug # 默认是 INFO 等级, 此处设置为 debug

配置好后, 再去执行刚刚的单元测试方法, 可以直观的看到, 我们传入的参数 1 被 ${ id } 给直接替换成了 1
Mybatis 的多种标签使用以及 Spring 框架单元测试_第11张图片

2.2 #{ } 预执行替换

除了 ${ } 的方式, 还有一种 #{ } 预执行方式, 哪什么又是预执行方式, 同样我们修改 SQL 语句后再来执行单元测试看看

<select id="getUserById" resultType="com.example.demo.entity.UserEntity" >
    select * from userinfo where id=#{id} # 注意这里是 #{ }
</select>

执行测试后可以看到, 这里的 id = ? 这个问号就是 JDBC 里写 SQL 语句的占位符, 预执行就是下面的 1 ( Integer ), 它会先去获取参数, 然后再进行替换
Mybatis 的多种标签使用以及 Spring 框架单元测试_第12张图片

可以看到, #{ } 和 ${ } 好像是一样的, 他们都是传入了一个整数 1哪他们有什么区别呢 ? 接着往下看

2.3 #{ } 和 ${ } 的区别

2.3.1. 非整形类型无法直接使用 ${ } 而可以使用 #{ }

刚刚我们看到, 传入一个整数的时候, 无论是#{ } 还是 ${ } 的方式都是可以正确获取的, 那么这不是一样嘛 ? 非也, 下面细看这个业务场景 : 还是以用户表里查询一个用户, 只不过现在是根据名称来查询了

还是一样, 要先写 接口方法和 XML 实现

// 关于用户的操作都写在 UserMapper 里面
UserEntity getUserByName(@Param("username") String username);

先用 #{ } 方式

# 关于用户操作的接口方法实现都是在 UserMapper.xml 中
<select id="getUserByName" resultType="com.example.demo.entity.UserEntity">
    select * from userinfo where username=#{username}
</select>

创建单元测试方法, 在这需要提醒一下大家, 由于我们刚刚的接口方法是写在 UserMapper 里面的, 之前已经生成过这个测试类了, 此时它会报错提醒是否在该类中更新, 确认更新就行
Mybatis 的多种标签使用以及 Spring 框架单元测试_第13张图片

@Test
void getUserByName() {
    UserEntity user = userMapper.getUserByName("admin");
    System.out.println(user);
}

执行测试方法, 还是和之前一样的预处理, 并且正确返回了查询到的数据
Mybatis 的多种标签使用以及 Spring 框架单元测试_第14张图片

那我们再来看看使用 ${ } 能达到预期效果嘛 ?

<select id="getUserByName" resultType="com.example.demo.entity.UserEntity">
    select * from userinfo where username=${username}
</select>

再次执行单元测试方法, 发现它报错提示 where 字句错误, 我们再去看看 SQL 语句执行的是什么 ?
Mybatis 的多种标签使用以及 Spring 框架单元测试_第15张图片

一看 SQL 语句想必大家就明白了, 但我们差的是字符串, 应该加上单引号 ’ admin ', 才是正确的语句, 这也就说明了, 当我们是引用数据类型时, 如果用 ${ } 方式就是所见即所得的方法
Mybatis 的多种标签使用以及 Spring 框架单元测试_第16张图片

我们可以主动给它加上单引号看看是不是会正确的, 倒地是不是所见即所得

<select id="getUserByName" resultType="com.example.demo.entity.UserEntity">
    select * from userinfo where username='${username}' # 我主动加了 单引号
</select>

可以看到, ${ } 这种直接替换的模式就是所见即所得, 虽然同样可以主动加上单引号解决这个问题, 但是它有另一个严重问题 - SQL 注入问题
Mybatis 的多种标签使用以及 Spring 框架单元测试_第17张图片

2.3.2. SQL 注入问题

什么是 SQL 注入问题 ? 简单来说它就是一种常见的漏洞, 攻击者通过嵌入恶意的 SQL 语句从而执行非授权的数据库操作, 并且 SQL 注入通常发生在动态 SQL 语句拼接中( 动态语句下面会说 ).

来看下面这个业务场景 : 根据账号密码登录后并查询用户该用户

// 由于我们传的是一个用户实体类, 不再需要参数注解 @Parma
UserEntity login(UserEntity user); // 传对象更方便与后续的修改
<select id="login" resultType="com.example.demo.entity.UserEntity">
                    # 因为是字符串类型, ${ } 需要手动拼接单引号
    select * from userinfo where username='${username}' and password='${password}'
</select>

创建单引测试方法

@Test
void login() {
    String username = "admin";
    String password = "123";
    // 构建对象传入
    UserEntity inputUser = new UserEntity();
    inputUser.setUsername(username); // 设置 UserEntity 用户实体类的账号和密码
    inputUser.setPassword(password);
    UserEntity user = userMapper.login(inputUser); // 接受查询到的结果
    System.out.println(user);
}

运行测试方法后, 是可以正确查询到的.
Mybatis 的多种标签使用以及 Spring 框架单元测试_第18张图片

但是它存在 SQL 注入问题, 比如现在有人恶意注入了 SQL 语句, 传了一个特殊密码给你, 但是一定不是 admin 对应的 123 这个密码, 按理他是不能查询出来的才是正确的

@Test
void login() {
    String username = "admin";
    String password = "' or 1='1 "; // 注意我此时写的密码不再是 "123"
    // 构建对象传入
    UserEntity inputUser = new UserEntity();
    inputUser.setUsername(username); // 设置 UserEntity 用户实体类的账号和密码
    inputUser.setPassword(password);
    UserEntity user = userMapper.login(inputUser); // 接受查询到的结果
    System.out.println(user);
}

运行测试方法一看, 出大问题了, 密码不对居然还查询到了我的 admin ? 你想想这有多危险, 通过恶意的 SQL 语句查询到了本不该让你知道的敏感信息, 现在这就是密码泄漏了, 是非常危险的行为.
Mybatis 的多种标签使用以及 Spring 框架单元测试_第19张图片

那么, 他具体是如何出问题的呢 ? 为什么用 ${ } 会出这个问题, 通过 SQL 的执行就可以看出来
Mybatis 的多种标签使用以及 Spring 框架单元测试_第20张图片

最终执行的语句变成了查询全表, 我此时数据库里是一条数据, 查询到的就是一条, 但当你数据库中有很多用户数据, 被人恶意注入查询到了所有用户的账号密码, 这将是毁灭性的打击. 因此避免 SQL 注入是我们操作数据库需要解决的重要问题

那么, ${ } 的方式用不了, #{ } 的方式能行吗 ? 来试试
Mybatis 的多种标签使用以及 Spring 框架单元测试_第21张图片
测试发现, 当使用 #{ } 的方式时, 由于他是通过占位符的预处理的方式, 无论你传入的是什么, mybatis 会自动帮你去进行适配处理, 这个使用这个奇怪的密码 ’ or 1='1 不在会当做 SQL 关键字去执行, 而是直接当做了字符串放到了占位符中进行处理. 因此他们直接第二个重要的区别就是 #{ } 是安全的, 而#{ } 是非安全你得

2.3.3. #{ } 和 ${ }总结

这时候不免有人会问, #{ } 同样可以替换参数, 并且还可以避免 SQL 注入问题, 全部用 #{ } 就行了, 哪里还有 ${ ] 什么事呢 ?

但是, 我们刚刚提及到的是 #{ } 之所以安全是因为他把刚刚的那个奇怪密码当成了字符串处理了, 但是如果我们本身要执行的就是 SQL 关键字( 比如按照价格排序使用 order by XXX desc 关键字), 这时候在用 #{ } 去处理, 这个 SQL 指令就被当成了字符串从而不会被执行.

既然 ${ } 这么危险, 不得已要用又该如何去防范呢 ? 当传来的 SQL 语句的值是可以被枚举的时候, 这时候使用 ${ } 是相对安全的, 如果不能被枚举, 此时不知道传来的是什么语句, 这将是非常危险的. 因此无论是 #{ } 还是 ${ } 都是有其独特之处的, 还是需要根据自己的业务需求进行选择的.

3. 修改标签

修改标签和前面学的