【开发框架】- MyBatis核心要点

概述

本文将依赖Mybatis源码分析书作为指导,提取Mybatis的核心要点

主流ORM框架使用对比

本节主要分析访问数据库的多种方式,进而理解框架是如何一步一步在JDBC的基础上实现的

使用JDBC访问数据库

这里的示例代码仅仅作为样例参考,仅用于展示其使用方式,具体详细使用请参考官方文档

public class test {
    @Test
    public void testJdbc() {
        String url = "jdbc:mysql://localhost:3306/myblog?user=root&……";
        Connection conn = null;
        try {
            Class.forName("com.mysql.cj.jdbc.Driver");
            conn = DriverManager.getConnection(url);
            String author = "coolblog.xyz";
            String date = "2018.06.10";
            String sql = "SELECT id, title, author, content, create_time"
                    + " FROM article"
                    + " WHERE author = '" + author
                    + "' AND create_time > '" + date + "'";
            Statement stmt = conn.createStatement();
            ResultSet rs = stmt.executeQuery(sql);
            List<Article> articles = new ArrayList<>(rs.getRow());
            while (rs.next()) {
                Article article = new Article();
                article.setId(rs.getInt("id"));
                article.setTitle(rs.getString("title"));
                article.setAuthor(rs.getString("author"));
                article.setContent(rs.getString("content"));
                article.setCreateTime(rs.getDate("create_time"));
                articles.add(article);
            }
            System.out.println("Query SQL ==> " + sql);
            System.out.println("Query Result: ");
            articles.forEach(System.out::println);
        } catch (ClassNotFoundException e) {
            e.printStackTrace();
        } catch (SQLException e) {
            e.printStackTrace();
        } finally {
            try {
                conn.close();
            } catch (SQLException e) {
                e.printStackTrace();
            }
        }
    }
}

使用SpringJDBC访问数据库

application.xml

<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
       xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
       xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd">
    <context:property-placeholder location="jdbc.properties"/>
    <bean id="dataSource"
          class="org.springframework.jdbc.datasource.DriverManagerDataSource">
        <property name="driverClassName" value="${jdbc.driver}" />
        <property name="url" value="${jdbc.url}" />
        <property name="username" value="${jdbc.username}" />
        <property name="password" value="${jdbc.password}" />
    </bean>
    <bean id="jdbcTemplate"
          class="org.springframework.jdbc.core.JdbcTemplate">
        <property name="dataSource" ref="dataSource" />
    </bean>
</beans>

SpringJDBCTest

@RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration("classpath:chapter1/application.xml")
public class SpringJdbcTest {
    @Autowired
    private JdbcTemplate jdbcTemplate;
    @Test
    public void testSpringJdbc() {
        String author = "coolblog.xyz";
        String date = "2018.06.10";
        String sql = "SELECT id, title, author, content, create_time"
                + " FROM blog_article"
                + " WHERE author = '" + author
                + "' AND create_time > '" + date + "'";
        List<Article> articles = jdbcTemplate.query(sql, (rs, rowNum) -> {
            Article article = new Article();
            article.setId(rs.getInt("id"));
            article.setTitle(rs.getString("title"));
            article.setAuthor(rs.getString("author"));
            article.setContent(rs.getString("content"));
            article.setCreateTime(rs.getDate("create_time"));
            return article;
        });
        System.out.println("Query SQL ==> " + sql);
        System.out.println("Spring JDBC Query Result: ");
        articles.forEach(System.out::println);
    }
}

使用Mybatis访问数据库

Mapper.xml

<mapper namespace="xyz.coolblog.chapter1.dao.ArticleDao">
    <select id="findByAuthorAndCreateTime" resultType="Article">
        SELECT
        `id`, `title`, `author`, `content`, `create_time`
        FROM
        `article`
        WHERE
        `author` = #{author} AND `create_time` > #{createTime}
    </select>
</mapper>

Mapper Dao

public interface ArticleDao {
    List<Article> findByAuthorAndCreateTime(@Param("author") String author,
                                            @Param("createTime") String createTime);
}

Mybatis-config.xml

<configuration>
    <properties resource="jdbc.properties"/>
    <typeAliases>
        <typeAlias alias="Article"
                   type="xyz.coolblog.chapter1.model.ArticleDO"/>
        <typeAlias alias="Author" type="xyz.coolblog.model.AuthorDO"/>
    </typeAliases>
    <environments default="development">
        <environment id="development">
            <transactionManager type="JDBC"/>
            <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>
        <mapper resource="chapter1/mapper/ArticleMapper.xml"/>
    </mappers>
</configuration>

MyBatis Java Test

public class MyBatisTest {
    private SqlSessionFactory sqlSessionFactory;
    @Before
    public void prepare() throws IOException {
        String resource = "chapter1/mybatis-config.xml";
        InputStream inputStream = Resources.getResourceAsStream(resource);
        sqlSessionFactory = new
                SqlSessionFactoryBuilder().build(inputStream);
        inputStream.close();
    }
    @Test
    public void testMyBatis() throws IOException {
        SqlSession session = sqlSessionFactory.openSession();
        try {
            ArticleDao articleDao = session.getMapper(ArticleDao.class);
            List<Article> articles = articleDao.
                    findByAuthorAndCreateTime("coolblog.xyz", "2018-06-10");
        } finally {
            session.commit();
            session.close();
        }
    }
}

MyBatis 使用入门

单独使用MyBatis

数据库访问层的接口定义

public interface AuthorDao {
    Author findOne(@Param("id") int id);
}

接口对用的Mapper定义

<!-- AuthorMapper.xml -->
<mapper namespace="xyz.coolblog.chapter1.dao2.AuthorDao">
    <resultMap id="articleResult" type="Article">
        <id property="id" column="article_id" />
        <result property="title" column="title"/>
        <result property="type" column="type"/>
        <result property="content" column="content"/>
        <result property="createTime" column="create_time"/>
    </resultMap>
    <resultMap id="authorResult" type="Author">
        <id property="id" column="id"/>
        <result property="name" column="name"/>
        <result property="age" column="age"/>
        <result property="sex" column="sex"
                typeHandler="org.apache.ibatis.type.EnumOrdinalTypeHandler"/>
        <result property="email" column="email"/>
        <collection property="articles" resultMap="articleResult"/>
    </resultMap>
    <select id="findOne" resultMap="authorResult">
        SELECT
        au.id, au.name, au.age, au.sex, au.email, ar.id as article_id,
        ar.title, ar.type, ar.content, ar.create_time
        FROM
        author au, article ar
        WHERE
        au.id = ar.author_id AND au.id = #{id}
    </select>
</mapper>

MyBatis 的配置文件

<!-- mybatis-config2.xml -->
<configuration>
    <properties resource="jdbc.properties"/>
    <typeAliases>
        <typeAlias alias="Article"
                   type="xyz.coolblog.chapter1.model2.ArticleDO"/>
        <typeAlias alias="Author"
                   type="xyz.coolblog.chapter1.model2.AuthorDO"/>
    </typeAliases>
    <typeHandlers>
        <typeHandler handler="xyz.coolblog.mybatis.ArticleTypeHandler"
                     javaType="xyz.coolblog.constant.ArticleTypeEnum"/>
    </typeHandlers>
    <environments default="development">
        <environment id="development">
            <transactionManager type="JDBC"/>
            <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>
        <mapper resource="chapter1/mapper2/AuthorMapper.xml"/>
        <mapper resource="chapter1/mapper2/ArticleMapper.xml"/>
    </mappers>
</configuration>

单元测试

public class MyBatisTest2 {
    private SqlSessionFactory sqlSessionFactory;

    @Before
    public void prepare() throws IOException {
        String resource = "chapter1/mybatis-config2.xml";
        InputStream inputStream = Resources.getResourceAsStream(resource);
        sqlSessionFactory =
                new SqlSessionFactoryBuilder().build(inputStream);
        inputStream.close();
    }

    @Test
    public void testOne2One() {
        SqlSession session = sqlSessionFactory.openSession();
        try {
            ArticleDao articleDao = session.getMapper(ArticleDao.class);
            ArticleDO article = articleDao.findOne(1);
            AuthorDO author = article.getAuthor();
            article.setAuthor(null);
            System.out.println("\nauthor info:");
            System.out.println(author);
            System.out.println("\narticles info:");
            System.out.println(article);
        } finally {
            session.close();
        }
    }
}    

在 Spring 中使用MyBatis

Maven依赖配置

<project>
    <properties>
        <spring.version>4.3.17.RELEASE</spring.version>
    </properties>
    <dependencies>
        <dependency>
            <groupId>org.mybatis</groupId>
            <artifactId>mybatis</artifactId>
            <version>3.4.6</version>
        </dependency>
        <dependency>
            <groupId>org.mybatis</groupId>
            <artifactId>mybatis-spring</artifactId>
            <version>1.3.2</version>
        </dependency>
        <dependency>
            <groupId>org.springframework</groupId>
            <artifactId>spring-core</artifactId>
            <version>${spring.version}</version>
        </dependency>
        <dependency>
            <groupId>org.springframework</groupId>
            <artifactId>spring-beans</artifactId>
            <version>${spring.version}</version>
        </dependency>
        <dependency>
            <groupId>org.springframework</groupId>
            <artifactId>spring-context</artifactId>
            <version>${spring.version}</version>
        </dependency>
        <dependency>
            <groupId>org.springframework</groupId>
            <artifactId>spring-jdbc</artifactId>
            <version>${spring.version}</version>
        </dependency>
        <dependency>
            <groupId>org.springframework</groupId>
            <artifactId>spring-test</artifactId>
            <version>${spring.version}</version>
            <scope>test</scope>
        </dependency>
    </dependencies>
</project>

Spring集成配置

<!-- application-mybatis.xml -->
<beans>
    <context:property-placeholder location="jdbc.properties"/>
    <!-- 配置数据源 -->
    <bean id="dataSource"
          class="org.apache.ibatis.datasource.pooled.PooledDataSource">
        <property name="driver" value="${jdbc.driver}" />
        <property name="url" value="${jdbc.url}" />
        <property name="username" value="${jdbc.username}" />
        <property name="password" value="${jdbc.password}" />
    </bean>
    <!-- 配置 SqlSessionFactory -->
    <bean id="sqlSessionFactory"
          class="org.mybatis.spring.SqlSessionFactoryBean">
        <!-- 配置 mybatis-config.xml 路径 -->
        <property name="configLocation"
                  value="classpath:chapter1/mybatis-config3.xml"/>
        <!-- 给 SqlSessionFactory 配置数据源,这里引用上面的数据源配置 -->
        <property name="dataSource" ref="dataSource"/>
        <!-- 配置 SQL 映射文件 -->
        <property name="mapperLocations" value="chapter1/mapper2/*.xml"/>
    </bean>
    <!-- 配置 MapperScannerConfigurer -->
    <bean class="org.mybatis.spring.mapper.MapperScannerConfigurer">
        <!-- 配置 Dao 接口所在的包 -->
        <property name="basePackage" value="xyz.coolblog.chapter1.dao2"/>
    </bean>
</beans>

Mybatis配置

<!-- mybatis-config3.xml -->
<configuration>
    <settings>
        <setting name="cacheEnabled" value="true"/>
    </settings>
    <typeAliases>
        <typeAlias alias="Article"
                   type="xyz.coolblog.chapter1.model2.Article"/>
        <typeAlias alias="Author"
                   type="xyz.coolblog.chapter1.model2.Author"/>
    </typeAliases>
    <typeHandlers>
        <typeHandler handler="xyz.coolblog.mybatis.ArticleTypeHandler"
                     javaType="xyz.coolblog.constant.ArticleTypeEnum"/>
    </typeHandlers>
</configuration>

单元测试

@RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration("classpath:chapter1/application-mybatis.xml")
public class SpringWithMyBatisTest implements ApplicationContextAware {
    private ApplicationContext applicationContext;
    /**
     * 自动注入 AuthorDao,无需再通过 SqlSession 获取
     */
    @Autowired
    private AuthorDao authorDao;
    @Autowired
    private ArticleDao articleDao;

    @Before
    public void printBeanInfo() {
        ListableBeanFactory lbf = applicationContext;
        String[] beanNames = lbf.getBeanDefinitionNames();
        Arrays.sort(beanNames);
        System.out.println("\n--------------☆ bean name ☆-------------");
        Arrays.asList(beanNames).subList(0, 5).forEach(System.out::println);
        AuthorDao authorDao =
                (AuthorDao) applicationContext.getBean("authorDao");
        ArticleDao articleDao =
                (ArticleDao) applicationContext.getBean("articleDao");
        System.out.println("\n-----------☆ bean class info ☆------------");
        System.out.println("AuthorDao Class: " + authorDao.getClass());
        System.out.println("ArticleDao Class: " + articleDao.getClass());
        System.out.println("\n-------xxxx--------xxxx-------xxx--------\n");
    }

    @Test
    public void testOne2One() {
        Article article = articleDao.findOne(1);
        Author author = article.getAuthor();
        article.setAuthor(null);
        System.out.println("\nauthor info:");
        System.out.println(author);
        System.out.println("\narticles info:");
        System.out.println(article);
    }
}

以上没有列出__数据库访问层的接口定义__,Mapper定义文件,参考单独使用MyBatis配置

原理分析

配置文件的解析过程

  • 良好的开源项目必定提供良好的配置,以支持其高级特性以及更好的扩展

配置文件说明

  • properties
<properties resource="jdbc.properties">
    <property name="jdbc.username" value="coolblog"/>
    <property name="hello" value="world"/>
</properties>

  • settings
    • 解析 settings 子节点的内容,并将解析结果转成 Properties 对象
    • 为 Configuration 创建元信息对象
    • 通过 MetaClass 检测 Configuration 中是否存在某个属性的 setter 方法, 不存在则抛异常
    • 若通过 MetaClass 的检测,则返回 Properties 对象,方法逻辑结束
<settings>
    <setting name="cacheEnabled" value="true"/>
    <setting name="lazyLoadingEnabled" value="true"/>
    <setting name="autoMappingBehavior" value="PARTIAL"/>
</settings>
  • typeAliases
<typeAliases>
    <package name="xyz.coolblog.chapter2.model1"/>
    <package name="xyz.coolblog.chapter2.model2"/>
</typeAliases>

<typeAliases>
    <typeAlias alias="article" type="xyz.coolblog.chapter2.model.Article" />
    <typeAlias alias="author" type="xyz.coolblog.chapter2.model.Author" />
</typeAliases>
  • plugins
<plugins>
    <plugin interceptor="xyz.coolblog.mybatis.ExamplePlugin">
        <property name="key" value="value"/>
    </plugin>
</plugins>
  • environments
<environments default="development">
    <environment id="development">
        <transactionManager type="JDBC"/>
        <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>
  • typeHandlers
<!-- 自动扫描-->
<typeHandlers>
    <package name="xyz.coolblog.handlers"/>
</typeHandlers>
<!-- 手动配置 -->
<typeHandlers>
<typeHandler jdbcType="TINYINT"
             javaType="xyz.coolblog.constant.ArticleTypeEnum"
             handler="xyz.coolblog.mybatis.ArticleTypeHandler"/>
</typeHandlers>

映射文件解析过程

实际是解析Mapper的配置,并映射到对应的pojo对象上,实现数据自动映射

缓存配置文件

cache

<cache
        eviction="FIFO"
        flushInterval="60000"
        size="512"
        readOnly="true"/>
  • 按先进先出的策略淘汰缓存项
  • 缓存的容量为 512 个对象引用
  • 缓存每隔 60 秒刷新一次
  • 缓存返回的对象是写安全的,即在外部修改对象不会影响到缓存内部存储对象

第三方缓存或者内置缓存

<cache type="org.mybatis.caches.ehcache.EhcacheCache"/>
    <property name="timeToIdleSeconds" value="3600"/>
    <property name="timeToLiveSeconds" value="3600"/>
    <property name="maxEntriesLocalHeap" value="1000"/>
    <property name="maxEntriesLocalDisk" value="10000000"/>
    <property name="memoryStoreEvictionPolicy" value="LRU"/>
</cache>

cache-ref

<!-- Mapper1.xml -->
<mapper namespace="xyz.coolblog.dao.Mapper1">
    <!-- Mapper1 与 Mapper2 共用一个二级缓存 -->
    <cache-ref namespace="xyz.coolblog.dao.Mapper2"/>
</mapper>
        <!-- Mapper2.xml -->
<mapper namespace="xyz.coolblog.dao.Mapper2">
    <cache/>
</mapper>

resultMap

resultMap 元素是 MyBatis 中最重要最强大的元素。它可以让你从 90% 的 JDBC ResultSets 数据提取代码中解放出来, 并在一些情形下允许你做一些 JDBC 不支持的事情。 实际上,在对复杂语句进行联合映射的时候,它很可能可以代替数千行的同等功能的代码。 ResultMap 的设计思想是,简单的语句不需要明确的结果映射,而复杂一点的语句只需要描述它们的关系就行了。

基于resultType简单pojo配置

这种配置,使用resultType指定pojo,MyBatis 会在幕后自动创建一个 ResultMap,再基于属性名来映射列到 JavaBean 的属性上

基于resultMap配置,下面是关于ResultMap属性配置说明

id&result

这些是结果映射最基本的内容。id 和 result 都将一个列的值映射到一个简单数据类型(字符串,整型,双精度浮点数,日期等)的属性或字段

<id property="id" column="post_id"/>
<result property="subject" column="post_subject"/>

constructor

//指定name防止出现构造函数参数顺序问题导致出现歧义
<constructor>
   <idArg column="id" javaType="int" name="id" />
   <arg column="age" javaType="_int" name="age" />
   <arg column="username" javaType="String" name="username" />
</constructor>

association

单一对象引用

基本配置

<association property="author" column="blog_author_id" javaType="Author">
  <id property="id" column="author_id"/>
  <result property="username" column="author_username"/>
</association>
  • 嵌套查询:通过执行另外一个 SQL 映射语句来返回预期的复杂类型。
<resultMap id="blogResult" type="Blog">
  <association property="author" column="author_id" javaType="Author" select="selectAuthor"/>
</resultMap>

<select id="selectBlog" resultMap="blogResult">
  SELECT * FROM BLOG WHERE ID = #{id}
</select>

<select id="selectAuthor" resultType="Author">
  SELECT * FROM AUTHOR WHERE ID = #{id}
</select>

这种方式很简单, 但是对于大型数据集合和列表将不会表现很好。 问题就是我们熟知的 “N+1 查询问题”。

  • 嵌套结果:使用嵌套结果映射来处理重复的联合结果的子集。首先,让我们来查看这个元素的属性。所有的你都会看到,它和普通的只由 select 和 resultMap 属性的结果映射不同。

嵌套查询SQL语句

<select id="selectBlog" resultMap="blogResult">
  select
    B.id            as blog_id,
    B.title         as blog_title,
    B.author_id     as blog_author_id,
    A.id            as author_id,
    A.username      as author_username,
    A.password      as author_password,
    A.email         as author_email,
    A.bio           as author_bio
  from Blog B left outer join Author A on B.author_id = A.id
  where B.id = #{id}
</select>

嵌套查询两种配置

<!--嵌套查询重用配置 -->
<resultMap id="blogResult" type="Blog">
  <id property="id" column="blog_id" />
  <result property="title" column="blog_title"/>
  <association property="author" column="blog_author_id" javaType="Author" resultMap="authorResult"/>
</resultMap>

<resultMap id="authorResult" type="Author">
  <id property="id" column="author_id"/>
  <result property="username" column="author_username"/>
  <result property="password" column="author_password"/>
  <result property="email" column="author_email"/>
  <result property="bio" column="author_bio"/>
</resultMap>

<!-- 嵌套查询单一配置-->
```xml
<resultMap id="blogResult" type="Blog">
  <id property="id" column="blog_id" />
  <result property="title" column="blog_title"/>
  <association property="author" javaType="Author">
    <id property="id" column="author_id"/>
    <result property="username" column="author_username"/>
    <result property="password" column="author_password"/>
    <result property="email" column="author_email"/>
    <result property="bio" column="author_bio"/>
  </association>
</resultMap>

collection

对象集合引用配置

  • 集合的嵌套查询
<resultMap id="blogResult" type="Blog">
  <collection property="posts" javaType="ArrayList" column="id" ofType="Post" select="selectPostsForBlog"/>
</resultMap>

<select id="selectBlog" resultMap="blogResult">
  SELECT * FROM BLOG WHERE ID = #{id}
</select>

<select id="selectPostsForBlog" resultType="Post">
  SELECT * FROM POST WHERE BLOG_ID = #{id}
</select>
  • 集合的嵌套结果

嵌套查询SQL

<select id="selectBlog" resultMap="blogResult">
  select
  B.id as blog_id,
  B.title as blog_title,
  B.author_id as blog_author_id,
  P.id as post_id,
  P.subject as post_subject,
  P.body as post_body,
  from Blog B
  left outer join Post P on B.id = P.blog_id
  where B.id = #{id}
</select>

嵌套查询配置

<!-- 嵌套查询单一配置-->
<resultMap id="blogResult" type="Blog">
  <id property="id" column="blog_id" />
  <result property="title" column="blog_title"/>
  <collection property="posts" ofType="Post">
    <id property="id" column="post_id"/>
    <result property="subject" column="post_subject"/>
    <result property="body" column="post_body"/>
  </collection>
</resultMap>

<!--嵌套查询重用配置 -->
<resultMap id="blogResult" type="Blog">
  <id property="id" column="blog_id" />
  <result property="title" column="blog_title"/>
  <collection property="posts" ofType="Post" resultMap="blogPostResult" columnPrefix="post_"/>
</resultMap>

<resultMap id="blogPostResult" type="Post">
  <id property="id" column="id"/>
  <result property="subject" column="subject"/>
  <result property="body" column="body"/>
</resultMap>

SQL操作

select

<select
  id="selectPerson"
  parameterType="int"
  parameterMap="deprecated"
  resultType="hashmap"
  resultMap="personResultMap"
  flushCache="false"
  useCache="true"
  timeout="10000"
  fetchSize="256"
  statementType="PREPARED"
  resultSetType="FORWARD_ONLY">

insert, update 和 delete

<insert
  id="insertAuthor"
  parameterType="domain.blog.Author"
  flushCache="true"
  statementType="PREPARED"
  keyProperty=""
  keyColumn=""
  useGeneratedKeys=""
  timeout="20">

<update
  id="updateAuthor"
  parameterType="domain.blog.Author"
  flushCache="true"
  statementType="PREPARED"
  timeout="20">

<delete
  id="deleteAuthor"
  parameterType="domain.blog.Author"
  flushCache="true"
  statementType="PREPARED"
  timeout="20">

sql

这个元素可以被用来定义可重用的 SQL 代码段,可以包含在其他语句中。它可以被静态地(在加载参数) 参数化. 不同的属性值通过包含的实例变化

<sql id="userColumns"> ${alias}.id,${alias}.username,${alias}.password </sql>

动态 SQL

MyBatis 的强大特性之一便是它的动态 SQL。如果你有使用 JDBC 或其它类似框架的经验,你就能体会到根据不同条件拼接 SQL 语句的痛苦。例如拼接时要确保不能忘记添加必要的空格,还要注意去掉列表最后一个列名的逗号。利用动态 SQL 这一特性可以彻底摆脱这种痛苦。

插件机制

事务管理

  • [待分析]Spring事务管理是如何代理Mybatis内部事务的

参考资料

Search

    Post Directory