Files
rikako-note/mybatis/mybatis.md
2022-08-31 00:03:20 +08:00

23 KiB
Raw Blame History

mybatis框架

Mybatis中主要的类及其生命周期

SqlSessionFactoryBuilder

对于SqlSessionFactoryBuilder其推荐的生命周期是创建之后立即销毁不必被保留其理想的作用域是方法的作用域。

可以将SqlSessionFactoryBuilder进行保留并用其创建多个SqlSessionFactory实例。但是为了保证XML解析后资源被正常的释放应该在将其创建之后立刻丢弃

SqlSessionFactory

对于SqlSessionFactory实例其理想的作用域和生命周期都应该是整个应用的执行期间。在应用运行期间都会通过该工厂实例来获取SqlSession对象。

SqlSessionFactory实例在创建完成之后都不应该被修改或者是被重新创建。

SqlSession

对于SqlSession对象来说其生命周期和作用域应该是方法级别或者是请求级别。SqlSession对象并不是线程安全的无法在多线程环境之下被共享。因而无法将其作为Servlet或是HttpSession的属性或是静态属性否则会产生非预期的并发安全问题。
推荐情况下SqlSession应该在接受到http请求时被创建并且在请求返回httpResponse时被销毁。
并且SqlSession必须要被保证正确的关闭。

try(SqlSession sqlSession=sqlSessionFactory.openSession()) {
    // operations on sqlSession
}

Mapper实例

Mapper实例是通过SqlSession实例来获取的故而Mapper实例最大的生命周期和作用域和SqlSession。Mapper最推荐的生命周期和作用域是方法级别的在方法中获取Mapper并使用后即可丢弃无需在方法外保存Mapper对象。 虽然Mapper也可以和SqlSession一样在request范围内有效但是会发现保留太多Mapper级别的资源而不销毁会很快的产生失控。 Mapper实例和SqlSession实例不同并不需要被显式的销毁。

Mybatis中的配置文件

Mybatis中配置文件主要有properties和setting两部分。

properties

对于那些可外部化、可替换的属性可以通过java的.properties文件进行配置或者通过<properties>标签中配置的子属性来进行传入,具体如下:

<properties resource="org/mybatis/example/config.properties">
  <property name="username" value="dev_user"/>
  <property name="password" value="F2Fa3!33TYyg"/>
</properties>

此时在整个xml配置文件中都可以使用${username}和${password}变量或者从config.properties文件中导入的变量。
在向SqlSessionFactoryBuilder.build方法中传入propertis属性时优先级如下

/*
*   首先会读入xml配置文件中properties标签中的变量内容
*   然后会读入properties标签resource或者url属性指定的properties属性
*   最后会读入作为参数传入的props属性
*   按上述顺序读入属性,后读入的属性会覆盖前面读入的同名属性
*/

SqlSessionFactory factory =
  sqlSessionFactoryBuilder.build(reader, props);

// ... or ...

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

settings

配置settings属性可以修改mybatis运行时的行为。

typeAliases

typeAliases标签中可以定义全限定类名的别名在定义typeAliases之后xml文件中可以使用别名来代替全限定类名

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

如下是java常用类型的别名

Aias Mapped Type
_byte byte
_int int
byte Byte
string String
long Long

typeHandler

每当mybatis向PreparedStatement设置值或者从ResultSet中获取值时typeHandler都会使用合适的方法来将ResultSet中的属性传递给java类对象或者从Java类对象中获取属性并将其传递给PreparedStatement的sql语句。
mybatis内置了很多typeHandler用户可以实现自己的TypeHandler通过实现TypeHandler接口或者继承BaseTypeHandler类。

/**
  * 该方法会覆盖默认的针对String类型和varchar类型的typeHandler
**/
// ExampleTypeHandler.java
@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);
  }
}
<!-- mybatis-config.xml -->
<typeHandlers>
  <typeHandler handler="org.mybatis.example.ExampleTypeHandler"/>
</typeHandlers>

mybatis会通过泛型参数来获知其typeHandler想要处理的javaType但是可以通过两种方法来覆盖该种行为

  • 在typeHandler元素中添加javaType属性例如javaType="String")
  • 通过@MappedTypes注解指定该typeHandler想要对应的javaType列表如果javaType属性在typeHandler元素中被指定那么该注解内容将会被忽略

同样的jdbcType也可以通过两种方式来指定

  • 在typeHandler元素中添加jdbcType属性例如jdbcType="VARCHAR")
  • 为typeHandler类添加@MappedJdbcTypes注解并且在jdbcType属性被指定时该注解被忽略

当决定在ResultMap中使用typeHandler时java类型已知但是jdbc类型未知。因此ResultMap采用javaType=[javaType],jdbcType=null的组合来选择typeHandler。为了使typeHandler在ResultMap中能够被使用需要在@MappedJdbcTypes加入includeNullJdbcType=true。

如果对于某一个java类型只有一个typeHandler被注册那么该typeHandler会自动被使用即使includeNullJdbcType=true并没有被设置。

EnumTypeHandler

对于枚举类型的处理可以使用EnumTypeHandler和EnumOridinalTypeHandler。

默认情况下mybatis会使用EnumTypeHandler来处理枚举类型会将枚举类型的转化成其名字的字符串。
可以强制将EnumOridinalTypeHandler指定给sql语句的typeHandler属性此时会将枚举类型转换成其对应的数字值。

<!-- 将枚举类型通过数字存储 -->
<!-- mybatis-config.xml -->
<typeHandlers>
  <typeHandler handler="org.apache.ibatis.type.EnumOrdinalTypeHandler"
    javaType="java.math.RoundingMode"/>
</typeHandlers>

可以在不同的地方使用不同的枚举类型处理器将其映射为整数类型或字符串类型。只需在mapper使用非默认类型时显示指定即可。

<mapper namespace="org.apache.ibatis.submitted.rounding.Mapper">
    <resultMap type="org.apache.ibatis.submitted.rounding.User" id="usermap">
        <id column="id" property="id"/>
        <result column="name" property="name"/>
        <result column="funkyNumber" property="funkyNumber"/>
        <!-- 默认情况下在指定完typeHandler为EnumOrdinalTypeHandler后其roundingMode被映射为数字 -->
        <result column="roundingMode" property="roundingMode"/>
    </resultMap>

    <select id="getUser" resultMap="usermap">
        select * from users
    </select>
    <insert id="insert">
        insert into users (id, name, funkyNumber, roundingMode) values (
            #{id}, #{name}, #{funkyNumber}, #{roundingMode}
        )
    </insert>

    <!-- 为resultMap显示指定column的typeHandler为EnumTypeHandler -->
    <resultMap type="org.apache.ibatis.submitted.rounding.User" id="usermap2">
        <id column="id" property="id"/>
        <result column="name" property="name"/>
        <result column="funkyNumber" property="funkyNumber"/>
        <result column="roundingMode" property="roundingMode"
         typeHandler="org.apache.ibatis.type.EnumTypeHandler"/>
    </resultMap>
    <select id="getUser2" resultMap="usermap2">
        select * from users2
    </select>
    <!-- insert语句中在#{}中显式指定typeHandler -->
    <insert id="insert2">
        insert into users2 (id, name, funkyNumber, roundingMode) values (
            #{id}, #{name}, #{funkyNumber}, #{roundingMode, typeHandler=org.apache.ibatis.type.EnumTypeHandler}
        )
    </insert>

</mapper>

ObjectFactory

mybatis会使用ObjectFactory来创建返回对象。ObjectFactory仅仅会调用默认构造函数或者参数化的构造函数来创建返回对象。

environment

Mybatis可以设置复数个环境但是对于每一个SqlSessionFactory对象只能够选择一个环境。为了指定构建的环境可以将其以参数的方式传递给SqlSessionFactoryBuilder.build。如果environment参数被省略那么默认的环境将会被采用。
环境的配置格式如下:

<!-- 设置默认环境为development -->
<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>

transactionManager

当在spring中使用mybatis框架时无需在environment中指定transactionManagerSpring会用自己的transactionManager来覆盖environment中的定义。

datasource

在datasource标签中通过标准jdbc接口来定义。datasource可以分为如下三个类型POOLED|UNPOOLED|JNDI

  • UNPOOLED对于每次请求都会都会打开一个新的连接并且在请求结束之后关闭该链接。
  • POOLED会在连接池中池化链接可以避免每次请求都会创建链接和身份认证的开销。在高并发场景下池化数据源的响应时间要由于未池化的数据源。
  • JNDIJNDI数据源通常和特定容器一起使用例如EJB服务器该类服务器会集中或者在外部配置该数据源。

mappers

通过mappers属性可以向mybatis注册mapper.xml文件。

<mappers>
  <mapper resource="org/mybatis/builder/AuthorMapper.xml"/>
  <mapper resource="org/mybatis/builder/BlogMapper.xml"/>
  <mapper resource="org/mybatis/builder/PostMapper.xml"/>
</mappers>

Mybatis Mapper

select

通常来说数据库操作中select的频率远远大于update、insert、delete的频率。
select元素有如下属性

  • id 命名空间中唯一的标识符用来引用该sql声明
  • parameterType 传递参数的全限定类名或者alias该属性是可选的mybatis会根据根据实际传递的参数来计算应该使用的typeHandler
  • resultType方法预期返回类型的全类名

如果方法的返回类型是集合类型那么resultType为集合中包含元素的类型而不是集合本身的类型。

  • resultMap对于外部resultMap的命名引用

对于resultType和resultMap应该只使用其中的一个同一条select语句中不应该既包含resultType又包含resultMap

  • flushCache如果该属性设置为true在该Statement被调用时会导致本地缓存和二级缓存都被刷新。默认情况下flushCache被设置为false。
  • useCache如果该属性设置为true会导致该Statement的查询结果被缓存在二级缓存中。默认情况下useCache属性为true。

mybatis缓存结构

  • 一级缓存: mybatis一级缓存是针对SqlSession的缓存如果SqlSession查询数据会将第一次查询到的数据存放在Map缓冲区中。如果后续SqlSession没有对数据进行添加、修改、删除等操作那么缓存将会失效。
    默认情况下若未开启事务那么每条语句都可以看作一个事务多条select语句之间都会刷新缓 存,一级缓存不会起作用。
  • 二级缓存mybatis中二级缓存是针对Mapper级别的。二级缓存针对mapper.xml中所有Statement的作用域。二级缓存可以在多个SqlSession之间进行共享。 在执行sql查询之前如果执行了插入或者删除操作那么二级缓存会失效。 二级缓存需要手动配置settings标签中cacheEnabled默认是true只需要在需开启缓存的mapper.xml中加入cache标签即可开启二级缓存
  • timeoutdrvier会等待数据库返回查询结果的超时时间如果超过该timeout上线那么会抛出异常。默认情况下该timeout是未设置的取决于底层的driver

update、insert、delete

修改语句通常有如下属性:

  • id
  • parameterType
  • flushCache
  • timeout
  • statementType: STATEMENT/PREPARED/CALLABLE其中之一默认值为PREPARED

statementType通常可以设置为如下值之一

  • STATEMENT:mybatis会使用Statement
  • PREPARED:mybatis会使用PreparedStatement
  • CALLABLE:mybatis会使用CallableStatement
  • useGeneratedKeys当useGeneratedKeys被设置为true时会调用JDBC中的getGeneratedKeys方法从数据库内部获取自动生成的key。默认情况下该值为false

该属性仅针对insert和update语句

  • keyProperty后跟随字段或者字段列表对于想要数据库内部自动生成的字段mybatis会通过getGeneratedKeys方法返回的值来设置该字段的内容对于想要自定义字段自动生成如随机生成的字段mybatis会通过insert元素selectKey子元素中的值来设置
<!-- 使用mysql数据库自动生成的key如auto_increment) -->
<insert id="insertAuthor" useGeneratedKeys="true"
    keyProperty="id">
  insert into Author (username,password,email,bio)
  values (#{username},#{password},#{email},#{bio})
</insert>


<!-- 通过自定义方式生成的主键来填充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>

selectKey

对于selectKey子元素其通常具有如下属性

  • keyProperty该属性指定selectKey生成的结果应该被设置到insert语句的哪个地方
  • resultTypeselectKey语句执行结果的返回类型
  • order可以是“BEFORE"或者”AFTER"如果被设置为“BEFORE"那么会先执行selectKey然后将产生结果设置到keyProperty最后才会执行insert语句

sql

sql标签通常被用来定义一些可以复用的sql片段sql片段可以接受一个参数。
sql标签定义的sql片段可以被include标签进行引用并且include标签的refid属性对应sql标签的id属性。

如果一个sql片段嵌套了另一个sql片段那么在sql语句include外层sql片段时可以同时为内层和外层sql片段的变量进行赋值操作

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

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

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

parameter

  • 简单的参数映射
<!-- 在该简单映射中对于基本数据类型或者如String、Integer等没有属性的简单数据类型会用该类数据的值完全替换参数的值 -->
<select id="selectUsers" resultType="User">
  select id, username, password
  from users
  where id = #{id}
</select>
  • 向参数传递复杂对象例如User类型对象那么将会从对象中获取id、username、password等属性并传递给参数
<insert id="insertUser" parameterType="User">
  insert into users (id, username, password)
  values (#{id}, #{username}, #{password})
</insert>

mybatis $

对于mybatis可以通过#{}来设置PreparedStatement的占位参数但是当想要动态设置sql statement中的元数据如表名称、字段名称可以通过${}来插入一个未修改的字符串进行拼串。

// 未使用${}
@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);

// 使用${}之后
@Select("select * from user where ${column} = #{value}")
User findByColumn(@Param("column") String column, @Param("value") String value);

resultMap

  • 对于简单的statement其映射不需要用到resultMap
<!-- 返回集中所有的column会自动映射到HashMap中的key -->
<select id="selectUsers" resultType="map">
  select id, username, hashedPassword
  from some_table
  where id = #{id}
</select>
  • 对于POJO当select的返回列名和POJO类的属性名相同时会自动生成resultMap来将column和POJO对象属性关联到一起
<select id="selectUsers" resultType="com.someapp.model.User">
  select
    user_id             as "id",
    user_name           as "userName",
    hashed_password     as "hashedPassword"
  from some_table
  where id = #{id}
</select>
  • 自定义外部的resultMap来映射到User类
<resultMap id="userResultMap" type="com.someapp.model.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>
  • 高级结果集映射
<!-- Very Complex Statement -->
<select id="selectBlogDetails" resultMap="detailedBlogResultMap">
  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,
       A.favourite_section as author_favourite_section,
       P.id as post_id,
       P.blog_id as post_blog_id,
       P.author_id as post_author_id,
       P.created_on as post_created_on,
       P.section as post_section,
       P.subject as post_subject,
       P.draft as draft,
       P.body as post_body,
       C.id as comment_id,
       C.post_id as comment_post_id,
       C.name as comment_name,
       C.comment as comment_text,
       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>

<!-- Very Complex Result Map -->
<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"/>
    <result property="email" column="author_email"/>
    <result property="bio" column="author_bio"/>
    <result property="favouriteSection" column="author_favourite_section"/>
  </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>

resultMap的元素和属性

  • resultMap中可嵌套如下元素
    • constructor在通过构造方法实例化类时将select语句返回的result注入到构造方法中
      • idArg标识为id的参数将result标识为id可以提高整体的性能
      • arg将result注入到constructor中
    • id标识为id的result将result标识为id可以提高整体性能
    • resultselect语句返回的result将被注入到返回POJO类型的field
    • association复杂关联返回results中的部分results将会被包装到这种关联中
      • 关联被嵌套在resultMap中关联本身可以是resultMap或者可以引用一个外部resultMap
    • collection复杂类型的集合
      • 集合被嵌套在resultMap中其本身可以是一个resultMap或者其可以引用要给外部的resultMap
    • discrimination鉴别器可以通过结果值来判断使用哪个resultMap
      • case条件选项当值为xx时使用特定的resultMap
      • case同样是一个resultMapcase可以是resultMap本身也可以引用其他的resultMap
  • resultMap元素可包含如下属性
    • id在命名空间内可以通过该id引用该resultMap
    • typejava类的全限定类名或typeAlias
    • autoMapping为resultMap开启或关闭自动映射

id和result元素

id和result元素是resultMap的基础id和result都将一个column字段值映射到java类型的field域。

id和result标签的区别是当result被标识为id时该result将会被看作对象的标识符在比较对象时id result将会被使用。这样可以提高整体性能尤其是在缓存或是嵌套resultMap时。

  • id和result元素的属性
    • propertycolumn映射到的java对象的field
    • column数据库表的列名
    • javaTypejava类型的全限定类名或者typeAlias通常映射到java bean时mybatis可以推断java类型但是当映射到HashMap时需要显式指定javaType
    • jdbcType
    • typeHandler

constructor元素

当想要构建不可变对象时可以通过constructor属性来向构造方法中注入result构建对象。

public class User {
   //...
   public User(Integer id, String username, int age) {
     //...
  }
//...
}
<constructor>
   <!-- 该constructor会自动匹配构造方法参数为(Integer,String,int)的构造函数 -->
   <idArg column="id" javaType="int"/>
   <arg column="username" javaType="String"/>
   <arg column="age" javaType="_int"/>
</constructor>
  • constructor元素的属性
    • column数据库表列名
    • javaType
    • jdbcType
    • typeHandler
    • resultMap
    • select