前言
-
由于SpringBoot的流行,现在可以说是注解开发的黄金时代,Mybatis作为当下最流行的持久层框架,自然也支持注解开发的形式,有了注解开发,就可以放弃映射文件的配置,大大减少了开发的代码量,但是,支持快速开发的同时,也带来了一些问题,下面笔者用自己的角度分析一下Mybatis注解开发相对于XML开发的优点与缺点,由于笔者也是初学者,缺少项目经验,难免分析得很片面,想深入了解的请移步其他高质量文章。
-
Mybatis注解开发相对于XML开发的优点与缺点:
- 优点
- 不需要写映射配置文件,适合简单的数据处理,代码简洁,一目了然。
- 对于简单的sql语句来说结构更加清晰。
- 缺点
- 不利于线上维护。由于注解都写在java文件中,每次维护都需要修改源码,重新打包,而基于XML的开发就有效的避免了这一点。
- 对于特别复杂的sql配置起来不方便,甚至比配置xml更加繁琐。而且在注解配置sql语句时是字符串形式,没有代码提示,出错不容易发现。
- 配置较复杂sql语句时可读性不强,因为sql字符串中涉及到特殊字符的转义,而这些转义的地方恰恰影响阅读,例如经常出现的双引号,xml中基本上只有<和&需要转义,而且出现次数比较少(还有">"最好也转义)。
-
以下实例采用的技术栈
- Mybatis、MySQL、MySQL-jdbc、log4j、junit4
一、数据库建表
使用MySQL数据库分别建立用户表、账户表,其中一个用户可同时拥有多个账号、每个账号只能属于一个用户,对应一对多的关系,账户表通过用户编号列的外键与用户表连接。同时分别插入几条数据
二、实体类
在com.dzp.pojo包下新建两个实体类User和Account,User中包含一个Account类型的列表,用于表示用户与账户之间一对多的关系。分别提供对应的getter、setter和toString方法,对应代码如下:
package com.dzp.pojo;
import java.io.Serializable;
import java.util.Date;
import java.util.List;
/**
* @ClassName User
* @Author DaiZhipeng
* @Date 2021/4/16
* @Description 用户持久化类
*/
public class User implements Serializable {
private Integer userId;
private String userName;
private String userAddress;
private String userSex;
private Date userBirthday;
/* 该用户关联的账户信息 */
private List<Account> accounts;
public List<Account> getAccounts() {
return accounts;
}
public void setAccounts(List<Account> accounts) {
this.accounts = accounts;
}
public String getUserName() {
return userName;
}
public void setUserName(String userName) {
this.userName = userName;
}
public String getUserAddress() {
return userAddress;
}
public void setUserAddress(String userAddress) {
this.userAddress = userAddress;
}
public String getUserSex() {
return userSex;
}
public void setUserSex(String userSex) {
this.userSex = userSex;
}
public Date getUserBirthday() {
return userBirthday;
}
public void setUserBirthday(Date userBirthday) {
this.userBirthday = userBirthday;
}
public Integer getUserId() {
return userId;
}
public void setUserId(Integer userId) {
this.userId = userId;
}
@Override
public String toString() {
return "User{" +
"userId=" + userId +
", userName='" + userName + '\'' +
", userAddress='" + userAddress + '\'' +
", userSex='" + userSex + '\'' +
", userBirthday=" + userBirthday +
", accounts=" + accounts +
'}';
}
}
package com.dzp.pojo;
import java.io.Serializable;
/**
package com.dzp.pojo;
import java.io.Serializable;
/**
* @ClassName Account
* @Author DaiZhipeng
* @Date 2021/4/16
* @Description 账户持久化类
*/
public class Account implements Serializable {
private Integer id;
private Integer uid;
private Double money;
public Integer getId() {
return id;
}
public void setId(Integer id) {
this.id = id;
}
public Integer getUid() {
return uid;
}
public void setUid(Integer uid) {
this.uid = uid;
}
public Double getMoney() {
return money;
}
public void setMoney(Double money) {
this.money = money;
}
@Override
public String toString() {
return "Account{" +
"id=" + id +
", uid=" + uid +
", money=" + money +
'}';
}
}
三、Mybatis核心配置
在resource目录下新建mybatis-config.xml文件,进行数据源、别名、日志、缓存、指定带有注解的接口位置等相应配置,由于配置一对多关联映射时@Many注解中的fetchType属性可以覆盖延迟加载的全局开关,因此在核心配置文件中可以不用配置延迟加载。
<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE configuration
PUBLIC "-//mybatis.org//DTD Config 3.0//EN"
"http://mybatis.org/dtd/mybatis-3-config.dtd">
<configuration>
<!-- 引入外部配置文件-->
<properties resource="db.properties"/>
<settings>
<!-- 输出日志-->
<setting name="logImpl" value="log4j"/>
<!-- 开启二级缓存-->
<setting name="cacheEnabled" value="true"/>
</settings>
<!-- 配置别名-->
<typeAliases>
<package name="com.dzp.pojo"/>
</typeAliases>
<!-- 环境配置-->
<environments default="mysql">
<environment id="mysql">
<transactionManager type="jdbc"></transactionManager>
<dataSource type="POOLED">
<property name="driver" value="${jdbc.driver}"/>
<property name="url" value="${jdbc.url}"/>
<property name="username" value="${jdbc.username}"/>
<property name="password" value="${jdbc.password}"/>
</dataSource>
</environment>
</environments>
<!-- 指定带有注解的接口位置-->
<mappers>
<package name="com.dzp.mapper"/>
</mappers>
</configuration>
四、dao层接口
在com.dzp.mapper包下新建IUserMapper和IAccountMapper两个接口。
- IUserMapper中在接口名称上面使用@CacheNamespace(blocking = true)开启二级缓存
- 接口中提供一个findUsers()方法,用于查询所有用户信息以及关联的账户,还可以传入两个参数根据用户名和地址进行模糊查询
- 用户与账户为一对多关系,查询时通过@Many注解中的fetchType属性开启了用户关联账户信息的延迟加载
- 使用了<script><where><if><bind>等动态sql标签实现模糊查询
-
<script>用于在Mybatis注解开发中包裹使用动态SQL的语句
-
<where>用于判断组合条件下拼装的SQL语句(加入where关键字以及去除多余的and和or)
-
<if>判断语句,实现简单的条件选择
-
<bind>为当前语句创建一个上下文变量,可有效防止模糊查询时SQL注入的问题
-
- 结果映射
-
@Results,包含了两个参数,id:唯一标识符,value:@Result注解类型的数组。代替的是标签<resultMap>
-
@Result,代替了 <id>标签和<result>标签,属性:id,是否是主键字段;column,数据库的列名;property,需要装配的属性名;one,需要使用的@One 注解(@Result(one=@One)()));many,需要使用的@Many 注解(@Result(many=@many)()));
-
@One 注解(一对一),代替了<assocation>标签,在注解中用来指定子查询返回单一对象。
-
@Many 注解(多对一),代替了<Collection>标签,在注解中用来指定子查询返回对象集合。
-
@Param,传递多个参数的时候可以为参数命名
-
- 二级缓存
-
@CacheNamespace(blocking = true) 用于打开二级缓存,需要先在核心配置文件中开启二级缓存:<setting name="cacheEnabled" value="true"/>
-
- 注意:由于用注解配置sql语句时是以字符串的形式,因此在sql语句中使用双引号等特殊字符时需要用斜杠进行转义,但是对于xml映射文件中需要转义的字符在这里可以不进行转义,如 > 。
package com.dzp.mapper;
import com.dzp.pojo.User;
import org.apache.ibatis.annotations.*;
import org.apache.ibatis.mapping.FetchType;
import java.util.List;
/**
* @ClassName IUserMapper
* @Author DaiZhipeng
* @Date 2021/4/16
* @Description 用户持久层接口
*/
/* 开启二级缓存*/
@CacheNamespace(blocking = true)
public interface IUserMapper {
/**
* @Description查询所有(可根据用户名和地址模糊查询)
* 注:用户与账户为一对多关系,查询时开启了用户关联账户信息的懒加载,
* 使用了<script><where><if><bind>等动态sql标签实现模糊查询
* 其中<script>用于在Mybatis注解开发中包裹使用动态SQL的语句
* <where>用于判断组合条件下拼装的SQL语句(加入where关键字以及去除多余的and和or)
* <if>判断语句,实现简单的条件选择
* <bind>为当前语句创建一个上下文变量,可有效防止模糊查询时SQL注入的问题
* @return
*/
/* 查询语句 */
@Select("<script>" +
"select * from user" +
"<where>" +
"<if test=\" username != null and username != '' \">and username like #{search1}</if>" +
"<if test=\" address != null and address != '' \">and address like #{search2}</if>" +
"</where>" +
"<bind name='search1' value=\"'%'+ username +'%' \"/>" +
"<bind name='search2' value=\"'%'+ address +'%' \"/>" +
"</script>")
/* 一对多关系结果映射 */
@Results(id = "userMap",value = {
@Result(id=true,property = "userId",column = "id"),
@Result(property = "userName",column = "username"),
@Result(property = "userAddress",column = "address"),
@Result(property = "userSex",column = "sex"),
@Result(property = "userBirthday",column = "birthday"),
@Result(property = "accounts",column = "id",
many = @Many(select = "com.dzp.mapper.IAccountMapper.findById",fetchType = FetchType.LAZY))
})// ↑指定子查询 ↑开启延迟加载
List<User> findUsers(@Param("username") String username,@Param("address") String address);
}
IAccountMapper代码如下,主要提供findById方法用于根据用户编号查询用户关联的账户:
package com.dzp.mapper;
import com.dzp.pojo.Account;
import org.apache.ibatis.annotations.Select;
import java.util.List;
/**
* @ClassName IAccountMapper
* @Author DaiZhipeng
* @Date 2021/4/16
* @Description 账户持久层接口
*/
public interface IAccountMapper {
/**
* 根据id查账户
* @param id
* @return
*/
@Select("select * from account where uid=#{id}")
List<Account> findById(Integer id);
}
五、junit单元测试
1 前期准备
到这里,Mybatis的配置基本完成,不用再去编写繁琐的映射文件,下面对动态SQL、关联映射、延迟加载、二级缓存逐一进行测试。
首先新建MybatisAnnotationsTest测试类。这里是通过SqlSession的getMapper()方法获取动态代理对象的方式来调用查询方法的,因此我们先在类下创建如下四个属性:
-
private InputStream in;
-
private SqlSessionFactory sqlSessionFactory;
-
private SqlSession session;
-
private IUserMapper userMapper;
然后通过junit提供的两个注解,完成前期准备和后期完善:
-
@Before//用于在测试方法执行前执行
-
@After//用于在测试方法执行后执行
这里我们需要做的工作如下:
public class MybatisAnnotationsTest {
private InputStream in;
private SqlSessionFactory sqlSessionFactory;
private SqlSession session;
private IUserMapper userMapper;
@Before//junit的注解,用于在测试方法执行前执行
public void init() throws IOException {
//1.获取mybatis-config配置文件的输入流
in = Resources.getResourceAsStream("mybatis-config.xml");
//2.通过SqlSessionFactoryBuilder构建SqlSessionFactory对象
sqlSessionFactory = new SqlSessionFactoryBuilder().build(in);
//3.通过SqlSessionFactory对象创建Sqlsession对象
session = sqlSessionFactory.openSession();
//4.通过Sqlsession对象通过反射的方式获取IUserMapper接口的代理对象用于执行查询方法
userMapper = session.getMapper(IUserMapper.class);
}
@After//junit的注解,用于在测试方法执行后执行
public void destroy() throws IOException {
session.close();
in.close();
}
下面就可以编写测试方法对动态SQL、关联映射、延迟加载、二级缓存等进行测试了。
2 动态SQL测试
编写如下测试方法:
/**
* 动态Sql测试
*/
@Test
public void testFindUser(){
List<User> users = userMapper.findUsers("八","武汉");
for(User user:users) {
System.out.println(user);
System.out.println(user.getAccounts());
}
运行测试方法,得到如下输出,可以看到打印出查询结果的同时多出了很多不必要的信息,这些是log4j的日志输出,这些信息有助于我们下面的测试分析:
通过输出的查询结果得知通过注解配置的动态SQL起作用了,达到了模糊查询的效果,这里需要注意的是通过@select、@insert、@update、@delete这类注解配置动态sql时,一定要用<script>标签包裹起来,不然会报错。
3 关联映射测试
通过动态SQL测试的输出结果就可以看出,在查询用户的同时查询出了每个用户下账户的信息,而且通过日志可以看出是分别执行了两条sql语句。由于查询到信息就立即进行输出了,所以感受不到延迟加载的作用,下面具体针对延迟加载这一块进行测试。
4 延迟加载测试
编写如下测试方法:
/**
* 延迟加载测试
*/
@Test
public void testFindUser(){
List<User> users = userMapper.findUsers("老八","武汉");
for(User user:users) {
System.out.println(user.getUserName());
System.out.println("--------方法已经调用了,但是用到accounts的时候才会调用子查询↓---------");
System.out.println(user.getAccounts());
}
}
得到以下输出结果:
延迟加载的概念: 在真正使用数据时才发起查询,不用的时候不查询,也叫按需加载、懒加载
通过输出结果可以得知,由于开启了延迟加载,调用findUsers方法时,只是执行了查询用户的sql,并没有执行查询用户账户的sql,当我们需要打印用户账户的时候,Mybatis才会发起查询,这一点通过日志可以很明显的看出来。
5 二级缓存测试
编写如下测试方法:
/**
* 二级缓存测试
*/
@Test
public void testFindUser(){
List<User> users = userMapper.findUsers("老八","武汉");
for(User user:users) {
System.out.println(user);
}
session.close();//关闭session,相当于清除了一级缓存
SqlSession session1 = sqlSessionFactory.openSession();//重新获取新的session
IUserMapper userMapper1 = session1.getMapper(IUserMapper.class);//重新获取新的代理对象
System.out.println("-----------调用第二次查询-----------");
List<User> users1 = userMapper1.findUsers("老八","武汉");
for(User user:users1) {
System.out.println(user);
}
}
得到以下输出结果:
二级缓存的概念: SqlSessionFactory对象的缓存,由同一个SqlSessionFactory创建的SqlSession共享其缓存。
测试时,在执行完第一个查询之后,通过session.close()关闭session,重新获取一个新的session再次执行查询,这样就避免了一级缓存的影响。通过输出结果可知,我们分别用两个不同的session执行了两次查询方法,但是通过日志可以看出只执行了一次sql,第二次的结果是直接在缓存中拿的。
另外补充一点:
一级缓存是session中的缓存,查询结果是通过对象来保存的,也就是说两次查询拿到的对象是同一个。
二级缓存是SqlSessionFactory中的缓存,查询结果是通过数据(键值对)来保存的,所以两次拿到的对象不是同一个,从缓存拿到的对象是由缓存的数据动态组装出来的。
总结
在使用Mybatis开发时,我们要根据项目的大小、复杂度、维护频率等因素来选择是基于xml还是基于注解,两种开发方式各有各的好处,虽然说注解开发逐渐开始流行,但是对于Mybatis来说,有一句话必须是需要明白的:只有xml才能真正表现出mybatis的强大。