Mybatis最全学习文档

1.Mybatis

1. 简介

  1. 什么是Mybatis
    1. MyBatis 是一款优秀的持久层框架,它支持自定义 SQL、存储过程以及高级映射
    2. MyBatis 免除了几乎所有的 JDBC 代码以及设置参数和获取结果集的工作
    3. MyBatis 可以通过简单的 XML 或注解来配置和映射原始类型、接口和 Java POJO(Plain Old Java Objects,普通老式 Java 对象)为数据库中的记录

2. 组件以及组件的最佳作用域

1. SqlSessionFactoryBuilder

  1. 这个类可以被实例化、使用和丢弃,一旦创建了 SqlSessionFactory,就不再需要它了
  2. 因此 SqlSessionFactoryBuilder 实例的最佳作用域是方法作用域(也就是局部方法变量)
  3. 你可以重用 SqlSessionFactoryBuilder 来创建多个 SqlSessionFactory 实例,但最好还是不要一直保留着它,以保证所有的 XML 解析资源可以被释放

2. SqlSessionFactory

  1. 每个基于 MyBatis 的应用都是以一个 SqlSessionFactory 的实例为核心的

  2. SqlSessionFactory 的实例可以通过 SqlSessionFactoryBuilder 获得。而 SqlSessionFactoryBuilder 则可以从 XML 配置文件或一个预先配置的 Configuration 实例来构建出 SqlSessionFactory 实例

  3. SqlSessionFactory 一旦被创建就应该在应用的运行期间一直存在,没有任何理由丢弃它或重新创建另一个实例

  4. 使用 SqlSessionFactory 的最佳实践是在应用运行期间不要重复创建多次,多次重建 SqlSessionFactory 被视为一种代码“坏习惯”。因此 SqlSessionFactory 的最佳作用域是Application作用域。 有很多方法可以做到,最简单的就是使用单例模式或者静态单例模式

  5. 从 XML 文件中构建 SqlSessionFactory 的实例非常简单,建议使用类路径下的资源文件进行配置,但也可以使用任意的输入流(InputStream)实例,比如用文件路径字符串或 file:// URL 构造的输入流

  6. MyBatis 包含一个名叫 Resources 的工具类,它包含一些实用方法,使得从类路径或其它位置加载资源文件更加容易

  7. 获取SqlSessionFactory

    1. xml配置文件获取
    String resource = "org/mybatis/example/mybatis-config.xml";
    InputStream inputStream = Resources.getResourceAsStream(resource);
    SqlSessionFactory sqlSessionFactory = new SqlSessionFactoryBuilder().build(inputStream);
    
    1. 配置类获取
    DataSource dataSource = BlogDataSourceFactory.getBlogDataSource();
    TransactionFactory transactionFactory = new JdbcTransactionFactory();
    Environment environment = new Environment("development", transactionFactory, dataSource);
    Configuration configuration = new Configuration(environment);
    configuration.addMapper(BlogMapper.class);
    SqlSessionFactory sqlSessionFactory = new SqlSessionFactoryBuilder().build(configuration);
    
    1. Mybatis配置文件
      1. XML 配置文件中包含了对 MyBatis 系统的核心设置
      2. 包括获取数据库连接实例的数据源(DataSource)以及决定事务作用域和控制方式的事务管理器(TransactionManager)
    <?xml version="1.0" encoding="UTF-8" ?>
    <!DOCTYPE configuration
      PUBLIC "-//mybatis.org//DTD Config 3.0//EN"
      "https://mybatis.org/dtd/mybatis-3-config.dtd">
    <configuration>
      <environments default="development">
        <environment id="development">
          <transactionManager type="JDBC"/>
          <dataSource type="POOLED">
            <property name="driver" value="${driver}"/>
            <property name="url" value="${url}"/>
            <property name="username" value="${username}"/>
            <property name="password" value="${password}"/>
          </dataSource>
        </environment>
      </environments>
      <mappers>
        <mapper resource="org/mybatis/example/BlogMapper.xml"/>
      </mappers>
    </configuration>
    

3. SqlSession

  1. 提供了在数据库执行 SQL 命令所需的所有方法。你可以通过 SqlSession 实例来直接执行已映射的 SQL 语句

  2. 每个线程都应该有它自己的 SqlSession 实例

  3. SqlSession 的实例不是线程安全的,因此是不能被共享的,所以它的最佳的作用域是请求(request)或方法作用域

    1. 注意
      1. 绝对不能将 SqlSession 实例的引用放在一个类的静态域
      2. 甚至一个类的实例变量也不行
      3. 也绝不能将 SqlSession 实例的引用放在任何类型的托管作用域中,比如 Servlet 框架中的 HttpSession
  4. 如果你现在正在使用一种 Web 框架,考虑将 SqlSession 放在一个和 HTTP 请求相似的作用域中, 换句话说,每次收到 HTTP 请求,就可以打开一个 SqlSession,返回一个响应后,就关闭它。 这个关闭操作很重要,为了确保每次都能执行关闭操作,你应该把这个关闭操作放到 finally 块中。 下面的示例就是一个确保 SqlSession 关闭的标准模式:

try (SqlSession session = sqlSessionFactory.openSession()) {
  Blog blog = (Blog) session.selectOne("org.mybatis.example.BlogMapper.selectBlog", 101);
}


try (SqlSession session = sqlSessionFactory.openSession()) {
  BlogMapper mapper = session.getMapper(BlogMapper.class);
  Blog blog = mapper.selectBlog(101);
}
  1. Mapper

    1. 接口

      1. 映射器是一些绑定映射语句的接口
      2. 映射器接口的实例是从 SqlSession 中获得的
      3. 虽然从技术层面上来讲,任何映射器实例的最大作用域与请求它们的 SqlSession 相同。但方法作用域才是映射器实例的最合适的作用域。也就是说,映射器实例应该在调用它们的方法中被获取,使用完毕之后即可丢弃。
      4. 映射器实例并不需要被显式地关闭。尽管在整个请求作用域保留映射器实例不会有什么问题,但是你很快会发现,在这个作用域上管理很多 SqlSession 的资源会让你忙不过来。 因此,最好将映射器放在方法作用域内
      public interface BlogMapper {
        Blog selectBlog(int id);
      }
      
      try (SqlSession session = sqlSessionFactory.openSession()) {
        BlogMapper mapper = session.getMapper(BlogMapper.class);
        // 你的应用逻辑代码
      }
      
    2. xml

      1. 负责给出接口定义的执行SQL,从POJO到数据库记录的映射关系定义
      <?xml version="1.0" encoding="UTF-8" ?>
      <!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "https://mybatis.org/dtd/mybatis-3-mapper.dtd">
        
      <mapper namespace="org.mybatis.example.BlogMapper">
        <select id="selectBlog" resultType="Blog">
          select * from Blog where id = #{id}
        </select>
      </mapper>
      
      1. 调用接口的定义实现
        1. namespace+方法名,命名空间就是用来进行Mapper隔离的一种机制
      try (SqlSession session = sqlSessionFactory.openSession()) {
       Blog blog = (Blog) session.selectOne("org.mybatis.example.BlogMapper.selectBlog", 101);
       
       // 升级版,不使用方法全限定名,直接使用接口名称获取
       BlogMapper mapper = session.getMapper(BlogMapper.class);
       Blog blog = mapper.selectBlog(101);
        // 你的应用逻辑代码
      }
      
      1. xml的命名空间
        1. 在之前版本的 MyBatis 中,命名空间(Namespaces) 的作用并不大,是可选的。但现在,随着命名空间越发重要,你必须指定命名空间。
        2. 命名空间的作用有两个,一个是利用更长的全限定名来将不同的语句隔离开来,同时也实现了接口绑定
        3. 就算你觉得暂时用不到接口绑定,你也应该遵循这里的规定,以防哪天你改变了主意。 长远来看,只要将命名空间置于合适的 Java 包命名空间之中,你的代码会变得更加整洁,也有利于你更方便地使用 MyBatis
        4. 命名解析
          1. 为了减少输入量,MyBatis 对所有具有名称的配置元素(包括语句,结果映射,缓存等)使用了如下的命名解析规则。
          2. 全限定名(比如 “com.mypackage.MyMapper.selectAllThings)将被直接用于查找及使用。
          3. 短名称(比如 “selectAllThings”)如果全局唯一也可以作为一个单独的引用。 如果不唯一,有两个或两个以上的相同名称(比如 “com.foo.selectAllThings” 和 “com.bar.selectAllThings”),那么使用时就会产生“短名称不唯一”的错误,这种情况下就必须使用全限定名
    3. 注解

      package org.mybatis.example;
      public interface BlogMapper {
        @Select("SELECT * FROM blog WHERE id = #{id}")
        Blog selectBlog(int id);
      }
      

3. Mybatis的XML配置文件

<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE configuration PUBLIC "-//mybatis.org//DTD Config 3.0//EN" "https://mybatis.org/dtd/mybatis-3-config.dtd">
<configuration>
</configuration>

1. configuration

  1. 全局配置

2. properties

  1. 属性配置
<properties resource="org/mybatis/example/config.properties">
  <property name="username" value="dev_user"/>
  <property name="password" value="F2Fa3!33TYyg"/>
   <!-- 启用默认值特性 -->
   <property name="org.apache.ibatis.parsing.PropertyParser.enable-default-value" value="true"/>
    <!-- 修改默认值的分隔符,默认为:分割,如果需要使用三元运算符,可以修改为?: -->
   <property name="org.apache.ibatis.parsing.PropertyParser.default-value-separator" value="?:"/>
</properties>
  1. 设置好的属性可以直接在整个配置文件中动态替换属性值
<dataSource type="POOLED">
  <property name="driver" value="${driver}"/>
  <property name="url" value="${url}"/>
  <property name="username" value="${username:默认值}"/>
  <property name="password" value="${password}"/>
</dataSource>
  1. Java代码配置
SqlSessionFactory factory = new SqlSessionFactoryBuilder().build(reader, props);

SqlSessionFactory factory = new SqlSessionFactoryBuilder().build(reader, environment, props);
  1. 配置的优先顺序(加载顺序)
    1. properties标签内的属性
    2. resource/url属性对应的配置文件中的属性
    3. java代码传递的属性
    4. 以上的属性存在同名会进行覆盖,因此,最终的属性优先级和加载顺序相反

3. settings(所有配置)

  1. Mybatis的设置配置信息

    <settings>
      <setting name="cacheEnabled" value="true"/>
      <setting name="lazyLoadingEnabled" value="true"/>
      <setting name="aggressiveLazyLoading" value="true"/>
      <setting name="multipleResultSetsEnabled" value="true"/>
      <setting name="useColumnLabel" value="true"/>
      <setting name="useGeneratedKeys" value="false"/>
      <setting name="autoMappingBehavior" value="PARTIAL"/>
      <setting name="autoMappingUnknownColumnBehavior" value="WARNING"/>
      <setting name="defaultExecutorType" value="SIMPLE"/>
      <setting name="defaultStatementTimeout" value="25"/>
      <setting name="defaultFetchSize" value="100"/>
      <setting name="safeRowBoundsEnabled" value="false"/>
      <setting name="safeResultHandlerEnabled" value="true"/>
      <setting name="mapUnderscoreToCamelCase" value="false"/>
      <setting name="localCacheScope" value="SESSION"/>
      <setting name="jdbcTypeForNull" value="OTHER"/>
      <setting name="lazyLoadTriggerMethods" value="equals,clone,hashCode,toString"/>
      <setting name="defaultScriptingLanguage" value="org.apache.ibatis.scripting.xmltags.XMLLanguageDriver"/>
      <setting name="defaultEnumTypeHandler" value="org.apache.ibatis.type.EnumTypeHandler"/>
      <setting name="callSettersOnNulls" value="false"/>
      <setting name="returnInstanceForEmptyRow" value="false"/>
      <setting name="logPrefix" value="exampleLogPreFix_"/>
      <setting name="logImpl" value="SLF4J | LOG4J | LOG4J2 | JDK_LOGGING | COMMONS_LOGGING | STDOUT_LOGGING | NO_LOGGING"/>
      <setting name="proxyFactory" value="CGLIB | JAVASSIST"/>
      <setting name="vfsImpl" value="org.mybatis.example.YourselfVfsImpl"/>
      <setting name="useActualParamName" value="true"/>
      <setting name="configurationFactory" value="org.mybatis.example.ConfigurationFactory"/>
    </settings>
    
设置名描述有效值默认值
cacheEnabled全局性地开启或关闭所有映射器配置文件中已配置的任何缓存。true | falsetrue
lazyLoadingEnabled延迟加载的全局开关。当开启时,所有关联对象都会延迟加载, 特定关联关系中可通过设置 fetchType 属性来覆盖该项的开关状态。true | falsefalse
aggressiveLazyLoading开启时,任一方法的调用都会加载该对象的所有延迟加载属性。 否则,每个延迟加载属性会按需加载(参考 lazyLoadTriggerMethods)。true | falsefalse (在 3.4.1 及之前的版本中默认为 true)
multipleResultSetsEnabled是否允许单个语句返回多结果集(需要数据库驱动支持)。true | falsetrue
useColumnLabel使用列标签代替列名。实际表现依赖于数据库驱动,具体可参考数据库驱动的相关文档,或通过对比测试来观察。true | falsetrue
useGeneratedKeys允许 JDBC 支持自动生成主键,需要数据库驱动支持。如果设置为 true,将强制使用自动生成主键。尽管一些数据库驱动不支持此特性,但仍可正常工作(如 Derby)。true | falseFalse
autoMappingBehavior指定 MyBatis 应如何自动映射列到字段或属性。 NONE 表示关闭自动映射;PARTIAL 只会自动映射没有定义嵌套结果映射的字段。 FULL 会自动映射任何复杂的结果集(无论是否嵌套)。NONE, PARTIAL, FULLPARTIAL
autoMappingUnknownColumnBehaviorvariables指定发现自动映射目标未知列(或未知属性类型)的行为。NONE: 不做任何反应WARNING: 输出警告日志('org.apache.ibatis.session.AutoMappingUnknownColumnBehavior' 的日志等级必须设置为 WARNFAILING: 映射失败 (抛出 SqlSessionException)Note that there could be false-positives when autoMappingBehavior is set to FULL.NONE, WARNING, FAILINGNONE
reflectorFactorydefaultExecutorType配置默认的执行器。SIMPLE 就是普通的执行器;REUSE 执行器会重用预处理语句(PreparedStatement); BATCH 执行器不仅重用语句还会执行批量更新。SIMPLE REUSE BATCHSIMPLE
defaultStatementTimeout设置超时时间,它决定数据库驱动等待数据库响应的秒数。任意正整数未设置 (null)
defaultFetchSize为驱动的结果集获取数量(fetchSize)设置一个建议值。此参数只可以在查询设置中被覆盖。任意正整数未设置 (null)
defaultResultSetType指定语句默认的滚动策略。(新增于 3.5.2)defaultFetchSizeFORWARD_ONLY | SCROLL_SENSITIVE | SCROLL_INSENSITIVE | DEFAULT(等同于未设置)未设置 (null)
safeRowBoundsEnabled是否允许在嵌套configurationFactory语句中使用分页(RowBounds)。如果允许使用则设置为 false。true | falseFalse
safeResultHandlerEnabled是否允许在嵌套语句中使用结果处理器(ResultHandler)。如果允许使用则设置为 false。true | falseTrue
mapUnderscoreToCamelCase是否开启驼峰命名自动映射,即从经典数据库列名 A_COLUMN 映射到经典 Java 属性名 aColumn。true | falseFalse
localCacheScopeMyBatis 利用本地缓存机制(Local Cache)防止循环引用和加速重复的嵌套查询。 默认值为 SESSION,会缓存一个会话中执行的所有查询。 若设置值为 STATEMENT,本地缓存将仅用于执行语句,对相同 SqlSession 的不同查询将不会进行缓存。SESSION | STATEMENTSESSION
aggressiveLazyLoadingjdbcTypeForNull当没有为参数指定特定的 JDBC 类型时,空值的默认 JDBC 类型。 某些数据库驱动需要指定列的 JDBC 类型,多数情况直接用一般类型即可,比如 NULL、VARCHAR 或 OTHER。JdbcType 常量,常用值:NULL、VARCHAR 或 OTHER。OTHER
lazyLoadTriggerMethods指定对象的哪些方法触发一次延迟加载。用逗号分隔的方法列表。equals,clone,hashCode,toString
defaultScriptingLanguage指定动态 SQL 生成使用的默认脚本语言。一个类型别名或全限定类名。org.apache.ibatis.scripting.xmltags.XMLLanguageDriver
defaultEnumTypeHandler指定 Enum 使用的默认 TypeHandler 。(新增于 3.4.5)一个类型别名或全限定类名。org.apache.ibatis.type.EnumTypeHandler
callSettersOnNulls指定当结果集中值为 null 的时候是否调用映射对象的 setter(map 对象时为 put)方法,这在依赖于 Map.keySet() 或 null 值进行初始化时比较有用。注意基本类型(int、boolean 等)是不能设置成 null 的。true | falsefalse
returnInstanceForEmptyRow当返回行的所有列都是空时,MyBatis默认返回 null。 当开启这个设置时,MyBatis会返回一个空实例。 请注意,它也适用于嵌套的结果集(如集合或关联)。(新增于 3.4.2)true | falsefalse
logPrefix指定 MyBatis 增加到日志名称的前缀。任何字符串未设置
logImpl指定 MyBatis 所用日志的具体实现,未指定时将自动查找。SLF4J | LOG4J(3.5.9 起废弃) | LOG4J2 | JDK_LOGGING | COMMONS_LOGGING | STDOUT_LOGGING | NO_LOGGING未设置
proxyFactory指定 Mybatis 创建可延迟加载对象所用到的代理工具。CGLIB (3.5.10 起废弃) | JAVASSISTJAVASSIST (MyBatis 3.3 以上)
vfsImpl指定 VFS 的实现自定义 VFS 的实现的类全限定名,以逗号分隔。未设置
useActualParamName允许使用方法签名中的名称作为语句参数名称。 为了使用该特性,你的项目必须采用 Java 8 编译,并且加上 -parameters 选项。(新增于 3.4.1)true | falsetrue
configurationFactory指定一个提供 Configuration 实例的类。 这个被返回的 Configuration 实例用来加载被反序列化对象的延迟加载属性值。 这个类必须包含一个签名为static Configuration getConfiguration() 的方法。(新增于 3.2.3)一个类型别名或完全限定类名。未设置
shrinkWhitespacesInSql从SQL中删除多余的空格字符。请注意,这也会影响SQL中的文字字符串。 (新增于 3.5.5)true | falsefalse
defaultSqlProviderType指定一个拥有 provider 方法的 sql provider 类 (新增于 3.5.6). 这个类适用于指定 sql provider 注解上的type(或 value) 属性(当这些属性在注解中被忽略时)。 (e.g. @SelectProvider)类型别名或者全限定名未设置
nullableOnForEach为 ‘foreach’ 标签的 ‘nullable’ 属性指定默认值。(新增于 3.5.9)true | falsefalse
argNameBasedConstructorAutoMapping当应用构造器自动映射时,参数名称被用来搜索要映射的列,而不再依赖列的顺序。(新增于 3.5.10)true | falsefalse

4. typeAliases

  1. 类型别名定义

    1. 可以给java的类型设置一个缩写的名称,它仅用于XML配置,降低全限定名的书写

      <typeAliases>
        <typeAlias alias="Author" type="domain.blog.Author"/>
        <typeAlias alias="Blog" type="domain.blog.Blog"/>
        <typeAlias alias="Comment" type="domain.blog.Comment"/>
        <typeAlias alias="Post" type="domain.blog.Post"/>
        <typeAlias alias="Section" type="domain.blog.Section"/>
        <typeAlias alias="Tag" type="domain.blog.Tag"/>
        
        # 设置包的所有类的别名
        <package name="domain.blog"/>
      </typeAliases>
      
      1. 如果设置的是package name属性,则默认为类名简写的首字母小写
    2. 注解配置别名

      @Alias("author")
      public class Author {
      }
      
  2. 默认的别名定义信息

    别名映射的类型
    _bytebyte
    _char (since 3.5.10)char
    _character (since 3.5.10)char
    _longlong
    _shortshort
    _intint
    _integerint
    _doubledouble
    _floatfloat
    _booleanboolean
    stringString
    byteByte
    char (since 3.5.10)Character
    character (since 3.5.10)Character
    longLong
    shortShort
    intInteger
    integerInteger
    doubleDouble
    floatFloat
    booleanBoolean
    dateDate
    decimalBigDecimal
    bigdecimalBigDecimal
    bigintegerBigInteger
    objectObject
    date[]Date[]
    decimal[]BigDecimal[]
    bigdecimal[]BigDecimal[]
    biginteger[]BigInteger[]
    object[]Object[]
    mapMap
    hashmapHashMap
    listList
    arraylistArrayList
    collectionCollection
    iteratorIterator

5. typeHandlers

  1. java->db的类型转换器

    <typeHandlers>
      <typeHandler handler="org.mybatis.example.ExampleTypeHandler"/>
    </typeHandlers>
    
  2. MyBatis 在设置预处理语句(PreparedStatement)中的参数或从结果集中取出一个值时, 都会用类型处理器将获取到的值以合适的方式转换成 Java 类型

  3. Mybatis体提供的默认的类型处理器

类型处理器Java 类型JDBC 类型
BooleanTypeHandlerjava.lang.Boolean, boolean数据库兼容的 BOOLEAN
ByteTypeHandlerjava.lang.Byte, byte数据库兼容的 NUMERICBYTE
ShortTypeHandlerjava.lang.Short, short数据库兼容的 NUMERICSMALLINT
IntegerTypeHandlerjava.lang.Integer, int数据库兼容的 NUMERICINTEGER
LongTypeHandlerjava.lang.Long, long数据库兼容的 NUMERICBIGINT
FloatTypeHandlerjava.lang.Float, float数据库兼容的 NUMERICFLOAT
DoubleTypeHandlerjava.lang.Double, double数据库兼容的 NUMERICDOUBLE
BigDecimalTypeHandlerjava.math.BigDecimal数据库兼容的 NUMERICDECIMAL
StringTypeHandlerjava.lang.StringCHAR, VARCHAR
ClobReaderTypeHandlerjava.io.Reader-
ClobTypeHandlerjava.lang.StringCLOB, LONGVARCHAR
NStringTypeHandlerjava.lang.StringNVARCHAR, NCHAR
NClobTypeHandlerjava.lang.StringNCLOB
BlobInputStreamTypeHandlerjava.io.InputStream-
ByteArrayTypeHandlerbyte[]数据库兼容的字节流类型
BlobTypeHandlerbyte[]BLOB, LONGVARBINARY
DateTypeHandlerjava.util.DateTIMESTAMP
DateOnlyTypeHandlerjava.util.DateDATE
TimeOnlyTypeHandlerjava.util.DateTIME
SqlTimestampTypeHandlerjava.sql.TimestampTIMESTAMP
SqlDateTypeHandlerjava.sql.DateDATE
SqlTimeTypeHandlerjava.sql.TimeTIME
ObjectTypeHandlerAnyOTHER 或未指定类型
EnumTypeHandlerEnumeration TypeVARCHAR 或任何兼容的字符串类型,用来存储枚举的名称(而不是索引序数值)
EnumOrdinalTypeHandlerEnumeration Type任何兼容的 NUMERICDOUBLE 类型,用来存储枚举的序数值(而不是名称)。
SqlxmlTypeHandlerjava.lang.StringSQLXML
InstantTypeHandlerjava.time.InstantTIMESTAMP
LocalDateTimeTypeHandlerjava.time.LocalDateTimeTIMESTAMP
LocalDateTypeHandlerjava.time.LocalDateDATE
LocalTimeTypeHandlerjava.time.LocalTimeTIME
OffsetDateTimeTypeHandlerjava.time.OffsetDateTimeTIMESTAMP
OffsetTimeTypeHandlerjava.time.OffsetTimeTIME
ZonedDateTimeTypeHandlerjava.time.ZonedDateTimeTIMESTAMP
YearTypeHandlerjava.time.YearINTEGER
MonthTypeHandlerjava.time.MonthINTEGER
YearMonthTypeHandlerjava.time.YearMonthVARCHARLONGVARCHAR
JapaneseDateTypeHandlerjava.time.chrono.JapaneseDateDATE
  1. 自定义的类型处理器

    1. 实现TypeHandler接口或者继承BaseTypeHandler
    @MappedJdbcTypes(JdbcType.VARCHAR)
    public class ExampleTypeHandler extends BaseTypeHandler<String> {
    
      @Override
      public void setNonNullParameter(PreparedStatement ps, int i, String parameter, JdbcType jdbcType) throws SQLException {
        ps.setString(i, parameter);
      }
    
      @Override
      public String getNullableResult(ResultSet rs, String columnName) throws SQLException {
        return rs.getString(columnName);
      }
    
      @Override
      public String getNullableResult(ResultSet rs, int columnIndex) throws SQLException {
        return rs.getString(columnIndex);
      }
    
      @Override
      public String getNullableResult(CallableStatement cs, int columnIndex) throws SQLException {
        return cs.getString(columnIndex);
      }
    }
    

6. objectFacotry

  1. 对象工厂

    <!-- mybatis-config.xml -->
    <objectFactory type="org.mybatis.example.ExampleObjectFactory">
      <property name="someProperty" value="100"/>
    </objectFactory>
    
  2. 每次 MyBatis 创建结果对象的新实例时,它都会使用一个对象工厂(ObjectFactory)实例来完成实例化工作

  3. 默认的对象工厂需要做的仅仅是实例化目标类,要么通过默认无参构造方法,要么通过存在的参数映射来调用带有参数的构造方法

  4. 如果想覆盖对象工厂的默认行为,可以通过创建自己的对象工厂来实现

public class ExampleObjectFactory extends DefaultObjectFactory {
  // 无参构造方法
  @Override
  public <T> T create(Class<T> type) {
    return super.create(type);
  }

  // 有参构造方法
  @Override
  public <T> T create(Class<T> type, List<Class<?>> constructorArgTypes, List<Object> constructorArgs) {
    return super.create(type, constructorArgTypes, constructorArgs);
  }

  // 在<objectFactory>内部配置的属性会设置到当前方法中,用于配置对象工厂
  @Override
  public void setProperties(Properties properties) {
    super.setProperties(properties);
  }

  @Override
  public <T> boolean isCollection(Class<T> type) {
    return Collection.class.isAssignableFrom(type);
  }
}

7. plugins

  1. 动态增强功能的插件

    <!-- mybatis-config.xml -->
    <plugins>
      <plugin interceptor="org.mybatis.example.ExamplePlugin">
        <property name="someProperty" value="100"/>
      </plugin>
    </plugins>
    
  2. MyBatis 允许你在映射语句执行过程中的某一点进行拦截调用

    1. 默认情况下,MyBatis 允许使用插件来拦截的方法
      1. Executor (update, query, flushStatements, commit, rollback, getTransaction, close, isClosed)
      2. ParameterHandler (getParameterObject, setParameters)
      3. ResultSetHandler (handleResultSets, handleOutputParameters)
      4. StatementHandler (prepare, parameterize, batch, update, query)
    2. 通过 MyBatis 提供的强大机制,使用插件是非常简单的,只需实现 Interceptor 接口,并指定想要拦截的方法签名即可
  3. 定义插件

    @Intercepts({@Signature(type= Executor.class,method = "update",args = {MappedStatement.class,Object.class})})
    public class ExamplePlugin implements Interceptor {
      private Properties properties = new Properties();
    
      @Override
      public Object intercept(Invocation invocation) throws Throwable {
        // implement pre processing if need
        Object returnObject = invocation.proceed();
        return returnObject;
      }
    
      @Override
      public void setProperties(Properties properties) {
        this.properties = properties;
      }
    }
    
    1. 上面的插件将会拦截在 Executor 实例中所有的 “update” 方法调用, Executor 是负责执行底层映射语句的内部对象
  4. 覆盖配置类达到插件的作用

    1. 除了用插件来修改 MyBatis 核心行为以外,还可以通过完全覆盖配置类来达到目的
    2. 只需继承配置类Configuration后覆盖其中的某个方法,再把它传递到 SqlSessionFactoryBuilder.build(myConfig) 方法即可
    3. 注意: 这样做=可能会极大影响 MyBatis 的运行行为,请慎之又慎

8. environments

  1. 环境配置

    <environments default="development">
      <environment id="development">
        <transactionManager type="JDBC">
          <property name="..." value="..."/>
        </transactionManager>
        <dataSource type="POOLED">
          <property name="driver" value="${driver}"/>
          <property name="url" value="${url}"/>
          <property name="username" value="${username}"/>
          <property name="password" value="${password}"/>
        </dataSource>
      </environment>
    </environments>
    
    1. 每个环境都存在一个唯一的ID,默认的Id为development的环境

    2. 每个环境中需要配置

      1. 事务管理器

        1. 在 MyBatis 中有两种类型的事务管理器(也就是 type=“[JDBC|MANAGED]”):

          1. JDBC

            1. 这个配置直接使用了 JDBC 的提交和回滚功能,它依赖从数据源获得的连接来管理事务作用域
            2. 默认情况下,为了与某些驱动程序兼容,它在关闭连接时启用自动提交
            3. 然而,对于某些驱动程序来说,启用自动提交不仅是不必要的,而且是一个代价高昂的操作。因此,从 3.5.10 版本开始,你可以通过将 “skipSetAutoCommitOnClose” 属性设置为 “true” 来跳过这个步骤
            <transactionManager type="JDBC">
              <property name="skipSetAutoCommitOnClose" value="true"/>
            </transactionManager>
            
          2. MANAGED

            1. 这个配置几乎没做什么
            2. 它从不提交或回滚一个连接,而是让容器来管理事务的整个生命周期(比如 JEE 应用服务器的上下文)
            3. 默认情况下它会关闭连接,然而一些容器并不希望连接被关闭,因此需要将 closeConnection 属性设置为 false 来阻止默认的关闭行为
            <transactionManager type="MANAGED">
              <property name="closeConnection" value="false"/>
            </transactionManager>
            
          3. 这两种事务管理器类型都不需要设置任何属性,他们其实就是实际类的别名,我们可以使用TransactionFactory 接口实现类的全限定名或类型别名代替它们

            public interface TransactionFactory {
              default void setProperties(Properties props) { // 从 3.5.2 开始,该方法为默认方法
                // 空实现
              }
              Transaction newTransaction(Connection conn);
              Transaction newTransaction(DataSource dataSource, TransactionIsolationLevel level, boolean autoCommit);
            }
            
            1. 在事务管理器实例化后,所有在 XML 中配置的属性将会被传递给 setProperties() 方法。你的实现还需要创建一个 Transaction 接口的实现类

              public interface Transaction {
                Connection getConnection() throws SQLException;
                void commit() throws SQLException;
                void rollback() throws SQLException;
                void close() throws SQLException;
                Integer getTimeout() throws SQLException;
              }
              
            2. 使用这两个接口TransactionFactory+Transaction,就可以完全自定义Mybatis的事务处理

          4. 如果使用Spring+Mybatis,则没有必要配置事务管理器,因为Spring会使用自动的管理器来覆盖前面的配置

      2. 数据源

        1. dataSource 元素使用标准的 JDBC 数据源接口来配置 JDBC 连接对象的资源

        2. 大多数 MyBatis 应用程序会按上面示例中的例子来配置数据源。虽然数据源配置是可选的,但如果要启用延迟加载特性,就必须配置数据源。

        3. 有三种内建的数据源类型(也就是 type=“[UNPOOLED|POOLED|JNDI]”)

          1. UNPOOLED

            1. 这个数据源的实现会每次请求时打开和关闭连接,虽然有点慢,但对那些数据库连接可用性要求不高的简单应用程序来说,是一个很好的选择。
            2. 性能表现则依赖于使用的数据库,对某些数据库来说,使用连接池并不重要,这个配置就很适合这种情形
            3. 只需要配置的下面五种属性
              1. driver – 这是 JDBC 驱动的 Java 类全限定名(并不是 JDBC 驱动中可能包含的数据源类)。
              2. url – 这是数据库的 JDBC URL 地址。
              3. username – 登录数据库的用户名。
              4. password – 登录数据库的密码。
              5. defaultTransactionIsolationLevel – 默认的连接事务隔离级别。
              6. defaultNetworkTimeout – 等待数据库操作完成的默认网络超时时间(单位:毫秒)
          2. POOLED

            1. 这种数据源的实现利用“连接池”的概念将 JDBC 连接对象组织起来,避免了创建新的连接实例时所必需的初始化和认证时间。 这种处理方式很流行,能使并发 Web 应用快速响应请求
            2. 除了上述提到 UNPOOLED 下的属性外,还有更多属性用来配置 POOLED 的数据源:
              1. poolMaximumActiveConnections` – 在任意时间可存在的活动(正在使用)连接数量,默认值:10
              2. poolMaximumIdleConnections – 任意时间可能存在的空闲连接数。
              3. poolMaximumCheckoutTime – 在被强制返回之前,池中连接被检出(checked out)时间,默认值:20000 毫秒(即 20 秒)
              4. poolTimeToWait – 这是一个底层设置,如果获取连接花费了相当长的时间,连接池会打印状态日志并重新尝试获取一个连接(避免在误配置的情况下一直失败且不打印日志),默认值:20000 毫秒(即 20 秒)。
              5. poolMaximumLocalBadConnectionTolerance – 这是一个关于坏连接容忍度的底层设置, 作用于每一个尝试从缓存池获取连接的线程。 如果这个线程获取到的是一个坏的连接,那么这个数据源允许这个线程尝试重新获取一个新的连接,但是这个重新尝试的次数不应该超过 poolMaximumIdleConnectionspoolMaximumLocalBadConnectionTolerance 之和。 默认值:3(新增于 3.4.5)
              6. poolPingQuery – 发送到数据库的侦测查询,用来检验连接是否正常工作并准备接受请求。默认是“NO PING QUERY SET”,这会导致多数数据库驱动出错时返回恰当的错误消息。
              7. poolPingEnabled – 是否启用侦测查询。若开启,需要设置 poolPingQueryJNDI – 这个数据源实现是为了能在如 EJB 或应用服务器这类容器中使用,容器可以集中或在外部配置数据源,然后放置一个 JNDI 上下文的数据源引用。这种数据源配置只需要两个属性:
                • initial_context – 这个属性用来在 InitialContext 中寻找上下文(即,initialContext.lookup(initial_context))。这是个可选属性,如果忽略,那么将会直接从 InitialContext 中寻找 data_source 属性。
                • data_source – 这是引用数据源实例位置的上下文路径。提供了 initial_context 配置时会在其返回的上下文中进行查找,没有提供时则直接在 InitialContext 中查找
              8. 性为一个可执行的 SQL 语句(最好是一个速度非常快的 SQL 语句),默认值:false。
              9. poolPingConnectionsNotUsedFor – 配置 poolPingQuery 的频率。可以被设置为和数据库连接超时时间一样,来避免不必要的侦测,默认值:0(即所有连接每一时刻都被侦测 — 当然仅当 poolPingEnabled 为 true 时适用)。
          3. JNDI

            1. 这个数据源实现是为了能在如 EJB 或应用服务器这类容器中使用,容器可以集中或在外部配置数据源,然后放置一个 JNDI 上下文的数据源引用
            2. 这种数据源配置只需要两个属性:
              1. initial_context – 这个属性用来在 InitialContext 中寻找上下文(即,initialContext.lookup(initial_context))。这是个可选属性,如果忽略,那么将会直接从 InitialContext 中寻找 data_source 属性。
              2. data_source – 这是引用数据源实例位置的上下文路径。提供了 initial_context 配置时会在其返回的上下文中进行查找,没有提供时则直接在 InitialContext 中查找
          4. 自定义数据源

            1. 实现DataSourceFactory接口

              public interface DataSourceFactory {
                void setProperties(Properties props);
                DataSource getDataSource();
              }
              
              public class C3P0DataSourceFactory extends UnpooledDataSourceFactory {
              
                public C3P0DataSourceFactory() {
                  this.dataSource = new ComboPooledDataSource();
                }
              }
              
              <dataSource type="org.myproject.C3P0DataSourceFactory">
                <property name="driver" value="org.postgresql.Driver"/>
                <property name="url" value="jdbc:postgresql:mydb"/>
                <property name="username" value="postgres"/>
                <property name="password" value="root"/>
              </dataSource>
              
  2. MyBatis 可以配置成适应多种环境,这种机制有助于将 SQL 映射应用于多种数据库之中, 现实情况下有多种理由需要这么做。例如,开发、测试和生产环境需要有不同的配置;或者想在具有相同 Schema 的多个生产数据库中使用相同的 SQL 映射,**不过要记住:尽管可以配置多个环境,但每个 SqlSessionFactory 实例只能选择一种环境。**所以,如果你想连接两个数据库,就需要创建两个 SqlSessionFactory 实例,每个数据库对应一个。而如果是三个数据库,就需要三个实例,依此类推

  3. 每个数据库对应一个 SqlSessionFactory 实例

    1. 为了指定创建哪种环境,只要将它作为可选的参数传递给 SqlSessionFactoryBuilder 即可

      SqlSessionFactory factory = new SqlSessionFactoryBuilder().build(reader, environment);
      SqlSessionFactory factory = new SqlSessionFactoryBuilder().build(reader, environment, properties);
      
    2. 如果忽略了环境参数,那么将会加载默认环境

      SqlSessionFactory factory = new SqlSessionFactoryBuilder().build(reader);
      SqlSessionFactory factory = new SqlSessionFactoryBuilder().build(reader, properties);
      

9. databaseIdProvider

  1. 数据库厂商标识

    <databaseIdProvider type="DB_VENDOR" />
    
  2. MyBatis 可以根据不同的数据库厂商执行不同的语句,这种多厂商的支持是基于映射语句中的 databaseId 属性

  3. MyBatis 会加载带有匹配当前数据库 databaseId 属性和所有不带 databaseId 属性的SQL语句

    如果同时找到带有 databaseId 和不带 databaseId 的相同语句,则后者会被舍弃

  4. 为支持多厂商特性,只要像上面这样在 mybatis-config.xml 文件中加入 databaseIdProvider

  5. databaseIdProvider 对应的 DB_VENDOR(别名) 实现会将 databaseId 设置为 DatabaseMetaData#getDatabaseProductName() 返回的字符串。 由于通常情况下这些字符串都非常长,而且相同产品的不同版本会返回不同的值,你可能想通过设置属性别名来使其变短

<databaseIdProvider type="DB_VENDOR">
  <property name="SQL Server" value="sqlserver"/>
  <property name="DB2" value="db2"/>
  <property name="Oracle" value="oracle" />
</databaseIdProvider>
  1. 在提供了属性别名时,databaseIdProvider 的 DB_VENDOR 实现会将 databaseId 设置为数据库产品名与属性中的名称第一个相匹配的值,如果没有匹配的属性,将会设置为 “null”。

  2. 在这个例子中,如果 getDatabaseProductName() 返回“Oracle (DataDirect)”,databaseId 将被设置为“oracle”

  3. 可以通过实现接口 org.apache.ibatis.mapping.DatabaseIdProvider 并在 mybatis-config.xml 中注册来构建自己的 DatabaseIdProvider

    public interface DatabaseIdProvider {
      default void setProperties(Properties p) { // 从 3.5.2 开始,该方法为默认方法
        // 空实现
      }
      // 根据不同的数据库厂商返回对应的数据库ID
      String getDatabaseId(DataSource dataSource) throws SQLException;
    }
    

10. mappers

  1. 定义 SQL 映射语句,但首先,我们需要告诉 MyBatis 到哪里去找到这些语句
  2. 在自动查找资源方面,Java 并没有提供一个很好的解决方案,所以最好的办法是直接告诉 MyBatis 到哪里去找映射文件。 你可以使用相对于类路径的资源引用,或完全限定资源定位符(包括 file:/// 形式的 URL),或类名和包名等
<!-- 使用相对于类路径的资源引用 -->
<mappers>
  <mapper resource="org/mybatis/builder/AuthorMapper.xml"/>
  <mapper resource="org/mybatis/builder/BlogMapper.xml"/>
  <mapper resource="org/mybatis/builder/PostMapper.xml"/>
</mappers>

<!-- 使用完全限定资源定位符(URL) -->
<mappers>
  <mapper url="file:///var/mappers/AuthorMapper.xml"/>
  <mapper url="file:///var/mappers/BlogMapper.xml"/>
  <mapper url="file:///var/mappers/PostMapper.xml"/>
</mappers>

<!-- 使用映射器接口实现类的完全限定类名 -->
<mappers>
  <mapper class="org.mybatis.builder.AuthorMapper"/>
  <mapper class="org.mybatis.builder.BlogMapper"/>
  <mapper class="org.mybatis.builder.PostMapper"/>
</mappers>

<!-- 将包内的映射器接口全部注册为映射器 -->
<mappers>
  <package name="org.mybatis.builder"/>
</mappers>

4. Mapper映射文件

  1. MyBatis 的真正强大在于它的语句映射,映射器的 XML 文件就显得相对简单。如果拿它跟具有相同功能的 JDBC 代码进行对比,你会立即发现省掉了将近 95% 的代码。MyBatis 致力于减少使用成本,让用户能更专注于 SQL 代码
  2. SQL 映射文件只有很少的几个顶级元素(按照应被定义的顺序列出):

1.cache

  1. 该命名空间的缓存配置

  2. MyBatis 内置了一个强大的事务性查询缓存机制,它可以非常方便地配置和定制。

  3. 为了使它更加强大而且易于配置,我们对 MyBatis 3 中的缓存实现进行了许多改进。

  4. 默认情况下,只启用了本地的会话缓存,它仅仅对一个会话中的数据进行缓存。

  5. 要启用全局的二级缓存,只需要在你的 SQL 映射文件中添加一行

    <cache/>
    
  6. 基本上就是这样。这个简单语句的效果如下:

    • mapper文件中的所有 select 语句的结果将会被缓存。
    • mapper文件中的所有 insert、update 和 delete 语句会刷新缓存。
    • 缓存会使用最近最少使用算法(LRU, Least Recently Used)算法来清除不需要的缓存。
    • 缓存不会定时进行刷新(也就是说,没有刷新间隔)。
    • 缓存会保存列表或对象(无论查询方法返回哪种)的 1024 个引用。
    • 缓存会被视为读/写缓存,这意味着获取到的对象并不是共享的,可以安全地被调用者修改,而不干扰其他调用者或线程所做的潜在修改
  7. 缓存只作用于 cache 标签所在的mapper文件中的语句

    1. 如果你混合使用 Java API 和 XML 映射文件

      1. MyBatis 提供了两种方式来配置 SQL 映射:一种是使用 XML 文件,另一种是使用 Java 注解或 API
    2. 在共用接口中的语句将不会被默认缓存,你需要使用 @CacheNamespaceRef 注解指定缓存作用域

    interface A extends Common{}  A.xml
    interface B extends Common{}  B.xml
    
    # 这里的接口语句并不会进行缓存,因为它本身不知道使用那个映射的缓存,所以使用注解标注
    @CacheNamespaceRef(A.class)
    interface Common{}
    
  8. 这些属性可以通过 cache 元素的属性来修改

    <cache
      eviction="FIFO"
      flushInterval="60000"
      size="512"
      readOnly="true"/>
    
    1. 这个更高级的配置创建了一个 FIFO 缓存,每隔 60 秒刷新,最多可以存储结果对象或列表的 512 个引用,而且返回的对象被认为是只读的,因此对它们进行修改可能会在不同线程中的调用者产生冲突。

      可用的清除策略有:

      • LRU – 最近最少使用:移除最长时间不被使用的对象。
      • FIFO – 先进先出:按对象进入缓存的顺序来移除它们。
      • SOFT – 软引用:基于垃圾回收器状态和软引用规则移除对象。
      • WEAK – 弱引用:更积极地基于垃圾收集器状态和弱引用规则移除对象
      • 默认的清除策略是 LRU。
    2. flushInterval(刷新间隔)属性可以被设置为任意的正整数,设置的值应该是一个以毫秒为单位的合理时间量。 默认情况是不设置,也就是没有刷新间隔,缓存仅仅会在调用语句时刷新。

    3. size(引用数目)属性可以被设置为任意正整数,要注意欲缓存对象的大小和运行环境中可用的内存资源。默认值是 1024。

    4. readOnly(只读)属性可以被设置为 true 或 false。只读的缓存会给所有调用者返回缓存对象的相同实例。 因此这些对象不能被修改。这就提供了可观的性能提升。而可读写的缓存会(通过序列化)返回缓存对象的拷贝。 速度上会慢一些,但是更安全,因此默认值是 false

    5. 二级缓存是事务性的

      1. 这意味着,当 SqlSession 完成并提交时,或是完成并回滚,但没有执行 flushCache=true 的 insert/delete/update 语句时,缓存会获得更新
  9. 使用自定义缓存

    1. 除了上述自定义缓存的方式,你也可以通过实现你自己的缓存,或为其他第三方缓存方案创建适配器,来完全覆盖缓存行为

      <cache type="com.domain.something.MyCustomCache"/>
      
    2. 实现Cache接口

      public interface Cache {
        String getId();
        int getSize();
        void putObject(Object key, Object value);
        Object getObject(Object key);
        boolean hasKey(Object key);
        Object removeObject(Object key);
        void clear();
      }
      
    3. 为了对你的缓存进行配置,只需要简单地在你的缓存实现中添加公有的 JavaBean 属性,然后通过 cache 元素传递属性值

      1. 例如,下面的例子将在你的缓存实现上调用一个名为 setCacheFile(String file) 的方法

        <cache type="com.domain.something.MyCustomCache">
          <property name="cacheFile" value="/tmp/my-custom-cache.tmp"/>
        </cache>
        
      2. 你可以使用所有简单类型作为 JavaBean 属性的类型,MyBatis 会进行转换

      3. 你也可以使用占位符(如 ${cache.file}),以便替换成在配置文件属性中定义的值。

      4. 从版本 3.4.2 开始,MyBatis 已经支持在所有属性设置完毕之后,调用一个初始化方法。

        1. 如果想要使用这个特性,请在你的自定义缓存类里实现 org.apache.ibatis.builder.InitializingObject 接口
        public interface InitializingObject {
          void initialize() throws Exception;
        }
        
    4. 对缓存的配置(如清除策略、可读或可读写等),不能应用于自定义缓存

      1. 请注意,缓存的配置和缓存实例会被绑定到 SQL 映射文件的命名空间中

      2. 因此,同一命名空间中的所有语句和缓存将通过命名空间绑定在一起

      3. 每条语句可以自定义与缓存交互的方式,或将它们完全排除于缓存之外,这可以通过在每条语句上使用两个简单属性来达成

      <select ... flushCache="false" useCache="true"/>
      <insert ... flushCache="true"/>
      <update ... flushCache="true"/>
      <delete ... flushCache="true"/>
      
      1. 如果你想改变默认的行为,只需要设置 flushCache 和 useCache 属性
      2. 比如,某些情况下你可能希望特定 select 语句的结果排除于缓存之外,或希望一条 select 语句清空缓存。类似地,你可能希望某些 update 语句执行时不要刷新缓存,都可以进行配置

2. cache-ref

  1. 引用其它命名空间的缓存配置

  2. 对某一命名空间的语句,只会使用该命名空间的缓存进行缓存或刷新

  3. 但你可能会想要在多个命名空间中共享相同的缓存配置和实例。要实现这种需求,你可以使用 cache-ref 元素来引用另一个缓存

<cache-ref namespace="com.someone.application.data.SomeMapper"/>
@CacheNamespaceRef(SomeMapper.class)
interface Mapper{}

3. resultMap(结果集映射)

  1. 描述如何从数据库结果集中加载对象,是最复杂也是最强大的元素

  2. resultMap 元素是 MyBatis 中最重要最强大的元素。它可以让你从 90% 的 JDBC ResultSets 数据提取代码中解放出来,并在一些情形下允许你进行一些 JDBC 不支持的操作

  3. 实际上,在为一些比如连接的复杂语句编写映射代码的时候,一份 resultMap 能够代替实现同等功能的数千行代码。ResultMap 的设计思想是,对简单的语句做到零配置,对于复杂一点的语句,只需要描述语句之间的关系就行了

    1. 下列语句只是简单地将所有的列映射到 HashMap 的键上,这由 resultType 属性指定
    2. 虽然在大部分情况下都够用,但是 HashMap 并不是一个很好的领域模型。你的程序更可能会使用 JavaBean 或 POJO(Plain Old Java Objects,普通老式 Java 对象)作为领域模型,MyBatis 对两者都提供了支持
    <select id="selectUsers" resultType="map">
      select id, username, hashedPassword
      from some_table
      where id = #{id}
    </select>
    
    public class User {
      private int id;
      private String username;
      private String hashedPassword;
    }
    
    <select id="selectUsers" resultType="com.someapp.model.User">
      select id, username, hashedPassword
      from some_table
      where id = #{id}
    </select>
    
    # 配置类的别名
    <!-- mybatis-config.xml 中 -->
    <typeAlias type="com.someapp.model.User" alias="User"/>
    
    # 使用类型别名
    <!-- SQL 映射 XML 中 -->
    <select id="selectUsers" resultType="User">
      select id, username, hashedPassword
      from some_table
      where id = #{id}
    </select>
    
    1. 在上述这些情况下,使用resultType属性,MyBatis 会在幕后自动创建一个 ResultMap,再根据属性名来映射列到 JavaBean 的属性上。如果列名和属性名不能匹配上,可以在 SELECT 语句中设置列别名(这是一个基本的 SQL 特性)来完成匹配

      <select id="selectUsers" resultType="User">
        select
          user_id             as "id",
          user_name           as "userName",
          hashed_password     as "hashedPassword"
        from some_table
        where id = #{id}
      </select>
      
    2. 对于上述这些情况,都没有显示配置resultMap,你完全可以不用显式地配置它们,但是我们也可以进行显示配置,这也是解决列名不匹配的另外一种方式

      <resultMap id="userResultMap" type="User">
        <id property="id" column="user_id" />
        <result property="username" column="user_name"/>
        <result property="password" column="hashed_password"/>
      </resultMap>
      
      <select id="selectUsers" resultMap="userResultMap">
        select user_id, user_name, hashed_password
        from some_table
        where id = #{id}
      </select>
      
  4. 高级结果映射,多表关联

    1. 案例
    <!-- 非常复杂的语句 -->
    <select id="selectBlogDetails" resultMap="detailedBlogResultMap">
      select
           B.id as blog_id,
           B.title as blog_title,
           A.id as author_id,
           A.username as author_username,
           A.password as author_password,
           P.id as post_id,
           P.blog_id as post_blog_id,
           C.id as comment_id,
           C.post_id as comment_post_id,
           T.id as tag_id,
           T.name as tag_name
      from Blog B
           left outer join Author A on B.author_id = A.id
           left outer join Post P on B.id = P.blog_id
           left outer join Comment C on P.id = C.post_id
           left outer join Post_Tag PT on PT.post_id = P.id
           left outer join Tag T on PT.tag_id = T.id
      where B.id = #{id}
    </select>
    
    1. 把它映射到一个智能的对象模型,这个对象表示了一篇博客,它由某位作者所写,有很多的博文,每篇博文有零或多条的评论和标签

      <!-- 非常复杂的结果映射 -->
      <resultMap id="detailedBlogResultMap" type="Blog">
        <constructor>
          <idArg column="blog_id" javaType="int"/>
        </constructor>
        <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"/>
        </association>
        <collection property="posts" ofType="Post">
          <id property="id" column="post_id"/>
          <result property="subject" column="post_subject"/>
          <association property="author" javaType="Author"/>
          <collection property="comments" ofType="Comment">
            <id property="id" column="comment_id"/>
          </collection>
          <collection property="tags" ofType="Tag" >
            <id property="id" column="tag_id"/>
          </collection>
          <discriminator javaType="int" column="draft">
            <case value="1" resultType="DraftPost"/>
          </discriminator>
        </collection>
      </resultMap>
      
      1. constructor

        1. 用于在实例化类时,注入结果到构造方法中

          1. idArg - ID 参数,标记出作为 ID 的结果可以帮助提高整体性能
          2. arg将被注入到构造方法的一个普通结果
          <constructor>
             <idArg column="id" javaType="int" name="id" />
             <arg column="age" javaType="_int" name="age" />
             <arg column="username" javaType="String" name="username" />
          </constructor>
          
        2. 通过修改对象属性的方式,可以满足大多数的数据传输对象(Data Transfer Object, DTO)以及绝大部分领域模型的要求

        3. 但有些情况下你想使用不可变类。 一般来说,很少改变或基本不变的包含引用或数据的表,很适合使用不可变类。

        4. 构造方法注入允许你在初始化时为类设置属性的值,而不用暴露出公有方法。MyBatis 也支持私有属性和私有 JavaBean 属性来完成注入,但有一些人更青睐于通过构造方法进行注入constructor 元素就是为此而生的

        public class User {
           public User(Integer id, String username, int age) {
          }
        }
        
      2. 简单属性映射

        1. id

          1. 一个 ID 结果;标记出作为 ID 的结果可以帮助提高整体性能
          2. 元素是结果映射的基础。id元素将一个列的值映射到一个简单数据类型(String, int, double, Date 等)的属性或字段
          3. 和result效果一样,但是id映射的字段最好是唯一性,例如主键
        2. result

          1. 注入到字段或 JavaBean 属性的普通结果
          2. 元素是结果映射的基础。result元素将一个列的值映射到一个简单数据类型(String, int, double, Date 等)的属性或字段
          3. 和id效果一样,但是result映射的字段是普通字段
        3. id和result的属性配置

          属性描述
          property映射到列结果的字段或属性。如果 JavaBean 有这个名字的属性(property),会先使用该属性。否则 MyBatis 将会寻找给定名称的字段(field)。 无论是哪一种情形,你都可以使用常见的点式分隔形式进行复杂属性导航。 比如,你可以这样映射一些简单的东西:“username”,或者映射到一些复杂的东西上:“address.street.number”。
          column数据库中的列名,或者是列的别名。一般情况下,这和传递给 resultSet.getString(columnName) 方法的参数一样。
          javaType一个 Java 类的全限定名,或一个类型别名(关于内置的类型别名,可以参考上面的表格)。 如果你映射到一个 JavaBean,MyBatis 通常可以推断类型。然而,如果你映射到的是 HashMap,那么你应该明确地指定 javaType 来保证行为与期望的相一致。
          jdbcTypeJDBC 类型,所支持的 JDBC 类型参见这个表格之后的“支持的 JDBC 类型”。 只需要在可能执行插入、更新和删除的且允许空值的列上指定 JDBC 类型。这是 JDBC 的要求而非 MyBatis 的要求。如果你直接面向 JDBC 编程,你需要对可以为空值的列指定这个类型。
          typeHandler我们在前面讨论过默认的类型处理器。使用这个属性,你可以覆盖默认的类型处理器。 这个属性值是一个类型处理器实现类的全限定名,或者是类型别名。
      3. 复杂的属性配置

        1. association

          1. 关联(association)元素处理“单个对象”类型的关系

            1. 比如,在示例中,一个博客有一个用户
            2. 关联结果映射和其它类型的映射工作方式差不多
            3. 你需要指定目标属性名以及属性的javaType(很多时候 MyBatis 可以自己推断出来),在必要的情况下你还可以设置 JDBC 类型,如果你想覆盖获取结果值的过程,还可以设置类型处理器
            4. 关联的不同之处是,你需要告诉 MyBatis 如何加载关联对象
          2. MyBatis 有两种不同的方式加载关联:

            1. 嵌套 Select 查询:通过执行另外一个 SQL 映射语句来加载期望的复杂类型
              1. 这种方式虽然很简单,但在大型数据集或大型数据表上表现不佳
              2. 这个问题被称为“N+1 查询问题”。 概括地讲,N+1 查询问题是这样子的
                1. 你执行了一个单独的 SQL 语句来获取结果的一个列表(就是“+1”)。
                2. 对列表返回的每条记录,你执行一个 select 查询语句来为每条记录加载详细信息(就是“N”)。
                3. 这个问题会导致成百上千的 SQL 语句被执行。有时候,我们不希望产生这样的后果。
                4. 好消息是,MyBatis 能够对这样的查询进行延迟加载,因此可以将大量语句同时运行的开销分散开来
                5. 然而,如果你加载记录列表之后立刻就遍历列表以获取嵌套的数据,就会触发所有的延迟加载查询,性能可能会变得很糟糕
            <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>
            
            属性描述
            column数据库中的列名,或者是列的别名。一般情况下,这和传递给 resultSet.getString(columnName) 方法的参数一样。 注意:在使用复合主键的时候,你可以使用 column="{prop1=col1,prop2=col2}" 这样的语法来指定多个传递给嵌套 Select 查询语句的列名。这会使得 prop1prop2 作为参数对象,被设置为对应嵌套 Select 语句的参数。
            select用于加载复杂类型属性的映射语句的 ID,它会从 column 属性指定的列中检索数据,作为参数传递给目标 select 语句。 具体请参考下面的例子。注意:在使用复合主键的时候,你可以使用 column="{prop1=col1,prop2=col2}" 这样的语法来指定多个传递给嵌套 Select 查询语句的列名。这会使得 prop1prop2 作为参数对象,被设置为对应嵌套 Select 语句的参数。
            fetchType可选的。有效值为 lazyeager。 指定属性后,将在映射中忽略全局配置参数 lazyLoadingEnabled,使用属性的值。
            1. 嵌套结果映射:使用嵌套的结果映射来处理连接结果的重复子集
            <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>
            

            等效于

            <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>
            
            1. 如果存在自关联的情况,或者需要同时两次关联同一张表的情况该如何映射呢?

              1. 方式一: 对于每一个关联定义一个resultMap进行映射
              2. 方式二: 使用association的columnPrefix属性,将两次关联的结果使用前缀区分,这样就可以重复使用相同的resultMap
              <select id="selectBlog" resultMap="blogResult">
                select
                  B.id            as blog_id,
                  B.title         as blog_title,
                  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,
                  CA.id           as co_author_id,
                  CA.username     as co_author_username,
                  CA.password     as co_author_password,
                  CA.email        as co_author_email,
                  CA.bio          as co_author_bio
                from Blog B
                left outer join Author A on B.author_id = A.id
                left outer join Author CA on B.co_author_id = CA.id
                where B.id = #{id}
              </select>
              
              <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>
              
              <resultMap id="blogResult" type="Blog">
                <id property="id" column="blog_id" />
                <result property="title" column="blog_title"/>
                <association property="author" resultMap="authorResult" />
                <association property="coAuthor" resultMap="authorResult" columnPrefix="co_" />
              </resultMap>
              
            属性描述
            resultMap结果映射的 ID,可以将此关联的嵌套结果集映射到一个合适的对象树中。 它可以作为使用额外 select 语句的替代方案。它可以将多表连接操作的结果映射成一个单一的 ResultSet。这样的 ResultSet 有部分数据是重复的。 为了将结果集正确地映射到嵌套的对象树中, MyBatis 允许你“串联”结果映射,以便解决嵌套结果集的问题。使用嵌套结果映射的一个例子在表格以后。
            columnPrefix当连接多个表时,你可能会不得不使用列别名来避免在 ResultSet 中产生重复的列名。指定 columnPrefix 列名前缀允许你将带有这些前缀的列映射到一个外部的结果映射中。 详细说明请参考后面的例子。
            notNullColumn默认情况下,在至少一个被映射到属性的列不为空时,子对象才会被创建。 你可以在这个属性上指定非空的列来改变默认行为,指定后,Mybatis 将只在这些列中任意一列非空时才创建一个子对象。可以使用逗号分隔来指定多个列。默认值:未设置(unset)。
            autoMapping如果设置这个属性,MyBatis 将会为本结果映射开启或者关闭自动映射。 这个属性会覆盖全局的属性 autoMappingBehavior。注意,本属性对外部的结果映射无效,所以不能搭配 selectresultMap 元素使用。默认值:未设置(unset)。
          3. 关联多结果集

            1. 从版本 3.2.3 开始,MyBatis 提供了另一种解决 N+1 查询问题的方法。

            2. 某些数据库允许存储过程返回多个结果集,或一次性执行多个语句,每个语句返回一个结果集。 我们可以利用这个特性,在不使用连接的情况下,只访问数据库一次就能获得相关数据。

            3. 在例子中,存储过程执行下面的查询并返回两个结果集。第一个结果集会返回博客(Blog)的结果,第二个则返回作者(Author)的结果

              SELECT * FROM BLOG WHERE ID = #{id}
              SELECT * FROM AUTHOR WHERE ID = #{id}
              
              1. 在映射语句中,必须通过 resultSets 属性为每个结果集指定一个名字,多个名字使用逗号隔开
              <select id="selectBlog" resultSets="blogs,authors" resultMap="blogResult" statementType="CALLABLE">
                {call getBlogsAndAuthors(#{id,jdbcType=INTEGER,mode=IN})}
              </select>
              
              1. 我们可以指定使用 “authors” 结果集的数据来填充 “author” 关联:
              <resultMap id="blogResult" type="Blog">
                <id property="id" column="id" />
                <result property="title" column="title"/>
                <association property="author" javaType="Author" resultSet="authors" column="author_id" foreignColumn="id">
                  <id property="id" column="id"/>
                  <result property="username" column="username"/>
                  <result property="password" column="password"/>
                  <result property="email" column="email"/>
                  <result property="bio" column="bio"/>
                </association>
              </resultMap>
              
          属性描述
          column当使用多个结果集时,该属性指定结果集中用于与 foreignColumn 匹配的列(多个列名以逗号隔开),以识别关系中的父类型与子类型。
          foreignColumn指定外键对应的列名,指定的列将与父类型中 column 的给出的列进行匹配。
          resultSet指定用于加载复杂类型的结果集名字。
        2. collection

          1. 一个复杂类型的集合嵌套结果映射

          2. 集合可以是 resultMap 元素,或是对其它结果映射的引用

          3. collection与association概念几乎是一样的,只是关联的对象是一个集合类型,而不是单个类型

          4. 集合类型的使用

            <collection property="posts" ofType="domain.blog.Post">
              <id property="id" column="post_id"/>
              <result property="subject" column="post_subject"/>
              <result property="body" column="post_body"/>
            </collection>
            
            private List<Post> posts;
            
          5. 加载集合关联的两种方式,和association一样

            1. 集合的嵌套 Select 查询

              <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>
              
              1. 和association不同的是,使用ofType来描述JavaBean对象,javaType描述的是集合类型,一般情况下,Mybatis可以自动推断javaType属性,所以可以不写

                <collection property="posts" javaType="ArrayList" column="id" ofType="Post" select="selectPostsForBlog"/>
                
            2. 集合的嵌套结果映射

              <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,可以使用下面方式替换,记得给字段添加别名前缀
              <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>
              
            3. 集合的多结果集(ResultSet)

              1. 和association一样,上面两种方式也都会触发N+1查询,我们也可以使用多结果集,使用存储过程完成

                SELECT * FROM BLOG WHERE ID = #{id}
                SELECT * FROM POST WHERE BLOG_ID = #{id}
                
              2. 在映射语句中,必须通过 resultSets 属性为每个结果集指定一个名字,多个名字使用逗号隔开

                <select id="selectBlog" resultSets="blogs,posts" resultMap="blogResult">
                  {call getBlogsAndPosts(#{id,jdbcType=INTEGER,mode=IN})}
                </select>
                
              3. 我们指定 “posts” 集合将会使用存储在 “posts” 结果集中的数据进行填充

                <resultMap id="blogResult" type="Blog">
                  <id property="id" column="id" />
                  <result property="title" column="title"/>
                  <collection property="posts" ofType="Post" resultSet="posts" column="id" foreignColumn="blog_id">
                    <id property="id" column="id"/>
                    <result property="subject" column="subject"/>
                    <result property="body" column="body"/>
                  </collection>
                </resultMap>
                
        3. 总结

          1. 对关联或集合的映射,并没有深度、广度或组合上的要求,但在映射时要留意性能问题
          2. 在探索最佳实践的过程中,应用的单元测试和性能测试会是你的好帮手
          3. 而 MyBatis 的好处在于,可以在不对你的代码引入重大变更(如果有)的情况下,允许你之后改变你的想法
      4. discriminator

        1. 使用结果值来决定使用哪个 resultMap

        2. case – 基于某些值的结果映射

          1. 可以将case当做是一个resultMap,value表示决定符合该值的情况使用该结果映射
          2. 它的概念就像java的switch
          3. 嵌套结果映射 – case 也是一个结果映射,因此具有相同的结构和元素;或者引用其它的结果映射
          <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"/>
            # 对于每一组结果,例如carResult,并不会自动继承vehicleResult的映射
            # 而是只让指定的carResult中的字段生效,如果继承vehicleResult的映射,需要使用extends继承
            <discriminator javaType="int" column="vehicle_type">
              <case value="1" resultMap="carResult"/>
              <case value="2" resultMap="truckResult"/>
              <case value="3" resultMap="vanResult"/>
              <case value="4" resultMap="suvResult"/>
            </discriminator>
          </resultMap>
          
          #  默认只让指定的carResult中的字段生效,如果继承父标签的映射,需要使用extends继承
          <resultMap id="carResult" type="Car" extends="vehicleResult">
            <result property="doorCount" column="door_count" />
          </resultMap>
          
          # 如果不想每一个结果集都单独定义一个ResultMap,可以使用下面这种嵌套类型
          <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"/>
            <discriminator javaType="int" column="vehicle_type">
              <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>
          
          1. 上述代码表示,当type列的值为1,使用carResult这个映射关系,type为2时,使用truckResult这个映射关系
      5. 自动映射

        1. 在简单的场景下,MyBatis 可以为你自动映射查询结果

        2. 但如果遇到复杂的场景,你需要构建一个结果映射resultMap

        3. 我们可以混合使用这两种策略。让我们深入了解一下自动映射是怎样工作的。

        4. 当自动映射查询结果时,MyBatis 会获取结果中返回的列名并在 Java 类中查找相同名字的属性(忽略大小写),这意味着如果发现了 ID 列和 id 属性,MyBatis 会将列 ID 的值赋给 id 属性

        5. 通常数据库列使用大写字母组成的单词命名,单词间用下划线分隔;而 Java 属性一般遵循驼峰命名法约定。为了在这两种命名方式之间启用自动映射,需要将 mapUnderscoreToCamelCase 设置为 true

        6. 甚至在提供了结果映射resultMap后,自动映射也能工作。在这种情况下,对于每一个结果映射,在 ResultSet 出现的列,如果没有设置手动映射,将被自动映射。在自动映射处理完毕后,再处理手动映射。

        7. 在下面的例子中,iduserName 列将被自动映射,hashed_password 列将根据配置进行映射

        <select id="selectUsers" resultMap="userResultMap">
          select
            user_id             as "id",
            user_name           as "userName",
            hashed_password
          from some_table
          where id = #{id}
        </select>
        
        resultMap id="userResultMap" type="User">
          <result property="password" column="hashed_password"/>
        </resultMap>
        
        1. 有三种自动映射等级

          1. NONE

            1. 禁用自动映射。仅对手动映射的属性进行映射。
          2. PARTIAL

            1. 对除在内部定义了嵌套结果映射(也就是连接的属性)以外的属性进行映射
          3. FULL

            1. 自动映射所有属性
          4. 默认值是 PARTIAL

            1. 这是有原因的。当对连接查询的结果使用 FULL 时,连接查询会在同一行中获取多个不同实体的数据,因此可能导致非预期的映射
          5. 例如

            <select id="selectBlog" resultMap="blogResult">
              select
                B.id,
                B.title,
                A.username,
              from Blog B left outer join Author A on B.author_id = A.id
              where B.id = #{id}
            </select>
            
            <resultMap id="blogResult" type="Blog">
              <association property="author" resultMap="authorResult"/>
            </resultMap>
            
            <resultMap id="authorResult" type="Author">
              <result property="username" column="author_username"/>
            </resultMap>
            
            1. 在该结果映射中,BlogAuthor 均将被自动映射

            2. 但是注意 Author 有一个 id 属性,在 ResultSet (查询的是B.id)中也有一个名为 id 的列,所以 Author 的 id 将填入 Blog 的 id,这可不是你期望的行为。所以,要谨慎使用 FULL

            3. 无论设置的自动映射等级是哪种,你都可以通过在结果映射上设置 autoMapping 属性来为指定的结果映射设置启用/禁用自动映射

            <resultMap id="userResultMap" type="User" autoMapping="false">
              <result property="password" column="hashed_password"/>
            </resultMap>
            
  5. ResultMap的属性配置

    属性描述
    id当前命名空间中的一个唯一标识,用于标识一个结果映射。
    type类的完全限定名, 或者一个类型别名(关于内置的类型别名,可以参考上面的表格)。
    autoMapping如果设置这个属性,MyBatis 将会为本结果映射开启或者关闭自动映射。 这个属性会覆盖全局的属性 autoMappingBehavior。默认值:未设置(unset)。
  6. 最佳实践

    1. 最好逐步建立结果映射
    2. 单元测试可以在这个过程中起到很大帮助
    3. 如果你尝试一次性创建像上面示例那么巨大的结果映射,不仅容易出错,难度也会直线上升。
    4. 所以,从最简单的形态开始,逐步迭代,而且别忘了单元测试!
    5. 有时候,框架的行为像是一个黑盒子(无论是否开源)。因此,为了确保实现的行为与你的期望相一致,最好编写单元测试。 并且单元测试在提交 bug 时也能起到很大的作用

4. parameterMap

  1. 老式风格的参数映射。此元素已被废弃,并可能在将来被移除!

5. sql

  1. 可被其它语句引用的可重用语句块

  2. 这个元素可以用来定义可重用的 SQL 代码片段,以便在其它语句中使用。

  3. 参数可以静态地(在加载的时候)确定下来,并且可以在不同的 include 元素中定义不同的参数值

    1. 定义sql片段
    <sql id="userColumns"> ${alias}.id,${alias}.username,${alias}.password </sql>
    
    1. 引用sql片段
    <select id="selectUsers" resultType="map">
      select
        <include refid="userColumns"><property name="alias" value="t1"/></include>,
        <include refid="userColumns"><property name="alias" value="t2"/></include>
      from some_table t1
        cross join some_table t2
    </select>
    
    1. 也可以在 include 元素的 refid 属性或内部语句中使用属性值
    <sql id="sometable">
      ${prefix}Table
    </sql>
    
    <sql id="someinclude">
      # 最终会引用id为sometable的sql片段
      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>
    

6. insert | update | delete

  1. 映射插入语句

    1. 插入单个

      <insert id="insertAuthor">
        insert into Author (id,username,password,email,bio)
        values (#{id},#{username},#{password},#{email},#{bio})
      </insert>
      
    2. 插入集合

    <insert id="insertAuthor" useGeneratedKeys="true" keyProperty="id">
      insert into Author (username, password, email, bio) values
      // 如果插入的是集合的话,可以使用foreach标签插入
      <foreach item="item" collection="list" separator=",">
        (#{item.username}, #{item.password}, #{item.email}, #{item.bio})
      </foreach>
    </insert>
    
    1. 获取自增主键

      <insert id="insertAuthor" useGeneratedKeys="true" keyProperty="id">
       insert into Author (username,password,email,bio)
        values (#{username},#{password},#{email},#{bio})
      </insert>
      
      1. 如果你的数据库支持自动生成主键的字段(比如 MySQL 和 SQL Server),那么你可以设置 useGeneratedKeys=”true”,然后再把 keyProperty 设置为目标属性就 OK 了。这样,自动生成的ID就会映射到Author对象中
      2. 对于不支持自动生成主键列的数据库和可能不支持自动生成主键的 JDBC 驱动,MyBatis 有另外一种方法来生成主键
      3. 这里有一个简单(也很傻)的示例,它可以生成一个随机 ID(不建议实际使用,这里只是为了展示 MyBatis 处理问题的灵活性和宽容度)
        1. 首先会运行 selectKey 元素中的语句,并设置 Author 的 id,然后才会调用插入语句。这样就实现了数据库自动生成主键类似的行为
      <insert id="insertAuthor">
        <selectKey keyProperty="id" resultType="int" order="BEFORE">
          select CAST(RANDOM()*1000000 as INTEGER) a from SYSIBM.SYSDUMMY1
        </selectKey>
        insert into Author
          (id, username, password, email,bio, favourite_section)
        values
          (#{id}, #{username}, #{password}, #{email}, #{bio}, #{favouriteSection,jdbcType=VARCHAR})
      </insert>
      
    2. selectKey 元素

    <selectKey
      keyProperty="id"
      resultType="int"
      order="BEFORE"
      statementType="PREPARED">
    
属性描述
keyPropertyselectKey 语句结果应该被设置到的目标属性。如果生成列不止一个,可以用逗号分隔多个属性名称。
keyColumn返回结果集中生成列属性的列名。如果生成列不止一个,可以用逗号分隔多个属性名称。
resultType结果的类型。通常 MyBatis 可以推断出来,但是为了更加准确,写上也不会有什么问题。MyBatis 允许将任何简单类型用作主键的类型,包括字符串。如果生成列不止一个,则可以使用包含期望属性的 Object 或 Map。
order可以设置为 BEFOREAFTER。如果设置为 BEFORE,那么它首先会生成主键,设置 keyProperty 再执行插入语句。如果设置为 AFTER,那么先执行插入语句,然后是 selectKey 中的语句 - 这和 Oracle 数据库的行为相似,在插入语句内部可能有嵌入索引调用。
statementType和前面一样,MyBatis 支持 STATEMENTPREPAREDCALLABLE 类型的映射语句,分别代表 Statement, PreparedStatementCallableStatement 类型。
  1. 特殊情况

    1. 在一些特殊情况下,一些数据库允许 INSERT、UPDATE 或 DELETE 语句返回结果集(例如:PostgreSQL 和 MariaDB 数据库的 RETURNING 子句,或者是 MS SQL Server 的 OUTPUT 子句)

    2. 这种类型的语句必须使用 <select>,用于映射返回的数据

      <select id="insertAndGetAuthor" resultType="domain.blog.Author"
            affectData="true" flushCache="true">
        insert into Author (username, password, email, bio)
        values (#{username}, #{password}, #{email}, #{bio})
        returning id, username, password, email, bio
      </select>
      
  2. 映射更新语句

    1. 配置同insert

      <update id="updateAuthor">
        update Author set
          username = #{username},
          password = #{password},
          email = #{email},
          bio = #{bio}
        where id = #{id}
      </update>
      
  3. 映射删除语句

    1. 配置同insert

      <delete id="deleteAuthor">
        delete from Author where id = #{id}
      </delete>
      
  4. 配置信息

    <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">
    
    属性描述
    id在命名空间中唯一的标识符,可以被用来引用这条语句。
    parameterType将会传入这条语句的参数的类全限定名或别名。这个属性是可选的,因为 MyBatis 可以根据语句中实际传入的参数计算出应该使用的类型处理器(TypeHandler),默认值为未设置(unset)。
    parameterMap用于引用外部 parameterMap 的属性,目前已被废弃。请使用行内参数映射和 parameterType 属性。
    flushCache将其设置为 true 后,只要语句被调用,都会导致本地缓存和二级缓存被清空,默认值:(对 insert、update 和 delete 语句)true。
    timeout这个设置是在抛出异常之前,驱动程序等待数据库返回请求结果的秒数。默认值为未设置(unset)(依赖数据库驱动)。
    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)中,当主键列不是表中的第一列的时候,是必须设置的。如果生成列不止一个,可以用逗号分隔多个属性名称。
    databaseId如果配置了数据库厂商标识(databaseIdProvider),MyBatis 会加载所有不带 databaseId 或匹配当前 databaseId 的语句;如果带和不带的语句都有,则不带的会被忽略。

7. select

  1. 映射查询语句

  2. 查询语句是 MyBatis 中最常用的元素之一,它光能把数据存到数据库中价值并不大,还要能重新取出来才有用,多数应用也都是查询比修改要频繁

  3. MyBatis 的基本原则之一是:在每个插入、更新或删除操作之间,通常会执行多个查询操作。因此,MyBatis 在查询和结果映射做了相当多的改进。一个简单查询的 select 元素是非常简单的

<select id="selectPerson" parameterType="int" resultType="hashmap">
  SELECT * FROM PERSON WHERE ID = #{id}
</select>
  1. 这个语句名为 selectPerson,接受一个 int(或 Integer)类型的参数,并返回一个 HashMap 类型的对象,其中的键是列名,值便是结果行中的对应值。

    1. 注意参数符号:#{id}

    2. 这就告诉 MyBatis 创建一个预处理语句(PreparedStatement)参数,在 JDBC 中,这样的一个参数在 SQL 中会由一个“?”来标识,并被传递到一个新的预处理语句中,例如

      // 近似的 JDBC 代码,非 MyBatis 代码...
      String selectPerson = "SELECT * FROM PERSON WHERE ID=?";
      PreparedStatement ps = conn.prepareStatement(selectPerson);
      ps.setInt(1,id);
      
  2. 可以配置的属性

<select
  id="selectPerson"
  parameterType="int"
  parameterMap="deprecated"
  resultType="hashmap"
  resultMap="personResultMap"
  flushCache="false"
  useCache="true"
  timeout="10"
  fetchSize="256"
  statementType="PREPARED"
  resultSetType="FORWARD_ONLY">
属性描述
id在命名空间中唯一的标识符,可以被用来引用这条语句。
parameterType将会传入这条语句的参数的类全限定名或别名。这个属性是可选的,因为 MyBatis 可以根据语句中实际传入的参数计算出应该使用的类型处理器(TypeHandler),默认值为未设置(unset)。
parameterMap用于引用外部 parameterMap 的属性,目前已被废弃。请使用行内参数映射和 parameterType 属性。
resultType期望从这条语句中返回结果的类全限定名或别名。 注意,如果返回的是集合,那应该设置为集合包含的类型,而不是集合本身的类型。 resultType 和 resultMap 之间只能同时使用一个。
resultMap对外部 resultMap 的命名引用。结果映射是 MyBatis 最强大的特性,如果你对其理解透彻,许多复杂的映射问题都能迎刃而解。 resultType 和 resultMap 之间只能同时使用一个。
flushCache将其设置为 true 后,只要语句被调用,都会导致本地缓存和二级缓存被清空,默认值:false。
useCache将其设置为 true 后,将会导致本条语句的结果被二级缓存缓存起来,默认值:对 select 元素为 true。
timeout这个设置是在抛出异常之前,驱动程序等待数据库返回请求结果的秒数。默认值为未设置(unset)(依赖数据库驱动)。
fetchSize这是一个给驱动的建议值,尝试让驱动程序每次批量返回的结果行数等于这个设置值。 默认值为未设置(unset)(依赖驱动)。
statementType可选 STATEMENT,PREPARED 或 CALLABLE。这会让 MyBatis 分别使用 Statement,PreparedStatement 或 CallableStatement,默认值:PREPARED。
resultSetTypeFORWARD_ONLY,SCROLL_SENSITIVE, SCROLL_INSENSITIVE 或 DEFAULT(等价于 unset) 中的一个,默认值为 unset (依赖数据库驱动)。
databaseId如果配置了数据库厂商标识(databaseIdProvider),MyBatis 会加载所有不带 databaseId 或匹配当前 databaseId 的语句;如果带和不带的语句都有,则不带的会被忽略。
resultOrdered这个设置仅针对嵌套结果 select 语句:如果为 true,则假设结果集以正确顺序(排序后)执行映射,当返回新的主结果行时,将不再发生对以前结果行的引用。 这样可以减少内存消耗。默认值:false
resultSets这个设置仅适用于多结果集的情况。它将列出语句执行后返回的结果集并赋予每个结果集一个名称,多个名称之间以逗号分隔。
affectData当写入INSERT、UPDATE或DELETE语句返回数据时,将此设置为true,以便正确地控制事务。
请参见事务控制方法。
默认值:false(从3.5.12开始)

8. 参数

  1. 对于所有的语句,实际上都会有参数映射

    1. 对于下面SQL,参数类型parameterType会自动映射为int类型
    <select id="selectUsers" resultType="User">
      select id, username, password
      from users
      where id = #{id}
    </select>
    
    1. 如果User类型的参数传递到了语句中,会查找id,username,password属性我们也可以指定这些参数的类型

      <insert id="insertUser" parameterType="User">
        insert into users (id, username, password)
        values (#{id}, #{username}, #{password})
      </insert>
      
      #{property,javaType=int,jdbcType=NUMERIC}
      
    2. JDBC 要求,如果一个列允许使用 null 值,并且会使用值为 null 的参数,就必须要指定 JDBC 类型(jdbcType)

    3. 要更进一步地自定义类型处理方式,可以指定一个特殊的类型处理器类(或别名)、

      #{age,javaType=int,jdbcType=NUMERIC,typeHandler=MyTypeHandler}
      
    4. 如果需要保留小数

      // 指定保留小数点
      #{height,javaType=double,jdbcType=NUMERIC,numericScale=2}
      
    5. mode属性

      1. mode 属性允许你指定 INOUTINOUT 参数

      2. 如果参数的 modeOUTINOUT,将会修改参数对象的属性值,以便作为输出参数返回

      3. 如果 modeOUT(或 INOUT),而且 jdbcTypeCURSOR(也就是 Oracle 的 REFCURSOR),你必须指定一个 resultMap 引用来将结果集 ResultSet 映射到参数的类型上

      4. 要注意这里的 javaType 属性是可选的,如果留空并且 jdbcType 是 CURSOR,它会被自动地被设为 ResultSet

        #{department, mode=OUT, jdbcType=CURSOR, javaType=ResultSet, resultMap=departmentResultMap}
        
    6. 字符串替换**${}**

      1. 默认情况下,使用 #{} 参数语法时,MyBatis 会创建 PreparedStatement 参数占位符,并通过占位符安全地设置参数(就像使用 ? 一样)。 这样做更安全,更迅速,通常也是首选做法

      2. 不过有时你就是想直接在 SQL 语句中直接插入一个不转义的字符串。 比如 ORDER BY 子句,这时候你可以使用${}

        ORDER BY ${columnName}
        
        1. 这样,Mybati就不会修改或者转义改字符串
      3. 当 SQL 语句中的元数据(如表名或列名)是动态生成的时候,字符串替换将会非常有用

        1. 举个例子,如果你想 select 一个表任意一列的数据时
        @Select("select * from user where id = #{id}")
        User findById(@Param("id") long id);
        
        @Select("select * from user where name = #{name}")
        User findByName(@Param("name") String name);
        
        @Select("select * from user where email = #{email}")
        User findByEmail(@Param("email") String email);
        
        
        1. 我们就可以使用字符串替换,将上述三个SQL整合为一个SQL,让列名当做一个参数

          @Select("select * from user where ${column} = #{value}")
          User findByColumn(@Param("column") String column, @Param("value") String value);
          
      4. 对于需要接受用户输入的参数,使用${}是不安全的,会导致潜在的SQL注入风险,因此要使用这种方式,要么不允许用户输入这些字段或者自行转义并检验这些参数

5. 动态SQL

1. 概念

  1. 动态 SQL 是 MyBatis 的强大特性之一
  2. 如果你使用过 JDBC 或其它类似的框架,你应该能理解根据不同条件拼接 SQL 语句有多痛苦,例如拼接时要确保不能忘记添加必要的空格,还要注意去掉列表最后一个列名的逗号
  3. 利用动态 SQL,可以彻底摆脱这种痛苦。使用动态 SQL 并非一件易事,但借助可用于任何 SQL 映射语句中的强大的动态 SQL 语言,MyBatis 显著地提升了这一特性的易用性。
  4. 如果你之前用过 JSTL 或任何基于类 XML 语言的文本处理器,你对动态 SQL 元素可能会感觉似曾相识。在 MyBatis 之前的版本中,需要花时间了解大量的元素。借助功能强大的基于 OGNL 的表达式,MyBatis 3 替换了之前的大部分元素,大大精简了元素种类,现在要学习的元素种类比原来的一半还要少

2. if

​ 1. 根据IF中的判断条件,拼接标签内部的SQL

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

3. choose、when、otherwise

  1. 和if效果一样,但是它具有排他属性,就类似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>

4. trim、where、set

  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>
    </select>
    
    1. 当上述SQL没有任何条件满足时,此时SQL变为如下,会产生一个错误的结果

      SELECT * FROM BLOG WHERE
      
    2. 当上述SQL只满足第二个条件时,此时SQL变为如下,会产生一个错误的结果

      SELECT * FROM BLOG WHERE AND title like ‘someTitle’
      
  2. 为了处理上面这几种错误,Mybatis提供了Where,trim标签

    1. where标签会自动添加where关键字,若子句与And,Or开头,where元素会自动将他们去除
    2. trim表示是一个更加灵活的标签,它用于自定义需要添加或者删除的代码,它也能完成where的功能
      1. 如下案例,prefix:where 表示自动给标签内的代码段添加where前缀,prefixOverrides:表示遇到指定的关键字会被自动删除
    <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>
      </where>
    </select>
    
    
    <select id="findActiveBlogLike"  resultType="Blog">
      SELECT * FROM BLOG
      <trim prefix="WHERE" prefixOverrides="AND | OR ">
          <if test="state != null">
                 state = #{state}
            </if>
            <if test="title != null">
                AND title like #{title}
            </if>
     </trim>
    </select>
    
    
  3. 对于update语句,为了防止动态条件不满足导致最终SQL错误,提供了一个Set标签,和Where标签效果类似

    1. set标签会自动添加set关键字,并且删除最后的”,”逗号
    2. 同时我们也可以使用trim来完成一样的功能
    <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>
    
    <update id="updateAuthorIfNecessary">
      update Author
      <trim prefix="SET" suffixOverrides=",">
         <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>
     </trim>
     where id=#{id}
    </update>
    

5. foreach

  1. 动态 SQL 的另一个常见使用场景是对集合进行遍历(尤其是在构建 IN 条件语句的时候)

  2. foreach 元素的功能非常强大,它允许你指定一个集合,声明可以在元素体内使用的集合项(item)和索引(index)变量

  3. 它也允许你指定开头与结尾的字符串以及集合项迭代之间的分隔符,这个元素也不会错误地添加多余的分隔符

  4. 提示

    1. 你可以将任何可迭代对象(如 List、Set 等)、Map 对象或者数组对象作为集合参数传递给 foreach
    2. 当使用可迭代对象或者数组时,index 是当前迭代的序号,item 的值是本次迭代获取到的元素
    3. 当使用 Map 对象(或者 Map.Entry 对象的集合)时,index 是键,item 是值
    <select id="selectPostIn" resultType="domain.blog.Post">
      SELECT * FROM POST P
      <where>
        <foreach item="item" index="index" collection="list" open="id in (" separator="," close=")" nullable="true">
              #{item}
        </foreach>
      </where>
    </select>
    

6. Script

  1. 如果要在带注解的映射器接口类中使用动态 SQL,可以使用 script 元素

    @Update({"<script>",
          "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}",
          "</script>"})
        void updateAuthorValues(Author author);
    

7. bind

  1. bind 元素允许你在 OGNL 表达式以外创建一个变量,并将其绑定到当前的上下文
    1. 绑定一个name为pattern的变量,在SQL中拼接该变量的值
<select id="selectBlogsLike" resultType="Blog">
  <bind name="pattern" value="'%' + _parameter.getTitle() + '%'" />
  SELECT * FROM BLOG WHERE title LIKE #{pattern}
</select>

8. 多数据库支持

  1. 如果配置了 databaseIdProvider,你就可以在动态代码中使用名为 “_databaseId” 的变量来为不同的数据库构建特定的语句

    <insert id="insert">
      <selectKey keyProperty="id" resultType="int" order="BEFORE">
        <if test="_databaseId == 'oracle'">
          select seq_users.nextval from dual
        </if>
        <if test="_databaseId == 'db2'">
          select nextval for seq_users from sysibm.sysdummy1"
        </if>
      </selectKey>
      insert into users values (#{id}, #{name})
    </insert>
    

9. LanguageDriver 动态 SQL 中的插入脚本语言

  1. MyBatis 从 3.2 版本开始支持插入脚本语言,这允许你插入一种语言驱动,并基于这种语言来编写动态 SQL 查询语句。

  2. 可以通过实现以下LanguageDriver 接口来插入一种语言

    1. ParameterHandler: 参数处理器,通过自己的语言来处理参数
    2. SqlSource: Sql的来源,通过自己的语言来解析sql
    public interface LanguageDriver {
      ParameterHandler createParameterHandler(MappedStatement mappedStatement, Object parameterObject, BoundSql boundSql);
      SqlSource createSqlSource(Configuration configuration, XNode script, Class<?> parameterType);
      SqlSource createSqlSource(Configuration configuration, String script, Class<?> parameterType);
    }
    
  3. 实现自定义语言驱动后,你就可以在 mybatis-config.xml 文件中将它设置为默认语言:

    <typeAliases>
      <typeAlias type="org.sample.MyLanguageDriver" alias="myLanguage"/>
    </typeAliases>
    <settings>
      <setting name="defaultScriptingLanguage" value="myLanguage"/>
    </settings>
    
    1. 不设置默认的语言,你也可以使用 lang 属性为特定的语句指定语言,或者@Lang注解来指定语言

      <select id="selectBlog" lang="myLanguage">
        SELECT * FROM BLOG
      </select>
      
      public interface Mapper {
        @Lang(MyLanguageDriver.class)
        @Select("SELECT * FROM BLOG")
        List<Blog> selectBlog();
      }
      
  4. 可供使用的模板

    1. 引用Thymeleaf模板
    2. Freemark

6. JavaAPI

1. SqlSessionFactoryBuilder

  1. SqlSessionFactoryBuilder 有五个 build() 方法,每一种都允许你从不同的资源中创建一个 SqlSessionFactory 实例
SqlSessionFactory build(InputStream inputStream)
SqlSessionFactory build(InputStream inputStream, String environment)
SqlSessionFactory build(InputStream inputStream, Properties properties)
SqlSessionFactory build(InputStream inputStream, String env, Properties props)
SqlSessionFactory build(Configuration config)
  1. 使用Resources 工具类,完成对资源的加载
String resource = "org/mybatis/builder/mybatis-config.xml";
InputStream inputStream = Resources.getResourceAsStream(resource);
SqlSessionFactoryBuilder builder = new SqlSessionFactoryBuilder();
SqlSessionFactory factory = builder.build(inputStream);
  1. 使用JavaAPI构建

    1. 核心配置类Configuration,我们通过xml配置,最终也会转成Configuration来构建
    DataSource dataSource = BaseDataTest.createBlogDataSource();
    TransactionFactory transactionFactory = new JdbcTransactionFactory();
    
    Environment environment = new Environment("development", transactionFactory, dataSource);
    
    Configuration configuration = new Configuration(environment);
    configuration.setLazyLoadingEnabled(true);
    configuration.setEnhancementEnabled(true);
    configuration.getTypeAliasRegistry().registerAlias(Blog.class);
    configuration.getTypeAliasRegistry().registerAlias(Post.class);
    configuration.getTypeAliasRegistry().registerAlias(Author.class);
    configuration.addMapper(BoundBlogMapper.class);
    configuration.addMapper(BoundAuthorMapper.class);
    
    SqlSessionFactoryBuilder builder = new SqlSessionFactoryBuilder();
    SqlSessionFactory factory = builder.build(configuration);
    

2. SqlSessionFactory

  1. SqlSessionFactory 有六个方法创建 SqlSession 实例
  2. 通常来说,当你选择其中一个方法时,你需要考虑以下几点
    1. 事务处理:你希望在 session 作用域中使用事务作用域,还是使用自动提交(auto-commit)?(对很多数据库和/或 JDBC 驱动来说,等同于关闭事务支持)
    2. 数据库连接:你希望 MyBatis 帮你从已配置的数据源获取连接,还是使用自己提供的连接?
    3. 语句执行:你希望 MyBatis 复用 PreparedStatement 和/或批量更新语句(包括插入语句和删除语句)吗?
  3. 基于以上需求,有下列已重载的多个 openSession() 方法供使用
    1. 默认的 openSession() 方法没有参数,它会创建具备如下特性的 SqlSession:
      • 事务作用域将会开启(也就是手动提交)。
      • 将由当前环境配置的 DataSource 实例中获取 Connection 对象。
      • 事务隔离级别将会使用驱动或数据源的默认设置。
      • 预处理语句不会被复用,也不会批量处理更新
    2. ExecutorType:执行器的类型
      • ExecutorType.SIMPLE:该类型的执行器没有特别的行为。它为每个语句的执行创建一个新的预处理语句。
      • ExecutorType.REUSE:该类型的执行器会复用预处理语句。
      • ExecutorType.BATCH:该类型的执行器会批量执行所有更新语句,如果 SELECT 在多个更新中间执行,将在必要时将多条更新语句分隔开来,以方便理解
    3. 在 SqlSessionFactory 中还有一个方法,就是 getConfiguration()。这个方法会返回一个 Configuration 实例,你可以在运行时使用它来检查 MyBatis 的配置
SqlSession openSession()
SqlSession openSession(boolean autoCommit)
SqlSession openSession(Connection connection)
SqlSession openSession(TransactionIsolationLevel level)
SqlSession openSession(ExecutorType execType, TransactionIsolationLevel level)
SqlSession openSession(ExecutorType execType)
SqlSession openSession(ExecutorType execType, boolean autoCommit)
SqlSession openSession(ExecutorType execType, Connection connection)
Configuration getConfiguration();

3. SqlSession

  1. 使用 MyBatis 的主要 Java 接口就是 SqlSession

  2. 你可以通过这个接口来执行映射语句,获取映射器Mapper实例和管理事务

  3. 在介绍 SqlSession 接口之前,我们先来了解如何获取一个 SqlSession 实例。SqlSessions 是由 SqlSessionFactory 实例创建的

  4. SqlSessionFactory 对象包含创建 SqlSession 实例的各种方法

  5. 而 SqlSessionFactory 本身是由 SqlSessionFactoryBuilder 创建的,它可以从 XML、注解或 Java 配置代码来创建 SqlSessionFactory

  6. 提示

    1. 当 Mybatis 与一些依赖注入框架(如 Spring 或者 Guice)搭配使用时,SqlSession 将被依赖注入框架创建并注入,所以你不需要使用 SqlSessionFactoryBuilder 或者 SqlSessionFactory
  7. SqlSession方法很多,进行分组

    1. 基础版

      <T> T selectOne(String statement, Object parameter)
      <E> List<E> selectList(String statement, Object parameter)
      <T> Cursor<T> selectCursor(String statement, Object parameter)
      <K,V> Map<K,V> selectMap(String statement, Object parameter, String mapKey)
      int insert(String statement, Object parameter)
      int update(String statement, Object parameter)
      int delete(String statement, Object parameter)
      
      1. selectOne 和 selectList 的不同仅仅是 selectOne 必须返回一个对象或 null 值,如果返回值多于一个,就会抛出异常。如果你不知道返回对象会有多少,请使用 selectList。

      2. 如果需要查看某个对象是否存在,最好的办法是查询一个 count 值(0 或 1)。selectMap 稍微特殊一点,它会将返回对象的其中一个属性作为 key 值,将对象作为 value 值,从而将多个结果集转为 Map 类型值。由于并不是所有语句都需要参数,所以这些方法都具有一个不需要参数的重载形式

      3. 游标(Cursor)与列表(List)返回的结果相同,不同的是,游标借助迭代器实现了数据的惰性加载

        try (Cursor<MyEntity> entities = session.selectCursor(statement, param)) {
           for (MyEntity entity:entities) {
              // 处理单个实体
           }
        }
        
    2. 进阶版

    <E> List<E> selectList (String statement, Object parameter, RowBounds rowBounds)
    <T> Cursor<T> selectCursor(String statement, Object parameter, RowBounds rowBounds)
    <K,V> Map<K,V> selectMap(String statement, Object parameter, String mapKey, RowBounds rowbounds)
    void select (String statement, Object parameter, ResultHandler<T> handler)
    void select (String statement, Object parameter, RowBounds rowBounds, ResultHandler<T> handler)
    
    1. 他们允许进行分页,限制返回行的范围

      1. RowBounds 参数会告诉 MyBatis 略过指定数量的记录,并限制返回结果的数量。RowBounds 类的 offset 和 limit 值只有在构造函数时才能传入,其它时候是不能修改的

      2. 数据库驱动决定了略过记录时的查询效率。为了获得最佳的性能,建议将 ResultSet 类型设置为 SCROLL_SENSITIVE 或 SCROLL_INSENSITIVE(换句话说:不要使用 FORWARD_ONLY)。

        int offset = 100;
        int limit = 25;
        RowBounds rowBounds = new RowBounds(offset, limit);
        
    2. 可以提供自定义的结果处理逻辑ResultHandler接口,通常在数据量大的时使用

      1. ResultHandler 参数允许自定义每行结果的处理过程

        1. ResultContext 参数允许你访问结果对象和当前已被创建的对象数目
        2. 另外还提供了一个返回值为 Boolean 的 stop 方法,你可以使用此 stop 方法来停止 MyBatis 加载更多的结果。
        3. 使用 ResultHandler 的时候需要注意以下两个限制:
          • 使用带 ResultHandler 参数的方法时,收到的数据不会被缓存。
          • 当使用高级的结果映射集(resultMap)时,MyBatis 很可能需要多行结果来构造一个对象。如果你使用了 ResultHandler,你可能会接收到关联(association)或者集合(collection)中尚未被完整填充的对象。
        public interface ResultHandler<T> {
          void handleResult(ResultContext<? extends T> context);
        }
        
      2. 你可以将它添加到 List 中、创建 Map 和 Set,甚至丢弃每个返回值,只保留计算后的统计结果

      3. 你可以使用 ResultHandler 做很多事,这其实就是 MyBatis 构建 结果列表的内部实现办法。

      4. 从版本 3.4.6 开始,ResultHandler 会在存储过程的 REFCURSOR 输出参数中传递使用的 CALLABLE 语句。

      5. 例如

        1. 以下代码将所有结果都查询出来了,再遍历每一行的结果,使用ResultHandler进行一一处理,最终返回保留在处理器中的所有结果
        @Override
        public <K, V> Map<K, V> selectMap(String statement, Object parameter, String mapKey, RowBounds rowBounds) {
          final List<? extends V> list = selectList(statement, parameter, rowBounds);
          final DefaultMapResultHandler<K, V> mapResultHandler = new DefaultMapResultHandler<>(mapKey,
              configuration.getObjectFactory(), configuration.getObjectWrapperFactory(), configuration.getReflectorFactory());
          final DefaultResultContext<V> context = new DefaultResultContext<>();
          for (V o : list) {
            context.nextResultObject(o);
            mapResultHandler.handleResult(context);
          }
          return mapResultHandler.getMappedResults();
        }
        
    3. 其他版

      1. 立即批量更新方法
        1. 当你将 ExecutorType 设置为 ExecutorType.BATCH 时,可以使用这个方法清除(执行)缓存在 JDBC 驱动类中的批量更新语句
      List<BatchResult> flushStatements()
      
      1. 事务控制方法
        1. 有四个方法用来控制事务作用域。
        2. 当然,如果你已经设置了自动提交或你使用了外部事务管理器,这些方法就没什么作用了。
        3. 然而,如果你正在使用由 Connection 实例控制的 JDBC 事务管理器,那么这四个方法就会派上用场
        4. 默认情况下 MyBatis 不会自动提交事务,除非它侦测到调用了插入、更新、删除或 select with affectData enabled 方法改变了数据库
        5. 如果你没有使用这些方法提交修改,那么你可以在 commit 和 rollback 方法参数中传入 true 值,来保证事务被正常提交(注意,在自动提交模式或者使用了外部事务管理器的情况下,设置 force 值对 session 无效)
        6. 大部分情况下你无需调用 rollback(),因为 MyBatis 会在你没有调用 commit 时替你完成回滚操作。不过,当你要在一个可能多次提交或回滚的 session 中详细控制事务,回滚操作就派上用场了。
      void commit()
      void commit(boolean force)
      void rollback()
      void rollback(boolean force)
      
      1. 本地缓存
        1. Mybatis 使用到了两种缓存:本地缓存(local cache)和二级缓存(second level cache)
        2. 每当一个新 session 被创建,MyBatis 就会创建一个与之相关联的本地缓存
        3. 任何在 session 执行过的查询结果都会被保存在本地缓存中,所以,当再次执行参数相同的相同查询时,就不需要实际查询数据库了。本地缓存将会在做出修改、事务提交或回滚,以及关闭 session 时清空。
        4. 默认情况下,本地缓存数据的生命周期等同于整个 session 的周期
        5. 由于缓存会被用来解决循环引用问题和加快重复嵌套查询的速度,所以无法将其完全禁用。但是你可以通过设置 localCacheScope=STATEMENT 来只在语句执行时使用缓存。
        6. 注意,如果 localCacheScope 被设置为 SESSION,对于某个对象,MyBatis 将返回在本地缓存中唯一对象的引用。对返回的对象(例如 list)做出的任何修改将会影响本地缓存的内容,进而将会影响到在本次 session 中从缓存返回的值。因此,不要对 MyBatis 所返回的对象作出更改,以防后患
        7. 你可以随时调用下面方法来清空本地缓存
      void clearCache()
      
      1. 确保 SqlSession 被关闭

        1. 对于你打开的任何 session,你都要保证它们被妥善关闭,这很重要

        2. 保证妥善关闭的最佳代码模式是这样的

          SqlSession session = sqlSessionFactory.openSession();
          try (SqlSession session = sqlSessionFactory.openSession()) {
              // 假设下面三行代码是你的业务逻辑
              session.insert(...);
              session.update(...);
              session.delete(...);
              session.commit();
          }
          
      void close()
      
      1. 和 SqlSessionFactory 一样,你可以调用当前使用的 SqlSession 的 getConfiguration 方法来获得 Configuration 实例
      Configuration getConfiguration()
      
      1. 使用映射器

        1. SqlSession的各个 insert、update、delete 和 select 方法都很强大,但也有些繁琐,它们并不符合类型安全,只接受映射语句的statement的Id,因此,使用Mapper类来执行映射语句是更常见的做法

        2. 一个映射器类就是一个仅需声明与 SqlSession 方法相匹配方法的接口

          1. @MapKey(“id”),标注,返回的Map是有id字段作为key
          public interface AuthorMapper {
            // (Author) SqlSession.selectOne("selectAuthor",5);
            Author selectAuthor(int id);
            // (List<Author>) SqlSession.selectList(“selectAuthors”)
            List<Author> selectAuthors();
            // (Map<Integer,Author>) SqlSession.selectMap("selectAuthors", "id")
            @MapKey("id")
            Map<Integer, Author> selectAuthors();
            // SqlSession.insert("insertAuthor", author)
            int insertAuthor(Author author);
            // SqlSession.updateAuthor("updateAuthor", author)
            int updateAuthor(Author author);
            // SqlSession.delete("deleteAuthor",5)
            int deleteAuthor(int id);
          }
          
          1. 总之,每个映射器方法签名应该匹配相关联的 SqlSession 方法,字符串参数 ID 无需匹配。而是由方法名匹配映射语句的 ID。
          2. 此外,返回类型必须匹配期望的结果类型,返回单个值时,返回类型应该是返回值的类,返回多个值时,则为数组或集合类,另外也可以是游标(Cursor)。所有常用的类型都是支持的,包括:原始类型、Map、POJO 和 JavaBean。
          3. 提示
            1. 映射器接口不需要去实现任何接口或继承自任何类。只要方法签名可以被用来唯一识别对应的映射语句就可以了。
            2. 映射器接口可以继承自其他接口。在使用 XML 来绑定映射器接口时,保证语句处于合适的命名空间中即可。唯一的限制是,不能在两个具有继承关系的接口中拥有相同的方法签名(这是潜在的危险做法,不可取)。
          4. 你可以传递多个参数给一个映射器方法
            1. 在多个参数的情况下,默认它们将会以 param 加上它们在参数列表中的位置来命名
              1. 比如:#{param1}、#{param2}等
            2. 如果你想(在有多个参数时)自定义参数的名称,那么你可以在参数上使用 @Param(“paramName”) 注解。
          5. 你也可以给方法传递一个 RowBounds 实例来限制查询结果
      <T> T getMapper(Class<T> type)
      

7. SQL语句构建器

1. 产生的背景(面临的问题)

  1. Java 程序员面对的最痛苦的事情之一就是在 Java 代码中嵌入 SQL 语句

    1. 这通常是因为需要动态生成 SQL 语句
    2. 不然我们可以将它们放到外部文件或者存储过程中
  2. MyBatis 在 XML 映射中具备强大的 SQL 动态生成能力

    1. 但有时,我们还是需要在 Java 代码里构建 SQL 语句
    2. 此时,MyBatis 有另外一个特性可以帮到你,让你从处理典型问题中解放出来,比如加号、引号、换行、格式化问题、嵌入条件的逗号管理及 AND 连接
  3. 例如

    1. 在 Java 代码中动态生成 SQL 代码真的就是一场噩梦
    String sql = "SELECT P.ID, P.USERNAME, P.PASSWORD, P.FULL_NAME, "
    "P.LAST_NAME,P.CREATED_ON, P.UPDATED_ON " +
    "FROM PERSON P, ACCOUNT A " +
    "INNER JOIN DEPARTMENT D on D.ID = P.DEPARTMENT_ID " +
    "INNER JOIN COMPANY C on D.COMPANY_ID = C.ID " +
    "WHERE (P.ID = A.ID AND P.FIRST_NAME like ?) " +
    "OR (P.LAST_NAME like ?) " +
    "GROUP BY P.ID " +
    "HAVING (P.LAST_NAME like ?) " +
    "OR (P.FIRST_NAME like ?) " +
    "ORDER BY P.ID, P.FULL_NAME";
    

2. 解决方案

  1. MyBatis 3 提供了方便的工具类来帮助解决此问题
  2. 借助 SQL 类,我们只需要简单地创建一个实例,并调用它的方法即可生成 SQL 语句
  3. 让我们来用 SQL 类重写上面的例子
private String selectPersonSql() {
  return new SQL() {{
    SELECT("P.ID, P.USERNAME, P.PASSWORD, P.FULL_NAME");
    SELECT("P.LAST_NAME, P.CREATED_ON, P.UPDATED_ON");
    FROM("PERSON P");
    FROM("ACCOUNT A");
    INNER_JOIN("DEPARTMENT D on D.ID = P.DEPARTMENT_ID");
    INNER_JOIN("COMPANY C on D.COMPANY_ID = C.ID");
    WHERE("P.ID = A.ID");
    WHERE("P.FIRST_NAME like ?");
    OR();
    WHERE("P.LAST_NAME like ?");
    GROUP_BY("P.ID");
    HAVING("P.LAST_NAME like ?");
    OR();
    HAVING("P.FIRST_NAME like ?");
    ORDER_BY("P.ID");
    ORDER_BY("P.FULL_NAME");
  }}.toString();
}

3. SQL类(动态构建SQL的工具类)

1. 构建案例
// 匿名内部类风格
public String deletePersonSql() {
  return new SQL() {{
    DELETE_FROM("PERSON");
    WHERE("ID = #{id}");
  }}.toString();
}

// Builder / Fluent 风格
public String insertPersonSql() {
  String sql = new SQL()
    .INSERT_INTO("PERSON")
    .VALUES("ID, FIRST_NAME", "#{id}, #{firstName}")
    .VALUES("LAST_NAME", "#{lastName}")
    .toString();
  return sql;
}

// 动态条件(注意参数需要使用 final 修饰,以便匿名内部类对它们进行访问)
public String selectPersonLike(final String id, final String firstName, final String lastName) {
  return new SQL() {{
    SELECT("P.ID, P.USERNAME, P.PASSWORD, P.FIRST_NAME, P.LAST_NAME");
    FROM("PERSON P");
    if (id != null) {
      WHERE("P.ID like #{id}");
    }
    if (firstName != null) {
      WHERE("P.FIRST_NAME like #{firstName}");
    }
    if (lastName != null) {
      WHERE("P.LAST_NAME like #{lastName}");
    }
    ORDER_BY("P.LAST_NAME");
  }}.toString();
}

public String deletePersonSql() {
  return new SQL() {{
    DELETE_FROM("PERSON");
    WHERE("ID = #{id}");
  }}.toString();
}

public String insertPersonSql() {
  return new SQL() {{
    INSERT_INTO("PERSON");
    VALUES("ID, FIRST_NAME", "#{id}, #{firstName}");
    VALUES("LAST_NAME", "#{lastName}");
  }}.toString();
}

public String updatePersonSql() {
  return new SQL() {{
    UPDATE("PERSON");
    SET("FIRST_NAME = #{firstName}");
    WHERE("ID = #{id}");
  }}.toString();
}
2. 构建API迭代
  1. 从版本 3.4.2 开始,你可以像下面这样使用可变长度参数

    public String selectPersonSql() {
      return new SQL()
        .SELECT("P.ID", "A.USERNAME", "A.PASSWORD", "P.FULL_NAME", "D.DEPARTMENT_NAME", "C.COMPANY_NAME")
        .FROM("PERSON P", "ACCOUNT A")
        .INNER_JOIN("DEPARTMENT D on D.ID = P.DEPARTMENT_ID", "COMPANY C on D.COMPANY_ID = C.ID")
        .WHERE("P.ID = A.ID", "P.FULL_NAME like #{name}")
        .ORDER_BY("P.ID", "P.FULL_NAME")
        .toString();
    }
    
    public String insertPersonSql() {
      return new SQL()
        .INSERT_INTO("PERSON")
        .INTO_COLUMNS("ID", "FULL_NAME")
        .INTO_VALUES("#{id}", "#{fullName}")
        .toString();
    }
    
    public String updatePersonSql() {
      return new SQL()
        .UPDATE("PERSON")
        .SET("FULL_NAME = #{fullName}", "DATE_OF_BIRTH = #{dateOfBirth}")
        .WHERE("ID = #{id}")
        .toString();
    }
    
  2. 从版本 3.5.2 开始,你可以像下面这样构建批量插入语句

    public String insertPersonsSql() {
      // INSERT INTO PERSON (ID, FULL_NAME)
      //     VALUES (#{mainPerson.id}, #{mainPerson.fullName}) , (#{subPerson.id}, #{subPerson.fullName})
      return new SQL()
        .INSERT_INTO("PERSON")
        .INTO_COLUMNS("ID", "FULL_NAME")
        .INTO_VALUES("#{mainPerson.id}", "#{mainPerson.fullName}")
        .ADD_ROW()
        .INTO_VALUES("#{subPerson.id}", "#{subPerson.fullName}")
        .toString();
    }
    
  3. 从版本 3.5.2 开始,你可以像下面这样构建限制返回结果数的 SELECT 语句,

    public String selectPersonsWithOffsetLimitSql() {
      // SELECT id, name FROM PERSON
      //     LIMIT #{limit} OFFSET #{offset}
      return new SQL()
        .SELECT("id", "name")
        .FROM("PERSON")
        .LIMIT("#{limit}")
        .OFFSET("#{offset}")
        .toString();
    }
    
    public String selectPersonsWithFetchFirstSql() {
      // SELECT id, name FROM PERSON
      //     OFFSET #{offset} ROWS FETCH FIRST #{limit} ROWS ONLY
      return new SQL()
        .SELECT("id", "name")
        .FROM("PERSON")
        .OFFSET_ROWS("#{offset}")
        .FETCH_FIRST_ROWS_ONLY("#{limit}")
        .toString();
    }
    
3. SQL类的API详解
方法描述
SELECT(String)
SELECT(String...)
开始新的或追加到已有的 SELECT子句。可以被多次调用,参数会被追加到 SELECT 子句。 参数通常使用逗号分隔的列名和别名列表,但也可以是数据库驱动程序接受的任意参数。
SELECT_DISTINCT(String)
SELECT_DISTINCT(String...)
开始新的或追加到已有的 SELECT子句,并添加 DISTINCT 关键字到生成的查询中。可以被多次调用,参数会被追加到 SELECT 子句。 参数通常使用逗号分隔的列名和别名列表,但也可以是数据库驱动程序接受的任意参数。
FROM(String)
FROM(String...)
开始新的或追加到已有的 FROM子句。可以被多次调用,参数会被追加到 FROM子句。 参数通常是一个表名或别名,也可以是数据库驱动程序接受的任意参数。
JOIN(String)
JOIN(String...)
INNER_JOIN(String)
INNER_JOIN(String...)
LEFT_OUTER_JOIN(String)
LEFT_OUTER_JOIN(String...)
RIGHT_OUTER_JOIN(String)
RIGHT_OUTER_JOIN(String...)
基于调用的方法,添加新的合适类型的 JOIN 子句。 参数可以包含一个由列和连接条件构成的标准连接。
WHERE(String)
WHERE(String...)
插入新的 WHERE 子句条件,并使用 AND 拼接。可以被多次调用,对于每一次调用产生的新条件,会使用 AND 拼接起来。要使用 OR 分隔,请使用 OR()
OR()使用 OR 来分隔当前的 WHERE 子句条件。 可以被多次调用,但在一行中多次调用会生成错误的 SQL
AND()使用 AND 来分隔当前的 WHERE子句条件。 可以被多次调用,但在一行中多次调用会生成错误的 SQL。由于 WHEREHAVING都会自动使用 AND 拼接, 因此这个方法并不常用,只是为了完整性才被定义出来。
GROUP_BY(String)
GROUP_BY(String...)
追加新的 GROUP BY 子句,使用逗号拼接。可以被多次调用,每次调用都会使用逗号将新的条件拼接起来。
HAVING(String)
HAVING(String...)
追加新的 HAVING 子句。使用 AND 拼接。可以被多次调用,每次调用都使用AND来拼接新的条件。要使用 OR 分隔,请使用 OR()
ORDER_BY(String)
ORDER_BY(String...)
追加新的 ORDER BY 子句,使用逗号拼接。可以多次被调用,每次调用会使用逗号拼接新的条件。
LIMIT(String)
LIMIT(int)
追加新的 LIMIT 子句。 仅在 SELECT()、UPDATE()、DELETE() 时有效。 当在 SELECT() 中使用时,应该配合 OFFSET() 使用。(于 3.5.2 引入)
OFFSET(String)
OFFSET(long)
追加新的 OFFSET 子句。 仅在 SELECT() 时有效。 当在 SELECT() 时使用时,应该配合 LIMIT() 使用。(于 3.5.2 引入)
OFFSET_ROWS(String)
OFFSET_ROWS(long)
追加新的 OFFSET n ROWS 子句。 仅在 SELECT() 时有效。 该方法应该配合 FETCH_FIRST_ROWS_ONLY() 使用。(于 3.5.2 加入)
FETCH_FIRST_ROWS_ONLY(String)
FETCH_FIRST_ROWS_ONLY(int)
追加新的 FETCH FIRST n ROWS ONLY 子句。 仅在 SELECT() 时有效。 该方法应该配合 OFFSET_ROWS() 使用。(于 3.5.2 加入)
DELETE_FROM(String)开始新的 delete 语句,并指定删除表的表名。通常它后面都会跟着一个 WHERE 子句!
INSERT_INTO(String)开始新的 insert 语句,并指定插入数据表的表名。后面应该会跟着一个或多个 VALUES() 调用,或 INTO_COLUMNS() 和 INTO_VALUES() 调用。
SET(String)``SET(String...)对 update 语句追加 “set” 属性的列表
UPDATE(String)开始新的 update 语句,并指定更新表的表名。后面都会跟着一个或多个 SET() 调用,通常也会有一个 WHERE() 调用。
VALUES(String, String)追加数据值到 insert 语句中。第一个参数是数据插入的列名,第二个参数则是数据值。
INTO_COLUMNS(String...)追加插入列子句到 insert 语句中。应与 INTO_VALUES() 一同使用。
INTO_VALUES(String...)追加插入值子句到 insert 语句中。应与 INTO_COLUMNS() 一同使用。
ADD_ROW()添加新的一行数据,以便执行批量插入。(于 3.5.2 引入)
  1. 注意
  2. SQL 类将原样插入 LIMITOFFSETOFFSET n ROWS 以及 FETCH FIRST n ROWS ONLY 子句。换句话说,类库不会为不支持这些子句的数据库执行任何转换。
  3. 因此,用户应该要了解目标数据库是否支持这些子句
  4. 如果目标数据库不支持这些子句,产生的 SQL 可能会引起运行错误。
4. SqlBuilder 和 SelectBuilder (已经废弃)
  1. 在版本 3.2 之前,我们的实现方式不太一样,我们利用 ThreadLocal 变量来掩盖一些对 Java DSL 不太友好的语言限制

  2. 现在,现代 SQL 构建框架使用的构建器和匿名内部类思想已被人们所熟知。因此,我们废弃了基于这种实现方式的 SelectBuilder 和 SqlBuilder 类。

    1. 源码
    @Deprecated
    public class SelectBuilder {
    
      private static final ThreadLocal<SQL> localSQL = new ThreadLocal<>();
    
      static {
        BEGIN();
      }
      
      public static void BEGIN() {
        RESET();
      }
    
      public static void RESET() {
        localSQL.set(new SQL());
      }
      
      public static void SELECT(String columns) {
        sql().SELECT(columns);
      }
      
      public static String SQL() {
        try {
          return sql().toString();
        } finally {
          RESET();
        }
      }
    
      private static SQL sql() {
        return localSQL.get();
      }
    
    
    方法描述
    BEGIN() / RESET()这些方法清空 SelectBuilder 类的 ThreadLocal 状态,并准备好构建一个新的语句。开始新的语句时,BEGIN() 是最名副其实的(可读性最好的)。但如果由于一些原因(比如程序逻辑在某些条件下需要一个完全不同的语句),在执行过程中要重置语句构建状态,就很适合使用 RESET()
    SQL()该方法返回生成的 SQL() 并重置 SelectBuilder 状态(等价于调用了 BEGIN()RESET())。因此,该方法只能被调用一次!
  3. 下面的方法仅仅适用于废弃的 SqlBuilder 和 SelectBuilder 类

    1. SelectBuilder 和 SqlBuilder 类并不神奇,但最好还是知道它们的工作原理。
    2. SelectBuilder 以及 SqlBuilder 借助静态导入和 ThreadLocal 变量实现了对插入条件友好的简洁语法。
    3. 要使用它们,只需要静态导入这个类的方法即可,就像这样(只能使用其中的一条,不能同时使用)
    import static org.apache.ibatis.jdbc.SqlBuilder.*;
    import static org.apache.ibatis.jdbc.SelectBuilder.*;
    
    1. 案例
    
    /* 已被废弃 */
    public String selectBlogsSql() {
      BEGIN(); // 重置 ThreadLocal 状态变量
      SELECT("*");
      FROM("BLOG");
      return SQL();
    }
    /* 已被废弃 */
    private String selectPersonSql() {
      BEGIN(); // 重置 ThreadLocal 状态变量
      SELECT("P.ID, P.USERNAME, P.PASSWORD, P.FULL_NAME");
      SELECT("P.LAST_NAME, P.CREATED_ON, P.UPDATED_ON");
      FROM("PERSON P");
      FROM("ACCOUNT A");
      INNER_JOIN("DEPARTMENT D on D.ID = P.DEPARTMENT_ID");
      INNER_JOIN("COMPANY C on D.COMPANY_ID = C.ID");
      WHERE("P.ID = A.ID");
      WHERE("P.FIRST_NAME like ?");
      OR();
      WHERE("P.LAST_NAME like ?");
      GROUP_BY("P.ID");
      HAVING("P.LAST_NAME like ?");
      OR();
      HAVING("P.FIRST_NAME like ?");
      ORDER_BY("P.ID");
      ORDER_BY("P.FULL_NAME");
      return SQL();
    }
          
    

8. Mybatis注解

  1. Mapper相关注解

    1. 设计初期的 MyBatis 是一个 XML 驱动的框架
    2. 配置信息是基于 XML 的,映射语句也是定义在 XML 中的
    3. 而在 MyBatis 3 中,我们提供了其它的配置方式。MyBatis 3 构建在全面且强大的基于 Java 语言的配置 API 之上。它是 XML 和注解配置的基础。注解提供了一种简单且低成本的方式来实现简单的映射语句。
    4. 提示
      1. 不幸的是,Java 注解的表达能力和灵活性十分有限
      2. 尽管我们花了很多时间在调查、设计和试验上,但最强大的 MyBatis 映射并不能用注解来构建——我们真没开玩笑
      3. 而 C# 属性就没有这些限制,因此 MyBatis.NET 的配置会比 XML 有更大的选择余地
      4. 虽说如此,基于 Java 注解的配置还是有它的好处的
  2. 使用案例

1. @SelectKey

# 这个例子展示了如何使用 @SelectKey 注解来在插入前读取数据库序列的值:
@Insert("insert into table3 (id, name) values(#{nameId}, #{name})")
@SelectKey(statement="call next value for TestSequence", keyProperty="nameId", before=true, resultType=int.class)
int insertTable3(Name name);


# 这个例子展示了如何使用 @SelectKey 注解来在插入后读取数据库自增列的值:
@Insert("insert into table2 (name) values(#{name})")
@SelectKey(statement="call identity()", keyProperty="nameId", before=false, resultType=int.class)
int insertTable2(Name name);

2. @Flush

# 这个例子展示了如何使用 @Flush 注解来调用 SqlSession#flushStatements():
@Flush
List<BatchResult> flush();

3. @Results

1. @Result
2. @ConstructorArgs
2. @Arg
#这些例子展示了如何通过指定 @Result 的 id 属性来命名结果集:
@Results(id = "userResult", value = {
  @Result(property = "id", column = "uid", id = true),
  @Result(property = "firstName", column = "first_name"),
  @Result(property = "lastName", column = "last_name")
})
@Select("select * from users where id = #{id}")
User getUserById(Integer id);

@Results(id = "companyResults")
@ConstructorArgs({
  @Arg(column = "cid", javaType = Integer.class, id = true),
  @Arg(column = "name", javaType = String.class)
})
@Select("select * from company where id = #{id}")
Company getCompanyById(Integer id);

4. @SqlProvider

1. @SelectProvider
2. @InsertProvider
3. @UpdateProvider
4. @DeleteProvider

以上注解的作用就是使用一个SQL构建器提供构建好的SQL进行执行

在Java代码中拼接SQL非常有用

# 这个例子展示了如何使用单个参数的 @SqlProvider 注解:
@SelectProvider(type = UserSqlBuilder.class, method = "buildGetUsersByName")
List<User> getUsersByName(String name);

class UserSqlBuilder {
  public static String buildGetUsersByName(final String name) {
    return new SQL(){{
      SELECT("*");
      FROM("users");
      if (name != null) {
        WHERE("name like #{value} || '%'");
      }
      ORDER_BY("id");
    }}.toString();
  }
}


# 这个例子展示了如何使用多个参数的 @SqlProvider 注解:
@SelectProvider(type = UserSqlBuilder.class, method = "buildGetUsersByName")
List<User> getUsersByName(@Param("name") String name, @Param("orderByColumn") String orderByColumn);

class UserSqlBuilder {

  // 如果不使用 @Param,就应该定义与 mapper 方法相同的参数
  public static String buildGetUsersByName(
      final String name, final String orderByColumn) {
    return new SQL(){{
      SELECT("*");
      FROM("users");
      WHERE("name like #{name} || '%'");
      ORDER_BY(orderByColumn);
    }}.toString();
  }

  // 如果使用 @Param,就可以只定义需要使用的参数
  public static String buildGetUsersByName(@Param("orderByColumn") final String orderByColumn) {
    return new SQL(){{
      SELECT("*");
      FROM("users");
      WHERE("name like #{name} || '%'");
      ORDER_BY(orderByColumn);
    }}.toString();
  }
}
  1. 全局配置@SqlProvider

    Configuration configuration = new Configuration();
    configuration.setDefaultSqlProviderType(TemplateFilePathProvider.class); // 让所有映射方法在同一个 sql provider 类里面
    
    
    # 等价
    // 在 sql provider 注解上可以省略 type/value 属性
    // 如果省略,MyBatis 将使用 defaultSqlProviderType 所指定的类
    public interface UserMapper {
    
      @SelectProvider // 等价于 @SelectProvider(TemplateFilePathProvider.class)
      User findUser(int id);
    
      @InsertProvider // 等价于 @InsertProvider(TemplateFilePathProvider.class)
      void createUser(User user);
    
      @UpdateProvider // 等价于 @UpdateProvider(TemplateFilePathProvider.class)
      void updateUser(User user);
    
      @DeleteProvider // 等价于 @DeleteProvider(TemplateFilePathProvider.class)
      void deleteUser(int id);
    }
    
  2. 以下例子展示了 ProviderMethodResolver(3.5.1 后可用)的默认实现使用方法:

    @SelectProvider(UserSqlProvider.class)
    List<User> getUsersByName(String name);
    
    // 在你的 provider 类中实现 ProviderMethodResolver 接口
    class UserSqlProvider implements ProviderMethodResolver {
      // 默认实现中,会将映射器方法的调用解析到实现的同名方法上
      public static String getUsersByName(final String name) {
        return new SQL(){{
          SELECT("*");
          FROM("users");
          if (name != null) {
            WHERE("name like #{value} || '%'");
          }
          ORDER_BY("id");
        }}.toString();
      }
    }
    
  3. 声明注解时使用 databaseId 属性

    @Select(value = "SELECT SYS_GUID() FROM dual", databaseId = "oracle") // 如果 DatabaseIdProvider 提供的是 "oracle",使用这条语句
    @Select(value = "SELECT uuid_generate_v4()", databaseId = "postgres") // 如果 DatabaseIdProvider 提供的是 "postgres",使用这条语句
    @Select("SELECT RANDOM_UUID()") // 如果 DatabaseIdProvider 没有配置或者没有对应的 databaseId, 使用这条语句
    String generateId();
    
  4. 注解列表

注解使用对象XML描述
@CacheNamespace<cache>为给定的命名空间(比如类)配置缓存。属性:implemetationevictionflushIntervalsizereadWriteblockingproperties
@PropertyN/A<property>指定参数值或占位符(placeholder)(该占位符能被 mybatis-config.xml 内的配置属性替换)。属性:namevalue。(仅在 MyBatis 3.4.2 以上可用)
@CacheNamespaceRef<cacheRef>引用另外一个命名空间的缓存以供使用。注意,即使共享相同的全限定类名,在 XML 映射文件中声明的缓存仍被识别为一个独立的命名空间。属性:valuename。如果你使用了这个注解,你应设置 value 或者 name 属性的其中一个。value 属性用于指定能够表示该命名空间的 Java 类型(命名空间名就是该 Java 类型的全限定类名),name 属性(这个属性仅在 MyBatis 3.4.2 以上可用)则直接指定了命名空间的名字。
@ConstructorArgs方法<constructor>收集一组结果以传递给一个结果对象的构造方法。属性:value,它是一个 Arg 数组。
@ArgN/A<arg><idArg>ConstructorArgs 集合的一部分,代表一个构造方法参数。属性:idcolumnjavaTypejdbcTypetypeHandlerselectresultMap。id 属性和 XML 元素 <idArg> 相似,它是一个布尔值,表示该属性是否用于唯一标识和比较对象。从版本 3.5.4 开始,该注解变为可重复注解。
@TypeDiscriminator方法<discriminator>决定使用何种结果映射的一组取值(case)。属性:columnjavaTypejdbcTypetypeHandlercases
@CaseN/A<case>表示某个值的一个取值以及该取值对应的映射。属性:valuetyperesults。results 属性是一个 Results 的数组,因此这个注解实际上和 ResultMap 很相似,由下面的 Results 注解指定。
@Results方法<resultMap>一组结果映射,指定了对某个特定结果列,映射到某个属性或字段的方式。属性:valueid。value 属性是一个 Result 注解的数组。而 id 属性则是结果映射的名称。从版本 3.5.4 开始,该注解变为可重复注解。
@ResultN/A<result><id>在列和属性或字段之间的单个结果映射。属性:idcolumnjavaTypejdbcTypetypeHandleronemany。id 属性和 XML 元素 <id> 相似,它是一个布尔值,表示该属性是否用于唯一标识和比较对象。one 属性是一个关联,和 <association> 类似,而 many 属性则是集合关联,和 <collection> 类似。这样命名是为了避免产生名称冲突。
@OneN/A<association>复杂类型的单个属性映射。属性: select,指定可加载合适类型实例的映射语句(也就是映射器方法)全限定名; fetchType,指定在该映射中覆盖全局配置参数 lazyLoadingEnabledresultMap(3.5.5以上可用), 结果集的完全限定名,该结果映射到查询结果中的集合对象; columnPrefix(3.5.5以上可用),在嵌套的结果集中对所查询的列进行分组的列前缀。 提示 注解 API 不支持联合映射。这是由于 Java 注解不允许产生循环引用。
@ManyN/A<collection>复杂类型的集合属性映射。属性: select,指定可加载合适类型实例集合的映射语句(也就是映射器方法)全限定名; fetchType,指定在该映射中覆盖全局配置参数 lazyLoadingEnabledresultMap(3.5.5以上可用),结果集的完全限定名,该结果映射到查询结果中的集合对象; columnPrefix(3.5.5以上可用),在嵌套的结果集中对所查询的列进行分组的列前缀。 提示 注解 API 不支持联合映射。这是由于 Java 注解不允许产生循环引用。
@MapKey方法供返回值为 Map 的方法使用的注解。它使用对象的某个属性作为 key,将对象 List 转化为 Map。属性:value,指定作为 Map 的 key 值的对象属性名。
@Options方法映射语句的配置属性<select xxx=xxx/>该注解允许你指定大部分开关和配置选项,它们通常在映射语句上作为属性出现。与在注解上提供大量的属性相比,Options 注解提供了一致、清晰的方式来指定选项。属性:useCache=trueflushCache=FlushCachePolicy.DEFAULTresultSetType=DEFAULTstatementType=PREPAREDfetchSize=-1timeout=-1useGeneratedKeys=falsekeyProperty=""keyColumn=""resultSets="", databaseId=""。注意,Java 注解无法指定 null 值。因此,一旦你使用了 Options 注解,你的语句就会被上述属性的默认值所影响。要注意避免默认值带来的非预期行为。 databaseId(3.5.5以上可用), 如果有一个配置好的 DatabaseIdProvider, MyBatis 会加载不带 databaseId 属性和带有匹配当前数据库 databaseId 属性的所有语句。如果同时存在带 databaseId 和不带 databaseId 属性的相同语句,则后者会被舍弃。 注意:keyColumn 属性只在某些数据库中有效(如 Oracle、PostgreSQL 等)。要了解更多关于 keyColumnkeyProperty 可选值信息,请查看“insert, update 和 delete”一节。
@Insert
@Update
@Delete
@Select
方法<insert><update><delete><select>每个注解分别代表将会被执行的 SQL 语句。它们用字符串数组(或单个字符串)作为参数。如果传递的是字符串数组,字符串数组会被连接成单个完整的字符串,每个字符串之间加入一个空格。这有效地避免了用 Java 代码构建 SQL 语句时产生的“丢失空格”问题。当然,你也可以提前手动连接好字符串。属性:value,指定用来组成单个 SQL 语句的字符串数组。 databaseId(3.5.5以上可用), 如果有一个配置好的 DatabaseIdProvider, MyBatis 会加载不带 databaseId 属性和带有匹配当前数据库 databaseId 属性的所有语句。如果同时存在带 databaseId 和不带 databaseId 属性的相同语句,则后者会被舍弃。
@InsertProvider
@UpdateProvider
@DeleteProvider
@SelectProvider
方法<insert>
<update>
<delete>
<select>
允许构建动态 SQL。这些备选的 SQL 注解允许你指定返回 SQL 语句的类和方法,以供运行时执行。(从 MyBatis 3.4.6 开始,可以使用 CharSequence 代替 String 来作为返回类型)。当执行映射语句时,MyBatis 会实例化注解指定的类,并调用注解指定的方法。你可以通过 ProviderContext 传递映射方法接收到的参数、“Mapper interface type” 和 “Mapper method”(仅在 MyBatis 3.4.5 以上支持)作为参数。(MyBatis 3.4 以上支持传入多个参数) 属性:valuetypemethoddatabaseIdvalue and type 属性用于指定类名 (type 属性是 value 的别名, 你必须指定任意一个。 但是你如果在全局配置中指定 defaultSqlProviderType ,两个属性都可以忽略)。 method 用于指定该类的方法名(从版本 3.5.1 开始,可以省略 method 属性,MyBatis 将会使用 ProviderMethodResolver 接口解析方法的具体实现。如果解析失败,MyBatis 将会使用名为 provideSql 的降级实现)。提示 接下来的“SQL 语句构建器”一章将会讨论该话题,以帮助你以更清晰、更便于阅读的方式构建动态 SQL。 databaseId(3.5.5以上可用), 如果有一个配置好的 DatabaseIdProvider, MyBatis 会加载不带 databaseId 属性和带有匹配当前数据库 databaseId 属性的所有语句。如果同时存在带 databaseId 和不带 databaseId 属性的相同语句,则后者会被舍弃。
@Param参数N/A如果你的映射方法接受多个参数,就可以使用这个注解自定义每个参数的名字。否则在默认情况下,除 RowBounds 以外的参数会以 “param” 加参数位置被命名。例如 #{param1}, #{param2}。如果使用了 @Param("person"),参数就会被命名为 #{person}
@SelectKey方法<selectKey>这个注解的功能与 <selectKey> 标签完全一致。该注解只能在 @Insert@InsertProvider@Update@UpdateProvider 标注的方法上使用,否则将会被忽略。如果标注了 @SelectKey 注解,MyBatis 将会忽略掉由 @Options 注解所设置的生成主键或设置(configuration)属性。属性:statement 以字符串数组形式指定将会被执行的 SQL 语句,keyProperty 指定作为参数传入的对象对应属性的名称,该属性将会更新成新的值,before 可以指定为 truefalse 以指明 SQL 语句应被在插入语句的之前还是之后执行。resultType 则指定 keyProperty 的 Java 类型。statementType 则用于选择语句类型,可以选择 STATEMENTPREPAREDCALLABLE 之一,它们分别对应于 StatementPreparedStatementCallableStatement。默认值是 PREPAREDdatabaseId(3.5.5以上可用), 如果有一个配置好的 DatabaseIdProvider, MyBatis 会加载不带 databaseId 属性和带有匹配当前数据库 databaseId 属性的所有语句。如果同时存在带 databaseId 和不带 databaseId 属性的相同语句,则后者会被舍弃。
@ResultMap方法N/A这个注解为 @Select 或者 @SelectProvider 注解指定 XML 映射中 <resultMap> 元素的 id。这使得注解的 select 可以复用已在 XML 中定义的 ResultMap。如果标注的 select 注解中存在 @Results 或者 @ConstructorArgs 注解,这两个注解将被此注解覆盖。
@ResultType方法N/A在使用了结果处理器的情况下,需要使用此注解。由于此时的返回类型为 void,所以 Mybatis 需要有一种方法来判断每一行返回的对象类型。如果在 XML 有对应的结果映射,请使用 @ResultMap 注解。如果结果类型在 XML 的 <select> 元素中指定了,就不需要使用其它注解了。否则就需要使用此注解。比如,如果一个标注了 @Select 的方法想要使用结果处理器,那么它的返回类型必须是 void,并且必须使用这个注解(或者 @ResultMap)。这个注解仅在方法返回类型是 void 的情况下生效。
@Flush方法N/A如果使用了这个注解,定义在 Mapper 接口中的方法就能够调用 SqlSession#flushStatements() 方法。(Mybatis 3.3 以上可用)

9. Mybatis使用日志

  1. Mybatis 通过使用内置的日志工厂提供日志功能。

  2. 内置日志工厂将会把日志工作委托给下面的实现之一

    1. SLF4J
    2. Apache Commons Logging
    3. Log4j 2
    4. Log4j(3.5.9起废弃)
    5. JDK logging
  3. MyBatis 内置日志工厂基于运行时自省机制选择合适的日志工具

    1. 它会使用第一个查找得到的工具(按上文列举的顺序查找)

    2. 如果一个都未找到,日志功能就会被禁用。

    3. 不少应用服务器(如 Tomcat 和 WebShpere)的类路径中已经包含 Commons Logging,所以在这种配置环境下的 MyBatis 会把它作为日志工具,记住这点非常重要。

    4. 这将意味着,在诸如 WebSphere 的环境中,它提供了 Commons Logging 的私有实现,你的 Log4J 配置将被忽略。MyBatis 将你的 Log4J 配置忽略掉是相当令人郁闷的(事实上,正是因为在这种配置环境下,MyBatis 才会选择使用 Commons Logging 而不是 Log4J)

    5. 如果你的应用部署在一个类路径已经包含 Commons Logging 的环境中,而你又想使用其它日志工具,你可以通过在 MyBatis 配置文件 mybatis-config.xml 里面添加一项 setting 来选择别的日志工具

      <setting name="logImpl" value="LOG4J"/>
      
      1. logImpl 可选的值有

        1. SLF4J

        2. LOG4J

        3. LOG4J2

        4. JDK_LOGGING

        5. COMMONS_LOGGING

        6. STDOUT_LOGGING

        7. NO_LOGGING

        8. 或者是实现了接口 org.apache.ibatis.logging.Log 的,且构造方法是以字符串为参数的类的完全限定名(可以参考org.apache.ibatis.logging.slf4j.Slf4jImpl.java的实现)

        9. 你也可以调用如下任一方法来使用日志工具来设置指定的日志实现

          org.apache.ibatis.logging.LogFactory.useSlf4jLogging();
          org.apache.ibatis.logging.LogFactory.useLog4JLogging();
          org.apache.ibatis.logging.LogFactory.useLog4J2Logging();
          org.apache.ibatis.logging.LogFactory.useJdkLogging();
          org.apache.ibatis.logging.LogFactory.useCommonsLogging();
          org.apache.ibatis.logging.LogFactory.useStdOutLogging();
          
          1. 如果你决定要调用以上某个方法,请在调用其它 MyBatis 方法之前调用它
          2. 另外,仅当运行时类路径中存在该日志工具时,调用与该日志工具对应的方法才会生效,否则 MyBatis 一概忽略
          3. 如你环境中并不存在 Log4J2,你却调用了相应的方法,MyBatis 就会忽略这一调用,转而以默认的查找顺序查找日志工具

1.日志源码解析


public final class LogFactory {

  public static final String MARKER = "MYBATIS";
  // 日志实现具体类的构造方法
  private static Constructor<? extends Log> logConstructor;

  static {
  	// 按照尝试使用以下日志
    tryImplementation(LogFactory::useSlf4jLogging);
    tryImplementation(LogFactory::useCommonsLogging);
    tryImplementation(LogFactory::useLog4J2Logging);
    tryImplementation(LogFactory::useLog4JLogging);
    tryImplementation(LogFactory::useJdkLogging);
    tryImplementation(LogFactory::useNoLogging);
  }
  // 私有构造
  private LogFactory() {}
  // 获取指定日志
  public static Log getLog(Class<?> clazz) {
    return getLog(clazz.getName());
  }
  // 根据日志实现构造方法创建日志对象	
  public static Log getLog(String logger) {
    try {
      return logConstructor.newInstance(logger);
    } catch (Throwable t) {
      throw new LogException("Error creating logger for logger " + logger + ".  Cause: " + t, t);
    }
  }

  public static synchronized void useCustomLogging(Class<? extends Log> clazz) {
    setImplementation(clazz);
  }

  public static synchronized void useSlf4jLogging() {
    setImplementation(org.apache.ibatis.logging.slf4j.Slf4jImpl.class);
  }

  public static synchronized void useCommonsLogging() {
    setImplementation(org.apache.ibatis.logging.commons.JakartaCommonsLoggingImpl.class);
  }

  @Deprecated
  public static synchronized void useLog4JLogging() {
    setImplementation(org.apache.ibatis.logging.log4j.Log4jImpl.class);
  }

  public static synchronized void useLog4J2Logging() {
    setImplementation(org.apache.ibatis.logging.log4j2.Log4j2Impl.class);
  }

  public static synchronized void useJdkLogging() {
    setImplementation(org.apache.ibatis.logging.jdk14.Jdk14LoggingImpl.class);
  }

  public static synchronized void useStdOutLogging() {
    setImplementation(org.apache.ibatis.logging.stdout.StdOutImpl.class);
  }

  public static synchronized void useNoLogging() {
	setImplementation(org.apache.ibatis.logging.nologging.NoLoggingImpl.class);
  }
  // 尝试使用日志的日志	
  private static void tryImplementation(Runnable runnable) {
    // 如果没有设定指定的实现日志
    if (logConstructor == null) {
      try {
        // 执行具体的逻辑,设置具体的日志实现
        runnable.run();
      } catch (Throwable t) {
        // ignore
        // 设置失败,忽略该日志
      }
    }
  }
  // 设置具体的日志实现
  private static void setImplementation(Class<? extends Log> implClass) {
    try {
      // 获取实现类中带有String参数的构造方法
      Constructor<? extends Log> candidate = implClass.getConstructor(String.class);
      // 创建日志对象
      Log log = candidate.newInstance(LogFactory.class.getName());
      if (log.isDebugEnabled()) {
        log.debug("Logging initialized using '" + implClass + "' adapter.");
      }
      // 设置指定的日志对象
      logConstructor = candidate;
    } catch (Throwable t) {
      // 如果实例化失败,表示该日志不可用,会被忽略
      throw new LogException("Error setting Log implementation.  Cause: " + t, t);
    }
  }

}

2. 日志配置

1. SLF4J + Logback
  1. 步骤1

  2. 添加 SLF4J + Logback 的 jar 包

  3. 因为我们使用的是 SLF4J(Logback),就要确保它的 jar 包在应用中是可用的。要启用 SLF4J(Logback),只要将 jar 包添加到应用的类路径中即可

  4. 对于 web 应用或企业级应用,则需要将 logback-classic.jar, logback-core.jarslf4j-api.jar 添加到 WEB-INF/lib 目录下;对于独立应用,可以将它添加到JVM 的 -classpath 启动参数中

  5. 如果你使用 maven, 你可以通过在 pom.xml 中添加下面的依赖来下载 jar 文件

    <dependency>
      <groupId>ch.qos.logback</groupId>
      <artifactId>logback-classic</artifactId>
      <version>1.5.0</version>
    </dependency>
    
  6. 步骤2

    1. 配置 Logback

    2. 新建logback.xml

      <?xml version="1.0" encoding="UTF-8"?>
      <!DOCTYPE configuration>
      <configuration>
      
        <appender name="stdout" class="ch.qos.logback.core.ConsoleAppender">
          <encoder>
            <pattern>%5level [%thread] - %msg%n</pattern>
          </encoder>
        </appender>
      
        <logger name="org.mybatis.example.BlogMapper">
          <level value="trace"/>
        </logger>
        <root level="error">
          <appender-ref ref="stdout"/>
        </root>
      
      </configuration>
      
      1. 你也可以将日志的记录方式从接口级别切换到语句级别,从而实现更细粒度的控制

      2. 如下配置只对 selectBlog 语句记录日志

        <logger name="org.mybatis.example.BlogMapper.selectBlog">
          <level value="trace"/>
        </logger>
        
      3. 与此相对,可以对一组映射器接口记录日志,只要对映射器接口所在的包开启日志功能

        <logger name="org.mybatis.example">
          <level value="trace"/>
        </logger>
        
      4. 某些查询可能会返回庞大的结果集,此时只想记录其执行的 SQL 语句而不想记录结果该怎么办?

        1. 为此,Mybatis 中 SQL 语句的日志级别被设为DEBUG(JDK 日志设为 FINE)
        2. 结果的日志级别为 TRACE(JDK 日志设为 FINER)
        3. 所以,只要将日志级别调整为 DEBUG 即可达到目的
        <logger name="org.mybatis.example">
          <level value="debug"/>
        </logger>
        
      5. 需要对具体的xml命名空间(或内部的方法)单独配置日志(与Mapper接口完全一样)

          <logger name="org.mybatis.example.BlogMapper">
            <level value="trace"/>
          </logger>
          
        <logger name="org.mybatis.example.BlogMapper.selectBlog">
          <level value="trace"/>
        </logger>
        
2. Log4j2
  1. 依赖

    <!-- pom.xml -->
    <dependency>
      <groupId>org.apache.logging.log4j</groupId>
      <artifactId>log4j-core</artifactId>
      <version>2.x.x</version>
    </dependency>
    
  2. 新建log4j2.xml

    <!-- log4j2.xml -->
    <?xml version="1.0" encoding="UTF-8"?>
    <Configuration xmlns="http://logging.apache.org/log4j/2.0/config">
    
      <Appenders>
        <Console name="stdout" target="SYSTEM_OUT">
          <PatternLayout pattern="%5level [%t] - %msg%n"/>
        </Console>
      </Appenders>
    
      <Loggers>
        <Logger name="org.mybatis.example.BlogMapper" level="trace"/>
        <Root level="error" >
          <AppenderRef ref="stdout"/>
        </Root>
      </Loggers>
    
    </Configuration>
    
  3. 其他的和Logback一样

3. Log4j
  1. 依赖

    <!-- pom.xml -->
    <dependency>
      <groupId>log4j</groupId>
      <artifactId>log4j</artifactId>
      <version>1.2.17</version>
    </dependency>
    
  2. 新建log4j.properties

    # log4j.properties
    log4j.rootLogger=ERROR, stdout
    
    log4j.logger.org.mybatis.example.BlogMapper=TRACE
    
    log4j.appender.stdout=org.apache.log4j.ConsoleAppender
    log4j.appender.stdout.layout=org.apache.log4j.PatternLayout
    log4j.appender.stdout.layout.ConversionPattern=%5p [%t] - %m%n
    
4. JDK logging
  1. 新建logging.properties

    # logging.properties
    handlers=java.util.logging.ConsoleHandler
    .level=SEVERE
    
    org.mybatis.example.BlogMapper=FINER
    
    java.util.logging.ConsoleHandler.level=ALL
    java.util.logging.ConsoleHandler.formatter=java.util.logging.SimpleFormatter
    java.util.logging.SimpleFormatter.format=%1$tT.%1$tL %4$s %3$s - %5$s%6$s%n
    

10. 入门使用

  1. 依赖

    1. mybatis依赖

      <dependency>
          <groupId>org.mybatis</groupId>
          <artifactId>mybatis</artifactId>
          <version>3.5.12</version>
      </dependency>
      
    2. 日志配置依赖

    3. mysql依赖

       <dependency>
           <groupId>mysql</groupId>
           <artifactId>mysql-connector-java</artifactId>
           <version>8.0.19</version>
       </dependency>
      
  2. 创建Mybatis的配置文件

    1. mybatis-config.xml
    <?xml version="1.0" encoding="UTF-8" ?>
    <!DOCTYPE configuration PUBLIC "-//mybatis.org//DTD Config 3.0//EN" "https://mybatis.org/dtd/mybatis-3-config.dtd">
    <configuration>
        <properties resource="jdbc.properties"/>
        # 设置实体的别名
        <typeAliases>
            <package name="luck.mybatis.entity"/>
        </typeAliases>
        <environments default="development">
            <environment id="development">
                <transactionManager type="JDBC"/>
                <dataSource type="POOLED">
                    <property name="driver" value="${driver}"/>
                    <property name="url" value="${url}"/>
                    <property name="username" value="${username}"/>
                    <property name="password" value="${password}"/>
                </dataSource>
            </environment>
        </environments>
        # 设置Mapper接口的包名
        <mappers>
            <package name="luck.mybatis.mapper"/>
        </mappers>
    </configuration>
    
    
    1. 数据库配置文件jdbc.properties

      driver=com.mysql.jdbc.Driver
      url=jdbc:mysql://localhost:3306/mybatis?useUnicode=true&characterEncoding=utf8&zeroDateTimeBehavior=convertToNull&useSSL=true&serverTimezone=GMT%2B8
      username=root
      password=123456
      
  3. 新建主体内容

    1. 新建数据库表

      create table users(
          id       int auto_increment primary key,
          username varchar(50)  not null,
          password varchar(100) not null,
          email    varchar(100) null,
          constraint username  unique (username));
      
    2. 新建表对应的实体类

      @Data
      public class Users {
          private Integer id;
          private String username;
          private String password;
          private String email;
      }
      
    3. 新建表对应的Mapper接口和Mapper映射文件(与注解实现二选一)

      1. UserMapper.java

        1. 这个案例是测试@SelectProvider注解的用法,详见@SelectProvider
        public interface UsersMapper {
            @SelectProvider(type = SqlBuilder.class, method = "findByUserName")
            Users findByUserName(String username);
        
            class SqlBuilder {
                public String findByUserName(String username) {
                    SQL sql = new SQL();
                    sql.SELECT("*").FROM("users");
                    if (StrUtil.isNotBlank(username)) {
                        sql.WHERE("username = #{username}");
                    }
                    return sql.toString();
                }
            }
        }
        
      2. UserMapper.xml (与UserMapper.java中的注解二选一)

        <?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="luck.mybatis.mapper.UsersMapper">
            <select id="findByUserName" resultType="users">
                select * from users where username=#{username}
            </select>
        </mapper>
        
  4. 使用XML文件构建SqlSessionFactory,SqlSessionFactory是创建SqlSession的工厂类,SqlSession是获取Mapper接口执行映射语句的核心类

    public class App {
        private static final SqlSessionFactory sqlSessionFactory;
    
        static {
            sqlSessionFactory = new SqlSessionFactoryBuilder().build(ResourceUtil.getStream("mybatis-config.xml"));
        }
    
        public static void main(String[] args) {
            SqlSession sqlSession = sqlSessionFactory.openSession();
            UsersMapper usersMapper = sqlSession.getMapper(UsersMapper.class);
            Users luck = usersMapper.findByUserName("luck");
            sqlSession.close();
            System.out.println(luck);
        }
    }
    

2. Mybatis-Spring

1. 简介

  1. MyBatis-Spring 会帮助你将 MyBatis 代码无缝地整合到 Spring 中
  2. 它将允许 MyBatis 参与到 Spring 的事务管理之中,创建映射器 mapper 和 SqlSession 并注入到 bean 中,以及将 Mybatis 的异常转换为 Spring 的 DataAccessException
  3. 最终,可以做到应用代码不依赖于 MyBatis,Spring 或 MyBatis-Spring

2. 版本兼容

MyBatis-SpringMyBatisSpring FrameworkSpring BatchJava
3.03.5+6.0+5.0+Java 17+
2.13.5+5.x4.xJava 8+
2.03.5+5.x4.xJava 8+
1.33.4+3.2.2+2.1+Java 6+

3. 核心组件

1. SqlSessionFactoryBean

  1. 在基础的 MyBatis 用法中,是通过 SqlSessionFactoryBuilder 来创建 SqlSessionFactory

  2. 而在 MyBatis-Spring 中,则使用 SqlSessionFactoryBean 来创建

  3. 将SqlSessionFactory注入到Spring中

    1. 需要注意的是 SqlSessionFactoryBean 实现了 Spring 的 FactoryBean 接口
    2. 这意味着由 Spring 最终创建的 bean 并不是 SqlSessionFactoryBean 本身,而是工厂类(SqlSessionFactoryBean)的 getObject() 方法的返回结果
    3. 这种情况下,Spring 将会在应用启动时为你创建 SqlSessionFactory,并使用 sqlSessionFactory 这个名字存储起来
  4. 配置类和xml配置

    <bean id="sqlSessionFactory" class="org.mybatis.spring.SqlSessionFactoryBean">
      <property name="dataSource" ref="dataSource" />
    </bean>
    
    @Configuration
    public class MyBatisConfig {
      @Bean
      public SqlSessionFactory sqlSessionFactory() {
        SqlSessionFactoryBean factoryBean = new SqlSessionFactoryBean();
        factoryBean.setDataSource(dataSource());
        return factoryBean.getObject();
      }
    }
    
  5. 通常,在 MyBatis-Spring 中,你不需要直接使用 SqlSessionFactoryBean 或对应的 SqlSessionFactory

  6. 相反,session 的工厂 bean (SqlSessionFactory)将会被注入到 MapperFactoryBean 或其它继承于 SqlSessionDaoSupport 的 DAO(Data Access Object,数据访问对象)中

1. 属性配置
  1. SqlSessionFactory 有一个唯一的必要属性:用于 JDBC 的 DataSource。这可以是任意的 DataSource 对象,它的配置方法和其它 Spring 数据库连接是一样的

  2. 一个常用的属性是 configLocation,它用来指定 MyBatis 的 XML 配置文件路径。它在需要修改 MyBatis 的基础配置非常有用。通常,基础配置指的是 或 元素。

  3. 需要注意的是,这个配置文件并不需要是一个完整的 MyBatis 配置。确切地说,任何环境配置(),数据源()和 MyBatis 的事务管理器()都会被忽略掉,也就是说在mybatis-config配置的这些是不会生效的。因为SqlSessionFactoryBean 会创建它自有的 MyBatis 环境配置(Environment),并按要求设置自定义环境的值。

  4. 如果 MyBatis 在Mapper类对应的路径下找不到与之相对应的映射器 XML 文件,那么也需要配置文件。这时有两种解决办法

    1. 第一种是手动在 MyBatis 的 XML 配置文件中的 部分中指定 XML 文件的类路径;

    2. 第二种是设置工厂 bean 的 **mapperLocations **属性。

      1. mapperLocations 属性接受多个资源位置。
      2. 这个属性可以用来指定 MyBatis 的映射器 XML 配置文件的位置。
      3. 属性的值是一个 Ant 风格的字符串,可以指定加载一个目录中的所有文件,或者从一个目录开始递归搜索所有目录
        1. 如下代码会从sample/config/mappers找mapper.xml
      <bean id="sqlSessionFactory" class="org.mybatis.spring.SqlSessionFactoryBean">
        <property name="dataSource" ref="dataSource" />
        <property name="mapperLocations" value="classpath*:sample/config/mappers/**/*.xml" />
      </bean>
      
  5. 如果你使用了多个数据库,那么需要设置 databaseIdProvider 属性

    <bean id="databaseIdProvider" class="org.apache.ibatis.mapping.VendorDatabaseIdProvider">
      <property name="properties">
        <props>
          <prop key="SQL Server">sqlserver</prop>
          <prop key="DB2">db2</prop>
          <prop key="Oracle">oracle</prop>
          <prop key="MySQL">mysql</prop>
        </props>
      </property>
    </bean>
    
    <bean id="sqlSessionFactory" class="org.mybatis.spring.SqlSessionFactoryBean">
      <property name="dataSource" ref="dataSource" />
      <property name="mapperLocations" value="classpath*:sample/config/mappers/**/*.xml" />
      <property name="databaseIdProvider" ref="databaseIdProvider"/>
    </bean>
    
  6. 自 1.3.0 版本开始,新增的 configuration 属性能够在没有对应的 MyBatis XML 配置文件的情况下,直接设置 Configuration 实例

    <bean id="sqlSessionFactory" class="org.mybatis.spring.SqlSessionFactoryBean">
      <property name="dataSource" ref="dataSource" />
      <property name="configuration">
        <bean class="org.apache.ibatis.session.Configuration">
          <property name="mapUnderscoreToCamelCase" value="true"/>
        </bean>
      </property>
    </bean>
    

2. 事务

  1. 概念

    1. 使用 MyBatis-Spring 的其中一个主要原因是它允许 MyBatis 参与到 Spring 的事务管理中,而不是给 MyBatis 创建一个新的专用事务管理器
    2. MyBatis-Spring 借助了 Spring 中的 DataSourceTransactionManager 来实现事务管理。
    3. 一旦配置好了 Spring 的事务管理器,你就可以在 Spring 中按你平时的方式来配置事务
    4. 并且支持 @Transactional 注解和 AOP 风格的配置
    5. 在事务处理期间,一个单独的 SqlSession 对象将会被创建和使用
    6. 当事务完成时,这个 session 会以合适的方式提交或回滚。
    7. 事务配置好了以后,MyBatis-Spring 将会透明地管理事务,这样在你的 DAO 类中就不需要额外的代码了
  2. 标准事务

    1. 要开启 Spring 的事务处理功能,在 Spring 的配置文件中创建一个 DataSourceTransactionManager 对象

    2. 注意:为事务管理器指定的 DataSource 必须和用来创建 SqlSessionFactoryBean 的是同一个数据源,否则事务管理器就无法工作了

      <bean id="transactionManager" class="org.springframework.jdbc.datasource.DataSourceTransactionManager">
        <constructor-arg ref="dataSource" />
      </bean>
      

      或者

      public class DataSourceConfig {
        @Bean
        public DataSourceTransactionManager transactionManager() {
          return new DataSourceTransactionManager(dataSource());
        }
      }
      
  3. 容器管理事务

    1. 如果你正使用一个 JEE 容器而且想让 Spring 参与到容器管理事务(Container managed transactions,CMT)的过程中,那么 Spring 应该被设置为使用 JtaTransactionManager 或由容器指定的一个子类作为事务管理器

    2. 最简单的方式是使用 Spring 的事务命名空间或使用 JtaTransactionManagerFactoryBean

      <tx:jta-transaction-manager />
      

      或者

      @Configuration
      public class DataSourceConfig {
        @Bean
        public JtaTransactionManager transactionManager() {
          return new JtaTransactionManagerFactoryBean().getObject();
        }
      }
      
    3. 注意

      1. 如果你想使用由容器管理的事务,而不想使用 Spring 的事务管理,你就不能配置任何的 Spring 事务管理器

      2. 必须配置 SqlSessionFactoryBean 以使用基本的 MyBatis 的 ManagedTransactionFactory

        <bean id="sqlSessionFactory" class="org.mybatis.spring.SqlSessionFactoryBean">
          <property name="dataSource" ref="dataSource" />
          <property name="transactionFactory">
            <bean class="org.apache.ibatis.transaction.managed.ManagedTransactionFactory" />
          </property>
        </bean>
        

        或者

        @Configuration
        public class MyBatisConfig {
          @Bean
          public SqlSessionFactory sqlSessionFactory() {
            SqlSessionFactoryBean factoryBean = new SqlSessionFactoryBean();
            factoryBean.setDataSource(dataSource());
            factoryBean.setTransactionFactory(new ManagedTransactionFactory());
            return factoryBean.getObject();
          }
        }
        
  4. 编程式事务管理

    1. MyBatis 的 SqlSession 提供几个方法来在代码中处理事务

    2. 但是当使用 MyBatis-Spring 时,你的 bean 将会注入由 Spring 管理的 SqlSession 或Mapper。也就是说,Spring 总是为你处理了事务。

    3. 你不能在 Spring 管理的 SqlSession 上调用 SqlSession.commit()SqlSession.rollback()SqlSession.close() 方法

    4. 如果这样做了,就会抛出 UnsupportedOperationException 异常。在使用注入的Mapper时,这些方法也不会暴露出来。

    5. 无论 JDBC 连接是否设置为自动提交,调用 SqlSession 数据方法或在 Spring 事务之外调用任何在Mapper方法,事务都将会自动被提交。

    6. 如果你想编程式地控制事务,请参考 Spring的编程式事务

    7. 下面的代码展示了如何使用 PlatformTransactionManager 手工管理事务

      1. 方式1,使用TransactionManager
      public class UserService {
        private final PlatformTransactionManager transactionManager;
        public UserService(PlatformTransactionManager transactionManager) {
          this.transactionManager = transactionManager;
        }
        public void createUser() {
          TransactionStatus txStatus =
              transactionManager.getTransaction(new DefaultTransactionDefinition());
          try {
            userMapper.insertUser(user);
          } catch (Exception e) {
            transactionManager.rollback(txStatus);
            throw e;
          }
          transactionManager.commit(txStatus);
        }
      }
      
      1. 方式2,使用TransactionTemplate

        1. 在使用 TransactionTemplate 的时候,可以省略对 commitrollback 方法的调用

          1. 注意:虽然这段代码使用的是一个Mapper,但换成 SqlSession 也是可以工作的
          public class UserService {
            private final PlatformTransactionManager transactionManager;
            public UserService(PlatformTransactionManager transactionManager) {
              this.transactionManager = transactionManager;
            }
            public void createUser() {
              TransactionTemplate transactionTemplate = new TransactionTemplate(transactionManager);
              transactionTemplate.execute(txStatus -> {
                userMapper.insertUser(user);
                return null;
              });
            }
          }
          

3. Spring中的SqlSession

  1. 在 MyBatis 中,你可以使用 SqlSessionFactory 来创建 SqlSession
  2. 一旦你获得一个 session 之后,你可以使用它来执行映射了的语句,提交或回滚连接,最后,当不再需要它的时候,你可以关闭 session
  3. 使用 MyBatis-Spring 之后,你不再需要直接使用 SqlSessionFactory 了,因为你的 bean 可以被注入一个线程安全的 SqlSession,它能基于 Spring 的事务配置来自动提交、回滚、关闭 session
1. SqlSessionTemplate
  1. SqlSessionTemplate 是 MyBatis-Spring 的核心

  2. 作为 SqlSession 的一个实现,这意味着可以使用它无缝代替你代码中已经在使用的 SqlSession

  3. SqlSessionTemplate 是线程安全的,可以被多个 DAO 或Mapper所共享使用

  4. 为什么是线程安全的?因为在SqlSessionTemplate是一个静态代理对象,它内部实际上代理了一个实际工作的SqlSession,这个SqlSession会在每次操作数据库都会创建新的,保证了所有线程都有自己独立的SqlSession实例

  5. 当调用 SQL 方法时(包括由 getMapper() 方法返回的Mapper中的方法),SqlSessionTemplate 将会保证使用的 SqlSession 与当前 Spring 的事务相关

  6. 此外,它管理 session 的生命周期,包含必要的关闭、提交或回滚操作

  7. 另外,它也负责将 MyBatis 的异常翻译成 Spring 中的 DataAccessExceptions

  8. 由于模板可以参与到 Spring 的事务管理中,并且由于其是线程安全的,可以供多个Mapper类使用,你应该总是SqlSessionTemplate 来替换 MyBatis 默认的 DefaultSqlSession 实现。在同一应用程序中的不同类之间混杂使用可能会引起数据一致性的问题。

  9. 可以使用 SqlSessionFactory 作为构造方法的参数来创建 SqlSessionTemplate 对象

    <bean id="sqlSession" class="org.mybatis.spring.SqlSessionTemplate">
      <constructor-arg index="0" ref="sqlSessionFactory" />
    </bean>
    

    或者

    @Configuration
    public class MyBatisConfig {
      @Bean
      public SqlSessionTemplate sqlSession() throws Exception {
        return new SqlSessionTemplate(sqlSessionFactory());
      }
    }
    
  10. 现在,这个 bean 就可以直接注入到了。你需要在你的 bean 中添加一个 SqlSession 属性,就OK

    @Component
    public class UserDaoImpl implements UserDao {
      private SqlSession sqlSession;
      public void setSqlSession(SqlSession sqlSession) {
        this.sqlSession = sqlSession;
      }
    
      public User getUser(String userId) {
        return sqlSession.selectOne("org.mybatis.spring.sample.mapper.UserMapper.getUser", userId);
      }
    }
    
  11. SqlSessionTemplate 还有一个接收 ExecutorType 参数的构造方法。这允许你使用下 Spring 配置来批量操作,实际上就是JDBC的executeBatch效果

    <bean id="sqlSession" class="org.mybatis.spring.SqlSessionTemplate">
      <constructor-arg index="0" ref="sqlSessionFactory" />
      <constructor-arg index="1" value="BATCH" />
    </bean>
    

    或者

    @Configuration
    public class MyBatisConfig {
      @Bean
      public SqlSessionTemplate sqlSession() throws Exception {
        return new SqlSessionTemplate(sqlSessionFactory(), ExecutorType.BATCH);
      }
    }
    

    因此,我们可以通过如下代码就能完成批量操作

    public class UserService {
      private final SqlSession sqlSession;
      public UserService(SqlSession sqlSession) {
        this.sqlSession = sqlSession;
      }
      public void insertUsers(List<User> users) {
        for (User user : users) {
          sqlSession.insert("org.mybatis.spring.sample.mapper.UserMapper.insertUser", user);
        }
      }
    }
    
    1. 注意
      1. 只需要在希望语句执行的方法与 SqlSessionTemplate 中的默认设置不同时使用这种配置。
      2. 这种配置的弊端在于,当调用这个方法时,不能存在使用不同 ExecutorType 的进行中的事务
      3. 要么确保对不同 ExecutorTypeSqlSessionTemplate 的调用处在不同的事务中,要么完全不使用事务
2. SqlSessionDaoSupport
  1. SqlSessionDaoSupport 是一个抽象的支持类,用来为你提供 SqlSession。调用 getSqlSession() 方法你会得到一个 SqlSessionTemplate

    1. 类似于这种效果
    public class UserDaoImpl extends SqlSessionDaoSupport implements UserDao {
      public User getUser(String userId) {
        return getSqlSession().selectOne("org.mybatis.spring.sample.mapper.UserMapper.getUser", userId);
      }
    }
    
  2. 在这个类实现里面,通常更倾向于使用 MapperFactoryBean,因为它不需要额外的代码,都封装好了,直接使用

  3. 但是,如果你需要在 DAO 中做其它非 MyBatis 的工作或需要一个非抽象的实现类,那么这个类就很有用了

    1. 例如: 当Mapper没有成Bean的情况下,因为成Bean之后,直接注入,不需要操作SqlSession,因此,当我们要用到SqlSession的时候谈论才有意义,所以,我们要获取一个SqlSession在我们自定义的DAO中完成操作,此时我们自定义的DAO继承SqlSessionDaoSupport就可以得到我们想要的SqlSession来完成工作

    2. SqlSessionDaoSupport 需要通过属性设置一个 sqlSessionFactorySqlSessionTemplate

    3. 如果两个属性都被设置了,那么 SqlSessionFactory 将被忽略。

    4. 假设类 UserMapperImplSqlSessionDaoSupport 的子类

      <bean id="userDao" class="org.mybatis.spring.sample.dao.UserDaoImpl">
        <property name="sqlSessionFactory" ref="sqlSessionFactory" />
      </bean>
      

4. Spring中的Mapper文件

1. 使用
  1. 与其在数据访问对象(DAO)中手工编写使用 SqlSessionDaoSupportSqlSessionTemplate 的代码,还不如让 Mybatis-Spring 为你创建一个线程安全的映射器,这样你就可以直接注入到其它的 bean 中了

    <bean id="fooService" class="org.mybatis.spring.sample.service.FooServiceImpl">
      <constructor-arg ref="userMapper" />
    </bean>
    
    1. 注意代码中并没有任何的对 SqlSession 或 MyBatis 的引用。你也不需要担心创建、打开、关闭 session,MyBatis-Spring 将为你打理好一切
    public class FooServiceImpl implements FooService {
    
      private final UserMapper userMapper;
    
      public FooServiceImpl(UserMapper userMapper) {
        this.userMapper = userMapper;
      }
    
      public User doSomeBusinessStuff(String userId) {
        return this.userMapper.getUser(userId);
      }
    }
    
2. 注册Mapper
  1. 在你的 XML或者Java代码 中加入 MapperFactoryBean 以便将Mapper注册到 Spring 中

    1. 如果映射器接口 UserMapper 在相同的类路径下有对应的Mapper.xml配置文件,将会被 MapperFactoryBean 自动解析。不需要在 MyBatis 配置文件中显式配置映射器
    2. 除非映射器配置文件与接口类不在同一个类路径下,需要配置Mapper.xml的路径 参考 SqlSessionFactoryBean 的 configLocation 属性
    3. 注意
      1. MapperFactoryBean 需要配置一个 SqlSessionFactory 或 SqlSessionTemplate
      2. 它们可以分别通过 sqlSessionFactory 和 sqlSessionTemplate 属性来进行设置
      3. 如果两者都被设置,SqlSessionFactory 将被忽略
      4. 由于 SqlSessionTemplate 已经设置了一个sqlSessionFactory,因此MapperFactoryBean 将使用这个sqlSessionFactory
    <bean id="userMapper" class="org.mybatis.spring.mapper.MapperFactoryBean">
      <property name="mapperInterface" value="org.mybatis.spring.sample.mapper.UserMapper" />
      <property name="sqlSessionFactory" ref="sqlSessionFactory" />
    </bean>
    
    @Configuration
    public class MyBatisConfig {
      @Bean
      public MapperFactoryBean<UserMapper> userMapper() throws Exception {
        MapperFactoryBean<UserMapper> factoryBean = new MapperFactoryBean<>(UserMapper.class);
        factoryBean.setSqlSessionFactory(sqlSessionFactory());
        return factoryBean;
      }
    }
    
3. 扫描Mapper
  1. 我们其实不需要一个个地注册你的所有映射器
  2. 你可以让 MyBatis-Spring 对类路径进行扫描来发现它们,有几种办法来发现映射器:
1. 使用 <mybatis:scan/>
<beans xmlns="http://www.springframework.org/schema/beans"
  xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
  xmlns:mybatis="http://mybatis.org/schema/mybatis-spring"
  xsi:schemaLocation="
  http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd
  http://mybatis.org/schema/mybatis-spring http://mybatis.org/schema/mybatis-spring.xsd">
  <mybatis:scan base-package="org.mybatis.spring.sample.mapper" />
</beans>
  1. base-package 属性允许你设置映射器接口文件的基础包
    1. 通过使用逗号或分号分隔,你可以设置多个包
    2. 并且会在你所指定的包中递归搜索Mapper
  2. 注意
    1. 不需要为 <mybatis:scan/> 指定 SqlSessionFactorySqlSessionTemplate
    2. 这是因为它将使用能够被自动注入的 MapperFactoryBean
    3. 但如果你正在使用多个数据源(DataSource),自动注入可能不适合你
    4. 在这种情况下,你可以使用 factory-reftemplate-ref 属性指定你想使用的 bean 名称
    5. <context:component-scan/> 无法发现并注册Mapper。Mapper的本质是接口,为了将它们注册到 Spring 中,扫描器必须知道如何为找到的每个接口创建一个 MapperFactoryBean,最终生成Mapper的代理对象Bean
  3. <mybatis:scan/> 支持基于标记接口或注解的过滤操作
    1. annotation 属性中,可以指定Mapper应该具有的特定注解,就是标注了指定注解的类才会被Mapper发现
    2. 而在 marker-interface 属性中,可以指定Mapper应该继承的父接口。当这两个属性都被设置的时候,被扫描的Mapper必须满足这两个条件。
    3. 默认情况下,这两个属性为空,因此在基础包中的所有接口都会被作为Mapper进行扫描
  4. 被扫描的Mapper会按照 Spring 对自动扫描组件的默认命名策略进行命名
    1. 也就是说,如果没有使用注解显式指定名称,将会使用Mapper的首字母小写非全限定类名作为名称
    2. 但如果发现Mapper具有 @Component 或 JSR-330 标准中 @Named 注解,会使用注解中的名称作为名称
    3. 提醒一下,你可以设置 annotation 属性为你自定义的注解,然后在你的注解上设置 org.springframework.stereotype.Component 或 javax.inject.Named(需要使用 Java SE 6 以上)注解,这样你的注解既可以作为标记,也可以作为一个名字提供器来使用了
2. 使用 @MapperScan
  1. 当你正在使用 Spring 的基于 Java 的配置时(也就是 @Configuration),相比于使用 <mybatis:scan/>,你会更喜欢用 @MapperScan

    @Configuration
    @MapperScan("org.mybatis.spring.sample.mapper")
    public class AppConfig {
    }
    
    1. 这个注解具有与 <mybatis:scan/> 元素一样的工作方式
    2. 它也可以通过 markerInterfaceannotationClass 属性设置标记接口或注解类
    3. 通过配置 sqlSessionFactorysqlSessionTemplate 属性,你还能指定一个 SqlSessionFactorySqlSessionTemplate
    4. 从 2.0.4 起,如果 basePackageClassesbasePackages 没有定义, 扫描将基于声明这个注解的类所在的包
3. 注册MapperScannerConfigurer的Bean
  1. MapperScannerConfigurer 是一个 BeanDefinitionRegistryPostProcessor,这样就可以作为一个 bean,包含在经典的 ApplicationContext 应用上下文中

  2. 为了配置 MapperScannerConfigurer,使用下面的 Spring 配置

    <bean class="org.mybatis.spring.mapper.MapperScannerConfigurer">
      <property name="basePackage" value="org.mybatis.spring.sample.mapper" />
    </bean>
    
    1. 如果你需要指定 sqlSessionFactorysqlSessionTemplate,那你应该要指定的是 bean名称而不是 bean 的引用,因此要使用 value 属性而不是通常的 ref 属性

      <property name="sqlSessionFactoryBeanName" value="sqlSessionFactory" />
      
      
    2. 在 MyBatis-Spring 1.0.2 之前,sqlSessionFactoryBeansqlSessionTemplateBean 属性是唯一可用的属性

    3. 但由于 MapperScannerConfigurer 在启动过程中比 PropertyPlaceholderConfigurer 运行得更早,经常会产生错误

    4. 基于这个原因,上述的属性已被废弃,现在建议使用 sqlSessionFactoryBeanNamesqlSessionTemplateBeanName 属性。

4. 特性
  1. <mybatis:scan/>@MapperScan 都在 MyBatis-Spring 1.2.0 中被引入

  2. @MapperScan 需要你使用 Spring 3.1+

  3. 从 2.0.2 版本开始,mapper 扫描机制支持控制 mapper bean 的懒加载 (lazy-initialization) ,这个选项是可选的。添加这个选项是为了支持 Spring Boot 2.2 中的懒加载特性。 默认的选项值为 false (即不开启懒加载)。 如果开发者想使用懒加载的特性,需要显式地将其设置为 true如果开发者想使用懒加载的特性,需要首先知道其局限性。

    1. 如果有下列情况,懒加载将在你的应用中不起作用:
      1. 当使用 <association>(@One) 与 <collection>(@Many) 指向其它的 mapper
      2. 当使用 <include>其它的 mapper 的一部分包含进来时
      3. 当使用 <cache-ref>(@CacheNamespaceRef) 指向其它的 mapper 的缓存时
      4. 当使用 <select resultMap="...">(@ResultMap) 指向其它的 mapper 的结果集时
    2. 然而,通过使用 @DependsOn(Spring 的特性)在初始化依赖 bean 的同时,可以使用懒加载
    @DependsOn("vendorMapper")
    public interface GoodsMapper {
    }
    
  4. Mapper Bean的作用域

    1. 2.0.6 起,开发者可以通过 mapper 扫描的特性,使用(default-scope 的)选项和作用域注解来指定扫描的 bean 的作用域(@Scope@RefreshScope 等)
    2. 添加这个可选项是为了支持 Spring Cloud 的 refresh 作用域的特性
    3. 可选项默认为空( 相当于指定 singleton 作用域)
    4. 当被扫描的 bean 定义在 singleton 作用域(默认作用域),且若最终的作用域不是 singleton 时,为其创建一个基于作用域的代理 bean ,default-scope 作用于 mapper bean(MapperFactoryBean)

4. Spring中使用MybatisAPI

  1. 使用 MyBatis-Spring,你可以继续直接使用 MyBatis 的 API。只需简单地使用 SqlSessionFactoryBean 在 Spring 中创建一个 SqlSessionFactory,然后按你的方式在代码中使用工厂即可

    public class UserDaoImpl implements UserDao {
      // SqlSessionFactory 一般会由 SqlSessionDaoSupport 进行设置
      private final SqlSessionFactory sqlSessionFactory;
    
      public UserDaoImpl(SqlSessionFactory sqlSessionFactory) {
        this.sqlSessionFactory = sqlSessionFactory;
      }
    
      public User getUser(String userId) {
        // 注意对标准 MyBatis API 的使用 - 手工打开和关闭 session
        try (SqlSession session = sqlSessionFactory.openSession()) {
          return session.selectOne("org.mybatis.spring.sample.mapper.UserMapper.getUser", userId);
        }
      }
    }
    
  2. 注意事项

    1. 小心使用此选项,错误地使用会产生运行时错误,更糟糕地,会产生数据一致性的问题。
    2. 直接使用 API 时,注意以下弊端:
      • 它不会参与到 Spring 的事务管理之中。
      • 如果 SqlSession 使用与 Spring 事务管理器使用的相同 DataSource,并且有进行中的事务,代码会抛出异常。
      • MyBatis 的 DefaultSqlSession 是线程不安全的。如果在 bean 中注入了它,会发生错误。
      • 使用 DefaultSqlSession 创建的映射器也不是线程安全的。如果你将它们注入到 bean 中,会发生错误。
      • 你必须确保总是在 finally 块中来关闭 SqlSession

3. Mybatis-SpringBoot

1. 版本要求

MyBatis-Spring-Boot-StarterMyBatis-SpringSpring BootJava
3.03.03.0 - 3.117 或更高
2.32.12.5 - 2.78 或更高
2.2 (EOL)2.0(2.0.6 以上可开启所有特性)2.5 - 2.78 或更高
2.1 (EOL)2.0(2.0.6 以上可开启所有特性)2.1 - 2.48 或更高
2.0 (EOL)2.02.0 或 2.18 或更高
1.3 (EOL)1.31.56 或更高
1.2 (EOL)1.31.46 或更高
1.1 (EOL)1.31.36 或更高
1.0 (EOL)1.21.36 或更高

2. 快速开始

  1. 依赖

    1. 要使用 MyBatis-Spring-Boot-Starter 模块,你只需要将 mybatis-spring-boot-autoconfigure.jar 文件以及它的依赖( mybatis.jar, mybatis-spring.jar 等) 放在类路径下

    2. maven

      <dependency>
          <groupId>org.mybatis.spring.boot</groupId>
          <artifactId>mybatis-spring-boot-starter</artifactId>
          <version>3.0.2</version>
      </dependency>
      
    3. gradle

      dependencies {
        implementation("org.mybatis.spring.boot:mybatis-spring-boot-starter:3.0.2")
      }
      
  2. 要与 Spring 一起使用 MyBatis,你至少需要一个 SqlSessionFactory 和一个 mapper 接口

  3. MyBatis-Spring-Boot-Starter 将会完成一下功能

    1. 自动扫描存在的 DataSource
    2. 将使用 SqlSessionFactoryBean 创建并注册一个 SqlSessionFactory 的实例,并将扫描到的 DataSource 作为数据源
    3. 将创建并注册一个从 SqlSessionFactory 中得到的 SqlSessionTemplate 的实例
    4. 自动扫描你的 mapper,将它们与 SqlSessionTemplate 相关联,并将它们注册到Spring 的环境(context)中去,这样它们就可以被注入到你的 bean 中
  4. 案例

    1. mapper

      @Mapper
      public interface CityMapper {
      
        @Select("SELECT * FROM CITY WHERE state = #{state}")
        City findByState(@Param("state") String state);
      
      }
      
    2. SpringBoot应用

      @SpringBootApplication
      public class SampleMybatisApplication implements CommandLineRunner {
      
        private final CityMapper cityMapper;
      
        public SampleMybatisApplication(CityMapper cityMapper) {
          this.cityMapper = cityMapper;
        }
      
        public static void main(String[] args) {
          SpringApplication.run(SampleMybatisApplication.class, args);
        }
      
        @Override
        public void run(String... args) throws Exception {
          System.out.println(this.cityMapper.findByState("CA"));
        }
      
      }
      

3. 配置

1. “Mapper扫描”的进阶用法

  1. MyBatis-Spring-Boot-Starter 将默认搜寻带有 @Mapper 注解的 mapper 接口。
  2. 你可能想指定一个自定义的注解或接口来扫描,如果那样的话,你就必须使用 @MapperScan 注解了。 MyBatis-Spring 参考
  3. MyBatis-Spring-Boot-Starter 如果在应用中至少存在一个SqlSessionFactoryBean,MyBatis-Spring-Boot-Starter将不会自动开始扫描mapper。
  4. 因此,如果你想停止自动扫描机制,你需要使用@Bean方法来明确注册你的mapper。
  5. 简而言之,存在多个SqlSessionFactoryBean的情况,这意味着你需要手动注册你的mapper,而不是依赖自动扫描机制
    1. 使用MapperFactoryBean注册

2. 直接使用SqlSession

  1. 一个 SqlSessionTemplate 的实例被创建并添加到 Spring 的环境中,因此你可以使用 MyBatis API,让它像下面一样被注入到你的 bean 中(Spring 4.3 以上可用)
@Component
public class CityDao {

  private final SqlSession sqlSession;

  public CityDao(SqlSession sqlSession) {
    this.sqlSession = sqlSession;
  }

  public City selectCityById(long id) {
    return this.sqlSession.selectOne("selectCityById", id);
  }

}

3. 配置属性

  1. 像其他的 Spring Boot 应用一样,配置参数在 application.properties (或 application.yml )

    # application.properties
    mybatis.type-aliases-package=com.example.domain.model
    mybatis.type-handlers-package=com.example.typehandler
    mybatis.configuration.map-underscore-to-camel-case=true
    mybatis.configuration.default-fetch-size=100
    mybatis.configuration.default-statement-timeout=30
    ...
    
    # application.yml
    mybatis:
        type-aliases-package: com.example.domain.model
        type-handlers-package: com.example.typehandler
        configuration:
            map-underscore-to-camel-case: true
            default-fetch-size: 100
            default-statement-timeout: 30
    
  2. MyBatis 在它的配置项中,使用 mybatis 作为前缀,可用的配置项如下:

配置项(properties)描述
config-locationMyBatis XML 配置文件的路径。
check-config-location指定是否对 MyBatis XML 配置文件的存在进行检查。
mapper-locationsXML 映射文件的路径。
type-aliases-package搜索类型别名的包名。(包使用的分隔符是 “,; \t\n”)
type-aliases-super-type用于过滤类型别名的父类。如果没有指定,MyBatis会将所有从 type-aliases-package 搜索到的类作为类型别名处理。
type-handlers-package搜索类型处理器的包名。(包使用的分隔符是 “,; \t\n”)
executor-typeSQL 执行器类型: SIMPLE, REUSE, BATCH
default-scripting-language-driver默认的脚本语言驱动(模板引擎),此功能需要与 mybatis-spring 2.0.2 以上版本一起使用。
configuration-properties可在外部配置的 MyBatis 配置项。指定的配置项可以被用作 MyBatis 配置文件和 Mapper 文件的占位符。更多细节 见 MyBatis 参考页面
lazy-initialization是否启用 mapper bean 的延迟初始化。设置 true 以启用延迟初始化。此功能需要与 mybatis-spring 2.0.2 以上版本一起使用。
mapper-default-scope通过自动配置扫描的 mapper 组件的默认作用域。该功能需要与 mybatis-spring 2.0.6 以上版本一起使用。
inject-sql-session-on-mapper-scan设置是否注入 SqlSessionTemplateSqlSessionFactory 组件 (如果你想回到 2.2.1 或之前的行为,请指定 false )。如果你和 spring-native 一起使用,应该设置为 true (默认)。
configuration.*MyBatis Core 提供的Configuration 组件的配置项。有关可用的内部配置项,请参阅MyBatis 参考页面。注:此属性不能与 config-location 同时使用。
scripting-language-driver.thymeleaf.*MyBatis ThymeleafLanguageDriverConfig 组件的 properties keys。有关可用的内部配置项,请参阅 MyBatis Thymeleaf 参考页面
scripting-language-driver.freemarker.*MyBatis FreemarkerLanguageDriverConfig 组件的 properties keys。有关可用的内部配置项,请参阅 MyBatis FreeMarker 参考页面。这个特性需要与 mybatis-freemarker 1.2.0 以上版本一起使用。
scripting-language-driver.velocity.*MyBatis VelocityLanguageDriverConfig 组件的 properties keys。有关可用的内部属性,请参阅 MyBatis Velocity 参考页面。这个特性需要与 mybatis-velocity 2.1.0 以上版本一起使用。

4. ConfigurationCustomizer自定义配置

  1. MyBatis-Spring-Boot-Starter 提供了使用 Java Config 来自定义 MyBatis 配置的可能。

  2. MyBatis-Spring-Boot-Starter 将自动寻找实现了 ConfigurationCustomizer 接口的组件,调用自定义 MyBatis 配置的方法。( 1.2.1 及以上的版本可用)

    @Configuration
    public class MyBatisConfig {
      @Bean
      ConfigurationCustomizer mybatisConfigurationCustomizer() {
        return new ConfigurationCustomizer() {
          @Override
          public void customize(Configuration configuration) {
            // customize ...
          }
        };
      }
    }
    

5. SqlSessionFactoryBeanCustomizer自定义配置

  1. MyBatis-Spring-Boot-Starter 提供了使用 Java Config 来自定义自动配置生成的 SqlSessionFactoryBean

  2. MyBatis-Spring-Boot-Starter 将自动寻找实现了 SqlSessionFactoryBeanCustomizer 接口的组件,调用自定义 SqlSessionFactoryBean 的方法。( 2.2.2 及以上的版本可用)

    @Configuration
    public class MyBatisConfig {
      @Bean
      SqlSessionFactoryBeanCustomizer sqlSessionFactoryBeanCustomizer() {
        return new SqlSessionFactoryBeanCustomizer() {
          @Override
          public void customize(SqlSessionFactoryBean factoryBean) {
            // customize ...
          }
        };
      }
    }
    

6. 使用 SpringBootVFS

  1. MyBatis-Spring-Boot-Starter 提供了 SpringBootVFS 作为 VFS 的实现类

  2. VFS 用于从应用或应用服务器中寻找类 (例如: 类型别名的目标类,类型处理器类)

  3. SpringBootVFS 是 MyBatis 在 Spring Boot 中使用的虚拟文件系统(Virtual File System)

  4. 它允许 MyBatis 在 Spring Boot 打包的jar中查找和加载 XML 映射文件。

  5. 如果你使用可执行的 jar 文件来运行 Spring boot 应用,你需要使用 SpringBootVFS

  6. 由于拥有自动配置的特性,MyBatis-Spring-Boot-Starter 会自动启用它

  7. 但在你手动配置(MyBatis-Spring-Boot-Starter)的时候 (例如: 当你使用多个 DataSource 的时候)

@Configuration
public class MyBatisConfig {
  @Bean
  public SqlSessionFactory masterSqlSessionFactory() throws Exception {
    SqlSessionFactoryBean factoryBean = new SqlSessionFactoryBean();
    factoryBean.setDataSource(masterDataSource());
    factoryBean.setVfs(SpringBootVFS.class); // Sets the SpringBootVFS class into SqlSessionFactoryBean
    // ...
    return factoryBean.getObject();
  }
}

7. 扫描Mybatis组件

  1. MyBatis-Spring-Boot-Starter 将扫描实现以下由 MyBatis 提供的接口的组件
1. Interceptor (拦截器)
2. TypeHandler (类型处理器)
3. LanguageDriver (插入脚本语言)(需要 mybatis-spring 2.0.2 以上配合使用)
  1. 如果只有一个 LangaugeDriver ,它将自动地将其作为默认的脚本语言,按照指定的模板规则来解析mapper.xml中的sql

    1. 默认情况

      <select id="getUserById" resultType="User" parameterType="java.lang.Long">
          SELECT * FROM users WHERE id = #{id}
      </select>
      
    2. Thymeleaf

      <select id="getUserById" resultType="User" parameterType="java.lang.Long">
          SELECT * FROM users WHERE id = /*@{parameter.id}*/
      </select>
      
  2. 如果你想自定义 LangaugeDriver 的配置,请注册用户定义的组件

    1. ThymeleafLanguageDriver

      1. 按照Thymeleaf模板进行解析

        @Configuration
        public class MyBatisConfig {
          @Bean
          ThymeleafLanguageDriverConfig thymeleafLanguageDriverConfig() {
            return ThymeleafLanguageDriverConfig.newInstance(c -> {
              // ... 自定义代码
            });
          }
        }
        
    2. FreeMarkerLanguageDriverConfig

      1. 按照FreeMarker模板进行解析

        	@Configuration
        public class MyBatisConfig {
          @Bean
          FreeMarkerLanguageDriverConfig freeMarkerLanguageDriverConfig() {
            return FreeMarkerLanguageDriverConfig.newInstance(c -> {
              // ... 自定义代码
            });
          }
        }
        
    3. VelocityLanguageDriver

      1. 使用Velocity模板解析

        @Configuration
        public class MyBatisConfig {
          @Bean
          VelocityLanguageDriverConfig velocityLanguageDriverConfig() {
            return VelocityLanguageDriverConfig.newInstance(c -> {
              // ... customization code
            });
          }
        }
        
4. DatabaseIdProvider
5. 组件配置类
@Configuration
public class MyBatisConfig {
  @Bean
  MyInterceptor myInterceptor() {
    return MyInterceptor();
  }
  @Bean
  MyTypeHandler myTypeHandler() {
    return MyTypeHandler();
  }
  @Bean
  MyLanguageDriver myLanguageDriver() {
    return MyLanguageDriver();
  }
  @Bean
  VendorDatabaseIdProvider databaseIdProvider() {
    VendorDatabaseIdProvider databaseIdProvider = new VendorDatabaseIdProvider();
    Properties properties = new Properties();
    properties.put("SQL Server", "sqlserver");
    properties.put("DB2", "db2");
    properties.put("H2", "h2");
    databaseIdProvider.setProperties(properties);
    return databaseIdProvider;
  }  
}

8. 使用案例

  • 12
    点赞
  • 21
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值