28.MyBatis应用分析与最佳实践

1.为什么使用mybatis

1.1.JDBC连接数据库

// 注册 JDBC 驱动
Class.forName("com.mysql.jdbc.Driver");

// 打开连接
conn = DriverManager.getConnection("jdbc:mysql://localhost:3306/gp-mybatis", "root", "123456");

// 执行查询
stmt = conn.createStatement();
String sql = "SELECT bid, name, author_id FROM blog where bid = 1";
ResultSet rs = stmt.executeQuery(sql);

// 获取结果集
while (rs.next()) {
    Integer bid = rs.getInt("bid");
    String name = rs.getString("name");
    String authorId = rs.getInt("author_id");
    blog.setAuthorId(authorId);
    blog.setBid(bid);
    blog.setName(name);
}

首 先 ,在 pom.xml中引入MySQL驱动的依赖。

第一步,Class.forName注册驱动。

第二步,获取一个Connection第三步,创建一个Statement对象。

第四步,execute方法执行SQL。execute方法返回一个ResultSet结果集。

第五步,通过ResultSet获取数据,给 POJO的属性赋值。

第六步,关闭数据库相关的资源,包括ResultSet、Statement、Connection。

JDBC连接数据库的问题

1、 重复代码
2、 资源管理
3、 结果集处理
4、 SQL耦合

1.2.Spring JDBC

Spring对原生的JDBC进行了封装。解决了以下问题:

1、代码重复一一Spring提供了一个模板方法JdbcTemplate,里面封装了各种各样
的 execute, query 和 update 方法。 JDBCTemplate这个类 :
它是JDCB的核心包的中心类。简化了 JDBC的使用,可以避免常见的异常。它封装了 JDBC的核心流程,应用只要提供SQL,提取结果集就可以了。它是线程安全的。
初始化的时候可以设置数据源,所以资源管理的问题也可以解决。

2、对结果集处理,Spring JDBC提供了一个RowMapper接口,可以把结果集转换成Java对象,它作为JdbcTemplate的参数使用。

Spring JDBC,对JDBC做了轻量级封装的框架,帮助我们解决的问题:

1、 对操作数据的增删改查的方法进行了封装;

2、 无论是QueryRunner还是JdbcTemplate,都可以传入一个数据源进行初始化,也就是资源管理这一部分的事情,可以交给专门的数据源组件去做,不用我们手动创建和关闭;

3、 可以帮助我们映射结果集,无论是映射成List、Map还是POJO。

但仍存在不足:

1、 SQL语句都是写死在代码里面的,依旧存在硬编码的问题;

2、 参数只能按固定位置的顺序传入(数组), 它是通过占位符去替换的不能传入
对象和Map,不能自动映射;

3、 在方法里面,可以把结果集映射成实体类,但是不能直接把实体类映射成数据
库的记录(没有自动生成SQL的功能);

4、 查询没有缓存的功能,性能还不够好。

要解决这些问题,使用这些工具类还是不够的,这个时候用到ORM框架了。

1.3.Hibernate

什么是ORM?为什么叫ORM?

ORM的全拼是Object Relational Mapping,也就是对象与关系的映射,对象是程
序里面的对象,关系是它与数据库里面的数据的关系。也就是说,ORM框架帮助我们解
决的问题是程序对象和关系型数据库的相互映射的问题。

O:对象—— M :映射—— R :关系型数据库

28.MyBatis应用分析与最佳实践_第1张图片

总 结 Hibernate的特性:

1、 根据数据库方言自动生成SQ L,移植性好;

2、 自动管理连接资源(支持数据源);

3、 实现了对象和关系型数据库的完全映射,操作对象就像操作数据库记录一样;

4、 提供了缓存功能机制。

Hibernate问题:

1、比如使用get、update()、save( ) 对象的这种方式,实际操作的是所有字段, 没有办法指定部分字段,换句话说就是不够灵活。

2、自动生成SQL的方式,如果要基于SQL去做一些优化的话,是非常困难的,也就是说可能会出现性能的问题。

3、不支持动态SQ L,比如分表中的表名、条件、参数变化等,无法根据条件自动生
成 SQL。

我们需要一个更加灵活的框架。

1.4.MyBatis

MyBatis 的前身是 ibatis, 2001 年开始开发,是 "internet"和 "abatis(障碍物)"两个单词的组合。04年捐赠给Apache。2010年更名为MyBatis。

在 MyBatis里面,SQL和代码是分离的,所以会写SQL基本上就会用MyBatis,没有额外的学习成本。

2.myBatis使用案例

2.1.MyBatis API方式

先引入 mybatis jar包。

<dependency>
    <groupId>org.mybatisgroupId>
    <artifactId>mybatisartifactId>
    <version>3.5.4-snapshotversion>
dependency>

创建一个全局配置文件,这里面是对MyBatis的核心行为的控制,比如
mybatis-config.xml。这里面只定义的数据源和Mapper映射器路径。



<configuration>

    <properties resource="db.properties">properties>  
    <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="BlogMapper.xml"/>
    mappers>
configuration>

db.properties

jdbc.driver=com.mysql.jdbc.Driver
jdbc.url=jdbc:mysql://localhost:3306/mybatis?useUnicode=true&characterEncoding=utf-8&rewriteBatchedStatements=true
jdbc.username=root
jdbc.password=123456

第二个就是我们的映射器文件:Mapper.xml,通常来说一张表对应一个,我们会在 这个里面配置我们增删改查的SQL语句,以及参数和返回的结果集的映射关系。



<mapper namespace="com.gupaoedu.mapper.BlogMapper">

    <resultMap id="BaseResultMap" type="blog">
        <id column="bid" property="bid" jdbcType="INTEGER"/>
        <result column="name" property="name" jdbcType="VARCHAR"/>
        <result column="author_id" property="authorId" jdbcType="INTEGER"/>
    resultMap>

    <select id="selectBlogById" resultMap="BaseResultMap" statementType="PREPARED" >
        select * from blog where bid = #{bid}
    select>
mapper>

配置好了,怎么通过MyBatis执行一个查询呢?

既然MyBatis的目的是简化JDBC的操作,那么它必须要提供一个可以执行增删改 查的对象,这个对象就是SqISession接口,我们把它理解为跟数据库的一个连接,或者一次会话。

SqISession怎么创建呢?因为数据源、MyBatis核心行为的控制(例如是否开启缓 存)都在全局配置文件中,所以必须基于全局配置文件创建。这里它不是直接new出来 的,而是通过一个工厂类创建的。

所以整个的流程就是这样的(如下代码)。最后我们通过SqISession接口上的方法,
传入我们的Statement ID来执行Mapper映射器中的SQL。

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

/**
  * 使用 MyBatis API方式
  * @throws IOException
  */
@Test
public void testStatement() throws IOException {
    SqlSession session = sqlSessionFactory.openSession();
    try {
        Blog blog = (Blog) session.selectOne("com.gupaoedu.mapper.BlogMapper.selectBlogById", 1);
        System.out.println(blog);
    } finally {
        session.close();
    }
}

通过这样的调用方式,解决了重复代码、资源管理、SQL耦合、结果集映射这4大问题。

但仍存在下面问题:

(1) Statement ID是硬编码,维护起来很不方便;

(2) 不能在编译时进行类型检查,如果namespace或者Statement ID输错了,
只能在运行的时候报错。

2.2.Mapper接口方式

所以我们通常会使用第二种方式,也是新版的MyBatis里面推荐的方式:定义一个
Mapper接口的方式。这个接口全路径必须跟Mapper.xml里面的namespace对应起 来,方法也要跟Statement ID---- 对应。

/**
  * 通过 SqlSession.getMapper(XXXMapper.class)  接口方式
  * @throws IOException
  */
@Test
public void testSelect() throws IOException {
    SqlSession session = sqlSessionFactory.openSession(); // ExecutorType.BATCH
    try {
        BlogMapper mapper = session.getMapper(BlogMapper.class);
        Blog blog = mapper.selectBlogById(1);
        System.out.println(blog);
    } finally {
        session.close();
    }
}

MyBatis的核心特性/解决主要的问题:

  • 使用连接池对连接进行管理
  • SQL和代码分离,集中管理
  • 结果集映射
  • 参数映射和
  • 动态SQL
  • 重复SQL的提取
  • 缓存管理
  • 插件机制

3.核心对象的生命周期

MyBatis里面的几个核心对象:
SqISessionFactoryBuiler、 SqISessionFactory、 SqlSession 和 Mapper 对象。这几个
核心对象在MyBatis的整个工作流程里面的不同环节发挥作用。如果说我们不用容器, 自己去管理这些对象的话,我们必须思考一个问题:什么时候创建和销毁这些对象?

1) SqISessionFactoryBuiler

首先是 SqISessionFactoryBuiler它是用来构建 SqISessionFactory 的 ,而 SqISessionFactory只需要一个,所以只要构建了这一个SqISessionFactory,它的使命
就完成了,也就没有存在的意义了。所以它的生命周期只存在于方法的局部。

2) SqISessionFactory (单例)

SqISessionFactory是用来创建SqISession的,每次应用程序访问数据库,都需要
创建一个会话。因为我们一直有创建会话的需要,所以SqISessionFactory应该存在于 应用的整个生命周期中(作用域是应用作用域)。创建SqISession只需要一个实例来做 这件事就行了,否则会产生很多的混乱,和浪费资源。所以我们要采用单例模式。

3) SqISession

SqISession是一个会话,因为它不是线程安全的,不能在线程间共享。所以我们在 请求开始的时候创建一个SqISession对象,在请求结束或者说方法执行完毕的时候要及
时关闭它(一次请求或者操作中)。

4) Mapper

Mapper (实际上是一个代理对象)是从SqISession中获取的。

BlogMapper mapper = session.getMapper(BlogMapper.class);

它的作用是发送SQL来操作数据库的数据。它应该在一个SqISession事务方法之
内。

总结如下:

28.MyBatis应用分析与最佳实践_第2张图片

4.核心配置解读

第一个是config文件。

一级标签

4.1.configuration

configuration是整个配置文件的根标签,实际上也对应着MyBatis里面最重要的配置类Configuration。它贯穿MyBatis执行流程的每一个环节。我们打开这个类看一 下,这里面有很多的属性,跟其他的子标签也能对应上。

4.2.properties

第一个一级标签是properties,用来配置参数信息,比如最常见的数据库连接信息。

为了避免直接把参数写死在xml配置文件中,我们可以把这些参数单独放在 properties文件中,用 properties标签引入进来,然 后 在 xml配置文件中用${}引用就
可以了。

可以用resource引用应用里面的相对路径,也可以用url指定本地服务器或者网络
的绝对路径。

4.3.settings

settlings里面是 MyBatis的一些核心配置。

4.4.typeAlias

TypeAlias是类型的别名,跟 Linux系统里面的alias一样,主要用来简化类名全路 径的拼写。比如我们的参数类型和返回值类型都可能会用到我们的Bean,如果每个地方 都配置全路径的话,那么内容就比较多,还可能会写错。

我们可以为自己的Bean创建别名,既可以指定单个类,也可以指定一个package,
自动转换。

<typeAliases>
    <typeAlias alias="blog" type="com.gupaoedu.domain.Blog" />
typeAliases>

配 置 了 别 名 以 后 ,在 配 置 文 件 中 只 需 要 写 别 名 就 可 以 了 ,比如
com.gupaoedu.domain.Blog 可以简化成 blog

<select id="selectBlogByBean"  parameterType="blog" resultType="blog" >
    select bid, name, author_id authorId from blog where name = '${name}'
select>

MyBatis里面有很多系统预先定义好的类型别名,在 TypeAliasRegistry中。所以可 以用 string 代替 java.lang.String。

4.5.typeHandlers

由于Java类型和数据库的JDBC类型不是— 对应的(比如String与varchar、char、
text), 所以我们把Java对象转换为数据库的值,和把数据库的值转换成Java对象,需 要经过一定的转换,这两个方向的转换就要用到TypeHandler

当参数类型和返回值是一个对象的时候,我没有做任何的配置,为什么对象里面的
个 String属性,可以转换成数据库里面的varchar字段?

这是因为MyBatis已经内置了很多TypeHandler (在 type包下), 它们全部全部 注册在TypeHandlerRegistry中,他们都继承了抽象类BaseTypeHandler,泛型就是要
处理的Java数据类型。

如果我们需要自定义一些类型转换规则,或者要在处理类型的时候做一些特殊的动
作,就可以编写自己的TypeHandler,跟系统自定义的TypeHandler —样,继承抽象类 BaseTypeHandler。 有4 个抽象方法必须实现,我们把它分成两类:

set方法从Java类型转换成JDBC类型的,get方法是从JDBC类型转换成Java类
型的。

从 Java类型到JDBC类型 从 JDBC类型到Java类型
setNonNullParameter:设置非空参数 getNullableResult:获取空结果集(根据列名),一般都是调用这个 getNullableResult:获取空结果集(根据下标值) getNullableResult:存储过程用的

举个例子:

一个商户,在登记的时候需要注册它的经营范围。比如1手机,2电脑,3相机,4
平板,在界面上是一个复选框(checkbox)

在数据库保存的是用逗号分隔的字符串,例 如 “1,3,4”, 而返回给程序的时候是整
形数组{1,3,4}。

在每次获取数据的时候转换?还是在bean的get方法里面转换?似乎都不太合适。

这时候我们可以写一个lnteger[]类型的TypeHandler。

public class MyTypeHandler extends BaseTypeHandler<String> {
    public void setNonNullParameter(PreparedStatement ps, int i, String parameter, JdbcType jdbcType)
            throws SQLException {
        // 设置 String 类型的参数的时候调用,Java类型到JDBC类型
        // 注意只有在字段上添加typeHandler属性才会生效
        // insertBlog name字段
        System.out.println("---------------setNonNullParameter1:"+parameter);
        ps.setString(i, parameter);
    }

    public String getNullableResult(ResultSet rs, String columnName) throws SQLException {
        // 根据列名获取 String 类型的参数的时候调用,JDBC类型到java类型
        // 注意只有在字段上添加typeHandler属性才会生效
        System.out.println("---------------getNullableResult1:"+columnName);
        return rs.getString(columnName);
    }

    public String getNullableResult(ResultSet rs, int columnIndex) throws SQLException {
        // 根据下标获取 String 类型的参数的时候调用
        System.out.println("---------------getNullableResult2:"+columnIndex);
        return rs.getString(columnIndex);
    }

    public String getNullableResult(CallableStatement cs, int columnIndex) throws SQLException {
        System.out.println("---------------getNullableResult3:");
        return cs.getString(columnIndex);
    }
}

第二步,在 mybatis-config.xml文件中注册:

<typeHandlers>
    <typeHandler handler="com.gupaoedu.type.MyTypeHandler">typeHandler>
typeHandlers>

第三步,在我们需要使用的字段上指定,比如:

插入值的时候,从 Java类型到JDBC类型,在字段属性中指定typehandler:


<insert id="insertBlog" parameterType="blog">
    insert into blog
    <trim prefix="(" suffix=")" suffixOverrides=",">
        <if test="bid != null">
            bid,
        if>
        <if test="name != null">
            name,
        if>
        <if test="authorId != null">
            author_id,
        if>
    trim>
    <trim prefix="values (" suffix=")" suffixOverrides=",">
        <if test="bid != null">
            #{bid,jdbcType=INTEGER},
        if>
        <if test="name != null">
            #{name,jdbcType=VARCHAR},
            
        if>
        <if test="authorId != null">
            #{authorId,jdbcType=INTEGER},
        if>
    trim>
insert>

返回值的时候,从 JDBC类型到Java类型,在 resultMap的列上指定typehandler:

<result column="name" property="name" jdbcType="VARCHAR" typeHandler="com.gupaoedu.type.MyTypeHandler"/>

4.6.object Factory

当我们把数据库返回的结果集转换为实体类的时候,需要创建对象的实例,由于我们不知道需要处理的类型是什么,有哪些属性,所以不能用new的方式去创建。只能通过反射来创建。

在 MyBatis里面,它提供了一个工厂类的接口,叫做ObjectFactory,专门用来创建对象的实例(MyBatis封装之后,简化了对象的创建),里面定义了 4 个方法。

方法 作用
void setProperties (Properties properties); 设置参数时调用
T create(Class type); 创建对象(调用无参构造函数)
T create(Class type, List> construetorArgTypes, List con struct orArg s); 创建对象(调用带参数构造函数)
boolean isCollection(Class type) 判断是否集合

ObjectFactory有一个默认的实现类DefaultobjectFactoryo 创建对象的方法最终 都调用了 instantiateClass(),这里面能看到反射的代码。

默认情况下,所有的对象都是由DefaultObjectFactory创建。

如果想要修改对象工厂在初始化实体类的时候的行为,就可以通过创建自己的对象
工厂,继承DefaultObjectFactory来实现(不再需要实现0 bjectFactory接 口 )。

1、 什么时候调用了 objectFactory.create?

创建DefaultResultSetHandler的时候,和创建对象的时候。

2、 创建对象后,已有的属性为什么被覆盖了?

在 DefaultResultSetHandler 类的 395 行 getRowValueQ方法里面里面调用了 applyPropertyMappings()

3、 返回结果的时候,ObjectFactory和 TypeHandler哪个先工作?

肯定是先创建对象,所以先是Object Factory,再是TypeHandler。

PS: step out可以看到一步步调用的层级

4.7.plugins

插件是MyBatis的一个很强大的机制。跟很多其他的框架一样,MyBatis预留了插 件的接口,让 MyBatis更容易扩展。

SqISession是对外提供的接口。而 SqISession增删改查的方法都是由Executor完
成 的 (点开DefualtSqISession源码的相关方法)。

Executor是真正的执行器的角色,也是实际的SQL逻辑执行的开始。

而 MyBatis中又把SQL的执行,按照过程,细分成了三个对象:ParameterHandler 处理参数,StatementHandler执行 SQL, StatementHandler处理结果集。

4.8.environments、environment

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>

一 个 environment标签就是一个数据源,代表一个数据库。这里面有两个关键的标 签,一个是事务管理器,一个是数据源。

4.9.transactionManager

如果配置的是JDBC,则会使用Connection对象的 commit、rollback、close()
管理事务。

如果配置成MANAGED,会把事务交给容器来管理,比 如 JBOSS, Weblogic。因
为我们跑的是本地程序,如果配置成MANAGE不会有任何事务。

如 果 是 Spring + MyBatis , 则 没 有 必 要 配 置 , 因 为 我 们 会 直 接 在
applicationContext.xml里面配置数据源和事务,覆 盖 MyBatis的配置。

4.10.dataSource

数据源,顾名思义,就是数据的来源,一个数据源就对应一个数据库。在 Java里面, 它是对数据库连接的一个抽象。

一般的数据源都会包括连接池管理的功能,所以很多时候也把DataSource直接称为连接池,准确的说法应该是:带连接池功能的数据源。

4.11.为什么要用连接池?

如果没有连接池,那么每一个用户、每_次会话连接数据库都需要直接创建和释放
连接,这个过程是会消耗的一定的时间的,并且会消耗应用和服务器的性能。

如果采用连接池技术,在应用程序里面关闭连接的时候,物理连接没有被真正关闭
掉,只是回到了连接池里面。

从这个角度来考虑,一般的连接池都会有初始连接数、最大连接数、回收时间等等
这些参数,提供提前创建/资源重用/数量控制/超时管理等等这些功能。

4.12.mappers

标签配置的是映射器,也就是Mapper.xml的路径。这里配置的目的是 让 MyBatis在启动的时候去扫描这些映射器,创建映射关系。

我们有四种指定Mapper文件的方式:

1、 使用相对于类路径的资源 引 用 (resource)

<mappers>
	<mapper resource="BlogMapper.xml">
mappers>

2、 使用完全限定资源定位符(绝对路径) (URL)

<mappers>
	<mapper resource="file:///app/sale/mappers/BlogMapper.xml">
mappers>

3、 使用映射器接口实现类的完全限定类名

<mappers>
	<mapper class="com.gupaoedu.mapper.BlogMapper"/>
mappers>

4、 将包内的映射器接口实现全部注册为映射器(最常用)

<mappers>
	<mapper class="com.gupaoedu.mapper">
mappers>

4.13.settings

4.14.Mapper.xml映射配置文件

映射器里面最主要的是配置了 SQL语句,也解决了我们的参数映射和结果集映射的问题。一共有8个标签:

  • -给定命名空间的缓存配置(是否开启二级缓存)。

  • - 其他命名空间缓存配置的引用。缓存相关两个标签我们在讲解缓存
    的时候会详细讲到。

  • -是最复杂也是最强大的元素,用来描述如何从数据库结果集中来加载对象。

  • - 可被其他语句引用的可重用语句块。

增删改查标签:

  • - 映射插入语句

  • - 映射更新语句

  • - 映射删除语句