MyBatis基础

原理

使用端

  • 引入框架依赖
  • 提供两部分配置信息:数据库配置信息 和 sql 配置信息
    • 数据库配置信息:mybatis-config.xml。不仅仅是存放数据库配置信息,还指定了 sql 配置信息文件路径
    • sql 配置信息:mapper.xml(对应 dao 层的 xml)

框架底层

准备工作(项目启动时完成)

  • 第一步:读取配置信息(因为 mybatis-config.xml 还指定了 mapper.xml 路径,所以一次读取所有的配置文件)得到一个输入流
  • 第二步:创建 Configuration
    • 包含所有 mybatis 的配置,比如数据库,sql配置等,还会维护 mppedStatements,这是个 map,key:mapper全限定名+方法名(比如 com.study.UserMapper.selectById),value:MppedStatement
  • 第三步:创建 SqlSession
    • 根据上一步得到的 Configuration 创建,方法:SqlSessionFactory build(Configuration config)
  • 第四步:为 mapper 生成代理对象并保存
    • 通过 JDK 代理生成 mapper 接口的对象,保存在 MapperRegistry.knownMappers 中(mybatis-config.xml 中通过 mappers 标签指定所有 mapper.xml,每个 mapper.xml 有 namespace 属性指定 mapper 接口全限定名)

执行 mapper 方法

  • 第一步:根据 SqlSession 获取 Mapper 接口代理对象
    • 调用 SqlSession.getMapper(Mapper.class)。其实获取的就是 MapperRegistry.knownMappers 中的对象,没获取到会抛出异常
  • 第二步:执行 mapper 方法
    • 执行的方法分为两种情况,一个是 Object 类的方法就直接执行,比如 toString、equals 等
    • 如果是不是 Objcet 类的方法,会继续往下走
  • 第三步:主要是代理对象的方法和 MppedStatement 关系
    • 方法名和 mapper 接口全限定名 是 MppedStatement 的 key,而 MppedStatement 里面有代理对象方法 MapperMethod ,这个方法里面有 sql 信息和 返回信息

源码

  • 创建 SqlSessionFactory:SqlSessionFactoryBuilder.build(InputStream inputStream)
    // 方法1,使用配置文件的流创建创建 SqlSessionFactory
    public SqlSessionFactory build(InputStream inputStream) {
        // 调用方法 2
        return build(inputStream, null, null);
    }
    // 方法2
    public SqlSessionFactory build(InputStream inputStream, String environment, Properties properties) {
        try {
            // 解析配置文件
            XMLConfigBuilder parser = new XMLConfigBuilder(inputStream, environment, properties);
            // 调用方法3。parser.parse() 返回 Configuration(mybatis-config.xml里面的每个标签和值设置到 Configuration 对象)
            return build(parser.parse());
        } catch (Exception e) {
            throw ExceptionFactory.wrapException("Error building SqlSession.", e);
        } finally {
            ErrorContext.instance().reset();
            try {
                inputStream.close();
            } catch (IOException e) {
                // Intentionally ignore. Prefer previous error.
            }
        }
    }
    // 方法3,最后是这里返回 SqlSessionFactory(如果是springboot,跳过方法1和方法2,直接调用这里,原因也很简单,因为 springboot 压根就没有 mybatis 的配置文件嘛)
    public SqlSessionFactory build(Configuration config) {
        return new DefaultSqlSessionFactory(config);
    }
    
  • 创建 SqlSession 流程:DefaultSqlSessionFactory.openSession()
    // 自动提交
    public SqlSession openSession() {
        // 参数就是解析配置文件后得到的 Configuration 对象里的 defaultExecutorType 属性
        return openSessionFromDataSource(configuration.getDefaultExecutorType(), null, false);
    }
    // 不会自动提交
    public SqlSession openSession(boolean autoCommit) {
    return openSessionFromDataSource(configuration.getDefaultExecutorType(), null, autoCommit);
    }
    /**
     + ExecutorType 指定Executor的类型,分为三种:SIMPLE, REUSE, BATCH,默认使用的是SIMPLE
     + TransactionIsolationLevel 指定事务隔离级别,使用null,则表示使用数据库默认的事务隔离界别
     + autoCommit 是否自动提交,传过来的参数为false,表示不自动提交
     */
    private SqlSession openSessionFromDataSource(ExecutorType execType, TransactionIsolationLevel level, boolean autoCommit) {
        Transaction tx = null;
        try {
            final Environment environment = configuration.getEnvironment();
            final TransactionFactory transactionFactory = getTransactionFactoryFromEnvironment(environment);
            tx = transactionFactory.newTransaction(environment.getDataSource(), level, autoCommit);
            // 创建Executor,即执行器。它是真正用来Java和数据库交互操作的类
            final Executor executor = configuration.newExecutor(tx, execType);
            // 创建 DefaultSqlSession
            return new DefaultSqlSession(configuration, executor, autoCommit);
        } catch (Exception e) {
            closeTransaction(tx); // may have fetched a connection so lets call close()
            throw ExceptionFactory.wrapException("Error opening session.  Cause: " + e, e);
        } finally {
            ErrorContext.instance().reset();
        }
    }
    
    
  • 创建执行器:Configuration.newExecutor(Transaction transaction, ExecutorType executorType)
    // 创建执行器(这个方法属于 Configuration 类,所以这个类里面能拿到所有的配置信息,包括缓存、驼峰、事务隔离等等)
    public Executor newExecutor(Transaction transaction, ExecutorType executorType) {
        executorType = executorType == null ? defaultExecutorType : executorType;
        executorType = executorType == null ? ExecutorType.SIMPLE : executorType;
        Executor executor;
        if (ExecutorType.BATCH == executorType) {
            executor = new BatchExecutor(this, transaction);
        } else if (ExecutorType.REUSE == executorType) {
            executor = new ReuseExecutor(this, transaction);
        } else {
            // ExecutorType 默认是 SIMPLE,所以默认的执行器是 SimpleExecutor
            executor = new SimpleExecutor(this, transaction);
        }
        // 是否开启缓存(二级缓存)
        if (cacheEnabled) {
            // 如果开启了,使用装饰器模式添加二级缓存功能
            executor = new CachingExecutor(executor);
        }
        executor = (Executor) interceptorChain.pluginAll(executor);
        return executor;
    }
    

应用

基于 sqlSession

// 常用方法
<T> T selectOne(String statement, Object parameter)
<E> List<E> selectList(String statement, Object parameter)
int insert(String statement, Object parameter)
int update(String statement, Object parameter)
int delete(String statement, Object parameter)

void commit()
void rollback()

示例

//加载核⼼配置⽂件
InputStream resourceAsStream = Resources.getResourceAsStream("myabtis-config.xml");
//获得sqlSession⼯⼚对象
SqlSessionFactory sqlSessionFactory = new SqlSessionFactoryBuilder().build(resourceAsStream);
//获得sqlSession对象
SqlSession sqlSession = sqlSessionFactory.openSession();
//sqlSession 的第一个参数是 MppedStatement ID 即 接口全限定名+方法名
List<User> userList = sqlSession.selectList("com.study.mapper.userMapper.findAll");
//打印结果
System.out.println(userList);
//释放资源
sqlSession.close();

基于 mapper

通过实现类实现

// mapper 接口
public interface UserDao {
    List<User> findAll() throws IOException;
}
// mapper 实现类
public class UserDaoImpl implements UserDao {
    public List<User> findAll() throws IOException {
        InputStream resourceAsStream = Resources.getResourceAsStream("myabtis-config.xml");
        SqlSessionFactory sqlSessionFactory = new SqlSessionFactoryBuilder().build(resourceAsStream);
        SqlSession sqlSession = sqlSessionFactory.openSession();
        List<User> userList = sqlSession.selectList("userMapper.findAll");
        sqlSession.close();
        return userList;
    }
}
// 测试
@Test
public void testTraditionDao() throws IOException {
    UserDao userDao = new UserDaoImpl();
    List<User> all = userDao.findAll();
    System.out.println(all);
}

基于代理(主流方式)

  • Mapper.xml ⽂件中的 namespace 与 mapper 接⼝的全限定名相同
  • Mapper 接⼝⽅法名和 Mapper.xml 中定义的每个 statement 的 id 相同
  • Mapper 接⼝⽅法的输⼊参数类型和 mapper.xml 中定义的每个sql的parameterType的类型相同
  • Mapper 接⼝⽅法的输出参数类型和 mapper.xml 中定义的每个 sql 的 resultType 的类型相同
// mapper 接口
public interface UserDao {
    List<User> findById(int id) throws IOException;
}
<!-- mapper.xml -->
<mapper namespace="com.stydu.mapper.UserMapper">
    <select id="findById" parameterType="int" resoult="com.stydu.entity.User">
        SELECT * FROM T_USER WHERE ID = #{id}
    </select>
</mapper>
// 测试
@Test
public void testProxyDao() throws IOException {
    InputStream resourceAsStream = Resources.getResourceAsStream("mybatis-config.xml");
    SqlSessionFactory sqlSessionFactory = new SqlSessionFactoryBuilder().build(resourceAsStream);
    SqlSession sqlSession = sqlSessionFactory.openSession();
    // 获得MyBatis框架⽣成的 UserMapper 接⼝的实现类
    UserMapper userMapper = sqlSession.getMapper(UserMapper.class);
    User user = userMapper.findById(1);
    System.out.println(user);
    sqlSession.close();
}

动态 sql

where 条件

有的时候 where 1=1 会影像性能,所以最好使用 标签

<select id="findByCondition" parameterType="user" resultType="user">
select * from User
    <where>
        <if test="id!=0">
            and id=#{id}
        </if>
        <if test="username!=null">
            and username=#{username}
        </if>
    </where>
</select>

循环

… … …
// 获得 MyBatis 框架⽣成的 UserMapper 接⼝的实现类
UserMapper userMapper = sqlSession.getMapper(UserMapper.class);
int[] ids = new int[]{2,5};
List<User> userList = userMapper.findByIdsAndName(ids, "Milk");
System.out.println(userList);
… … …
<select id="findByIds" resultType="user">
    select * from User
    <where>
	<!-- collection 的值和 mapper 接口指定的参数名相同 -->
        <foreach collection="ids" open="id in(" close=")" item="id" separator=",">
            #{id}
        </foreach>
    </where>
    <if test="name != '' and name != null">
        and name = #{name}
    </if>
</select>

片段抽取

<!--抽取sql⽚段简化编写-->
<sql id="selectUser"> select * from User</sql>

<select id="findById" parameterType="int" resultType="user">
    <include refid="selectUser" /> where id=#{id}
</select>

级联查询

一对一

一个订单对应一个用户

// 订单
public class Order {
    private int id;
    private Date ordertime;
    private double total;
    //代表当前订单从属于哪⼀个客户
    private User user;
}
}
// 用户
public class User {
    private int id;
    private String username;
    private String password;
    private Date birthday;
}
<mapper namespace="com.lagou.mapper.OrderMapper">
    <resultMap id="orderMap" type="com.lagou.domain.Order">
        <!-- 主表,订单表 -->
        <result property="id" column="id"></result>
        <result property="ordertime" column="ordertime"></result>
        <result property="total" column="total"></result>
        <!-- 从表,用户表 -->
        <association property="user" javaType="com.lagou.domain.User">
            <!-- 如果两个表的主键都叫 ID,要指定别名 -->
            <result column="uid" property="id"></result>
            <result column="username" property="username"></result>
            <result column="password" property="password"></result>
            <result column="birthday" property="birthday"></result>
        </association>
    </resultMap>

    <select id="findAll" resultMap="orderMap">
        <!-- select * from t_order o, t_user u where o.uid=u.id -->
        <!-- 上面使用交叉连接,没有处理别名,我比较喜欢左连接 -->
        select o.*,u.id as uid, u.username, u.password, u.birthday from t_order o left join t_user u on o.uid = u.id 
    </select>
</mapper>

一对多

// 订单表
public class Order {
    private int id;
    private Date ordertime;
    private double total;
}
// 用户表
public class User {
    private int id;
    private String username;
    private String password;
    private Date birthday;
    //代表当前⽤户具备哪些订单
    private List<Order> orderList;
}
<mapper namespace="com.lagou.mapper.UserMapper">
    <!-- 主表,用户 -->
    <resultMap id="userMap" type="com.lagou.domain.User">
        <result column="id" property="id"></result>
        <result column="username" property="username"></result>
        <result column="password" property="password"></result>
        <result column="birthday" property="birthday"></result>
        <!-- 从表,订单 -->
        <collection property="orderList" ofType="com.lagou.domain.Order">
            <result column="oid" property="id"></result>
            <result column="ordertime" property="ordertime"></result>
            <result column="total" property="total"></result>
        </collection>
    </resultMap>

    <select id="findAll" resultMap="userMap">
        select u.*,o.id as oid, o.ordertime, o.total from t_user left join t_order on u.id = o.uid 
    </select>
</mapper>

多对多

同一对多

缓存

一级缓存

是 SqlSession 级别的,对于查询,先查询缓存,如果缓存未查到就查数据库  
默认开启,如果需要关闭,有两种方式,一种是单独指定某个查询方法,第二种是全局设定
增、删、改会清空缓存
<!-- flushCache = true,关闭一级缓存,当前方法生效 -->
<select id="selectByPrimaryKey" parameterType="java.lang.String" resultMap="BaseResultMap" flushCache="true">
    select 
    <include refid="Base_Column_List" />
    from cbondissuer
    where OBJECT_ID = #{objectId,jdbcType=VARCHAR}
</select>
<!-- 默认是SESSION,也就是开启一级缓存 -->
<setting name="localCacheScope" value="STATEMENT"/>
  • Executor 里维护了一个 map,具体是 BaseExecutor.localCache
  • Exexutor.query 时会先查询 localCache,没有再查数据库

二级缓存

1,是 mapper.xml namespace 级别的,多个 SqlSession 都能查询到某个 mapper.xml 2,namespace 的缓存
3,当查询时:二级缓存 -> 一级缓存 -> 数据库
4,默认关闭,需手动开启,在 mybatis-config.xml 和 mapper.xml 中都要配置
5,增、删、改会默认清空缓存,查询默认使用缓存(可以配置,useCache 和 flushCachs)
6,如果开启二级缓存,所有的数据库实体必须实现 Serializable 接口
<!-- mybatis-config.xml 中配置打开二级缓存 -->
<!--开启⼆级缓存,父节点是 settings 标签 -->
<settings>
    <setting name="cacheEnabled" value="true"/>
</settings>
<!-- mapper.xml 中也需要配置 -->
<!-- 都是用默认配置的话,简单写个 cache 标签即可,父节点是 mapper 标签(和 select result 同级) -->
<cache/>
  • select 标签
    • flushCache 默认为 false,表示任何时候语句被调用,都不会去清空本地缓存和二级缓存
    • useCache 默认为 true,表示会将本条语句的结果进行二级缓存
  • insert、update、delete 标签
    • flushCache 默认为 true,表示任何时候语句被调用,都会导致本地缓存和二级缓存被清空
    • useCache 属性在该情况下没有
posted @ 2021-08-17 10:17  huanggy  阅读(51)  评论(0编辑  收藏  举报