Mybatis

文章目录

  • 1.概述
  • 2.入门
    • 2.1.环境搭建
    • 2.2.全局配置文件
    • 2.3.测试类
    • 2.4.流程分析
    • 2.5. SqlSession
    • 2.6.作用域分析
  • 3. CRUD
    • 3.1.基于xml
      • 3.1.1. CRUD
      • 3.1.2.参数细节
      • 3.1.3.类与表名不一致
      • 3.1.4.参考 Mapper 配置文件
    • 3.2. SqlMapConfig.xml 配置文件
      • 3.2.1.主要配置内容
      • 3.2.2. properties(属性)
      • 3.2.3. typeAliases(类型别名)
      • 3.2.4. mappers (映射器)
      • 3.2.5.其它标签的简介
    • 3.3.基于注解
  • 4.连接池与事务深入
    • 4.1.连接池
      • 4.1.1. JNDI
    • 4.2.获取连接的过程分析
    • 4.3.事务
      • 4.3.1.事务回顾
      • 4.3.2 .Mybatis的事务控制
  • 5.动态 sql
    • 5.1. if 标签
    • 5.2. where 标签
    • 5.3. for each 标签
    • 5.4.@SelectProvider注解
  • 6.多表查询
    • 6.1.一对一
    • 6.2.一对多(多对一)
    • 6.3.多对多
    • 6.4.使用注解
  • 7.延迟加载
  • 8.缓存机制
  • 9.其它拓展
    • 9.1.通用Mapper
    • 9.2.分页插件
    • 9.3. Mybatis-Plus

1.概述

​ Mybatis 是一个优秀的基于 java 的持久层框架,它内部封装了 jdbc ,使开发者只需要关注 sql 语句本身,而不需要花费精力去处理加载驱动、创建连接、创建 statement 等繁杂的过程。

  • Mybatis 通过 xml 或注解的方式将要执行的各种 statement 配置起来,并通过java对象和 statement 中sql的动态参数进行映射生成最终执行的 sql 语句,最后由 Mybatis 框架执行 sql 并将结果映射为 java 对象并返回。

  • 采用ORM 思想解决了实体和数据库映射的问题,对 jdbc 进行了封装,屏蔽了jdbc api底层访问细节,使我们不用与 jdbc api打交道,就可以完成对数据库的持久化操作。

持久层技术解决方案

  • JDBC技术:

    • Connection
    • PreparedStatement
    • ResultSet
  • Spring的 JdbcTemplate:

    • Spring 中对 jdbc 的简单封装
  • Apache 的 DBUtils:

    • 它和 Spring 的 JdbcTemplate 很像,也是对 Jdbc 的简单封装

以上这些都不是框架,JDBC是规范,Spring 的 JdbcTemplate 和 Apache 的 DBUtils 都只是工具类

ORM

Object Relational Mappging 对象关系映射

简单的说:

  • 就是把数据库表和实体类及实体类的属性对应起来

  • 让我们可以操作实体类就实现操作数据库表。

2.入门

2.1.环境搭建

  1. 创建 maven 工程并导入坐标

  2. 创建实体类和 dao 的接口

  3. 创建 Mybatis 的主配置文件 :SqlMapConifg.xml

  4. 创建映射配置文件:IUserDao.xml

    • 这里可以使用@Select注解,并指定 sql ,便可以去掉该配置文件(使用注解就得去掉)
    • 同时需要在SqlMapConfig.xml中的 mapper 配置时,使用 class 属性指定dao接口的全限定类名。
    public interface IUserDao { 
     	@Select("select * from user") 
        List<User> findAll(); 
    }
    
     
    <mappers> 
        <mapper class="com.leyou.dao.IUserDao"/> 
    mappers>
    

Mapper 文件

以下内容为 IUserDao.xml

  1. 在 Mybatis 中它把持久层的操作接口名称和映射文件也叫做:Mapper。所以:IUserDao ,也可以写成IUserMapper
  2. Mybatis 的映射配置文件位置必须和 dao 接口的包结构相同
  3. 映射配置文件的 mapper 标签 namespace 属性的取值必须是 dao 接口的全限定类名
  4. 映射配置文件的操作配置(select),id 属性的取值必须是 dao 接口的方法名
  5. 当我们遵从以上几点之后,我们在开发中就无须再写 dao 的实现类
<mapper namespace="com.leyou.dao.IUserDao"> 
     
    <select id="findAll" resultType="com.leyou.domain.User"> 
   	 	select * from user select>
mapper>

2.2.全局配置文件




<configuration>
    
    <environments default="mysql">
        
        <environment id="mysql">
            
            <transactionManager type="JDBC">transactionManager>
            
            <dataSource type="POOLED">
                
                <property name="driver" value="com.mysql.jdbc.Driver"/>
                <property name="url" value="jdbc:mysql://localhost:3306/mybatis"/>
                <property name="username" value="root"/>
                <property name="password" value="1234"/>
            dataSource>
        environment>
    environments>

    
   
    <mappers>
        <mapper class="com.leyou.dao.IUserDao"/>
    mappers>
configuration>

2.3.测试类

程序编写的思路

  1. 读取配置文件 加载方式区别解惑
  2. 创建 SqlSessionFactory 工厂
  3. 创建 SqlSession
  4. 创建 Dao接口的代理对象
  5. 执行 Dao 中的方法
  6. 释放资源
 /**
     * 入门案例
     * @param args
     */
    public static void main(String[] args)throws Exception {
        //1.读取配置文件
        InputStream in = Resources.getResourceAsStream("SqlMapConfig.xml");
        //2.创建SqlSessionFactory工厂
        SqlSessionFactoryBuilder builder = new SqlSessionFactoryBuilder();
        SqlSessionFactory factory = builder.build(in);
        //3.使用工厂生产SqlSession对象
        SqlSession session = factory.openSession();
        //4.使用SqlSession创建Dao接口的代理对象
        IUserDao userDao = session.getMapper(IUserDao.class);
        //5.使用代理对象执行方法
        List<User> users = userDao.findAll();
        for(User user : users){
            System.out.println(user);
        }
        //6.释放资源
        session.close();
        in.close();
    }
}

2.4.流程分析

Mybatis_第1张图片

2.5. SqlSession

sqlSession 深入

SqlSession 的语句执行方法

  • Object selectOne(String statement, Object parameter)
  • List selectList(String statement, Object parameter)
  • int insert(String statement, Object parameter)
  • int update(String statement, Object parameter)
  • int delete(String statement, Object parameter)

注意

  • 一般情况下(方法重名了),这里的 statement 不能只传递方法名(如 findAll),需要前面带上 namespace,即 namespace.方法

selectOne 和selectList 的不同仅仅是 selectOne 必须返回一个对象。如果多余一个,或者没有返回(或返回了null),那么就会抛出异常。如果你不知道需要多少对象,使用 selectList 。如果你想检查一个对象是否存在,那么最好返回统计数(0或1)。

因为并不是所有语句都需要参数,这些方法都是有不同重载版本的,它们可以不需要参数对象。

2.6.作用域分析

参考

SqlSessionFactoryBuilder

​ 这个类可以被实例化、使用和丢弃,一旦创建了 SqlSessionFactory,就不再需要它了。因此 SqlSessionFactoryBuilder 实例的最佳范围是**方法范围(**也就是局部方法变量)。你可以重用 SqlSessionFactoryBuilder 来创建多个 SqlSessionFactory 实例,但是最好还是不要让其一直存在以保证所有的 XML 解析资源开放给更重要的事情。

SqlSessionFactory

SqlSessionFactory 一旦被创建就应该在应用的运行期间一直存在,没有任何理由对它进行清除或重建。使用 SqlSessionFactory 的最佳实践是在应用运行期间不要重复创建多次,多次重建 SqlSessionFactory 被视为一种代码“坏味道(bad smell)”。因此  SqlSessionFactory 的最佳范围是应用范围。有很多方法可以做到,最简单的就是使用单例模式或者静态单例模式

SqlSession

每个线程都应该有它自己的 SqlSession 实例。SqlSession 的实例不是线程安全的,因此是不能被共享的,所以它的最佳的范围是请求或方法范围。绝对不能将 SqlSession 实例的引用放在一个类的静态域,甚至一个类的实例变量也不行。也绝不能将 SqlSession 实例的引用放在任何类型的管理范围中,比如 Serlvet 架构中的HttpSession。如果你现在正在使用一种 Web 框架,要考虑 SqlSession 放在一个和 HTTP 请求对象相似的范围中。换句话说,每次收到的 HTTP 请求,就可以打开一个 SqlSession,返回一个响应,就关闭它。这个关闭操作是很重要的,你应该把这个关闭操作放到 finally 块中以确保每次都能执行关闭。

使用 MyBatis-Spring 之后,你不再需要直接使用 SqlSessionFactory,因为你的 bean 可以被注入一个线程安全的 SqlSession,它能基于 Spring 的事务配置来自动提交、回滚、关闭 session。官方文档

3. CRUD

3.1.基于xml

3.1.1. CRUD

CRUD

  • insert
  • delete
  • update
  • select

模糊查询

  • 方式一:select * from user where username like #{username}
    • 没有加入%来作为模糊查询的条件,{username}也只是一个占位符,所以 sql 语句显示为
  • 方式二:select * from user where username like '%${value}%'
    • 如果用模糊查询的这种写法,那么${value}的写法就是固定的,不能写成其它名字。

#{}${}的区别

  • #{}表示一个占位符号 通过#{}可以实现preparedStatement向占位符中设置值,自动进行 java 类型和 jdbc类型转换,#{}可以有效防止 sql 注入
    • #{}可以接收简单类型值或 pojo 属性值。 如果parameterType传输单个简单类型值,#{}括号中可以是value或其它名称。
  • ${}表示拼接 sql 串 通过${}可以将parameterType 传入的内容拼接在 sql 中且不进行 jdbc 类型转换
    • ${}可以接收简单类型值或pojo属性值,如果parameterType传输单个简单类型值,${}括号中只能是 value。

3.1.2.参数细节

以查询为例

 
    <select id="findById" parameterType="INT" resultMap="userMap">
        select * from user where id = #{uid}
    select>
  • id="findAll":sql 语句所要应用 在 Dao接口的 方法名
  • parameterType="INT"参数类型。传入一个类的对象,就写类的全名称(注册可使用别名)。大小写不敏感。(常使用包装类)
  • resultType="int":可以指定结果集的类型,它支持基本类型和实体类类型,规则同parameterType
    • 实体类中的属性名称 必须和查询语句中的列名保持一致,否则无法实现封装。
    • Mysql 在windows系统中不区分大小写
    • Linux 中就需要小心使用了
  • sql 语句中使用#{}字符来表示占位符,它用的是 ognl 表达式
  • resultMap="userMap"结果类型,建立查询的列名和实体类的属性名称不一致时建立对应关系
    • 在 select 标签中使用 resultMap 属性指定引用即可,这里便是一处引用。
    • 后面会详细介绍

获取新增用户 id 的返回值

​ 新增用户后,同时还要返回当前新增用户的id值,因为id是由数据库的自动增长来实现的,所以就相当于我们要在新增后将自动增长 auto_increment的值返回。

 
    <insert id="saveUser" parameterType="user">
        
        <selectKey keyProperty="userId" keyColumn="id" resultType="int" order="AFTER">
            select last_insert_id();
        selectKey>
        insert into user(username,address,sex,birthday)values(#{userName},#{userAddress},#{userSex},#{userBirthday});
    insert>
  • order:定义获取 id 的时间
    • BEFORE:插入前
    • AFTER:插入后

ognl

它是 apache 提供的一种表达式语言,全称是:Object Graphic Navigation Language 对象图导航语言

  • 语法格式就是使用 #{对象.对象}的方式
    • #{user.username}它会先去找 user 对象,然后在 user 对象中找到 username 属性,并调用getUsername()方法把值取出来。
    • 但是我们在parameterType属性上指定了实体类名称,所以可以省略user.而直接写username

3.1.3.类与表名不一致

方式一:使用别名

  
<select id="findAll" resultType="com.leyou.domain.User"> 
    select id as userId,username as userName,birthday as userBirthday, sex as userSex,address as userAddress from user 
select>

该方法效率最高,但是如果查询很多,开发起来不是很方便

方式二:resultMap

resultMap 标签可以建立查询的列名和实体类的属性名称不一致时建立对应关系。从而实现封装。

  • 在 select 标签中使用 resultMap 属性指定引用即可。
  • 同时 resultMap 可以实现将查询结果映射为复杂类型的 pojo,比如在查询结果映射对象中包括 pojo 和 list 实现一对一查询和一对多查询。
 
<resultMap type="com.leyou.domain.User" id="userMap"> 
    <id column="id" property="userId"/> 
    <result column="username" property="userName"/> 
    <result column="sex" property="userSex"/> 
resultMap>
  • id 标签:用于指定主键字段
  • result 标签:用于指定非主键字段
  • column 属性:用于指定数据库列名
  • property 属性:用于指定实体类属性名称

3.1.4.参考 Mapper 配置文件

Mapper 配置文件参考



<mapper namespace="com.leyou.dao.IUserDao">

    
    <resultMap id="userMap" type="com.leyou.domain.User">
        
        <id property="userId" column="id">id>
        
        <result property="userName" column="username">result>
        <result property="userAddress" column="address">result>
        <result property="userSex" column="sex">result>
        <result property="userBirthday" column="birthday">result>
    resultMap>

    
    <select id="findAll" resultMap="userMap">
        
        select * from user;
    select>

    
    <insert id="saveUser" parameterType="user">
        
        <selectKey keyProperty="userId" keyColumn="id" resultType="int" order="AFTER">
            select last_insert_id();
        selectKey>
        insert into user(username,address,sex,birthday)values(#{userName},#{userAddress},#{userSex},#{userBirthday});
    insert>

    
    <update id="updateUser" parameterType="USER">
        update user set username=#{userName},address=#{userAddress},sex=#{userAex},birthday=#{userBirthday} where id=#{userId}
    update>

    
    <delete id="deleteUser" parameterType="java.lang.Integer">
        delete from user where id = #{uid}
    delete>
    
    
    <select id="findById" parameterType="INT" resultMap="userMap">
        select * from user where id = #{uid}
    select>

    
    <select id="findByName" parameterType="string" resultMap="userMap">
          select * from user where username like #{name}
        
   select>

    
    <select id="findTotal" resultType="int">
        select count(id) from user;
    select>

    
    <select id="findUserByVo" parameterType="com.leyou.domain.QueryVo" resultMap="userMap">
        select * from user where username like #{user.username}
    select>
mapper>

3.2. SqlMapConfig.xml 配置文件

3.2.1.主要配置内容

-properties(属性)
	--property
-settings(全局配置参数)
	--setting
-typeAliases(类型别名)
	--typeAliase
	--package
-typeHandlers(类型处理器)
-objectFactory(对象工厂)
-plugins(插件)
-environments(环境集合属性对象)
	--environment(环境子属性对象)
		---transactionManager(事务管理)
		---dataSource(数据源)
-mappers(映射器)
	--mapper
	--package

3.2.2. properties(属性)

我们可以采用两种方式指定属性配置。

  1. 直接指定
<properties> 
    <property name="jdbc.driver" value="com.mysql.jdbc.Driver"/> 
    <property name="jdbc.url" value="jdbc:mysql://localhost:3306/eesy"/>
    <property name="jdbc.username" value="root"/> <
    property name="jdbc.password" value="1234"/> 
properties>
  1. 从配置文件中导入
jdbc.driver=com.mysql.jdbc.Driver 
jdbc.url=jdbc:mysql://localhost:3306/eesy 
jdbc.username=root 
jdbc.password=1234
  
<properties url="file:///D:/IdeaProjects/day02_eesy_01mybatisCRUD/src/main/resources/jdbcConfig.properties">
properties>

最后,配置 datasource

  
            <dataSource type="POOLED">
                <property name="driver" value="${jdbc.driver}">property>
                <property name="url" value="${jdbc.url}">property>
                <property name="username" value="${jdbc.username}">property>
                <property name="password" value="${jdbc.password}">property>
            dataSource>

3.2.3. typeAliases(类型别名)

  • :单个别名定义
    • type 属性指定的是实体类全限定类名
    • alias 属性指定别名,当指定了别名就再区分大小写
  • :批量给包 别名
  • 该包下的实体类都会注册别名,并且类名就是别名不再区分大小写

    <typeAliases>
        
        <typeAlias type="com.leyou.domain.User" alias="user">typeAlias>

        
        <package name="com.leyou.domain">package>
    typeAliases>

3.2.4. mappers (映射器)

resource

使用相对于类路径的资源 ,如:

<mappers>
<mapper resource="com/leyou/dao/IUserDao.xml" /> mappers>

class

使用mapper接口类路径,此种方法要求 mapper 接口名称和 mapper映射文件名称相同,且放在同一个目录中。

<mappers>
<mapper class="com.leyou.dao.UserDao"/> mappers>

package

注册指定包下的所有 mapper 接口,此种方法要求 mapper 接口名称和 mapper 映射文件名称相同,且放在同一个目录中。

  • 当指定了之后就不需要在写 mapper 以及 resource 或者 class 了
<mappers>
 	<package name="com.leyou.dao">package> mappers>     

3.2.5.其它标签的简介

设置(settings)

这是 MyBatis 中极为重要的调整设置,它们会改变 MyBatis 的运行时行为

  • cacheEnabled:全局性地开启或关闭所有映射器配置文件中已配置的任何缓存。默认 true
  • lazyLoadingEnabled:延迟加载的全局开关。当开启时,所有关联对象都会延迟加载。 特定关联关系中可通过设置 fetchType 属性来覆盖该项的开关状态。默认 false
  • aggressiveLazyLoading:开启时,任一方法的调用都会加载该对象的所有延迟加载属性。 否则,每个延迟加载属性会按需加载。默认 false

类型处理器(typeHandlers)

MyBatis 在设置预处理语句(PreparedStatement)中的参数或从结果集中取出一个值时, 都会用类型处理器将获取到的值以合适的方式转换成 Java 类型。

对象工厂(objectFactory)

每次 MyBatis 创建结果对象的新实例时,它都会使用一个对象工厂(ObjectFactory)实例来完成实例化工作。 默认的对象工厂需要做的仅仅是实例化目标类,要么通过默认无参构造方法,要么通过存在的参数映射来调用带有参数的构造方法。 如果想覆盖对象工厂的默认行为,可以通过创建自己的对象工厂来实现。

插件(plugins)

MyBatis 允许你在映射语句执行过程中的某一点进行拦截调用。默认情况下,MyBatis 允许使用插件来拦截的方法调用包括:

  • Executor (update, query, flushStatements, commit, rollback, getTransaction, close, isClosed)
  • ParameterHandler (getParameterObject, setParameters)
  • ResultSetHandler (handleResultSets, handleOutputParameters)
  • StatementHandler (prepare, parameterize, batch, update, query)

如果你想做的不仅仅是监控方法的调用,那么你最好相当了解要重写的方法的行为。 因为在试图修改或重写已有方法的行为时,很可能会破坏 MyBatis 的核心模块。 这些都是更底层的类和方法,所以使用插件的时候要特别当心。

通过 MyBatis 提供的强大机制,使用插件是非常简单的,只需实现 Interceptor 接口,并指定想要拦截的方法签名即可。

环境配置(environments)

MyBatis 可以配置成适应多种环境,这种机制有助于将 SQL 映射应用于多种数据库之中, 现实情况下有多种理由需要这么做。例如,开发、测试和生产环境需要有不同的配置;或者想在具有相同 Schema 的多个生产数据库中使用相同的 SQL 映射。

不过要记住:尽管可以配置多个环境,但每个 SqlSessionFactory 实例只能选择一种环境。

所以,如果你想连接两个数据库,就需要创建两个 SqlSessionFactory 实例,每个数据库对应一个。

事务管理器(transactionManager)

在 MyBatis 中有两种类型的事务管理器(也就是 type="[JDBC|MANAGED]"):

  • JDBC – 这个配置直接使用了 JDBC 的提交和回滚设施,它依赖从数据源获得的连接来管理事务作用域。
  • MANAGED – 这个配置几乎没做什么。它从不提交或回滚一个连接,而是让容器来管理事务的整个生命周期(比如 JEE 应用服务器的上下文)。 默认情况下它会关闭连接。然而一些容器并不希望连接被关闭,因此需要将 closeConnection 属性设置为 false 来阻止默认的关闭行为。

如果你正在使用 Spring + MyBatis,则没有必要配置事务管理器,因为 Spring 模块会使用自带的管理器来覆盖前面的配置。

数据源(dataSource)

dataSource 元素使用标准的 JDBC 数据源接口来配置 JDBC 连接对象的资源。

  • 有三种内建的数据源类型(也就是 type="[UNPOOLED|POOLED|JNDI]"
  • 后面在讲述连接池会再次介绍

3.3.基于注解

常用注解

  • @Insert:实现新增
  • @Update:实现更新
  • @Delete:实现删除
  • @Select:实现查询
  • @Result:实现结果集封装
  • @Results:可以与@Result一起使用,封装多个结果集
  • @ResultMap:实现引用@Results定义的封装
  • @One:实现一对一结果集封装
  • @Many:实现一对多结果集封装
  • @SelectProvider: 实现动态SQL映射
  • @CacheNamespace:实现注解二级缓存的使用

具体实现

使用注解开发,则不需要 dao.xml, 但仍需要 主配置文件 sqlMapConfig.xml

直接在接口上使用标签并编写 sql 语句

    /**
     * 保存用户
     * @param user
     */
    @Insert("insert into user(username,address,sex,birthday)values(#{username},#{address},#{sex},#{birthday})")
    void saveUser(User user);

    /**
     * 更新用户
     * @param user
     */
    @Update("update user set username=#{username},sex=#{sex},birthday=#{birthday},address=#{address} where id=#{id}")
    void updateUser(User user);

    /**
     * 删除用户
     * @param userId
     */
    @Delete("delete from user where id=#{id} ")
    void deleteUser(Integer userId);

    /**
     * 根据id查询用户
     * @param userId
     * @return
     */
    @Select("select * from user  where id=#{id} ")
    User findById(Integer userId);

    /**
     * 根据用户名称模糊查询
     * @param username
     * @return
     */
    @Select("select * from user where username like #{username} ")
    //@Select("select * from user where username like '%${value}%' ")
    List<User> findUserByName(String username);

    /**
     * 查询总用户数量
     * @return
     */
    @Select("select count(*) from user ")
    int findTotalUser();

主配置文件中需要配置


    <mappers>
        <mapper class="com.leyou.dao.IUserDao">mapper>
    mappers>

4.连接池与事务深入

4.1.连接池

我们在实际开发中都会使用连接池。因为它可以减少我们获取连接所消耗的时间。而在 Mybatis 中也有连接池技术,但是它采用的是自己的连接池技术。

池思想示意图

Mybatis_第2张图片

分类

  • POOLED常用。采用传统的 javax.sql.DataSource规范中的连接池,mybatis 中有针对规范的实现。
  • UNPOOLED:采用传统的获取连接的方式(不使用连接池),虽然也实现 Javax.sql.DataSource 接口,但是并没有使用池的思想。
  • JNDI:采用服务器提供的 JNDI 技术实现,来获取 DataSource 对象,不同的服务器所能拿到 DataSource 是不一样。

JNDI 注意

  • 如果不是web 或者 maven 的war工程,是不能使用的。
  • 我们使用的 tomcat 服务器,采用连接池就是 dbcp 连接池。

            <dataSource type="POOLED">
                <property name="driver" value="${jdbc.driver}">property>
                <property name="url" value="${jdbc.url}">property>
                <property name="username" value="${jdbc.username}">property>
                <property name="password" value="${jdbc.password}">property>
            dataSource>

4.1.1. JNDI

​ JNDI:Java Naming and Directory Interface。是SUN公司推出的一套规范,属于 JavaEE 技术之一。目的是模仿 windows 系统中的注册表。百科

​ tomcat 已经集成该服务(内置并默认使用DBCP连接池),简单来说就是键值对的 mapping,而且在 tomcat 服务器启动的首页 configuration 中就已经有完成的示例代码。要想使用 tomcat 的 JNDI 服务,只需要导入相关的 jar 包,建立所需的配置文件,采用 JDK 的命名服务 API 根据配置名称即可获得相应的服务。

建议参考

比较

没有JNDI的做法:
程序员开发时,知道要开发访问MySQL数据库的应用,于是将一个对 MySQL JDBC 驱动程序类的引用进行了编码,并通过使用适当的 JDBC URL 连接到数据库。

用了JNDI之后的做法:
首先,在在 J2EE 容器中配置 JNDI 参数,定义一个数据源,也就是 JDBC 引用参数,给这个数据源设置一个名称;然后,在程序中,通过数据源名称引用数据源从而访问后台数据库。

4.2.获取连接的过程分析

配置

在 Mybatis 的SqlMapConfig.xml配置文件中,通过来实现 Mybatis 中连接池的配置。

MyBatis在初始化时,根据的 type 属性来创建相应类型的的数据源 DataSource。

  • type=”POOLED”:会创建 PooledDataSource 实例
  • type=”UNPOOLED” :会创建 UnpooledDataSource 实例
  • type=”JNDI”:会从 JNDI 服务上查找 DataSource 实例,然后返回使用

DataSource 的存取

  • MyBatis 是通过工厂模式来创建数据源 DataSource 对象的,

  • MyBatis定义了抽象的工厂接口:org.apache.ibatis.datasource.DataSourceFactory,通过其getDataSource()方法返回数据源DataSource。

过程

当我们需要创建 SqlSession 对象并需要执行 SQL 语句时,这时候 MyBatis才会去调用 dataSource对象来创建java.sql.Connection对象。也就是说,java.sql.Connection对象的创建一直延迟到执行 SQL 语句的时候。

@Test public void testSql() throws Exception { 
    InputStream in = Resources.getResourceAsStream("SqlMapConfig.xml"); 
    SqlSessionFactory factory = new SqlSessionFactoryBuilder().build(in); 
    SqlSession sqlSession = factory.openSession();
    List<User> list = sqlSession.selectList("findUserById",41); 
    System.out.println(list.size()); }

只有当第4句sqlSession.selectList("findUserById"),才会触发 MyBatis 在底层执行下面这个方法来创建java.sql.Connection对象。

我们可以通过断点调试,在PooledDataSource中找到如下popConnection()方法

Mybatis_第3张图片

简而言之

只有在我们执行 sql 语句时,才会去获取并打开连接。

4.3.事务

4.3.1.事务回顾

什么是事务?

如果一个包含多个步骤的业务操作,被事务管理,那么这些操作要么同时成功,要么同时失败。

事务的四大特性 ACID

  • 原子性(Atomicity):是不可分割的最小操作单位,要么同时成功,要么同时失败。
  • 持久性(Consistency):当事务提交或回滚后,数据库会持久化的保存数据。
  • 隔离性(Isolation):多个事务之间。相互独立。
  • 一致性(Durability):事务操作前后,数据总量不变

隔离性会产生的3个问题

  1. 脏读(Dirty read):一个事务,读取到另一个事务中没有提交的数据
  2. 不可重复读/虚读(Unrepeatableread):在同一个事务中,两次读取到的数据不一样。
  3. 幻读(Phantom read):一个事务操作(DML)数据表中所有记录,另一个事务添加了一条数据,则第一个事务查询不到自己的修改。

不可重复度和幻读区别:

  • 不可重复读的重点是修改,(同样的条件, 你读取过的数据, 再次读取出来发现不一样了)
  • 幻读的重点在于新增或者删除。(同样的条件, 第1次和第2次读出来的记录数不一样)

解决办法:四种隔离级别

  1. read uncommitted:读未提交
    • 产生的问题:脏读、不可重复读、幻读
  2. read committed:读已提交 (Oracle)
    • 产生的问题:不可重复读、幻读
  3. repeatable read:可重复读 (MySQL默认)
    • 产生的问题:幻读
    • 解决虚读问题,自己提交才会改变数据
  4. serializable:串行化
    • 可以解决所有的问题
    • 当前事务会等待另一个事务提交,才会生效修改,效率低。

4.3.2 .Mybatis的事务控制

实现方式

通过 sqlsession 对象的 commit 方法和 rollback 方法实现事务的提交和回滚。

事务提交方式

  @Before//用于在测试方法执行之前执行
    public void init()throws Exception{
        //1.读取配置文件,生成字节输入流
        in = Resources.getResourceAsStream("SqlMapConfig.xml");
        //2.获取SqlSessionFactory
        SqlSessionFactory factory = new SqlSessionFactoryBuilder().build(in);
        //3.获取SqlSession对象
        sqlSession = factory.openSession();
        //4.获取dao的代理对象
        userDao = sqlSession.getMapper(IUserDao.class);
    }
  • 在 JDBC 中,,通过setAutoCommit()方法就可以调整
  • Mybatis 中,本质上就是调用 JDBC 的 setAutoCommit()来实现事务控制。
    • 设置session = factory.openSession(true);来自动提交
    • 无参时,默认调用connection.setAutoCommit(false)所以需要手动提交

5.动态 sql

5.1. if 标签

我们根据实体类的不同取值,使用不同的 SQL 语句来进行查询。

  • 标签的 test 属性中写的是对象的属性名,如果是包装类的对象要使用 OGNL 表达式的写法。
  • where 1=1 的作用:因 where 后的筛选条件是不确定的(0或n),该式子,很好地解决了后续条件的拼接问题。
<select id="findUserByCondition" resultMap="userMap" parameterType="user">
        select * from user where 1=1
        <if test="userName != null">
          and username = #{userName}
        if>
        <if test="userSex != null">
            and sex = #{userSex}
        if>
select>

5.2. where 标签

为了简化上面where 1=1的条件拼装,我们可以采用标签来简化开发。

<select id="findUserByCondition" resultMap="userMap" parameterType="user">
        select * from user
        <where>
            <if test="userName != null">
                and username = #{userName}
            if>
            <if test="userSex != null">
                and sex = #{userSex}
            if>
        where>
    select>

5.3. for each 标签

SQL语句: select 字段from user where id in (?)

标签用于遍历集合,它的属性:

  • collection: 代表要遍历的集合元素,注意编写时不要写#{}
  • open: 代表语句的开始部分
  • close代表结束部分
  • item: 代表遍历集合的每个元素,生成的变量名
  • sperator: 代表分隔符

抽取重复代码

  • 可将重复的 sql 提取出来,使用时用 include 引用即可,最终达到 sql 重用的目的
   
    <sql id="defaultUser">
        select * from user
    sql>

    <select id="findUserInIds" resultMap="userMap" parameterType="queryvo">
        <include refid="defaultUser">include>
        <where>
            <if test="ids != null and ids.size()>0">
                <foreach collection="ids" open="and id in (" close=")" item="uid" separator=",">
                    #{uid}
                foreach>
            if>
        where>
    select>

5.4.@SelectProvider注解

@SelectProvider: 实现动态SQL映射。参考

简易示例

SelectSqlProvider:

public class SelectSqlProvider {

    public String selectByUserId(Long id) {
        StringBuffer buffer = new StringBuffer();
        buffer.append("SELECT * FROM user where ");
        buffer.append("id = ").append(id).append(";");
        return buffer.toString();
    }
}

UserMapper:

@ResultMap("BaseResultMap")
@SelectProvider(type = SelectSqlProvider.class, method = "selectByUserId")
User getUserByUserId(long id);

这一个方法在 xml 中没有对应的 sql ,在该方法上也没有 @Select 注解修饰,只有 @SelectProvider 注解,@SelectProvider 中两个属性,type 为提供 sql 的 class 类,method 为指定方法。

6.多表查询

多表之间的关系分类

6.1.一对一

  • 如:用户和账户
  • 分析:一个账户只能属于一个用户

实现方式:

  • 定义专门的 po 类(AccountUser)作为输出类型,其中定义了 sql 查询结果集所有的字段。(编写一个子类方式)

    • 在定义 AccountUser 类时可以继承 User 类(不常用)
    • 这里不需要使用 resultMap,查询时直接使用 resultType
  • 使用 resultMap ,定义专门的 resultMap 用于映射一对一查询结果。(从表实体类中引用一个主表实体类对象)

    • 在 Account 类中直接加入一个User类的对象(常用方式)

说明

  • 使用 association建立一对一(多对一)关系映射
  • javaType 定义封装到的对象

    <resultMap id="accountUserMap" type="account">
        <id property="id" column="aid">id>
        <result property="uid" column="uid">result>
        <result property="money" column="money">result>
        
        <association property="user" column="uid" javaType="user">
            <id property="id" column="id">id>
            <result column="username" property="username">result>
            <result column="address" property="address">result>
            <result column="sex" property="sex">result>
            <result column="birthday" property="birthday">result>
        association>
    resultMap>

	
    <select id="findAll" resultMap="accountUserMap">
        select u.*,a.id as aid,a.uid,a.money from account a , user u where u.id = a.uid;
    select>

    
    <select id="findAllAccount" resultType="accountuser">
        select a.*,u.username,u.address from account a , user u where u.id = a.uid;
    select>

6.2.一对多(多对一)

  • 如:用户和账户
  • 分析:一个用户可以有多个账户,多个账户也可以属于同一个用户

实现方式:

  • 建立两张表:用户表,账户表;让用户表和账户表之间具备一对多的关系:需要使用外键在账户表中添加

  • 建立两个实体类:用户实体类和账户实体类;让用户和账户的实体类能体现出来一对多的关系

    • 具体为:定义sql 时 使用左外连接查询,User类加入List

说明

collection 部分定义了用户关联的账户信息。表示关联查询结果集一对多的关系使用

  • property="accounts": 关联查询的结果集存储在 User 对象的上哪个属性
  • ofType="account": 指定关联查询的结果集中的对象类型即 List 中的对象类型。此处可以使用别名,也可以使用全限定名。
 
    <resultMap id="userAccountMap" type="user">
        <id property="id" column="id">id>
        <result property="username" column="username">result>
        <result property="address" column="address">result>
        <result property="sex" column="sex">result>
        <result property="birthday" column="birthday">result>
        
        <collection property="accounts" ofType="account">
            <id column="aid" property="id">id>
            <result column="uid" property="uid">result>
            <result column="money" property="money">result>
        collection>
    resultMap>

    
    <select id="findAll" resultMap="userAccountMap">
        select * from user u left outer join account a on u.id = a.uid
    select>

    
    <select id="findById" parameterType="INT" resultType="user">
        select * from user where id = #{uid}
    select>

6.3.多对多

  • 如:用户和角色
  • 分析:一个用户可以有多个角色,一个角色可以赋予多个用户

实现方式:

  • 建立两张表:用户表,角色表;让用户表和角色表具有多对多的关系。需要使用中间表,中间表中包含各自的主键,在中间表中是外键。
  • 建立两个实体类:用户实体类和角色实体类;让用户和角色的实体类能体现出来多对多的关系;各自包含对方一个集合引用
  • 就是两个 一对多,只需要注意查询时 sql 语句
 
    <resultMap id="userMap" type="user">
        <id property="id" column="id">id>
        <result property="username" column="username">result>
        <result property="address" column="address">result>
        <result property="sex" column="sex">result>
        <result property="birthday" column="birthday">result>
        
        <collection property="roles" ofType="role">
            <id property="roleId" column="rid">id>
            <result property="roleName" column="role_name">result>
            <result property="roleDesc" column="role_desc">result>
        collection>
    resultMap>

    
    <select id="findAll" resultMap="userMap">
        select u.*,r.id as rid,r.role_name,r.role_desc from user u
         left outer join user_role ur  on u.id = ur.uid
         left outer join role r on r.id = ur.rid
    select>


 
    <resultMap id="roleMap" type="role">
        <id property="roleId" column="rid">id>
        <result property="roleName" column="role_name">result>
        <result property="roleDesc" column="role_desc">result>
        <collection property="users" ofType="user">
            <id column="id" property="id">id>
            <result column="username" property="username">result>
            <result column="address" property="address">result>
            <result column="sex" property="sex">result>
            <result column="birthday" property="birthday">result>
        collection>
    resultMap>

    
    <select id="findAll" resultMap="roleMap">
       select u.*,r.id as rid,r.role_name,r.role_desc from role r
        left outer join user_role ur  on r.id = ur.rid
        left outer join user u on u.id = ur.uid
    select>

6.4.使用注解

实现复杂关系映射之前我们可以在映射文件中通过配置来实现,在使用注解开发时我们需要借助@Results注解@Result注解@One注解@Many注解

@Results 注解

代替的是标签,该注解中可以使用单个@Result 注解,也可以使用@Result 集合

  • 配置 id="userMap",供其它接口引用

@Resutl 注解

代替了标签和标签

  • id 是否是主键字段
  • column 数据库的列名
  • property 需要装配的属性名
  • one 需要使用的@One 注解(@Result(one=@One)()))
  • many 需要使用的@Many 注解(@Result(many=@many)()))

@ResultMap注解

实现引用 @Results定义的封装,

  • @ResultMap("userMap"),这是省略写法(单个的情况下)
  • 可以引用多个 results 的 id ,@ResultMap(value={"userMap","userMap2"})
 /**
     * 查询所有用户
     * @return
     */
    @Select("select * from user")
    @Results(id="userMap",value={
            @Result(id=true,column = "id",property = "userId"),
            @Result(column = "username",property = "userName"),
            @Result(column = "address",property = "userAddress"),
            @Result(column = "sex",property = "userSex"),
            @Result(column = "birthday",property = "userBirthday"),
            @Result(property = "accounts",column = "id",
                    many = @Many(select = "com.leyou.dao.IAccountDao.findAccountByUid",
                                fetchType = FetchType.LAZY))
    })
    List<User> findAll();

    /**
     * 根据id查询用户
     * @param userId
     * @return
     */
    @Select("select * from user  where id=#{id} ")
    @ResultMap("userMap")
    User findById(Integer userId);

@One 注解(一对一)

代替了标签,是多表查询的关键,在注解中用来指定子查询返回单一对象。

  • select 指定用来多表查询的 sqlmapper
  • fetchType 会覆盖全局的配置参数 lazyLoadingEnabled。如果要延迟加载都设置为LAZY 的值
 @Result(property = "user",column = "uid",one=@One(select="com.leyou.dao.IUserDao.findById",fetchType= FetchType.EAGER)

@Many 注解(多对一)

代替了标签,是是多表查询的关键,在注解中用来指定子查询返回对象集合。

  • 聚集元素用来处理“一对多”的关系。需要指定映射的 Java 实体类的属性,属性的 javaType(一般为 ArrayList)但是注解中可以不定义;
@Result(property = "accounts",column = "id",
                    many = @Many(select = "com.leyou.dao.IAccountDao.findAccountByUid",
                                fetchType = FetchType.LAZY)

7.延迟加载

延迟加载:就是在需要用到数据时才进行加载,不需要用到数据时就不加载数据。延迟加载也称懒加载.
好处:先从单表查询,需要时再从关联表去关联查询,大大提高数据库性能,因为查询单表要比关联查询多张表速
度要快。

坏处:

因为只有当需要用到数据时,才会进行数据库查询,这样在大批量数据查询时,因为查询工作也要消耗
时间,所以可能造成用户等待时间变长,造成用户体验下降

使用要求

关联对象的查询与主加载对象的查询必须是分别进行的select语句不能是使用多表连接所进行的select查询。因为,多表连接查询,实质是对一张表的查询,对由多个表连接后形成的一张表的查询。会一次性将多张表的所有信息查询出来。

实现原理

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

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

延迟加载的实现

修改配置文件

实现原理:在需要查询时,调用对方配置文件 中的配置来实现查询功能,所以进行如下配置:

  • select="com.leyou.dao.IAccountDao.findAccountByUid",用于指定查询 account 列表的 sql 语句(账户的 dao 全限定类名加上方法名称)
  • column="id">,用于指定 select 属性的 sql 语句的参数来源

修改 Dao.xml

   
<association property="user" column="uid" javaType="user" select="com.leyou.dao.IUserDao.findById">association>
  
   
        <collection property="accounts" ofType="account" select="com.leyou.dao.IAccountDao.findAccountByUid" column="id">collection>

Mybatis 中默认是不开启延迟加载的,我们需要在 Mybatis 的主配置文件 SqlMapConfig.xml文件中添加延迟加载的配置。

 
    <settings>
        
        <setting name="lazyLoadingEnabled" value="true"/>
        <setting name="aggressiveLazyLoading" value="false">setting>
    settings>

总结

  • 延迟加载:在真正使用数据时才发起查询,不用的时候不查询。按需加载(懒加载)

  • 立即加载:不管用不用,只要一调用方法,马上发起查询。

在对应的四种表关系中:一对多,多对一,一对一,多对多

  • 一对多,多对多:通常情况下我们都是采用延迟加载

  • 多对一,一对一:通常情况下我们都是采用立即加载

8.缓存机制

什么是缓存?

  • 存在于内存中的临时数据。
  • 减少和数据库的交互次数,提高执行效率。

什么样的数据能使用缓存,什么样的数据不能使用?

适用于缓存:

  • 经常查询并且不经常改变的。
  • 数据的正确与否对最终结果影响不大的。

不适用于缓存:

  • 经常改变的数据
  • 数据的正确与否对最终结果影响很大的。
  • 例如:商品的库存,银行的汇率,股市的牌价。

什么是一级缓存?

它指的是Mybatis中 **SqlSession 对象的缓存。**当我们执行查询之后,查询的结果会同时存入到 SqlSession 为我们提供一块区域中。该区域的结构是一个Map。当我们再次查询同样的数据,mybatis 会先去 sqlsession 中查询是否有,有的话直接拿出来用。

特点:

  • 当 SqlSession 对象消失时,mybatis 的一级缓存也就消失了。
  • 只要 SqlSession 没有 flush 或 close,它就存在。
  • 当调用 SqlSession 的修改,添加,删除,commit(),close() 等方法时,就会清空一级缓存。

什么是二级缓存?

它指的是 Mybatis 中 SqlSessionFactory 对象的缓存。由同一个 SqlSessionFactory 对象创建的 SqlSession 共享其缓存。建议参考

特点:

  • 二级缓存是 mapper映射级别的缓存,多个SqlSession去操作同一个Mapper映射的sql语句,多个 SqlSession可以共用二级缓存,二级缓存是跨 SqlSession 的。
  • 二级缓存 存放的内容为数据,不是对象,使用时会new一个新的 user对象,所以对象地址会变
  • 同样的,增删改会清空缓存
  • 缓存是以namespace为单位的,不同namespace下的操作互不影响。
  • 多表操作一定不要使用二级缓存,因为多表操作进行更新操作,一定会产生脏数据。

使用

SqlMapConfig.xml 文件开启二级缓存,默认为 true ,也可以不配置

 <settings>
        <setting name="cacheEnabled" value="true"/>
    settings>

配置相关的 Mapper 映射文件

  • 标签表示当前这个 mapper 映射将使用二级缓存,区分的标准就看 mapper 的 namespace 值。

  • UserDao.xml映射文件中的