网络日志

本文为Mybatis面经,其中难点问题做了详细解释

1. 什么是 Mybatis?

  • Mybatis 是一个半 ORM(对象关系映射)框架,它内部封装了 JDBC,开发时只需要关注 SQL 语句本身,不需要花费精力去处理加载驱动、创建连接、创建 statement 等繁杂的过程。程序员直接编写原生态 sql,可以严格控制 sql 执行性能,灵活度高
  • MyBatis 可以使用 XML 或注解来配置和映射原生信息,将 POJO 映射成数据库中的记录,避免了几乎所有的 JDBC 代码和手动设置参数以及获取结果集
  • 通过 xml 文件或注解的方式将要执行的各种 statement 配置起来,并通过 java 对象和 statement 中 sql 的动态参数进行映射生成最终执行的 sql 语句,最后由 mybatis 框架执行 sql 并将结果映射为 java 对象并返回(从执行 sql 到返回 result 的过程)

2. Mybaits 的优点?

  • 基于 SQL 语句编程,相当灵活,不会对应用程序或者数据库的现有设计造成任何影响,SQL 写在 XML 里,解除 sql 与程序代码的耦合,便于统一管理;提供 XML 标签,支持编写动态 SQL 语句,并可重用
  • 与 JDBC 相比,减少了50%以上的代码量,消除了 JDBC 大量冗余的代码,不需要手动开关连接
  • 很好的与各种数据库兼容(因为 MyBatis 使用 JDBC 来连接数据库,所以只要 JDBC 支持的数据库 MyBatis 都支持)
  • 能够与 Spring 很好的集成
  • 提供映射标签,支持对象与数据库的 ORM 字段关系映射;提供对象关系映射标签,支持对象关系组件维护-

3. MyBatis 框架的缺点?

  • SQL 语句的编写工作量较大,尤其当字段多、关联表多时,对开发人员编写 SQL 语句的功底有一定要求
  • SQL 语句依赖于数据库,导致数据库移植性差,不能随意更换数据库

4. MyBatis 框架适用场合?

MyBatis 专注于 SQL 本身,是一个足够灵活的 DAO 层解决方案

对性能的要求很高,或者需求变化较多的项目,如互联网项目,MyBatis 将是不错的选择

5. MyBatis 与 Hibernate 有哪些不同?

  • Mybatis 和 hibernate 不同,它不完全是一个 ORM 框架,因为 MyBatis 需要程序员自己编写 Sql 语句
  • Mybatis 直接编写原生态 sql,可以严格控制 sql 执行性能,灵活度高,非常适合对关系数据模型要求不高的软件开发,因为这类软件需求变化频繁,一但需求变化要求迅速输出成果。但是灵活的前提是 mybatis 无法做到数据库无关性 ,如果需要实现支持多种数据库的软件,则需要自定义多套 sql 映射文件,工作量大
  • Hibernate 对象/关系映射能力强, 数据库无关性好 ,对于关系模型要求高的软件,如果用 hibernate 开发可以节省很多代码,提高效率

6. MyBatis和其它持久化层技术对比

  • JDBC

    • SQL 夹杂在Java代码中耦合度高,导致硬编码内伤
    • 维护不易且实际开发需求中 SQL 有变化,频繁修改的情况多见
    • 代码冗长,开发效率低
  • Hibernate 和 JPA

    • 操作简便,开发效率高
    • 程序中的长难复杂 SQL 需要绕过框架
    • 内部自动生产的 SQL,不容易做特殊优化
    • 基于全映射的全自动框架,大量字段的 POJO 进行部分映射时比较困难。
    • 反射操作太多,导致数据库性能下降
  • MyBatis

    • 轻量级,性能出色
    • SQL 和 Java 编码分开,功能边界清晰。Java代码专注业务、SQL语句专注数据
    • 开发效率稍逊于HIbernate,但是完全能够接受

7. 谈谈MyBatis和JPA的区别

  • ORM映射不同:

    MyBatis是半自动的ORM框架,提供数据库与结果集的映射;

    JPA(默认采用Hibernate实现)是全自动的ORM框架,提供对象与数据库的映射。

  • 可移植性不同:

    JPA通过它强大的映射结构和HQL语言,大大降低了对象与数据库的耦合性;

    MyBatis由于需要写SQL,因此与数据库的耦合性直接取决于SQL的写法,如果SQL不具备通用性而用了很多数据库的特性SQL的话,移植性就会降低很多,移植时成本很高。

  • SQL优化上的区别:

    由于Mybatis的SQL都是写在XML里,因此优化SQL比Hibernate方便很多。

    而Hibernate的SQL很多都是自动生成的,无法直接维护SQL。虽有HQL,但功能还是不及SQL强大,见到报表等复杂需求时HQL就无能为力,也就是说HQL是有局限的Hhibernate虽然也支持原生SQL,但开发模式上却与ORM不同,需要转换思维,因此使用上不是非常方便。 总之写SQL的灵活度上Hibernate不及Mybatis。

8. MyBatis输入输出支持的类型有哪些?

parameterType:

MyBatis支持多种输入输出类型,包括:

简单的类型,如整数、小数、字符串等;

集合类型,如Map等;

自定义的JavaBean。

其中,简单的类型,其数值直接映射到参数上。对于Map或JavaBean则将其属性按照名称映射到参数上。

9. MyBatis里如何实现一对多关联查询?

MyBatis 实现一对多有 联合查询嵌套查询 。联合查询是几个表联合查询,只查询一次,通过在 resultMap 里面的 collection 节点配置一对多的类就可以完成;嵌套查询是先查一个表,根据这个表里面的结果的外键 id,去再另外一个表里面查询数据,也是通过配置 collection,但另外一个表的查询通过 select 节点配置。

一对多:例如:根据部门id查找部门以及部门中的员工信息

需要查询一对多、多对一的关系,需要在“一”的pojo中加入List<多>属性,在“多”的pojo中加入“一”。

也就是说,在Dept类中,要加入 private List<Emp> emps; ;在Emp类中,要加入 private Dept dept; 。然后给他们各自添加get、set方法,重写构造器和toString()

public class  Dept  { 
    private Integer did;
    private String deptName;
    private List<Emp> emps;
    //...构造器、get、set方法等
}

方法1:collection(联合查询)

DeptMapper接口

public interface DeptMapper { 
    /**
     * 获取部门以及部门中所有的员工信息
     */
    Dept getDeptAndEmp(@Param("did") Integer did);
 }

DeptMapper.xml

<resultMap id="deptAndEmpResultMap" type="Dept">
        <id property="did" column="did"></id>
        <result property="deptName" column="dept_name"></result>
<!--
            collection:处理一对多的映射关系
            ofType:表示该属性对应的集合中存储数据的类型
-->
        <collection property="emps" ofType="Emp">
            <id property="eid" column="eid"></id>
            <result property="empName" column="emp_name"></result>
            <result property="age" column="age"></result>
            <result property="sex" column="sex"></result>
            <result property="email" column="email"></result>
        </collection>
    </resultMap>
<!--        Dept getDeptAndEmp(@Param("did") Integer did);-->
    <select id="getDeptAndEmp" resultMap="deptAndEmpResultMap">
        select * from t_dept left join t_emp on t_dept.did = t_emp.did where t_dept.did = #{did} </select>

方法2: 分步查询(嵌套查询)

(1)查询部门信息

DeptMapper接口

public interface DeptMapper { 
    /**
     * 分步查询 查询部门及其所有的员工信息
     * 第一步  查询部门信息
     */
    Dept getDeptAndEmoByStepOne(@Param("did") Integer did);
}

DeptMapper.xml

<!--    分步查询-->
    <resultMap id="deptAndEmoByStepOneMap" type="Dept">
        <id property="did" column="did"></id>
        <result property="deptName" column="dept_name"></result>
        <collection property="emps"
                    select="com.atguigu.mybatis.mapper.EmpMapper.getDeptAndEmpByStepTwo"
                    column="did">
        </collection>
    </resultMap>
<!--        Dept getDeptAndEmoByStepOne(@Param("did") Integer did);-->
    <select id="getDeptAndEmoByStepOne" resultMap="deptAndEmoByStepOneMap">
        select * from t_dept where did = #{did} </select>
(2)根据部门id查询部门中的所有员工

EmpMapper

public interface  EmpMapper  { 
    /**
 * 分步查询 查询部门及其所有的员工信息
 * 第一步  查询部门信息
 * 第二步  根据查询员工信息
 */
    List<Emp> getDeptAndEmpByStepTwo(@Param("did") Integer did);
}

EmpMapper.xml

<!--    分步查询-->
<!--    List<Emp> getDeptAndEmpByStepTwo(@Param("did") Integer did);-->
    <select id="getDeptAndEmpByStepTwo" resultType="Emp">
        select * from t_emp where did = #{did} </select>

10. MyBatis获取参数值的两种方式-- #{} 和 ${} 的区别是什么

{} 是预编译处理,${} 是字符串替换。

Mybatis 在处理 #{} 时,会将 sql 中的 #{} 替换为 ? 号,调用 PreparedStatement 的 set 方法来赋值;Mybatis 在处理 ${} 时,就是把 ${} 替换成变量的值。

使用 #{} 可以有效的防止 SQL 注入,提高系统安全性。

  • ${}的本质就是字符串拼接
  • {}的本质就是占位符赋值

${}使用字符串拼接的方式拼接sql,若为字符串类型或日期类型的字段进行赋值时,需要手动加单引号;

但是#{}使用占位符赋值的方式拼接sql,此时为字符串类型或日期类型的字段进行赋值时,可以自动添加单引号(尽量使用这一种)。

  • 使用#设置参数时,MyBatis会创建预编译的SQL语句,然后在执行SQL时MyBatis会为预编译SQL中的占位符(?)赋值。预编译的SQL语句执行效率高,并且可以防止注入攻击。
  • 使用$设置参数时,MyBatis只是创建普通的SQL语句,然后在执行SQL语句时MyBatis将参数直接拼入到SQL里。这种方式在效率、安全性上均不如前者,但是可以解决一些特殊情况下的问题。例如,在一些动态表格(根据不同的条件产生不同的动态列)中,我们要传递SQL的列名,根据某些列进行排序,或者传递列名给SQL都是比较常见的场景,这就无法使用预编译的方式了。

总结:分成两种情况进行处理

  1. 实体类类型的参数 (若mapper接口中的方法参数为实体类对象时此时可以使用${}和#{},通过访问实体类对象中的属性名获取属性值,注意${}需要手动加单引号)
  2. 使用@Param标识参数

    ``java
    public interface ParameterMapper { 
    /**
     * 添加用户信息
     */
    int  insertUser(User user);
    }</pre>
    
    <pre class="prettyprint hljs less">public interface ParameterMapper { 
    /**
     * 验证登录 (使用@Param)
     */
    User checkLoginByParam(@Param("username") String username, @Param("password") String password);
    }

    11. 既然 ${}不安全,为什么还需要用它,什么时候会用到它?

它可以解决一些特殊情况下的问题。例如,在一些动态表格(根据不同的条件产生不同的动态列)中,我们要传递SQL的列名,根据某些列进行排序,或者传递列名给SQL都是比较常见的场景,这就无法使用预编译的方式了。

批量删除时:

只能使用${},如果使用#{},则解析后的sql语句为 delete from t_user where id in ('1,2,3') ,这样是将1,2,3看做是一个整体,只有id为1,2,3的数据会被删除。正确的语句应该是 delete from t_user where id in (1,2,3) ,或者 delete from t_user where id in ('1','2','3')

12. MyBatis的xml文件和Mapper接口是怎么绑定的?

是通过xml文件中 <mapper> 根标签的namespace属性进行绑定的,即namespace属性的值需要配置成接口的全限定名称,MyBatis内部就会通过这个值将这个接口与这个xml关联起来。

  • MyBatis中可以面向接口操作数据,要保证两个一致

    • mapper接口的全类名和映射文件的命名空间(namespace)保持一致
    • mapper接口中方法的方法名和映射文件中编写SQL的标签的id属性保持一致

      package com.atguigu.mybatis.mapper;  
      
      public interface  UserMapper  {
      
      /**  添加用户信息  */  
      int  insertUser();  
      }
      <?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.atguigu.mybatis.mapper.UserMapper">  
      <!--int insertUser();-->  
      <insert id="insertUser">  
      insert into t_user values(null,'张三','123',23,'女')  
      </insert>  
      </mapper>

      13. MyBatis分页和自己写的分页哪个效率高?

自己写的分页效率高。

在MyBatis中,我们可以通过分页插件实现分页,也可以通过分页SQL自己实现分页。其中,分页插件的原理是,拦截查询SQL,在这个SQL基础上自动为其添加limit分页条件。它会大大的提高开发的效率,但是无法对分页语句做出有针对性的优化,比如分页偏移量很大的情况,而这些在自己写的分页SQL里却是可以灵活实现的。

14. 了解MyBatis缓存机制吗?

MyBatis的缓存分为一级缓存和二级缓存。

14.1、MyBatis的一级缓存

  • 一级缓存是SqlSession级别的,通过同一个SqlSession查询的数据会被缓存,下次查询相同的数据,就会从缓存中直接获取,不会从数据库重新访问

使一级缓存失效的四种情况:

  1. 不同的SqlSession对应不同的一级缓存
  2. 同一个SqlSession但是查询条件不同
  3. 同一个SqlSession两次查询期间执行了任何一次增删改操作
  4. 同一个SqlSession两次查询期间手动清空了缓存

14.2、MyBatis的二级缓存

  • 二级缓存是SqlSessionFactory级别,通过同一个SqlSessionFactory创建的SqlSession查询的结果会被缓存;此后若再次执行相同的查询语句,结果就会从缓存中获取

二级缓存开启的条件

cacheEnabled="true"
<cache />

使二级缓存失效的情况: 两次查询之间执行了任意的增删改,会使一级和二级缓存同时失效

没有提交sqlsession时,数据会保存在一级缓存中,提交后,会保存在二级缓存中。

14.3、MyBatis缓存查询的顺序

  • 先查询二级缓存,因为二级缓存中可能会有其他程序已经查出来的数据,可以拿来直接使用
  • 如果二级缓存没有命中,再查询一级缓存
  • 如果一级缓存也没有命中,则查询数据库
  • SqlSession关闭之后,一级缓存中的数据会写入二级缓存

15. 当实体类中的属性名和表中的字段名不一样 ,怎么办 ?

字段名 (数据库里的名字例如emp_name)和 实体类中的属性名 不一致,但是字段名符合数据库的规则(使用 _ ),实体类中的属性 名符合Java的规则(使用驼峰),此时也可通过以下两种方式处理字段名和实体类中的属性的映射关系

<resultMap>

15.1 用起别名的方式保证字段名与属性名一致

和sql中一样,用字段名 属性名(如emp_name empName)来使二者一致。

<!--    List<Emp> getAllEmp();-->
    <select id="getAllEmp" resultType="Emp">
        select eid, emp_name empName, age, sex, email from t_emp
    </select>

15.2 逐一设置resultMap映射关系

在resultMap中,一一对应地设置属性名->字段名,再在select标签中添加resultMap=“对应resultMap的id”

<!--
        resultMap设置自定义映射关系
        id      唯一标识
        type    映射的实体类型

        子标签:id 设置主键的映射关系, result设置其他的映射关系
            property    设置映射关系中的属性名,必须是type属性所设置的实体类类型的属性名
            column      设置映射关系中的字段名,必须是sql语句查询出来的字段名

        如果使用resultMap,就所有属性都需要设置
-->
    <resultMap id="empResultMap" type="Emp">
        <id property="eid" column="eid"></id>
        <result property="empName" column="emp_name"></result>
        <result property="age" column="age"></result>
        <result property="sex" column="sex"></result>
        <result property="email" column="email"></result>
    </resultMap>

    <select id="getAllEmp" resultMap="empResultMap">
        select * from t_emp
    </select>

16. 模糊查询 like 语句该怎么写?

select * from t_user where username like "%"#{username}"%" 是最常用的

SQLMapper接口:

public interface  SQLMapper  { 
    /**
 * 根据用户名模糊查询用户信息
 */
    List<User> getUserByLike(@Param("username") String username);
}

SQLMapper.xml:

<!--    List<User> getUserByLike(@Param("username") String username);-->
<!--    使用#{},因为包括在单引号里,会被认为是字符串的一部分:select * from t_user where username like '%#{username}%'-->
<!--    三种方式-->
    <select id="getUserByLike" resultType="User">
     <!--  第一种 select * from t_user where username like '%${username}%'
       第二种 select * from t_user where username like concat('%', #{username}, '%')-->
       <!--第三种 推荐使用-->
        select * from t_user where username like "%"#{username}"%"
    </select>

17. 通常一个 Xml 映射文件,都会写一个 Dao 接口与之对应,请问,这个 Dao 接口的工作原理是什么?Dao 接口里的方法,参数不同时,方法能重载吗?

  • Dao 接口即 Mapper 接口。接口的全限名,就是映射文件中的 namespace 的值;接口的方法名,就是映射文件中 Mapper 的 Statement 的 id 值;接口方法内的参数,就是传递给 sql 的参数。
  • Mapper 接口是没有实现类的,当调用接口方法时,接口全限名+方法名拼接字符串作为 key 值,可唯一定位一个 MapperStatement。在 Mybatis 中,每一个 <select><insert><update><delete> 标签,都会被解析为一个 MapperStatement 对象。
  • 举例: com.mybatis3.mappers.StudentDao.findStudentById ,可以唯一找到 namespace 为 com.mybatis3.mappers.StudentDao 下面 id 为 findStudentById 的 MapperStatement。
  • Mapper 接口里的方法,是不能重载的,因为是使用 全限名+方法名 的保存和寻找策略。Mapper 接口的工作原理是 JDK 动态代理,Mybatis 运行时会使用 JDK 动态代理为 Mapper 接口生成代理对象 proxy,代理对象会拦截接口方法,转而执行 MapperStatement 所代表的 sql,然后将 sql 执行结果返回。

18. Mybatis 是如何进行分页的?分页插件的原理是什么?

Mybatis 使用 RowBounds 对象进行分页,它是针对 ResultSet 结果集执行的内存分页,而非物理分页。可以在 sql 内直接书写带有物理分页的参数来完成物理分页功能,也可以使用分页插件来完成物理分页。

分页插件的基本原理是使用 Mybatis 提供的插件接口,实现自定义插件,在插件的拦截方法内拦截待执行的 sql,然后重写 sql,根据 dialect 方言,添加对应的物理分页语句和物理分页参数。

19. Mybatis 是如何将 sql 执行结果封装为目标对象并返回的?都有哪些映射形式?

第一种是使用 <resultMap> 标签,逐一定义数据库列名和对象属性名之间的映射关系。

第二种是使用 sql 列的别名功能,将列的别名书写为对象属性名。

有了列名与属性名的映射关系后,Mybatis 通过反射创建对象,同时使用反射给对象的属性逐一赋值并返回,那些找不到映射关系的属性,是无法完成赋值的。

20. 如何执行批量删除和插入?

用 foreach(常用)

  • foreach常用属性:

       ,

    应用场景1: 通过数组实现批量删除(用的多)

DynamicSqlMapper接口

public interface DynamicSQLMapper { 
    /**
     * 通过数组实现批量删除
     */
    int deleteMoreByArray(@Param("eids") Integer[] eids);
}

DynamicSqlMapper.xml

<!--        int deleteMoreByArray(Integer[] eids);-->
    <delete id="deleteMoreByArray">
    <!--方法2:-->
        delete from t_emp where
        <foreach collection="eids"  item="eid" separator="or">
            eid = #{eid} </foreach>
    </delete>

测试类:

/**
     * 5、foreach
     */
    @Test  public  void  testDeleteMoreByArray(){ 
        SqlSession sqlSession = SqlSessionUtils.getSqlSession();
        DynamicSQLMapper mapper = sqlSession.getMapper(DynamicSQLMapper.class);
        int result = mapper.deleteMoreByArray(new Integer[]{ 7, 8, 9});
        System.out.println(result);
    }

应用场景2: 通过list集合实现批量添加(用的多)

DynamicSqlMapper接口

public interface DynamicSQLMapper { 
    /**
     * 通过list集合实现批量添加
     */
    int insertMoreByList(@Param("emps") List<Emp> emps);
}

DynamicSqlMapper.xml

<!--        int insertMoreByList(List<Emp> emps);-->
<!--    DynamicSqlMapper接口不加@Paras注解会报错:Parameter 'emps' not found. Available parameters are [arg0, collection, list]-->
<!--    int insertMoreByList(@Param("emps") List<Emp> emps);-->
    <insert id="insertMoreByList">
        insert into t_emp values
        <foreach collection="emps" item="emp" separator=",">
            (null, #{emp.empName}, #{emp.age}, #{emp.sex}, #{emp.email}, null)
        </foreach>
    </insert>

测试类:

@Test
public void testInsertMoreByList(){ 
    SqlSession sqlSession = SqlSessionUtils.getSqlSession();
    DynamicSQLMapper mapper = sqlSession.getMapper(DynamicSQLMapper.class);
    Emp emp1 = new Emp(null, "Mary", 23, "女", "11111@qq.com");
    Emp emp2 = new Emp(null, "Linda", 23, "女", "1144111@qq.com");
    Emp emp3 = new Emp(null, "Jackoline", 23, "女", "1122111@qq.com");
    List<Emp> emps = Arrays.asList(emp1, emp2, emp3);
    System.out.println(mapper.insertMoreByList(emps));
}

21. 添加功能获取自增的主键、如何获取自动生成的(主)键值

  • 使用场景

    • t_clazz(clazz_id,clazz_name)
    • t_student(student_id,student_name,clazz_id)
    1. 添加班级信息
    2. 获取新添加的班级的id
    3. 为班级分配学生,即将某学生的班级id修改为新添加的班级的id
  • 在mapper.xml中设置两个属性

    • useGeneratedKeys:设置使用自增的主键
    • keyProperty:因为增删改有统一的返回值是受影响的行数,因此只能将获取的自增的主键放在传输的参数user对象的某个属性中

      /**
       * 添加用户信息
       * @param user 
       * @date 2022/2/27 15:04
       */
      void insertUser(User user);
      <!--void insertUser(User user);-->
      <!--    方法的返回值是固定的
      useGeneratedKeys    设置当前标签中的sql使用了自增的主键 (id)
      keyProperty         将自增的主键的值 赋值给 传输到映射文件中的参数的某个属性(user.id)
      -->
      
      <insert id="insertUser" useGeneratedKeys="true" keyProperty="id">
      insert into t_user values (null,#{username},#{password},#{age},#{sex},#{email})
      </insert>
      //测试类
      @Test
      public void insertUser() { 
      SqlSession sqlSession = SqlSessionUtils.getSqlSession();
      SQLMapper mapper = sqlSession.getMapper(SQLMapper.class);
      User user = new User(null, "ton", "123", 23, "男", "123@321.com");
      mapper.insertUser(user);
      System.out.println(user);
      //输出:user{id=10, username='ton', password='123', age=23, sex='男', email='123@321.com'},自增主键存放到了user的id属性中
      }

      22. 在 mapper 中如何传递多个参数?

使用@Param标识参数

  • 可以通过@Param注解标识mapper接口中的方法参数,此时,会将这些参数放在map集合中

    1. 以@Param注解的value属性值为键,以参数为值;
    2. 以param1,param2…为键,以参数为值;
  • 只需要通过${}和#{}访问map集合的键就可以获取相对应的值,注意${}需要手动加单引号

    ParameterMapper接口:

       public interface ParameterMapper { 
        /**
         * 验证登录 (使用@Param)
         */
        User checkLoginByParam(@Param("username") String username, @Param("password") String password);
    }

    对应在ParameterMapper.xml中配置。

      <!--    以@Param的值为键,参数为值; 或以"param1"/"param2"为键,参数为值-->
    <!--    User checkLoginByParam(@Param("username") String username, @Param("password") String password);-->
        <select id="checkLoginByParam" resultType="User">
            select * from t_user where username = #{username} and password = #{password} </select>

    多个参数封装成map

  • 若mapper接口中的方法需要的参数为多个时,此时可以手动创建map集合,将这些数据放在map中只需要通过${}和#{}访问map集合的键就可以获取相对应的值,注意${}需要手动加单引号

    ParameterMapper接口:

      public interface  ParameterMapper  { 
        /**
     * 验证登录
     */
        User checkLoginByMap(Map<String, Object> map);
    }

    对应在ParameterMapper.xml中配置。

     <!--    User checkLoginByMap(Map<String, Object> map);-->
        <select id="checkLoginByMap" resultType="User">
            select * from t_user where username = #{username} and password = #{password} </select>

    23. Mybatis 动态 sql 有什么用?执行原理?有哪些动态 sql?

本质是一系列的标签

Mybatis 动态 sql 可以在 Xml 映射文件内,以标签的形式编写动态 sql,执行原理是根据表达式的值 完成逻辑判断并动态拼接 sql 的功能。

Mybatis 提供了9种动态sql标签:trim | where | set | foreach | if | choose | when | otherwise | bind。

  • Mybatis框架的动态SQL技术是一种根据特定条件动态拼装SQL语句的功能,它存在的意义是为了解决拼接SQL语句字符串时的痛点问题

if(常用)

if标签可通过test属性的表达式进行判断,若表达式的结果为true,则标签中的内容会执行;反之标签中的内容不会执行

  • 在where后面添加一个恒成立条件 1=1
  • 这个恒成立条件并不会影响查询的结果
  • 这个 1=1 可以用来拼接 and 语句,例如:当empName为null时

    • 如果不加上恒成立条件,则SQL语句为 select * from t_emp where and age = ? and sex = ? and email = ? ,此时 where 会与 and 连用,SQL语句会报错
    • 如果加上一个恒成立条件,则SQL语句为 select * from t_emp where 1= 1 and age = ? and sex = ? and email = ? ,此时不报错

应用场景:多条件查询

24. Xml 映射文件中,除了常见的 select|insert|updae|delete 标签之外,还有哪些标签?

<resultMap><parameterMap><sql><include><selectKey> ,加上动态 sql 的9个标签,其中 <sql> 为 sql 片段标签,通过 <include> 标签引入 sql 片段, <selectKey> 为不支持自增的主键生成策略标签。

25. Mybatis 的 Xml 映射文件中,不同的 Xml 映射文件,id 是否可以重复?

不同的 Xml 映射文件,如果配置了 namespace,那么 id 可以重复;如果没有配置 namespace,那么 id 不能重复;

原因就是 namespace+id 是作为 Map<String, MapperStatement> 的 key 使用的,如果没有 namespace,就剩下 id,那么,id 重复会导致数据互相覆盖。有了 namespace,自然 id 就可以重复,namespace 不同,namespace+id 自然也就不同。

但是,在以前的 Mybatis 版本的 namespace 是可选的,不过新版本的 namespace 已经是必须的了。

26. 为什么说 Mybatis 是半自动 ORM 映射工具?它与全自动的区别在哪里?

Hibernate 属于全自动 ORM 映射工具,使用 Hibernate 查询关联对象或者关联集合对象时,可以根据对象关系模型直接获取,所以它是全自动的。而 Mybatis 在查询关联对象或关联集合对象时,需要手动编写 sql 来完成,所以,称之为半自动 ORM 映射工具。

27. MyBatis 实现一对多有几种方式,怎么操作的?

有联合查询和嵌套查询。联合查询是几个表联合查询,只查询一次,通过在 resultMap 里面的 collection 节点配置一对多的类就可以完成;嵌套查询是先查一个表,根据这个表里面的结果的外键 id,去再另外一个表里面查询数据,也是通过配置 collection,但另外一个表的查询通过 select 节点配置。

28. Mybatis 是否支持延迟加载?如果支持,它的实现原理是什么?

Mybatis 仅支持 association 关联对象和 collection 关联集合对象的延迟加载,association 指的就是一对一,collection 指的就是一对多查询。在 Mybatis 配置文件中,可以配置是否启用延迟加载 lazyLoadingEnabled=true|false

它的原理是,使用 CGLIB 创建目标对象的代理对象,当调用目标方法时,进入拦截器方法,比如调用 a.getB().getName(),拦截器 invoke() 方法发现 a.getB() 是 null 值,那么就会单独发送事先保存好的查询关联B对象的 sql,把B查询上来,然后调用 a.setB(b),于是a的对象b属性就有值了,接着完成 a.getB().getName() 方法的调用。这就是延迟加载的基本原理。

当然了,不光是 Mybatis,几乎所有的包括 Hibernate,支持延迟加载的原理都是一样的。

28. 什么是 MyBatis 的接口绑定?有哪些实现方式?

接口绑定,就是在 MyBatis 中任意定义接口,然后把接口里面的方法和 SQL 语句绑定,我们直接调用接口方法就可以,这样比起原来了 SqlSession 提供的方法我们可以有更加灵活的选择和设置。

接口绑定有两种实现方式,一种是通过注解绑定,就是在接口的方法上面加上 @Select、@Update 等注解,里面包含 Sql 语句来绑定;另外一种就是通过 xml 里面写 SQL 来绑定,在这种情况下,要指定 xml 映射文件里面的 namespace 必须为接口的全路径名。当 Sql 语句比较简单时候,用注解绑定,当 SQL 语句比较复杂时候,用 xml 绑定,一般用 xml 绑定的比较多。

28. 使用 MyBatis 的 mapper 接口调用时有哪些要求?

Mapper 接口方法名和 mapper.xml 中定义的每个 sql 的 id 相同

Mapper 接口方法的输入参数类型和 mapper.xml 中定义的每个 sql 的 parameterType 的类型相同

Mapper 接口方法的输出参数类型和 mapper.xml 中定义的每个 sql 的 resultType 的类型相同

Mapper.xml 文件中的 namespace 即是 mapper 接口的类路径

29. 简述 Mybatis 的插件运行原理,以及如何编写一个插件?

Mybatis 仅可以编写针对 ParameterHandler、ResultSetHandler、StatementHandler、Executor 这4种接口的插件,Mybatis 使用 JDK 的动态代理,为需要拦截的接口生成代理对象以实现接口方法拦截功能,每当执行这4种接口对象的方法时,就会进入拦截方法,具体就是 InvocationHandler 的 invoke() 方法,当然,只会拦截那些你指定需要拦截的方法。

编写插件:实现 Mybatis 的 Interceptor 接口并复写 intercept() 方法,然后在给插件编写注解,指定要拦截哪一个接口的哪些方法即可,记住,别忘了在配置文件中配置你编写的插件。

30. 多对一映射处理

如:查询员工信息以及员工所对应的部门信息

需要查询一对多、多对一的关系,需要在“一”的pojo中加入List<多>属性,在“多”的pojo中加入“一”。

也就是说,在Dept类中,要加入 private List<Emp> emps ;;在Emp类中,要加入 private Dept dept; 。然后给他们各自添加get、set方法,重写构造器和toString()

方法1:使用association处理映射关系

  • association:处理多对一的映射关系
  • property:需要处理多对一的映射关系的属性名
  • javaType:该属性的类型

    <resultMap id="empAndDeptResultMapTwo" type="Emp">
    <id property="eid" column="eid"></id>
    <result property="empName" column="emp_name"></result>
    <result property="age" column="age"></result>
    <result property="sex" column="sex"></result>
    <result property="email" column="email"></result>
    
    <association property="dept" javaType="Dept">
        <id property="did" column="did"></id>
        <result property="deptName" column="dept_name"></result>
    </association>
    
    </resultMap>
    <!--Emp getEmpAndDept(@Param("eid")Integer eid);-->
    <select id="getEmpAndDept" resultMap="empAndDeptResultMapTwo">
    select * from t_emp left join t_dept on t_emp.eid = t_dept.did where t_emp.eid = #{eid} </selec>

    方法2:分步查询(用的较多)

1. 查询员工信息

  • select:设置分布查询的sql的唯一标识(namespace.SQLId或mapper接口的全类名.方法名)
  • column:设置分步查询的条件,也就是下一步查谁,查员工的部门

<pre class="prettyprint hljs less">//EmpMapper接口里的方法/**

  • 通过分步查询,员工及所对应的部门信息
  • 分步查询第一步:查询员工信息 */

Emp getEmpAndDeptByStepOne(@Param("eid") Integer eid);</pre>

EmpMapper.xml

<resultMap id="empAndDeptByStepResultMap" type="Emp">
    <id property="eid" column="eid"></id>
    <result property="empName" column="emp_name"></result>
    <result property="age" column="age"></result>
    <result property="sex" column="sex"></result>
    <result property="email" column="email"></result>
    <!--
        select: 设置分步查询的sql的唯一标识(namespace.SQLId或mapper接口的全类名.方法名)
        column:分步查询的条件,也就是下一步查谁,查员工的部门
        fetchType: 当开启了全局的延迟记载后,可通过此属性手动控制延迟加载的效果
        fetchType:"lazy/eager" lazy表示延迟加载,eager表示立即加载
-->

    <association property="dept"
                 select="com.atguigu.mybatis.mapper.DeptMapper.getEmpAndDeptByStepTwo"
                 column="did"
                 fetchType="eager">
    </association>
</resultMap>
<!--Emp getEmpAndDeptByStepOne(@Param("eid") Integer eid);-->
<select id="getEmpAndDeptByStepOne" resultMap="empAndDeptByStepResultMap">
    select * from t_emp where eid = #{eid} </select>

2. 查询部门信息

//DeptMapper里的方法
/**
 * 通过分步查询,员工及所对应的部门信息
 * 分步查询第二步:通过did查询员工对应的部门信息
 */
Dept getEmpAndDeptByStepTwo(@Param("did") Integer did);</pre>

<pre class="prettyprint hljs dust"><!--此处的resultMap仅是处理字段和属性的映射关系-->
<resultMap id="EmpAndDeptByStepTwoResultMap" type="Dept">
    <id property="did" column="did"></id>
    <result property="deptName" column="dept_name"></result>
</resultMap>
<!--Dept getEmpAndDeptByStepTwo(@Param("did") Integer did);-->
<select id="getEmpAndDeptByStepTwo" resultMap="EmpAndDeptByStepTwoResultMap">
    select * from t_dept where did = #{did} </select>

31、延迟加载

  • 分步查询的优点:可以实现延迟加载,但是必须在核心配置文件中设置全局配置信息:

       lazyLoadingEnabled
    aggressiveLazyLoading
  • 此时就可以实现按需加载,获取的数据是什么,就只会执行相应的sql。此时可通过association和collection中的fetchType属性设置当前的分步查询是否使用延迟加载,fetchType=“lazy(延迟加载)|eager(立即加载)”

mybatis-config.xml

<settings>
    <!--开启延迟加载-->
    <setting name="lazyLoadingEnabled" value="true"/>
</settings>
@Test
public void getEmpAndDeptByStepOne() { 
    SqlSession sqlSession = SqlSessionUtils.getSqlSession();
    EmpMapper mapper = sqlSession.getMapper(EmpMapper.class);
    Emp emp = mapper.getEmpAndDeptByStepOne(1);
    System.out.println(emp.getEmpName());
}
  • 关闭延迟加载,两条SQL语句都运行了

  • 开启延迟加载,只运行获取emp的SQL语句

通过fetchType参数,可以手动控制延迟加载或立即加载,否则根据全局配置的属性决定是延迟加载还是立即加载。

@Test
public void getEmpAndDeptByStepOne() { 
    SqlSession sqlSession = SqlSessionUtils.getSqlSession();
    EmpMapper mapper = sqlSession.getMapper(EmpMapper.class);
    Emp emp = mapper.getEmpAndDeptByStepOne(1);
    System.out.println(emp.getEmpName());
    System.out.println("----------------");
    System.out.println(emp.getDept());
}