在讲解 Mybatis 的标签之前, 要先介绍一下另一个 Java 的好帮手 Spring 框架内置的 JUnit 测试框架. 为什么要在 Mybatis 学习之前了解 JUnit 呢 ? 很大一部分原因不仅仅是因为单元测试是写完项目后开发人员自己需要做的, 更重要的是当前阶段学习中, 利用 JUnit 可以更简单的构造数据来帮我们学习 Mybatis 的用法.
可以想象一下, 如果不用 JUnit 我们要怎么去测这个 Mybatis 的标签呢 ? 当我们写好了 SQL 语句过后, 让 Interface 接口暴露出去, 让 service 去调用 Interface 然后再用 controller 去调用 service 一样可以完成, 然后通过访问路由方法, 一样是可以测的. 如果你得功能是在登陆页面之后的某个功能, 你是需要每次去不断授权登陆的才能看, 这将是非常麻烦的.
但是, 当你使用 Spring 内置的 JUnit 框架进行单元测试, 它将替你解决这些问题, 可以跳过授权, 帮你模拟数据等等, 更简单的让你测试你得代码, 堪称一大神器 ! ! !
Mybatis 既然是操作数据库的, 哪最基本的功能肯定是 CURD 即我们所谓的增删改查操作( 和前面的 CURD 不是一一对应, 感兴趣可以自己去看看英文单词 ). 那么同样是增删改查, Mybatis 里和我们直接操作 MySQL 这样的数据库有什么不一样呢 ? 下面就一起来看看
查询操作是非常常见而又基础的操作, 下面来看看这个业务, 根据 id 查询用户 :
如果是在 MySQL 中, 我们写的肯定是 : select * from userInfo where id = ?
那在 Mybatis 中又该如何去写呢 ? 在来复习一次上一篇文章中如何用 mybatis 写一个基本业务
@Mapper
public interface UserMapper {
// 根据 id 查询用户
UserEntity getUserById(@Param("id") Integer id);
}
@Param 注解 来自 org.apache.ibatis.annotations.Param 包底下, 而这里的 ibatis 其实就是现在的 mybatis 的前身, 改名了而已. 这个注解就是提供外部参数给 XML 中的 SQL 语句使用的
比如在 MySQL 中针对刚刚的业务根据 id 查询用户写的 SQL 语句为 :
select * from userinfo where id = ?
这里的 ? 就是占位符, 我们要输入这里的 id 为多少, 在 mybatis 里我们通过传入参数来达到替换这个类似占位符的功能. 只不过写法上不太一样, 下面会看到如何写这个语句
这里最主要就是通过传入 id 这个参数 赋值给 @Parma(“id”) 注解里面这个 id, 注解里的 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>
@Service // 托管给 Spring 框架
public class UserService {
// 如果不托管给 Spring, UserMapper 是无法注入进来的
@Autowired
private UserMapper userMapper;
public UserEntity getUserById(Integer id) {
return userMapper.getUserById(id);
}
}
@RestController
@RequestMapping("/user")
public class UserController {
@Autowired
private UserService userService;
@RequestMapping("/getId")
public UserEntity getUserById() {
// 此处需要自己手动传入 id
return userService.getUserById(1);
}
}
先来看看我之前的数据库中的 userinf 表有什么数据,
我们上面传入的是 id = 1, 看看能否返回这个正确的用户实体对象, 结果上看是拿取正确的
在看看拿取 id = 2 这个用户, 一样是可以正确拿取的
除了我们去构建 mybatis 的两个组件外, 剩下的都是在去为测试这个 SQL 语句是否正确做铺垫, 过程很麻烦, 既然刚刚说到 JUnit 可以很好地解决这个问题, 那我们就来用用看有多神, 被大家说是神器 !
Spring 中是内置了 JUnit 的, 只要你创建的是 Spring 项目, 我们是可以在依赖库中看到的, 现在直接去使用它就可以了.
Test 里面的这些设置, 只需要选择我们要测试的方法就行, 其他的最好是默认的, 这样可以一目了然的找到我们的测试方法在哪里, 名称是什么等待
最重要的是添加@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);
}
}
运行测试方法, 如果点的是方法坐标的一个箭头, 就是测试运行当前方法, 如果点的是类前面的两个箭头, 运行的就是当前这个类里的所有测试方法
绿色打钩显示正确运行, 并且我们也在控制台看到了结果获取到了数据库中用户表里 id 为 1 的那个用户. 相比之下, 比起我们遵循标准分层去调用测试是不是简单的太多了, 只需要生成测试方法就可以了. 不愧为神器 ! ! ! 太香啦~~
上面的 标签中, 我们提及了两个属性是必须要写的, 一个是 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 时返回的实体类的字段必须要和数据库中字段一致的原因. 否则就会拿不到对应的内容.
但是在开发中, 往往需求是很多的, 如果要求我们的 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 了.
但是通常我们还是使用 resultType 更多一些, 但 resultMap 也需要掌握, 避免出现类似情况时而没有办法操作.
除了 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>
执行单元测试方法可以看到, 起别名的方式也是可以解决实体类字段名和数据库字段名不相同的问题了.
刚刚说到, @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
除了 ${ } 的方式, 还有一种 #{ } 预执行方式, 哪什么又是预执行方式, 同样我们修改 SQL 语句后再来执行单元测试看看
<select id="getUserById" resultType="com.example.demo.entity.UserEntity" >
select * from userinfo where id=#{id} # 注意这里是 #{ }
</select>
执行测试后可以看到, 这里的 id = ? 这个问号就是 JDBC 里写 SQL 语句的占位符, 预执行就是下面的 1 ( Integer ), 它会先去获取参数, 然后再进行替换
可以看到, #{ } 和 ${ } 好像是一样的, 他们都是传入了一个整数 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 里面的, 之前已经生成过这个测试类了, 此时它会报错提醒是否在该类中更新, 确认更新就行
@Test
void getUserByName() {
UserEntity user = userMapper.getUserByName("admin");
System.out.println(user);
}
执行测试方法, 还是和之前一样的预处理, 并且正确返回了查询到的数据
那我们再来看看使用 ${ } 能达到预期效果嘛 ?
<select id="getUserByName" resultType="com.example.demo.entity.UserEntity">
select * from userinfo where username=${username}
</select>
再次执行单元测试方法, 发现它报错提示 where 字句错误, 我们再去看看 SQL 语句执行的是什么 ?
一看 SQL 语句想必大家就明白了, 但我们差的是字符串, 应该加上单引号 ’ admin ', 才是正确的语句, 这也就说明了, 当我们是引用数据类型时, 如果用 ${ } 方式就是所见即所得的方法
我们可以主动给它加上单引号看看是不是会正确的, 倒地是不是所见即所得
<select id="getUserByName" resultType="com.example.demo.entity.UserEntity">
select * from userinfo where username='${username}' # 我主动加了 单引号
</select>
可以看到, ${ } 这种直接替换的模式就是所见即所得, 虽然同样可以主动加上单引号解决这个问题, 但是它有另一个严重问题 - 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);
}
但是它存在 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 语句查询到了本不该让你知道的敏感信息, 现在这就是密码泄漏了, 是非常危险的行为.
那么, 他具体是如何出问题的呢 ? 为什么用 ${ } 会出这个问题, 通过 SQL 的执行就可以看出来
最终执行的语句变成了查询全表, 我此时数据库里是一条数据, 查询到的就是一条, 但当你数据库中有很多用户数据, 被人恶意注入查询到了所有用户的账号密码, 这将是毁灭性的打击. 因此避免 SQL 注入是我们操作数据库需要解决的重要问题
那么, ${ } 的方式用不了, #{ } 的方式能行吗 ? 来试试
测试发现, 当使用 #{ } 的方式时, 由于他是通过占位符的预处理的方式, 无论你传入的是什么, mybatis 会自动帮你去进行适配处理, 这个使用这个奇怪的密码 ’ or 1='1 不在会当做 SQL 关键字去执行, 而是直接当做了字符串放到了占位符中进行处理. 因此他们直接第二个重要的区别就是 #{ } 是安全的, 而#{ } 是非安全你得
这时候不免有人会问, #{ } 同样可以替换参数, 并且还可以避免 SQL 注入问题, 全部用 #{ } 就行了, 哪里还有 ${ ] 什么事呢 ?
但是, 我们刚刚提及到的是 #{ } 之所以安全是因为他把刚刚的那个奇怪密码当成了字符串处理了, 但是如果我们本身要执行的就是 SQL 关键字( 比如按照价格排序使用 order by XXX desc 关键字), 这时候在用 #{ } 去处理, 这个 SQL 指令就被当成了字符串从而不会被执行.
既然 ${ } 这么危险, 不得已要用又该如何去防范呢 ? 当传来的 SQL 语句的值是可以被枚举的时候, 这时候使用 ${ } 是相对安全的, 如果不能被枚举, 此时不知道传来的是什么语句, 这将是非常危险的. 因此无论是 #{ } 还是 ${ } 都是有其独特之处的, 还是需要根据自己的业务需求进行选择的.
// 根据用户 id 修改密码
int updatePassword(@Param("id") String id, @Param("newPassword") String password);
<update id="updatePassword"> # 只需要写 id 属性就行
update userinfo set password=#{newPassword}
</update>
执行单元测试方法
@Test
void updatePassword() {
int row = userMapper.updatePassword("1", "admin");
System.out.println(row);
}
之前提及过, JUnit 有一个最大的优点之一就是不会污染数据, 但是我们上面用单元测试执行的时候, 密码却被修改了. 这又是怎么回事不是说好了不会污染数据库嘛 ?
别急, 下面就来让 JUnit 不去污染数据, 那就是添加 @Transactional 事务注解. 事务大家都不陌生, 这里添加这个代码有什么用呢 ? 接线来就看神奇之处
在测试方法里, 我们传入密码为 “123”, 看看执行过后能不能给原本 “admin” 的密码修改
@Test
@Transactional // 开启事务
void updatePassword() {
int row = userMapper.updatePassword("1", "123");
System.out.println(row);
}
执行单元测试方法后可以看到执行时成功了的, 并且已经显示修改了一条数据
上数据库查看倒地修改成功了嘛 ? 我这三次数据库查询可以看到, 当我们添加 @Transactional 注解过后, 虽然执行成功了修改操作, 但是并没有在数据库中进行修改, 从而保证了不污染数据库
哪这是为什么呢 ? @Transactional 它是事务注解, 当开启事务后, 即使执行了事务, 进行回滚之后就会对数据进行复原. 在 SQL 执行里面也可以看到, 当测试结束过后它会自动进行事务回滚
结合下面这个业务来看 : 在用户表中新增一个用户 ( 添加指定的用户名和密码字段 )
// 新增用户 - 只添加用户名和密码 其余字段不添加
int insertUser(UserEntity user);
<insert id="insertUser">
insert into userinfo(username, password) values( #{username}, #{password} )
</insert>
执行单元测试方法, 此处没有加事务注解, 如果执行成功会正确添加到数据库中
@Test
void insertUser() {
// 构建对象传入
UserEntity inputUser = new UserEntity();
inputUser.setUsername("zhangsan");
inputUser.setPassword("123");
int row = userMapper.insertUser(inputUser);
System.out.println("新增行数 : " + row);
}
在
也就是说, 当我们通过添加标签添加一条数据后, 如果数据库里这个字段舍友自增 id, 那么我们是可以把这个新加这条数据的自增 id 获取到的.
还是刚刚的添加业务 : 在用户表中新增一个用户 ( 添加指定的用户名和密码字段 )
// 新增用户 - 只添加用户名和密码, 并且返回自增主键
int insertUserAndId(UserEntity user);
在
useGeneratedKeys 属性表示是否需要获取自增主键, 默认是 false 不获取的, 想要获取就需要设置为 true
keyProperty 属性表示获取到的自增主键放到个字段里
<insert id="insertUserAndId" useGeneratedKeys="true" keyProperty="id">
insert into userinfo(username, password) values( #{username}, #{password} )
</insert>
创建单元测试方法
@Test
void insertUserAndId() {
// 构建对象传入
UserEntity inputUser = new UserEntity();
inputUser.setUsername("lisi");
inputUser.setPassword("123");
int row = userMapper.insertUserAndId(inputUser);
System.out.println("新增行数 : " + row);
// 由于开启了获取自增主键并赋值给 id 这一列, 因此没有设置 id 的值便可以直接获取
System.out.println("Id : " + inputUser.getId());
}
执行单元测试, 由于已经开启了获取自增主键, 并且把获取到的自增主键的值放到了 id 这一列, 因此此时该对象中就可以直接获取了. 可以看到此时获取到的这条新增用户的自增 id 为 9
PS : 表中字段必须要有自增主键, 否则是无法正确获取并且执行报错的
同样还是结合业务来看 : 删除用户表中 id 为 2 的用户
// 删除指定 id 用户
int deleteUser(@Param("id") Integer id);
<delete id="deleteUser">
delete from userinfo where id=#{id}
</delete>
创建单元测试方法
@Test
@Transactional // 开启事务, 防止污染数据
void deleteUser() {
int row = userMapper.deleteUser(1);
System.out.println("删除行数 : " + row);
}
模糊匹配有所不同, 和之前咱们用直接用 like 有一些不一样的. 在这儿因为无法枚举用户的输入, 因此我们此处为了防止 SQL 注入问题, 只能去使用 #{ } 的占位符方式.
下面根据用户名来实现一个模糊查询
// 根据用户名模糊匹配用户
List<UserEntity> getUserOfLikeName(@Param("username") String username);
<select id="getUserOfLikeName" resultType="com.example.demo.entity.UserEntity">
select * from userinfo where username like %#{username}%
</select>
建立测试方法并执行
@Test
void getUserOfLikeName() {
List<UserEntity> list = userMapper.getUserOfLikeName("王");
list.stream().forEach(System.out::println); // lambda 表达式
}
提示 SQL 语句执行错误了, 但是#{ } 确实是可以用于字符串类型并且防止 SQL 注入, 但是仔细看 SQL 的执行’%‘王’%', 而我们期望的是 select * from userinfo where username like ‘%王’% , 王字是不包括在单引号之中的.
但是现在 ${ } 存在 SQL 注入问题无法使用, 而#{ } 又无法正确执行 SQL 语句, 哪这个模糊匹配该使用什么呢 ?
在 MySQL 中提供了一个拼接函数 contact( String1, String2, …)
同样的在 mybatis 中有这个拼接函数, 下面来重写 SQL 语句
<select id="getUserOfLikeName" resultType="com.example.demo.entity.UserEntity">
select * from userinfo where username like concat('%',#{username},'%')
</select>
Mybatis 中内置的方法有很多, 大家可以上 MySQL 的官方文档中去查看 ( 菜鸟教程 )
多表查询在数据库操作中是非常常见的但是又有点难的操作. 它体现的是一种一对多的思维. 什么是一对多呢 ? 比如一个用户可以写多篇文章, 但是一篇文章却只能有一个作者 ( 假设没有第二作者 ) , 而这里的一个用户对应多篇文章就是一对多的关系.
比如下面这个场景 : 在文章表中根据文章 id 查询用户 ( 一对一的多表关系 即一篇文章只有一个作者 )
先来看我们的文章表有什么字段 :
但是我们发现要返回用户的话, 这个表里的字段和用户表里有些不太一样, 光返回这个文章对象的话, 那就没有用户名可以展示了. 这样不知道是谁的文章了.
为了解决返回对象的问题, 需要在文章表中增添用户表里的 username 字段才能满足需求, 但是遵循标准设计规则来说, 是不能直接在 articleInfo 用户表上添加的, 这不利于单一性设计原则, 遵循设计标准我们可以添加一个 ArticleInfoVO 的文章扩展实体类
@Data
public class ArticleInfo {
private int id;
private String title;
private String content;
private LocalDateTime createtime;
private LocalDateTime updatetime;
private int uid;
private int rcount;
private int state;
}
@Data
public class ArticleInfoVO extends ArticleInfo {
// 建立在文章表的基础上扩展了 username 属性
private String username;
}
由于我们已经更换操作对象, 从用户操作变成了文章操作, 因此也是需要重新建立 Mybatis 的 Mapper 和 XML 的
实现接口方法, 根据文章 id 查询用户
@Mapper
public interface ArticleMapper {
ArticleInfoVO getDetailArticle(@Param("id") Integer id);
}
<?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.ArticleMapper">
<!-- 多表联合查询 -->
<select id="getDetailArticle" resultType="com.example.demo.entity.vo.ArticleInfoVO">
select a.*, u.username from articleinfo a
left join userinfo u on u.id=a.uid
where a.id=#{id}
</select>
</mapper>
重点来解释一下这个SQL 语句 :
结合上面的解释, 我们再来看这个多表查询就清楚多了. 也就是我们的业务要求根据传来的 id 查询文章并且返回文章和文章的归属人( 也就是这篇文章的作者 )
建立单元测试方法
@SpringBootTest
class ArticleMapperTest {
@Autowired
private ArticleMapper articleMapper;
@Test
void getDetailArticle() {
ArticleInfoVO articleInfoVO = articleMapper.getDetailArticle(1);
System.out.println(articleInfoVO);
}
}
执行测试方法后可以看到它查到一行, 并且信息也有, 但是我们打印的 ArticleInfoVO 不是继承了 ArticleInfo 嘛 ? 为什么没有呢 ?
而且数据库中这一条信息和我们所得到的信息是一致的, 但就是获取的 ArticleInfoVO 里面没有
检查后发现, 代码是没有问题的, 并且它也正确执行了, 但就是打印对象 articleInfoVO 结果没有继承到相应对象 ArticleInfo, 我们可以到缓存中看看这个类是怎么执行的
从它最后一条语句执行上可以看到, 它最后执行了我们的打印对象 toString() 方法, 并且它只返回了 this.getUsername 也就是获取用户名. 而我们打印的也恰好就是用户名没有其他信息
这个问题想必大家已经很熟悉了, 肯定是重写我们的 toString() 方法了, 并且需要加上父类的toString() 这样才能让我们的 ArticleInfoVO 打印信息时有父类信息
@Data
public class ArticleInfoVO extends ArticleInfo {
// 建立在文章表的基础上扩展了 username 属性
private String username;
@Override
public String toString() {
return "ArticleInfoVO{" +
"username='" + username + '\'' +
"} " + super.toString();
}
}
刚刚我们实现的是一个一对一的多表查询, 但是如果我们现在查询的是一个作者的全部文章呢 ?
这时候就是一对多的关系了, 毕竟一个作者有可能是有多篇文章的, 下面就来实现一下
接口方法实现, 有多篇文章对象, 因此用 List
List<ArticleInfoVO> getAllArticleByUserId(@Param("id") Integer id);
XML 实现 : 这里需要注意的是, 此处我们查询的还是文章, 因此主表依然是文章. 并且现在是一个作者有多篇文章, 哪文章是怎么判定归属人的至关重要, 也就是通过文章表的 uid 来判断是谁写的.
<select id="getAllArticleByUserId" resultType="com.example.demo.entity.vo.ArticleInfoVO">
select a.*, u.username from articleinfo a
left join userinfo u on u.id=a.uid // 这里需要注意是在判定文章归属的情况下
where u.id=#{id} // 此处是根据用户 id 查询它有多少文章
</select>
生成单元测试方法
@Test
void getAllArticleByUserId() {
List<ArticleInfoVO> list = articleMapper.getAllArticleByUserId(1);
list.stream().forEach(System.out::println); // 输出流 lambda 表达式
}
运行后发现, 可以查到两条数据, 我们上数据库中查看是否正确
可以看到, 文章表中两篇文章都是 id 为 1 的 admin 发布的.
动态 SQL 拼接, 在之前学习 JDBC 的过程中想必都不陌生. 但是 JDBC 去拼接 SQL 的时候, 你是否也遇到过拼接时逗号没处理 ? 空格加错了 ? 这是非常容易处理错的, 现在 Mybatis 对 JDBC 进行了进一步封装, 操作动态 SQL 就变得简单起来了, 不会在容易出之前的错误, 让你摆脱痛苦.
比如在注册时, 有用户添加头像, 这并不是一个必填选项. 你可以选择注册时就添加, 也可以选择不添加直接注册. 当你选择了不添加头像直接注册时, 当你注册登录过后会给你一个默认的初始头像.
一般我们存储照片都是在数据库中存其图片路径, 并通过查询后返回给前端设置到页面中. 当用户注册时, 头像这一栏是不确定的, 如果你注册时不传头像也就是此时前端传来的是一个 null 的空置, 如果我们直接进行拼接那么最终你得头像就会无法正常显示. 这并不是我们想要的, 我们想要的是即使前端没有传来头像为 null 我们需要判断过后让它显示为默认头像路径.
这就是一个简单的动态 SQL 问题. 如果前端传了头像 null 我就让它不拼接. 如果前端传了非 null 我就让它拼接传来的图片路径
针对于上面的问题, 我们要做的就是判断用户传来的 photo 这个字段是否非空从而做出不同处理. 在 JDBC 中如果进行这个操作, 要做的就是写两个 SQL 语句, 对这两种情况下分别进行处理. 但是我们说在 Mybatis 中可以更容易解决这件事, 就是因为它的动态标签. Mybatis 中的条件判断标签便可以让刚刚的问题变成一个 SQL 语句
// 根据是否有 photo 字段新增用户
int insertUserByPhoto(UserEntity user);
xml 中使用 < if > 条件动态标签, 那么既然是条件就需要写判断条件, 因此这里的 test 属性是必须要设置的.
<insert id="insertUserByPhoto">
insert into userinfo(username, password
<if test="photo != null">
, photo
</if>
) values(#{username}, #{password}
<if test="photo != null">
, #{photo}
</if>
)
</insert>
对于上面的 SQL 语句, 初看是十分突兀的. 下面就解释一下这个 SQL 语句是什么意思
那么, 有有人会有疑惑了, 如果第一个 < if > 标签成立, 第二个 < if > 标签不成立的情况下, 这个 SQL 语句肯定是错误的.
对于这个疑惑, 初学是很容易犯的. 如果前一个条件成立后一个不成立的情况下, 的确是肯定错误的. 但是需要注意 test 属性中的 photo 它是属性, 并非属性值 ! ! ! 而我们的 #{photo} 它才是属性值, 而 photo 它是数据库中的字段. 因此如果用户的头像没有传过来, 那么这个属性它就是空的, 只要第一个条件是成立的第二个条件肯定也是成立的. 不存在其中一个存在另一个不存在的情况.
因此这个动态 SQL 语句就可以分为两种情况了, 并且我的数据中设置 photo 默认值是 ’ ’ 的, 并非默认的是 null
建立单元测试方法测试一下
@Test
void insertUserByPhoto() {
UserEntity user = new UserEntity();
user.setUsername("xiaoqi");
user.setPassword("123");
user.setPhoto("img");
userMapper.insertUserByPhoto(user);
System.out.println(user);
}
此时传过来了 img 这张图片, 说明有 photo 字段, 并且 photo 的属性值就是 img
由于 < if > 标签的原因, 此时的 SQL 语句为 insert into userinfo(username, password, photo) values('xiaoqi', '123', 'img')
此时我并未设置 photo , 因此它默认传过去的是 null
@Test
void insertUserByPhoto() {
UserEntity user = new UserEntity();
user.setUsername("xiaoliu");
user.setPassword("123");
userMapper.insertUserByPhoto(user);
System.out.println(user);
}
传过来的是 null 说明没有这个字段, 此时我们插入数据库中的对象预期 photo 这一栏应该就是我们默认的 ’ ’ 空, 并非 null
可以看到, 我们没有传 photo 此时默认传来的是 null, 此时 insert into userinfo(username, password) values('xiaoliu', '123')
通过上面的这个例子, 对于动态 SQL 是什么简单来说就是根据不同的条件拼接不同的 SQL 语句, 从而解决复杂的场景下对于数据库的操作.
对于刚刚的 SQL 语句, 用
<insert id="insertUserByPhoto">
insert into userinfo(username, password
<if test="photo != null and photo != '' " >
, photo
</if>
) values(#{username}, #{password}
<if test="photo != null and photo != ''">
, #{photo}
</if>
)
</insert>
如果此时 username 和 photo 都为一个非必传参数, 我们用
<insert id="insertUserByPhotoAndUsername">
insert into userinfo(
<if test="username != null">
username,
</if> password,
<if test="photo != null">
photo
</if> values(
<if test="username != null">
#{username},
</if> #{password},
<if test="photo != null">
#{photo}
</if>
)
</insert>
极端情况下是什么, 我的 username 和 photo 都不传过来. 那么现在再来看这个 SQL 语句会变成什么样 ?
insert into userinfo(password,) values(#{password},)
发现了嘛 ? 这完全就是一个错误的 SQL 语句, 因为逗号的问题, 这种情况下使用
看到 trim 大家应该很熟悉, 我们常用来去除空格的操作就是使用它. 在 Mybatis 中
先来了解一下
对于刚刚的问题, 我们就可以设置整个语句的后缀属性值为 suffix = “,” 然后选择去除掉整个语句块内的后缀值 suffixOverrides
下面来试试, 由于我的 username 在数据中为一个必传字段, 因此无法验证, 这里我选择将它更改为 state 字段, 它的默认值设置为 “1”, 并非为空时默认的 0
对于这个的 SQL 语句为 :
<insert id="insertUserByPhotoAndState">
insert into userinfo(
<trim suffixOverrides=",">
username, password,
<if test="photo != null and photo != '' ">
photo,
</if>
<if test="state != null and state != '' ">
state
</if>
</trim>
) values(
<trim suffixOverrides=",">
#{username}, #{password},
<if test="photo != null and photo != '' ">
#{photo},
</if>
<if test="state != null and state != '' ">
#{state}
</if>
</trim>
)
</insert>
经过分析, 出现逗号在末尾多余的情况都是在 userinfo ( ) 和 values ( ) 的括号里面, 因此我们的
生成单元测试方法看看 :
inser into userinfo(username, password,) values(#{username}, #{password},)
可以看到, 两个括号内都是多了后面的一个逗号的. 如果测试能通过, 说明我们的 是起作用了的.
@Test
@Transactional
void insertUserByPhotoAndState() {
UserEntity user = new UserEntity();
user.setUsername("老八");
user.setPassword("123");
user.setPhoto("img");
userMapper.insertUserByPhotoAndState(user);
System.out.println(user);
}
执行后看到 SQL 的执行语句, 即使我们刚刚没有传入 photo 和 state 结尾多出来了逗号, 但由于 < trim > 标签的存在, 帮我们解决了这个问题.
insert into userinfo(username, password, photo,) values(#{username}, #{password}, #{photo},)
可以看到, 在 userinfo( ) 和 values( ) 两个括号里面仍然是多余了结尾的逗号, 看看这次 还能解决嘛 ?
可以看到, 同样是可以解决的.
当然, 除了上面的写法外, 我们看到
比如, 现在连括号我们都可以不用写了, 括号都让它给我们自己拼接
<insert id="insertUserByPhotoAndState">
insert into userinfo
<trim prefix="(" suffix=")" suffixOverrides=",">
username, password,
<if test="photo != null and photo != '' ">
photo,
</if>
<if test="state != null and state != '' ">
state
</if>
</trim>
values
<trim prefix="(" suffix=")" suffixOverrides=",">
#{username}, #{password},
<if test="photo != null and photo != '' ">
#{photo},
</if>
<if test="state != null and state != '' ">
#{state},
</if>
</trim>
</insert>
此时整个 SQL 语句的意思就变成了 : 在判断
比如我在 #{state} 后面还加了逗号, 此时我的参数都传入的情况下, values( ) 括号里面回多出来一个逗号.
@Test
@Transactional
void insertUserByPhotoAndState() {
UserEntity user = new UserEntity();
user.setUsername("老八");
user.setPassword("123");
user.setPhoto("img") ;
user.setState(1);
userMapper.insertUserByPhotoAndState(user);
System.out.println(user);
}
可以看到, < trim > 标签它是非常好用的. 无论你是要去掉前缀还是后缀它都可以实现. 并且在去掉之前主动加上前缀或者后缀来帮我们构建 SQL 也是可以的 ( 比如我刚刚实现的执行前加上前缀" ( ", 执行后加上后缀 " ) " 这个操作 ). 可以根据根据自己的业务需求进行选择.
< where > 标签很明显, 就是一个条件标签. 和之前学的意思是一样的. 但在 Mybatis 中的 < where > 标签和 SQL 中直接使用 where 还有很多的不同.
下面来看这个场景, 我们上面提到的同样场景 : 根据文章的 id 和 文章标题模糊查询文章. 并且 id 和 文章的标题都是非必传的. 显而易见这又是一个动态 SQL
// 根据 id 和 title 来筛选文章
// 我们此处不用扩展类里的 username 字段, 此处返回 articleInfo 对象也行
List<ArticleInfoVO> getArticleByIdAndTitle(@Param("id") Integer id,
@Param("title") String title);
根据需求构建 SQL 语句
<select id="getArticleByIdAndTitle" resultType="com.example.demo.entity.vo.ArticleInfoVO">
select * from articleinfo
where
<if test="id != null and id != '' ">
id=#{id} and
</if>
<if test="title != null and title != '' ">
title like concat('%', #{title}, '%')
</if>
</select>
稍加一分析, 可以发现问题, 如果此时只传入 id 不传入 titile 那么, 结尾会多出来 end, 因此我们可以使用之前使用的 < trim > 标签来解决这个问题
<select id="getArticleByIdAndTitle" resultType="com.example.demo.entity.vo.ArticleInfoVO">
select * from articleinfo
where
<trim suffixOverrides="and">
<if test="id != null and id != '' ">
id=#{id} and
</if>
<if test="title != null and title != '' ">
title like concat('%', #{title}, '%')
</if>
</trim>
</select>
即使这样, 还是会有问题. 极端情况下当我们都不传入 id 和 title 的时候, 这时候 where 条件就空了, 整个 SQL 语句变成了 :
select * from articleinfo where
没有 where 条件, 它肯定是一个错误的 SQL 语句, 是无法执行的. 那么这个问题如何解决 ?
不难想到, 可以手动构建 where 条件 1=1 进去, 稍加调整一下 and 位置皆可以了. 这时候的 and 就需要重新考虑去除的位置为开头去除前缀了.
<select id="getArticleByIdAndTitle" resultType="com.example.demo.entity.vo.ArticleInfoVO">
select * from articleinfo
where 1 = 1
<trim prefixOverrides="and">
<if test="id != null and id != '' ">
and id=#{id}
</if>
<if test="title != null and title != '' ">
and title like concat('%', #{title}, '%')
</if>
</trim>
</select>
这样也能解决, 但还有更好的办法. 现在的问题就是这个 where 的问题, 没有条件的时候我们希望它不出现. 它完全可以利用我们之前学的 < trim > 标签. 当我们的 < trim > 标签里没有内容的时候, 它就不会给我们拼接 where, 也就不会出现这个问题了.
<select id="getArticleByIdAndTitle" resultType="com.example.demo.entity.vo.ArticleInfoVO">
select * from articleinfo
<trim prefix="where" suffixOverrides="and">
<if test="id != null and id != '' ">
id=#{id} and
</if>
<if test="title != null and title != '' ">
title like concat('%', #{title}, '%')
</if>
</trim>
</select>
一波三折过后, 这个 SQL 语句目前已经使我们能想到的最优情况了. 先来看看数据中文章内容
建立单元测试 :
@Test
@Transactional
void getArticleByIdAndTitle() {
List<ArticleInfoVO> list = articleMapper.getArticleByIdAndTitle(null, null);
list.stream().forEach(System.out::println);
}
此时的 SQL 语句为 :
select * from articleinfo
我们预期是这样的, < tirm > 标签自动为我们拼接 where 并且判断是否去掉 and, 最后执行的是全表查询
@Test
@Transactional
void getArticleByIdAndTitle() {
List<ArticleInfoVO> list = articleMapper.getArticleByIdAndTitle(1, null);
list.stream().forEach(System.out::println);
}
此时的 SQL 语句为 :
select * from articleinfo where id=1
此时我们查找的就是文章表中 id 为 1 的所有文章. 可以看到是可以正确查到的.
对于我们刚刚解决这个需求, 可谓是废了九牛二虎之力, 改了又改才能实现. 但是在 Mybatis 中为我们提供了 < where > 标签来避免这样的窘况.
<select id="getArticleByIdAndTitle" resultType="com.example.demo.entity.vo.ArticleInfoVO">
select * from articleinfo
<where>
<if test="id != null and id != '' ">
id=#{id}
</if>
<if test="title != null and title != '' ">
and title like concat('%', #{title}, '%')
</if>
</where>
</select>
为什么说它解决了我们的窘况呢 ? 在这里如果使用 < where > 标签, 当里面没有内容的时候, 会自动替我们舍弃 where 也就可以解决 where 没有条件时候的问题. 同时 < where > 标签还有个特点, 就是会替我们去除前缀. 什么意思呢 ? 来看看
<select id="getArticleByIdAndTitle" resultType="com.example.demo.entity.vo.ArticleInfoVO">
select * from articleinfo
<where>
<if test="id != null and id != '' ">
id=#{id}
</if>
<if test="title != null and title != '' ">
<!-- 这里按照之前, 我们应该使用 <trim> 标签去除前缀 and -->
and title like concat('%', #{title}, '%')
</if>
</where>
</select>
当 id 不传入, 只传入 title 的时候, SQL 语句变成了 :
select * from article where and title like concat('%, #{title}, '%')
很明显这是一个错误的语句, 但是我们上面执行的时候, 它是可以正确执行并查找到的. 就是因为这里的
总的来说, < where > 标签的功能等同于 < trim prefix=’ where ’ prefixOverrides = " and ">< /trim > 是非常强大和方便的条件标签.
对于设置字段属性值时, 通常在 MySQL 中我们使用的是 set 语句, 而在 Mybatis 的动态 SQL 中我们使用的是 < set > 标签
PS : 对于 set 来说通常是需要结合条件语句的. 对于 set 语句而言, 要求至少是有一个字段被设置的, 否则这就是一个错误的语句.
上面的是什么意思呢 ? 比如 username password title 都是非必传的. 但是在使用 < set > 标签去更新字段内容时, 要求 username password title 中必须有一个是要传过来的. 就像三个人轮休一样. 可以商量谁不来, 但是一定要有一个人来. ! ! !
我们可以看看这个 SQL 语句, 如果 username 不传, 那么就是一个错误的语句 因为 set 内容为空, 因此使用 set 时需要格外注意, 一定是即使都是非必传也要有一个一定可以传过来的或者其中一个非必传的传过来.
update articleinfo set username='zhangsan' where id=1
看这个需求, 根据 id 修改篇文章的 title 和 content. 通常在用户修改文章时, 可能只修改内容也可能只修改标题都是不确定的. 因此我们可以使用前面说到的 < set > 标签来解决这个问题
// 根据 id 修改指定文章
int updateById(ArticleInfo articleInfo);
<update id="updateById">
update articleinfo
<set>
<trim suffixOverrides=",">
<if test="title != null and title != '' ">
title=#{title},
</if>
<if test="content != null and content !='' ">
content=#{content}
</if>
</trim>
</set>
where id=#{id}
</update>
建立单元测试方法 :
@Test
@Transactional
void updateById() {
// 构建文章对象
ArticleInfo articleInfo = new ArticleInfo();
articleInfo.setId(1);
int row = articleMapper.updateById(articleInfo);
System.out.println("更新行数 : " + row);
}
执行后可以看到, set 后面没有被设置的内容是一个错误的 SQL 语句的. 因此在使用 < set > 标签的时候一定要注意, 无论如何都要传入一个设置内容.
< set > 和 < where >标签一样, 也有一个小特点, 来看看这个 SQL 语句
<update id="updateById">
update articleinfo
<set>
<if test="title != null and title != '' ">
title=#{title},
</if>
<if test="content != null and content !='' ">
content=#{content}
</if>
</set>
where id=#{id}
</update>
相比于上面, 我给 < tirm > 标签去掉了. 哪会不会只传入 titile 不传入 content 的时候没法去掉后缀的逗号而报错呢 ?
结果恰恰相反, 它反而正确执行了. 这就是因为 < set > 标签的特点, 它会帮我们自动去掉 < set >标签语句内的后缀的逗号
因此,
foreach 在 Java 中相比不陌生, 经常在增强循环中使用, 对集合进行遍历.
在数据库操作中, 常用于批量操作. 比如批量删除这个操作.( 一般不用于批量增加, 新增风险很大容易错 ). 来看看在文章表中, 批量删除指定 id 的文章.
// 删除指定 uid 的全部文章
int deleteByUid(List<Integer> list);
<delete id="deleteByUid">
delete from articleinfo
where id in
<foreach collection="list" item="aid" open="(" close=")" separator=",">
#{aid}
</foreach>
</delete>
这个< foreach > 标签一看非常的陌生, 和我们之前的
collection : 接口方法参数中指定的集合, 如 List、Set、Map、或者数组对象
item : 遍历时的每一个对象
open : 语句块开头拼接的字符串
close : 语句块结尾拼接的字符串
separtor : 每次遍历之间间隔的字符串
结合我们的接口方法, 这个 SQL 语句的含义就清晰了 :
进行单元测试 :
@Test
@Transactional
void deleteByUid() {
List<Integer> list = new ArrayList<>();
int row = articleMapper.deleteByUid(list);
System.out.println("删除数 : " + row);
}
当 list 当中没有对象时, 使用
PS : < foreach > 标签和
@Test
@Transactional
void deleteByUid() {
List<Integer> list = new ArrayList<>();
list.add(1);
list.add(2);
list.add(3);
list.add(4);
int row = articleMapper.deleteByUid(list);
System.out.println("删除数 : " + row);
}
对于 Mybatis 还有很多需要学习的, 除了掌握这些基本的以外, 如果还想更加深入的了解 Mybatis 中的其他内容以及更多的动态标签, 可以去 Mybatis 官网 查看