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

2020-08-12 16:21:59 浏览数 (1)

1.为什么使用mybatis

1.1.JDBC连接数据库

代码语言:javascript复制
// 注册 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 :关系型数据库

总 结 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包。

代码语言:javascript复制
<dependency>
    <groupId>org.mybatis</groupId>
    <artifactId>mybatis</artifactId>
    <version>3.5.4-snapshot</version>
</dependency>

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

代码语言:javascript复制
<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE configuration PUBLIC "-//mybatis.org//DTD Config 3.0//EN"
        "http://mybatis.org/dtd/mybatis-3-config.dtd">
<configuration>

    <properties resource="db.properties"></properties>  
    <environments default="development">
        <environment id="development">
            <transactionManager type="JDBC"/><!-- 单独使用时配置成MANAGED没有事务 -->
            <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

代码语言:javascript复制
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语句,以及参数和返回的结果集的映射关系。

代码语言:javascript复制
<?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.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。

代码语言:javascript复制
@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---- 对应。

代码语言:javascript复制
/**
  * 通过 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中获取的。

代码语言:javascript复制
BlogMapper mapper = session.getMapper(BlogMapper.class);

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

总结如下:

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, 自动转换。

代码语言:javascript复制
<typeAliases>
    <typeAlias alias="blog" type="com.gupaoedu.domain.Blog" />
</typeAliases>

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

代码语言:javascript复制
<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<T>。 有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。

代码语言:javascript复制
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文件中注册:

代码语言:javascript复制
<typeHandlers>
    <typeHandler handler="com.gupaoedu.type.MyTypeHandler"></typeHandler>
</typeHandlers>

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

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

代码语言:javascript复制
<!-- 动态SQL trim -->
<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},
            <!-- #{name,jdbcType=VARCHAR,typeHandler=com.gupaoedu.type.MyTypeHandler}, -->
        </if>
        <if test="authorId != null">
            #{authorId,jdbcType=INTEGER},
        </if>
    </trim>
</insert>

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

代码语言:javascript复制
<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> T create(Class<T> type);

创建对象(调用无参构造函数)

<T> T create(Class<T> type, List<Class<?>> construetorArgTypes, List<Object> con struct orArg s);

创建对象(调用带参数构造函数)

<T> boolean isCollection(Class<T> 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标签用来管理数据库的环境,比如我彳门可以有开发环境、测试环境、 生产环境的数据库。可以在不同的环境中使用不同的数据库地址或者类型。

代码语言:javascript复制
<environments default="development">
    <environment id="development">
        <transactionManager type="JDBC"/><!-- 单独使用时配置成MANAGED没有事务 -->
        <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

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

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

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

代码语言:javascript复制
<mappers>
	<mapper resource="BlogMapper.xml">
</mappers>

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

代码语言:javascript复制
<mappers>
	<mapper resource="file:///app/sale/mappers/BlogMapper.xml">
</mappers>

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

代码语言:javascript复制
<mappers>
	<mapper class="com.gupaoedu.mapper.BlogMapper"/>
</mappers>

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

代码语言:javascript复制
<mappers>
	<mapper class="com.gupaoedu.mapper">
</mappers>

4.13.settings

4.14.Mapper.xml映射配置文件

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

  • <cache>-给定命名空间的缓存配置(是否开启二级缓存)。
  • <cache-ref> - 其他命名空间缓存配置的引用。缓存相关两个标签我们在讲解缓存 的时候会详细讲到。
  • <resultMap>-是最复杂也是最强大的元素,用来描述如何从数据库结果集中来加载对象。
  • <sql> - 可被其他语句引用的可重用语句块。

增删改查标签:

  • <insert> - 映射插入语句
  • <update> - 映射更新语句
  • <delete> - 映射删除语句
  • <select> - 映射查询语句

4.15.总结

5.MyBatis最佳实践

5.1.动态SQL

MyBaits的动态SQL就帮助我们解决了这个问题,它是基于OGNL表达式的。 动态标签有哪些?

按照官网的分类,MyBatis的动态标签主要有四类:if, choose (when, otherwise), trim (where, set), foreach

  • if—— 需要判断的时候,条件写在test中:
  • choose (when, otherwise)-----需要选择一个条件的时候:
  • trim (where, set)---- 需要去掉where、and、逗号之类的符号的时候:
  • trim用来指定或者去掉前缀或者后缀:
  • foreach---- 需要遍历集合的时候:

动态SQL主要是用来解决SQL语句生成的问题。

5.2.批量操作

在 MyBatis里面是支持批量的操作的,包括批量的插入、更新、删除。我们可以直 接传入一个List、Set、Map或者数组,配合动态SQL的标签,MyBatis会自动帮我们 生成语法正确的SQL语句。

5.3.批量插入

在 Mapper文件里面,我们使用foreach标签拼接values部分的语句:

可以看到,动态SQL批量插入效率要比循环发送SQL执行要高得多。最关键的地方 就在于减少了跟数据库交互的次数,并且避免了开启和结束事务的时间消耗。

5.4.批量更新

批量更新的语法是这样的,通过case when,来匹配id相关的字段值。

所以在Mapper文件里面最关键的就是case when和 where的配置。

5.5.Batch Executor

当然MyBatis的动态标签的批量操作也是存在一定的缺点的,比如数据量特别大的 时候,拼接出来的SQL语句过大。

在我们的全局配置文件中,可以配置默认的Executor的类型(默认是SIMPLE)。 其中有一种 Batch Executor。

代码语言:javascript复制
<setting name="defaultExecutorType" value="BATCH" />

也可以在创建会话的时候指定执行器类型:

代码语言:javascript复制
SqlSession session = sqlSessionFactory.openSession(ExecutorType.BATCH);

思考:三种类型的区别? (通过doUpdate方法对比)

  1. SimpleExecutor:每执行一次 update 或 select,就开启一个 Statement 对象, 用完立刻关闭Statement对象。
  2. ReuseExecutor:执行 update 或 select,以 sql 作为 key 查找 Statement 对象, 存在就使用,不存在就创建,用完后,不关闭Statement对象 而是放置于Map内, 供下一次使用。简言之,就是重复使用Statement对象。
  3. BatchExecutor:执行 update (没有 select, JDBC 批处理不支持 select),将 所有sql都添加到批处理中(addBatch()), 等待统一执行(executeBatch()), 它缓 存了多个Statement对象,每个Statement对象都是addBatch()完毕后,等待逐一执 行 executeBatchQ批处理。与 JDBC批处理相同。

executeUpdate()是一个语句访问一次数据库,executeBatch。是一批语句访问一次 数据库 (具体一批发送多少条SQL跟服务端的max_allowed_packet有关)。

BatchExecutor 底层是对 JDBC ps.addBatch()和 ps. executeBatch的封装。

5.6.嵌套(关联)查询/ N 1 / 延迟加载

我们在查询业务数据的时候经常会遇到关联查询的情况,比如查询员工就会关联部 门 (一对一), 查询学生成绩就会关联课程(一对一),查询订单就会关联商品(一对 多 )。

映射结果有两个标签,一个是<resultType>,—个是<resultMap>。

<resultType>是select标签的一个属性,适用于返回JDK类型(比如Integer. String 等等)和实体类。这种情况下结果集的列和实体类的属性可以直接映射。如果返回的字 段无法直接映射,就要用<resultMap>来建立映射关系。

对于关联查询的这种情况,通常不能用<resultType>来映射。用<resultMap >映射, 要么就是修改dto (Data Transfer Object), 在里面增加字段,这个会导致增加很多无 关的字段。要么就是引用用关联的对象,比如Blog里面包含了一个Author对象(多对一), 这种情况下就要用到关联查询(association,或者嵌套查询),MyBatis可以帮我们自 动做结果的映射。

association 和 collection 的区别:

association是用于一对一和多对一,而 collection是用于一对多的关系。

一对一的关联查询有两种配置方式:

1、嵌套结果: 单元测试类:com.gupaoedu.MyBatisTest#testSelectBlogWithAuthorResult

代码语言:javascript复制
<!-- 根据文章查询作者,一对一查询的结果,嵌套查询 -->
<resultMap id="BlogWithAuthorResultMap" type="com.gupaoedu.domain.associate.BlogAndAuthor">
    <id column="bid" property="bid" jdbcType="INTEGER"/>
    <result column="name" property="name" jdbcType="VARCHAR"/>
    <!-- 联合查询,将author的属性映射到ResultMap -->
    <association property="author" javaType="com.gupaoedu.domain.Author">
        <id column="author_id" property="authorId"/>
        <result column="author_name" property="authorName"/>
    </association>
</resultMap>

2、嵌套查询:

单元测试类:com.gupaoedu.MyBatisTest#testSelectBlogWithAuthorQuery

代码语言:javascript复制
<!-- 另一种联合查询(一对一)的实现,但是这种方式有“N 1”的问题 -->
<resultMap id="BlogWithAuthorQueryMap" type="com.gupaoedu.domain.associate.BlogAndAuthor">
    <id column="bid" property="bid" jdbcType="INTEGER"/>
    <result column="name" property="name" jdbcType="VARCHAR"/>
    <association property="author" javaType="com.gupaoedu.domain.Author"
                 column="author_id" select="selectAuthor"/> <!-- selectAuthor 定义在下面-->
</resultMap>

<!-- 嵌套查询 -->
<select id="selectAuthor" parameterType="int" resultType="com.gupaoedu.domain.Author">
    select author_id authorId, author_name authorName
    from author where author_id = #{authorId}
</select>

我们只执行了一次查询Blog信息的SQL (所谓的1), 如果返回了 N条记录(比 如 10条 Blog), 因为一个Blog就有至少一个Author,就会再发送N条到数据库查询 Author信 息 (所谓的N), 这个就是我们所说的N 1的问题。这样会白白地浪费我们 的应用和数据库的性能。

如果我们用了嵌套查询的方式,怎么解决这个问题?能不能等到使用Author信息的 时候再去查询?这个就是我们所说的延迟加载,或者叫懒加载。

在 MyBatis里面可以通过开启延迟加载的开关来解决这个问题。

在 settings标签里面可以配置:

代码语言:javascript复制
<!-- 延迟加载的全局开关。当开启时,所有关联对象都会延迟加载。默认 false  -->
<setting name="lazyLoadingEnabled" value="true"/>
<!-- 当开启时,任何方法的调用都会加载该对象的所有属性。默认 false,可通过select标签的 fetchType来覆盖-->
<setting name="aggressiveLazyLoading" value="true"/>
<!--  Mybatis 创建具有延迟加载能力的对象所用到的代理工具,默认JAVASSIST -->
<setting name="proxyFactory" value="CGLIB" />

lazyLoadingEnabled决定了是否延迟加载(默认false)

aggressiveLazyLoading决定了是不是对象的所有方法都会触发查询。

5.7.翻页

在写存储过程的年代,翻页也是一件很难调试的事情,我们要实现数据不多不少准 确地返回,需要大量的调试和修改。但是如果自己手写过分页,就能清楚分页的原理。 逻辑翻页与物理翻页

在我们查询数据库的操作中,有两种翻页方式,一种是逻辑翻页(假分页),一种 是物理翻页(真分页)。逻辑翻页的原理是把所有数据查出来,在内存中删选数据。 物 理翻页是真正的翻页,比如MySQL使用limit语句,Oracle使用rownum语句,SQL Server使用top语句。

5.8.逻辑翻页

MyBatis里面有一个逻辑分页对象RowBounds,里面主要有两个属性,offset和 limit (从第几条开始,查询多少条)

我们可以在Mapper接口的方法上加上这个参数,不需要修改xml里面的SQL语句。

如果数据量大的话,这种翻页方式效率会很低(跟查询到内存中再使用 subList(start/end)没什么区别)。所以我们要用到物理翻页。

5.9.物理翻页

物理翻页是真正的翻页,它是通过数据库支持的语句来翻页。

第一种简单的办法就是传入参数(或者包装一个page对象),在 SQL语句中翻页。

第一个问题是我们要在Java业务代码里面去计算起止序号;第二个问题是:每个需 要翻页的Statement都要编写limit语句,会造成Mapper映射器里面很多代码冗余。

那我们就需要一种通用的方式,不需要去修改配置的任何一条SQL语句,我们只要 传入当前是第几页,每页多少条就可以了,自动计算出来起止序号。

我们最常用的做法就是使用翻页的插件,比如PageHelper

PageHelper是通过MyBatis的拦截器实现的,插件的具体原理我们后面的课再分 析。简单地来说,它会根据PageHelper的参数,改写我们的SQL语句。比如MySQL 会生成limit语句,Oracle会生成rownum语句,SQL Server会生成top语句。

5.10.MBG 与 Example

我们在项目中使用MyBaits的时候,针对需要操作的一张表,需要创建实体类、 Mapper映射器、Mapper接口,里面又有很多的字段和方法的配置,这部分的工作是 非常繁琐的。而大部分时候我们对于表的基本操作是相同的,比如根据主键查询、根据 Map查询、单条插入、批量插入、根据主键删除等等等等。当我们的表很多的时候,意 味着有大量的重复工作。

所以有没有一种办法,可以根据我们的表,自动生成实体类、Mapper映射器、 Mapper接口,里面包含了我们需要用到的这些基本方法和SQL呢?

MyBatis也提供了一个代码生成器,叫做MyBatis Generator,简称MBG (它是 MyBatis的一个插件)。我们只需要修改一个配置文件,使用相关的jar包命令或者Java 代码就可以帮助我们生成实体类、映射器和接口文件。

MBG的配9 置文件里面有一个Example的开关,这个东西用来构造复杂的筛选条件 的,换句话说就是根据我们的代码去生成where条件。

原理:在实体类中包含了两个有继承关系的Criteria,用其中自动生成的方法来构建 查询条件。把这个包含了 Criteria的实体类作为参数传到查询参数中,在解析Mapper 映射器的时候会转换成SQL条件。

5.11.通用Mapper

问题:当我们的表字段发生变化的时候,我们需要修改实体类和Mapper文件定义 的字段和方法。如果是增量维护,那么一个个文件去修改。如果是全量替换,我们还要 去对比用MBG生成的文件。字段变动一次就要修改一次,维护起来非常麻烦。

解决这个问题,我们有两种思路。

第 一 个 , 因 为 MyBatis的 Mapper是 支 持 继 承 的 ( 见 : https://github.com/mybatis/mybatis-3/issues/35 ) 。 所 以 我 们 可 以 把 我 们 的 Mapper.xml和 Mapper接口都分成两个文件。一个是MBG生成的,这部分是固定不变 的。然后创建DAO类继承生成的接口,变化的部分就在DAO里面维护。

所以以后只要修改Ext的文件就可以了。这么做有一个缺点,就是文件会增多。

思考:既然针对每张表生成的基本方法都是一样的,也就是公共的方法部分代码都 是一样的,我们能不能把这部分合并成一个文件,让它支持泛型呢?

编写一个支持泛型的通用接口,比如叫GPBaseMapper<T>,把实体类作为参数传 入。这个接口里面定义了大量的增删改查的基础方法,这些方法都是支持泛型的。

自 定 义 的 Mapper接 口 继 承 该 通 用 接 口 , 例 如 BlogMapper extends GPBaseMapper<Blog>,自动获得对实体类的操作方法。遇到没有的方法,我们依然 可以在我们自己的Mapper里面编写。

我们能想到的解决方案,早就有人做了这个事了,这个东西就叫做通用Mapper。

https://github.com/abel533/Mapper/wiki

用途:主要解决单表的增删改查问题,并不适用于多表关联查询的场景。

除了配置文件变动的问题之外,通用Mapper还可以解决:

1、 每个Mapper接口中大量的重复方法的定义; 2、 屏蔽数据库的差异; 3、 提供批量操作的方法; 4、 实现分页。

使用方式:在 Spring中使用时,引入jar包,替换applicationContext.xml中的 sqISessionFactory 和 configure。

代码语言:javascript复制
<bean class="tk.mybatis.spring.mapper.MapperScannerConfigurer"> 
    <property name="basePackage" value="com.gupaoedu.crud.dao">
</bean>

5.12.MyBatis-Plus

MyBatis-Plus是原生MyBatis的一个增强工具,可以在使用原生MyBatis的所有 功能的基础上,使用p山s特有的功能。

MyBatis-Plus的核心功能:

通 用 C R U D : 定义好Mapper接口后,只需要继承BaseMapper<T>接口即可获得通用的增删改查功能,无需编写缶可接口方法与配置文件。

条件构造器:通过EntityWrapper<T> (实体包装类),可以用于拼接SQL语 句,并且支持排序、分组查询等复杂的SQL。

代码生成器:支持一系列的策略配 置与全局配置,比 MyBatis的代码生成更好 用。 另外MyBatis-Plus也有分页的功能。

6.MyBatis常见问题

6.1.用注解还是用xml配置?

常用注解:@lnsert、@Select、 @Update、 @Delete、@Param、 @Results、 @Result

在 MyBatis的工程中,我们有两种配置SQL的方式。一种是在Mapper.xml中集中 管理,一种是在Mapper接口上,用注解方式您配置SQL。很多同学在工作中可能两种方 式都用过。那到底什么时候用XML的方式,什么时候用注解的方式呢?

注解的缺点是SQL无法集中管理,复杂的SQL很难配置。所以建议在业务复杂的项 目中只使用XML配置的形式,业务简单的项目中可以使用注解和XML混用的形式。

6.2.Mapper 接口无法注入或 Invalid bound statement (not found)

我们在使用MyBatis的时候可能会遇到Mapper接口无法注入,或 者 mapper statement id跟 Mapper接口方法无法绑定的情况。基于绑定的要求或者说规范,我们 可以从这些地方去检查一下:

1、扫描配置,xml文件和Mapper接口有没有被扫描到 2、namespace的值是否和接口全类名一致 3、检查对应的sql语句ID是否存在

6.3.怎么获取插入的最新自动生成的ID

insert成功之后,mybatis会将插入的值自动绑定到插入的对象的Id属性中,我们 用 get Id就能取到最新的ID。

6.4.什么时候用#{},什么时候用${}?

在 Mapper.xml里面配置传入参数,有两种写法:#{)、${}。作为OGNL表达式, 都可以实现参数的替换。这两种方式的区别在哪里?什么时候应该用哪一种?

要搞清楚这个问题,我们要先来说一下PrepareStatement和 Statement的区别。

1、 两个都是接口,PrepareStatement是继承自Statement的;

2、 Statement处理静态SQL, PreparedStatement主要用于执行带参数的语句;

3、 PreparedStatement的 addBatch。方法一次性发送多个查询给数据库;

4、 PS相似SQL只编译一次(对语句进行了缓存,相当于一个函数),比如语句相 同参数不同,可以减少编译次数;

5、PS可以防止SQL注入。

MyBatis任意语句的默认值:PREPARED

这两个符号的解析方式是不一样的:

#会解析为Prepared Statement的参数标记符,参数部分用?代替。传入的参数会 经过类型检查和安全检查。

$只会做字符串替换

#和$的区别:

1、 是否能防止SQL注入:$方式不会对符号转义,不能防止SQL注入

2、 性能:$方式没有预编译,不会缓存

结论:

1、 能用#的地方都用#

2、 常量的替换,比如排序条件中的字段名称,不用加单引号,可以使用$

6.5.XML中怎么使用特殊符号,比如小于&

1、 转义 < < (大于可以直接写)

2、 使用<![CDATA[]]>—— 当 XML遇到这种格式就会把口里面的内容原样输出,不 进行解析

6.6.如何实现模糊查询LIKE

1、字符串拼接 在Java代码中拼接% %,直接LIKE。因为没有预编译,存在SQL注入的风险,不推 荐使用。

2、CONCAT (推荐)

代码语言:javascript复制
<!-- 动态SQL choose -->
<select id="selectBlogListChoose" parameterType="blog" resultMap="BaseResultMap" >
    select bid, name, author_id authorId from blog
    <where>
        <choose>
            <when test="bid !=null">
                bid = #{bid, jdbcType=INTEGER}
            </when>
            <when test="name != null and name != ''">
                AND name LIKE CONCAT(CONCAT('%', #{name, jdbcType=VARCHAR}),'%')
            </when>
            <when test="authorId != null ">
                AND author_id = #{authorId, jdbcType=INTEGER}
            </when>
            <otherwise>
            </otherwise>
        </choose>
    </where>
</select>

3、bind标签

参考资料:

1.咕泡学院·MyBatis应用分析与最佳实践·青山

0 人点赞