Mybatis注解开发中关于动态sql、关联映射、延迟加载、二级缓存的简单应用


前言

  • 由于SpringBoot的流行,现在可以说是注解开发的黄金时代,Mybatis作为当下最流行的持久层框架,自然也支持注解开发的形式,有了注解开发,就可以放弃映射文件的配置,大大减少了开发的代码量,但是,支持快速开发的同时,也带来了一些问题,下面笔者用自己的角度分析一下Mybatis注解开发相对于XML开发的优点与缺点,由于笔者也是初学者,缺少项目经验,难免分析得很片面,想深入了解的请移步其他高质量文章。

  • Mybatis注解开发相对于XML开发的优点与缺点:

    • 优点
    1. 不需要写映射配置文件,适合简单的数据处理,代码简洁,一目了然。
    2. 对于简单的sql语句来说结构更加清晰。
    • 缺点
    1. 不利于线上维护。由于注解都写在java文件中,每次维护都需要修改源码,重新打包,而基于XML的开发就有效的避免了这一点。
    2. 对于特别复杂的sql配置起来不方便,甚至比配置xml更加繁琐。而且在注解配置sql语句时是字符串形式,没有代码提示,出错不容易发现。
    3. 配置较复杂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的强大。

  • 5
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值