Go To My HomePage

mybatis使用指南

一、简介

​ MyBatis 是一款优秀的持久层框架,它支持定制化 SQL、存储过程以及高级映射。MyBatis 避免了几乎所有的 JDBC 代码和手动设置参数以及获取结果集。MyBatis 可以使用简单的 XML 或注解来配置和映射原生信息,将接口和 Java 的 POJOs(Plain Old Java Objects,普通的 Java对象)映射成数据库中的记录。

二、启动

第一步、引入maven依赖

<dependency>
  <groupId>org.mybatis</groupId>
  <artifactId>mybatis</artifactId>
  <version>x.x.x</version>
</dependency>

第二步、构建 SqlSessionFactory

MyBatis 的应用都是以一个 SqlSessionFactory 的实例为核心的,SqlSessionFactory 的实例可以通过 SqlSessionFactoryBuilder 获得。

String resource = "mybatis-config.xml";
InputStream inputStream = Resources.getResourceAsStream(resource);
SqlSessionFactory sqlSessionFactory = new SqlSessionFactoryBuilder().build(inputStream);

简单的mybatis-config.xml

<?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>
  <environments default="development">
    <environment id="development">
      <transactionManager type="JDBC"/>
      <dataSource type="POOLED">
        <property name="driver" value="com.mysql.jdbc.Driver"/>
        <property name="url" value="jdbc:mysql://127.0.0.1:3306/mybatis"/>
        <property name="username" value="root"/>
        <property name="password" value="123456"/>
      </dataSource>
    </environment>
  </environments>
  <mappers>
    <mapper resource="mapper/PersonMapper.xml"/>
  </mappers>
</configuration>

第三步、SqlSessionFactory 中获取 SqlSession

SqlSession session = sqlSessionFactory.openSession();
//com.kun.start.mapper.PersonMapper是PersonMapper.xml的namespace,selectAll具体sql的ID
List<Object> persons = session.selectList("com.kun.start.mapper.PersonMapper.selectAll");
for (Object person : persons) {
  System.out.println(person);
}

也可以使用接口的方式调用

SqlSession session = sqlSessionFactory.openSession();
//会根据PersonMapper全限定命找到对应的PersonMapper.xml,动态代理此接口
PersonMapper mapper = session.getMapper(PersonMapper.class);
List<Person> persons = mapper.selectAll();
for (Person person : persons) {
  System.out.println(person);
}

作用域(Scope)和生命周期

  • SqlSessionFactoryBuilder:一旦创建了 SqlSessionFactory,就不再需要它,即会被丢弃。

  • SqlSessionFactory:SqlSessionFactory 一旦被创建就应该在应用的运行期间一直存在,没有任何理由对它进行清除或重建,应用中应该只存在一个。

  • SqlSession:每个线程都应该有它自己的 SqlSession 实例。SqlSession 的实例不是线程安全的,因此是不能被共享的,所以它的最佳的作用域是请求或方法作用域,不可以放在ThreadLocal当中

  • Mapper Instances:用过之后即可废弃,并不需要显式地关闭映射器实例。

三、XML 配置文件

常用的可能修改默认值的settings

参数 描述 默认值
lazyLoadingEnabled 懒加载的全局开关。当开启时,所有关联对象都会延迟加载。 特定关联关系中可通过设置fetchType属性来覆盖该项的开关状态。 false
aggressiveLazyLoading 当开启时,任何方法的调用都会加载该对象的所有属性。否则,每个属性会按需加载 false (true in ≤3.4.1)
mapUnderscoreToCamelCase 否开启自动驼峰命名规则(camel case)映射 False
lazyLoadTriggerMethods 指定对象的哪个方法触发延迟加载 equals,clone,hashCode,toString
defaultEnumTypeHandler 指定 Enum 使用的默认 TypeHandler 。 (从3.4.5开始) org.apache.ibatis.type.EnumTypeHandler

详细的setting设置

typeAliases设置

类型别名是为 java 类型设置一个短的名字,也可以在类上使用@Alias("xxx")。它只和 XML 配置有关。例如:

<typeAliases>
  <typeAlias alias="Author" type="domain.blog.Author"/>
  <typeAlias alias="Blog" type="domain.blog.Blog"/>
  <typeAlias alias="Comment" type="domain.blog.Comment"/>
</typeAliases>

可能需要更换的默认typeHandlers

类型处理器 Java 类型 JDBC 类型
EnumTypeHandler Enumeration Type VARCHAR-任何兼容的字符串类型,存储枚举的名称(而不是索引)
EnumOrdinalTypeHandler Enumeration Type 任何兼容的 NUMERICDOUBLE 类型,存储枚举的索引(而不是名称)。

自定义枚举处理器:

  1. 枚举类接口,定义统一规范

    public interface BaseEnum<E extends Enum<?>,T> {
      T getCode();
      String getMessage();
    }
    
  2. 自定义枚举类

    public enum InfoEnum implements BaseEnum<InfoEnum,String>{
       
      SUCCESS("200","成功"),
      FAILD("500","错误");
    
      private String code;
      private String message;
    
      static Map<String,InfoEnum> enumMap=new HashMap<String, InfoEnum>();
    
      static{
        (InfoEnum type:InfoEnum.values()){
          enumMap.put(type.getCode(), type);
        }
      }
    
      public static Map<String,InfoEnum> getAll() {
        return enumMap;
      }
    
      private InfoEnum(String code, String message) {
        this.code = code;
        this.message = message;
      }
       
      @Override
      public String getCode() {
        return this.code;
      }
       
      @Override
      public String getMessage() {
        return this.message;
      }
    }
    
  3. 自定义枚举类型处理器

    public class InfoEnumTypeHandler extends BaseTypeHandler<InfoEnum>{
       
      @Override
      public void setNonNullParameter(PreparedStatement ps, int i, InfoEnum parameter, JdbcType jdbcType)
        throws SQLException {
        ps.setString(i, parameter.getCode());
      }
       
      @Override
      public InfoEnum getNullableResult(ResultSet rs, String columnName) throws SQLException {
        String columnValue = rs.getString(columnName);
        return InfoEnum.getAll().get(columnValue);
      }
       
      @Override
      public InfoEnum getNullableResult(ResultSet rs, int columnIndex) throws SQLException {
        String columnValue = rs.getString(columnIndex);
        return InfoEnum.getAll().get(columnValue);
      }
       
      @Override
      public InfoEnum getNullableResult(CallableStatement cs, int columnIndex) throws SQLException {
        String columnValue = cs.getString(columnIndex);
        return InfoEnum.getAll().get(columnValue);
      }
    }
    
  4. 配置自定义类型处理器

    <!-- mybatis-config.xml -->
    <typeHandlers>
      <typeHandler handler="com.kun.start.typehandler.InfoEnumTypeHandler"/>
    </typeHandlers>
    

插件(plugins)

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

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

例自定义插件:

@Intercepts(value= {@Signature(type=Executor.class,method="query",args= {MappedStatement.class,Object.class,RowBounds.class,ResultHandler.class})})
public class ExamplePlugin implements Interceptor{

  @Override
  public Object intercept(Invocation invocation) throws Throwable {
    for (Object arg : invocation.getArgs()) {
      if(arg != null && arg instanceof MappedStatement) {
        MappedStatement ms = (MappedStatement) arg;
        System.out.println("id = " +ms.getId());
      }
    }
    return invocation.proceed();
  }

  @Override
  public Object plugin(Object target) {
    return Plugin.wrap(target, this);
  }

  @Override
  public void setProperties(Properties properties) {
    System.out.println(properties.get("pro"));
  }
}

四、XML映射文件

SQL 映射文件几个顶级元素

  • cache – 给定命名空间的缓存配置。
  • cache-ref – 其他命名空间缓存配置的引用。
  • resultMap – 是最复杂也是最强大的元素,用来描述如何从数据库结果集中来加载对象。
  • sql – 可被其他语句引用的可重用语句块。
  • insert – 映射插入语句
  • update – 映射更新语句
  • delete – 映射删除语句
  • select – 映射查询语句

select

  • 基本,#{id}会在预编译语中被替换为?,当只有一个参数时候,#{param}可以是任意名称,当参数个数大于1时,使用#{arg0},#{arg1},#{arg2}...或者#{param1},#{param2},#{param3}...,此时习惯使用@Param("[参数名称]")来指定参数的名称。

    <select id="selectPerson" parameterType="int" resultType="hashmap">
      SELECT * FROM PERSON WHERE ID = #{id}
    </select>
    
  • 常用可选属性

    属性 描述
    parameterType 将会传入这条语句的参数类的完全限定名或别名。这个属性是可选的,因为 MyBatis 可以通过 TypeHandler 推断出具体传入语句的参数,默认值为 unset
    resultType 从这条语句中返回的期望类型的类的完全限定名或别名。注意如果是集合情形,那应该是集合可以包含的类型,而不能是集合本身。使用 resultType 或 resultMap,但不能同时使用
    resultMap 外部 resultMap 的命名引用。结果集的映射是 MyBatis 最强大的特性,对其有一个很好的理解的话,许多复杂映射的情形都能迎刃而解。使用 resultMap 或 resultType,但不能同时使用
    flushCache 将其设置为 true,任何时候只要语句被调用,都会导致本地缓存和二级缓存都会被清空,默认值:false
    useCache 将其设置为 true,将会导致本条语句的结果被二级缓存,默认值:对 select 元素为 true
    statementType STATEMENT,PREPARED 或 CALLABLE 的一个。这会让 MyBatis 分别使用 Statement,PreparedStatement 或 CallableStatement,默认值:PREPARED

insert, update 和 delete

  • 常用可选属性(insert、update、delete)

    属性 描述
    parameterType 将要传入语句的参数的完全限定类名或别名。这个属性是可选的,因为 MyBatis 可以通过 TypeHandler 推断出具体传入语句的参数,默认值为 unset
    flushCache 将其设置为 true,任何时候只要语句被调用,都会导致本地缓存和二级缓存都会被清空,默认值:true
    statementType STATEMENT,PREPARED 或 CALLABLE 的一个。这会让 MyBatis 分别使用 Statement,PreparedStatement 或 CallableStatement,默认值:PREPARED
    useGeneratedKeys (仅对 insert 和 update 有用)这会令 MyBatis 使用 JDBC 的 getGeneratedKeys 方法来取出由数据库内部生成的主键(比如:像 MySQL 和 SQL Server 这样的关系数据库管理系统的自动递增字段),默认值:false
    keyProperty (仅对 insert 和 update 有用)唯一标记一个属性,MyBatis 会通过 getGeneratedKeys 的返回值或者通过 insert 语句的 selectKey 子元素设置它的键值,默认:unset。如果希望得到多个生成的列,也可以是逗号分隔的属性名称列表
    keyColumn (仅对 insert 和 update 有用)通过生成的键值设置表中的列名,这个设置仅在某些数据库(像 PostgreSQL)是必须的,当主键列不是表中的第一列的时候需要设置。如果希望得到多个生成的列,也可以是逗号分隔的属性名称列表
  • 有些数据库(例如Oracle)主键生成策略是序列化表,所以select引入子标签selectKey

    <insert id="insertUser" > 
      <selectKey resultType="int" keyProperty="id" order="BEFORE"> 
        select SEQ_USER_ID.nextval as id from dual 
      </selectKey> 
      insert into user (id,name,password) values (#{id},#{name},#{password}) 
    </insert>
    

    selectKey相关属性

    属性 描述
    keyProperty selectKey 语句结果应该被设置的目标属性。如果希望得到多个生成的列,也可以是逗号分隔的属性名称列表
    resultType 结果的类型。MyBatis 通常可以推算出来,但是为了更加确定写上也不会有什么问题。
    order 这可以被设置为 BEFORE 或 AFTER。如果设置为 BEFORE,那么它会首先选择主键,设置 keyProperty 然后执行插入语句。如果设置为 AFTER,那么先执行插入语句,然后是 selectKey 元素 - 这和像 Oracle 的数据库相似,在插入语句内部可能有嵌入索引调用
    statementType 与前面相同,MyBatis 支持 STATEMENT,PREPARED 和 CALLABLE 语句的映射类型

sql

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

<sql id="sometable">
  ${prefix}Table
</sql>

<sql id="someinclude">
  from <include refid="${include_target}"/>
</sql>

<select id="select" resultType="map">
  select field1, field2, field3
  <include refid="someinclude">
    <property name="prefix" value="Some"/>
    <property name="include_target" value="sometable"/>
  </include>
</select>

参数

使用 #{} 格式的语法会导致 MyBatis 创建 PreparedStatement 参数并安全地设置参数(使用 ? )。不过有时需要在 SQL 语句中插入一个不转义的字符串。比如 ORDER BY 就需要使用${}

缓存

Mybatis默认情况下是没有开启缓存的,除了局部的 session 缓存,可以增强处理循环。开启二级缓存需要加入cache标签

cache的效果如下:

  • 映射语句文件中的所有 select 语句将会被缓存。
  • 映射语句文件中的所有 insert,update 和 delete 语句会刷新缓存。
  • 缓存会使用 Least Recently Used(LRU,最近最少使用的)算法来收回。
  • 根据时间表(比如 no Flush Interval,没有刷新间隔), 缓存不会以任何时间顺序 来刷新。
  • 缓存会存储列表集合或对象(无论查询方法返回什么)的 1024 个引用。
  • 缓存会被视为是 read/write(可读/可写)的缓存,意味着对象检索不是共享的,而 且可以安全地被调用者修改,而不干扰其他调用者或线程所做的潜在修改。
<!--eviction:回收策略,flushInterval:刷新间隔,默认不设置,size:引用数目,默认1024,readOnly:返回对象是否只读(保证安全性),默认false-->
<cache eviction="FIFO" flushInterval="60000" size="512" readOnly="true"/>

收回策略有:

  • LRU – 最近最少使用的:移除最长时间不被使用的对象。
  • FIFO – 先进先出:按对象进入缓存的顺序来移除它们。
  • SOFT – 软引用:移除基于垃圾回收器状态和软引用规则的对象。
  • WEAK – 弱引用:更积极地移除基于垃圾收集器状态和弱引用规则的对象。

使用cache-ref标签贡献缓存配置实例

<cache-ref namespace="com.someone.application.data.SomeMapper"/>

第三方缓存框架ehcache的使用

第一步:引入maven依赖

<dependency>
  <groupId>org.ehcache</groupId>
  <artifactId>ehcache</artifactId>
  <version>版本号</version>
</dependency>
<dependency>
  <groupId>org.mybatis.caches</groupId>
  <artifactId>mybatis-ehcache</artifactId>
  <version>版本号</version>
</dependency>

第二步:配置ehcache配置文件

<!--name很重要,必须和Mapper的命名空间对应-->
<cache name="com.kun.xxx.PersonMapper" maxElementsInMemory="1024" external="false" timeToIdleSeconds="300" timeToLiveSeconds="600" overflowToDisk="true"/>

第三步:在mybatis配置文件中配置别名

<typeAlias type="org.mybatis.caches.ehcache.EhcacheCache" alias="ehcache" />

第四步:在需要引用缓存的Mapper文件中引用

<cache type="ehcache"/>

五、结果集

resultMap子元素

  • constructor - 用于在实例化类时,注入结果到构造方法中
    • idArg- ID 参数;标记出作为 ID 的结果可以帮助提高整体性能
    • arg - 将被注入到构造方法的一个普通结果
  • id – 标记出作为 ID 的结果可以帮助提高整体性能
  • result – 注入到字段或 JavaBean 属性的普通结果
  • association – 一个复杂类型的关联;许多结果将包装成这种类型
  • collection – 一个复杂类型的集合
  • discriminator – 使用结果值来决定使用哪个
    • case – 基于某些值的结果映射

使用constructor子标签 ,注意多个参数,需要在POJO类构造函数上使用@Param指定名称

<resultMap type="com.kun.start.model.Person" id="person">
  <constructor>
    <idArg column="id" name="id"/>	<!--指定主键提高效率-->
    <arg column="name" name="name"/>
  </constructor>
</resultMap>

使用association 子标签,进行多对一或者一对一关联查询,直接映射结构

<association property="author" column="blog_author_id" javaType="Author">
  <id property="id" column="author_id"/>
  <result property="username" column="author_username"/>
</association>

使用association 子标签,进行多对一或者一对一关联的嵌套查询,这种写法可能引起“N+1”问题,可以使用懒加载或关联的嵌套结果查询

<!--查询博客的作者-->
<resultMap id="blogResult" type="Blog">
  <association property="author" column="author_id" javaType="Author" select="selectAuthor"/>
</resultMap>

使用association 子标签,进行多对一或者一对一关联的嵌套结果查询,避免了“N+1”问题

<!--查询博客的作者-->
<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>

<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>

使用collection子标签,进行集合的嵌套查询,注意ofType指的是元素类型,javaType能推测出来可以不写

<!--查询博客下有多少文章-->
<resultMap id="blogResult" type="Blog">
  <collection property="posts" 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>

使用collection子标签,进行集合的嵌套结果查询,避免多次发送SQL,提高效率

<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>

<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>

使用discriminator 子标签完成鉴别器工作,相当于java中的switch语句。一般用于同表描述继承关系(不建议使用)

<!--根据鉴别字段的值自动映射成对应的子类-->
<resultMap id="vehicleResult" type="Vehicle">
  <id property="id" column="id" />
  <result property="vin" column="vin"/>
  <result property="year" column="year"/>
  <result property="make" column="make"/>
  <result property="model" column="model"/>
  <result property="color" column="color"/>
  <!-- javaType:返回的值类型(用于比较的列)-->
  <discriminator javaType="int" column="vehicle_type">
    <!-- resultType:对应哪个子类结果集,result子标签:子类特有属性 -->
    <case value="1" resultType="carResult">
      <result property="doorCount" column="door_count" />
    </case>
    <case value="2" resultType="truckResult">
      <result property="boxSize" column="box_size" />
      <result property="extendedCab" column="extended_cab" />
    </case>
    <case value="3" resultType="vanResult">
      <result property="powerSlidingDoor" column="power_sliding_door" />
    </case>
    <case value="4" resultType="suvResult">
      <result property="allWheelDrive" column="all_wheel_drive" />
    </case>
  </discriminator>
</resultMap>

六、动态 SQL

if,用于条件判断

<select id="findActiveBlogWithTitleLike"
        resultType="Blog">
  SELECT * FROM BLOG 
  WHERE state = ‘ACTIVE’ 
  <if test="title != null">
    AND title like #{title}
  </if>
</select>

choose, choose-when, choose-otherwise

有时我们不想应用到所有的条件语句,而只想从中择其一项。针对这种情况,MyBatis 提供了 choose 元素,它有点像 Java 中的 switch 语句

<select id="findActiveBlogLike" resultType="Blog">
  SELECT * FROM BLOG WHERE state = ‘ACTIVE’
  <choose>
    <when test="title != null">
      AND title like #{title}
    </when>
    <when test="author != null and author.name != null">
      AND author_name like #{author.name}
    </when>
    <otherwise>
      AND featured = 1
    </otherwise>
  </choose>
</select>

trim, where, set

用于解决where后跟无意义条件例如where 1=1问题

<select id="findActiveBlogLike"
        resultType="Blog">
  SELECT * FROM BLOG 
  <where> 
    <if test="state != null">
      state = #{state}
    </if> 
    <if test="title != null">
      AND title like #{title}
    </if>
    <if test="author != null and author.name != null">
      AND author_name like #{author.name}
    </if>
  </where>
</select>

where 元素只会在至少有一个子元素的条件返回 SQL 子句的情况下才去插入“WHERE”子句。而且,若语句的开头为“AND”或“OR”,where 元素也会将它们去除

类似于where解决select语句问题,set解决update语句问题

<update id="updateAuthorIfNecessary">
  update Author
  <set>
    <if test="username != null">username=#{username},</if>
    <if test="password != null">password=#{password},</if>
    <if test="email != null">email=#{email},</if>
    <if test="bio != null">bio=#{bio}</if>
  </set>
  where id=#{id}
</update>

这里,set元素会动态前置 SET 关键字,同时也会删掉无关的逗号,因为用了条件语句之后很可能就会在生成的 SQL 语句的后面留下这些逗号

where和set底层都是使用trim。prefix/suffix-加入的前(后)缀,prefixOverrides/suffixOverrides-前后覆盖的字符

<!--相当于set标签-->
<trim prefix="set" prefixOverrides="," suffixOverrides=",">
  ...		
</trim>

<!--相当于where标签-->
<trim prefix="where" prefixOverrides="and|or"  suffixOverrides=",">
  ...		
</trim>

foreach

动态 SQL 的另外一个常用的操作需求是对一个集合进行遍历,通常是在构建 IN 条件语句的时候。比如:

<select id="selectPersonIn" resultType="com.kun.Person">
  SELECT * FROM Person WHERE ID in
  <foreach item="item" index="index" collection="ids" open="(" separator="," close=")">
    #{item}
  </foreach>
</select>

item-变量集合去除的元素,index-元素的索引,collection-集合参数名称。

集合类型可以是list、set、map的key、map的value。【index 作为map 的key。item为map的值】,例如:

<foreach collection="ent"  item="id" separator="," open="(" close=")">
  #{id}
</foreach>

bind

可以将OGNL表达式的值绑定到一个变量中,方便后来引用这个变量的值。

<select id="getEmps" resultType="com.hand.mybatis.bean.Employee">
  <bind name="bindeName" value="'%'+eName+'%'"/> <!--eName是employee中一个属性值-->
  SELECT * FROM emp 
  <if test="_parameter!=null">
    where ename like #{bindeName}
  </if>
</select>