Mybatis 的应用3 —— XML 映射文件

SQL 映射文件几个顶级元素(按照它们应该被定义的顺序)为:

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

select

<select> 元素的使用示例如下:

<select id="selectUserById1" parameterType="int" resultType="hashmap">
    select * from tb_user where id = #{id}
</select>

这个语句被称作 selectUserById1,接受一个 int(或 Integer)类型的参数,并返回一个 HashMap 类型的对象,其中的键是列名,值便是结果行中的对应值。参数符号 #{id} 告诉 MyBatis 创建一个预处理语句参数

select 元素的属性有:

属性 描述
id 在命名空间中唯一的标识符,可以被用来引用这条语句。
parameterType 将会传入这条语句的参数类的完全限定名或别名。这个属性是可选的,因为 MyBatis 可以通过 TypeHandler 推断出具体传入语句的参数,默认值为 unset。
resultType 从这条语句中返回的期望类型的类的完全限定名或别名。注意如果是集合情形,那应该是集合可以包含的类型,而不能是集合本身。使用 resultType 或 resultMap,但不能同时使用。
resultMap 外部 resultMap 的命名引用。结果集的映射是 MyBatis 最强大的特性,对其有一个很好的理解的话,许多复杂映射的情形都能迎刃而解。使用 resultMap 或 resultType,但不能同时使用。
flushCache 将其设置为 true,任何时候只要语句被调用,都会导致本地缓存和二级缓存都会被清空,默认值:false。
useCache 将其设置为 true,将会导致本条语句的结果被二级缓存,默认值:对 select 元素为 true。
timeout 这个设置是在抛出异常之前,驱动程序等待数据库返回请求结果的秒数。默认值为 unset(依赖驱动)。
fetchSize 这是尝试影响驱动程序每次批量返回的结果行数和这个设置值相等。默认值为 unset(依赖驱动)。
statementType STATEMENT(非预编译),PREPARED (预编译)或 CALLABLE 的一个。这会让 MyBatis 分别使用 Statement,PreparedStatement 或 CallableStatement,默认值:PREPARED。当要实现动态传入表名、列名时,用 STATEMENT
resultSetType FORWARD_ONLY,SCROLL_SENSITIVE 或 SCROLL_INSENSITIVE 中的一个,默认值为 unset (依赖驱动)。
databaseId 如果配置了 databaseIdProvider,MyBatis 会加载所有的不带 databaseId 或匹配当前 databaseId 的语句;如果带或者不带的语句都有,则不带的会被忽略。
resultOrdered 这个设置仅针对嵌套结果 select 语句适用:如果为 true,就是假设包含了嵌套结果集或是分组了,这样的话当返回一个主结果行的时候,就不会发生有对前面结果集的引用的情况。这就使得在获取嵌套的结果集的时候不至于导致内存不够用。默认值:false。
resultSets 这个设置仅对多结果集的情况适用,它将列出语句执行后返回的结果集并每个结果集给一个名称,名称是逗号分隔的。

insert, update 和 delete

insert, update 和 delete 的属性有:

属性 描述
id 在命名空间中唯一的标识符,可以被用来引用这条语句。
parameterType 将会传入这条语句的参数类的完全限定名或别名。这个属性是可选的,因为 MyBatis 可以通过 TypeHandler 推断出具体传入语句的参数,默认值为 unset。
flushCache 将其设置为 true,任何时候只要语句被调用,都会导致本地缓存和二级缓存都会被清空,默认值:true(对应插入、更新和删除语句)。
timeout 这个设置是在抛出异常之前,驱动程序等待数据库返回请求结果的秒数。默认值为 unset(依赖驱动)。
fetchSize 这是尝试影响驱动程序每次批量返回的结果行数和这个设置值相等。默认值为 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 的语句;如果带或者不带的语句都有,则不带的会被忽略。

<insert> 元素的使用示例如下:

<insert id="insertUser" parameterType="User" statementType="PREPARED" keyProperty="id" useGeneratedKeys="true">
    insert into tb_user(username, password) values(#{username}, #{password})
</insert>

属性 useGeneratedKeys="true" 使用数据库的自动生成主键,并设置 keyProperty="id" 到对应的 JavaBean 属性,这使得自动生成的 key 添加到传进的参数 User 对应的属性 id

SqlSession session = sqlSessionFactory.openSession();
try {
    User user = new User();
    user.setUsername("lianwx");
    user.setPassword("123456");
    session.insert("insertUser", user);
    System.out.println("id :" + user.getId());
    // 一般情况下不会自动提交
    session.commit();
} catch (Exception e) {
    session.rollback();
    e.printStackTrace();
} finally {
    session.close();
}

属性 statementType="PREPARED" 表示使用预编译,如果需要使用非预编译,使用 statementType="STATEMENT"

<update> 元素的使用示例如下:

<update id="updateUser" parameterType="User">
    update tb_user set username = #{username}, password = #{password} where id = #{id}
</update>
SqlSession session = sqlSessionFactory.openSession();
try {
    User user = new User();
    user.setId(2);
    user.setUsername("lianwx");
    user.setPassword("654321");
    session.update("updateUser", user);
    session.commit();
} catch (Exception e) {
    session.rollback();
    e.printStackTrace();
} finally {
    session.close();
}

<delete> 元素的使用示例如下:

<delete id="deleteUser" parameterType="User">
    delete from tb_user where id = #{id}
</delete>
SqlSession session = sqlSessionFactory.openSession();
try {
    User user = new User();
    user.setId(2);
    session.delete("deleteUser", user);
    session.commit();
} catch (Exception e) {
    session.rollback();
    e.printStackTrace();
} finally {
    session.close();
}

ResultMap —— 解决复杂查询的映射问题

前面在映射文件中使用的:

<select id="selectUserById1" resultType="User" parameterType="Integer">
    select * from tb_user where id = #{id}
</select>

实际上 myBatis 会在幕后自动创建一个 ResultMap ,基于属性名来映射列到 JavaBean 的属性上。实际如下:

<resultMap id="userResultMap" type="User">
  <id property="id" column="id" />
  <result property="username" column="username"/>
  <result property="password" column="password"/>
</resultMap>

<select id="selectUserById1" resultMap="userResultMap" parameterType="Integer">
    select * from tb_user where id = #{id}
</select>

ResultMap 指明了对象的属性到列名的关系

<id> 元素声明为主键,<result> 元素声明为其他属性。<id> 元素也可以用<result> 元素替代,但会严重影响查询性能。 property 属性指对应 javaBean 的属性名,column 属性指查询返回表的列名。

<resultMap> 元素的概念视图:

  • constructor - 类在实例化时,用来注入结果到构造方法中
    • idArg - ID 参数;标记结果作为 ID 可以帮助提高整体效能
    • arg - 注入到构造方法的一个普通结果
  • id – 一个 ID 结果;标记结果作为 ID 可以帮助提高整体效能
  • result – 注入到字段或 JavaBean 属性的普通结果
  • association – 一个复杂的类型关联;许多结果将包成这种类型
    • 嵌入结果映射 – 结果映射自身的关联,或者参考一个
  • collection – 复杂类型的集
    • 嵌入结果映射 – 结果映射自身的集,或者参考一个
  • discriminator – 使用结果值来决定使用哪个结果映射
    • case – 基于某些值的结果映射
      • 嵌入结果映射 – 这种情形结果也映射它本身,因此可以包含很多相 同的元素,或者它可以参照一个外部的结果映射
属性 描述
id 当前命名空间中的一个唯一标识,用于标识一个result map.
type 类的全限定名, 或者一个类型别名 (内置的别名可以参考上面的表格).
autoMapping 如果设置这个属性,MyBatis将会为这个ResultMap开启或者关闭自动映射。这个属性会覆盖全局的属性autoMappingBehavior。默认值为:unset。

<resultMap> 的属性

属性 描述
id 当前命名空间中的一个唯一标识,用于标识一个result map.
type 类的全限定名, 或者一个类型别名 (内置的别名可以参考上面的表格).
autoMapping 如果设置这个属性,MyBatis将会为这个ResultMap开启或者关闭自动映射。这个属性会覆盖全局的属性autoMappingBehavior。默认值为:unset。
属性 描述
property 映射到列结果的字段或属性。如果匹配的是存在的,和给定名称相同 的 JavaBeans 的属性,那么就会使用。否则 MyBatis 将会寻找给定名称 property 的字段。这两种情形你可以使用通常点式的复杂属性导航。比如,你 可以这样映射一些东西: “username” ,或者映射到一些复杂的东西: “address.street.number” 。
column 从数据库中得到的列名,或者是列名的重命名标签。这也是通常和会 传递给 resultSet.getString(columnName)方法参数中相同的字符串。
javaType 一个 Java 类的完全限定名,或一个类型别名(参考上面内建类型别名 的列表) 。如果你映射到一个 JavaBean,MyBatis 通常可以断定类型。 然而,如果你映射到的是 HashMap,那么你应该明确地指定 javaType 来保证所需的行为。
jdbcType 在这个表格之后的所支持的 JDBC 类型列表中的类型。JDBC 类型是仅 仅需要对插入,更新和删除操作可能为空的列进行处理。这是 JDBC jdbcType 的需要,而不是 MyBatis 的。如果你直接使用 JDBC 编程,你需要指定 这个类型-但仅仅对可能为空的值。
typeHandler 我们在前面讨论过默认的类型处理器。使用这个属性,你可以覆盖默 认的类型处理器。这个属性值是类的完全限定名或者是一个类型处理 器的实现,或者是类型别名。

<id> & <result> 属性

属性 描述
property 映射到列结果的字段或属性。如果匹配的是存在的,和给定名称相同 的 JavaBeans 的属性,那么就会使用。否则 MyBatis 将会寻找给定名称 property 的字段。这两种情形你可以使用通常点式的复杂属性导航。比如,你 可以这样映射一些东西: “username” ,或者映射到一些复杂的东西: “address.street.number” 。
column 从数据库中得到的列名,或者是列名的重命名标签。这也是通常和会 传递给 resultSet.getString(columnName)方法参数中相同的字符串。
javaType 一个 Java 类的完全限定名,或一个类型别名(参考上面内建类型别名 的列表) 。如果你映射到一个 JavaBean,MyBatis 通常可以断定类型。 然而,如果你映射到的是 HashMap,那么你应该明确地指定 javaType 来保证所需的行为。
jdbcType 在这个表格之后的所支持的 JDBC 类型列表中的类型。JDBC 类型是仅 仅需要对插入,更新和删除操作可能为空的列进行处理。这是 JDBC jdbcType 的需要,而不是 MyBatis 的。如果你直接使用 JDBC 编程,你需要指定 这个类型-但仅仅对可能为空的值。
typeHandler 我们在前面讨论过默认的类型处理器。使用这个属性,你可以覆盖默 认的类型处理器。这个属性值是类的完全限定名或者是一个类型处理 器的实现,或者是类型别名。

PS1<resultMap> 还支持构造器的构造方法 <constructor> 元素( 包含 <idArg><arg> 子元素 ),相关属性见附录 - <constructor> 的子元素 <idArg> & <arg> 属性

PS2:支持的 JDBC 类型见附录 - jdbcType支持的 JDBC 类型

关联查询(联合查询)

假设除了前面的 tb_user 表外还有另外一张表:tb_user_detail ,表示用户的详细资料,表字段为:id, realname, address。并修改表 tb_user 增加字段 detail_id 外键关联表 tb_user_detail 字段 id

我们可以在程序中添加 JavaBean :

public class UserDetail {
    private int id;
    private String realname;
    private String address;
    
    // 省略 getter 和 setter
}

并修改 User :

public class User {

    private Integer id;
    private String username;
    private String password;
    // 新添加的属性
    private UserDetail userDetail;
    
    // 省略 getter 和 setter
}

则在映射文件 UserMapper.xml 中添加:

<resultMap id="userResultMap" type="User">
    <id property="id" column="user_id"/>
    <result property="username" column="username"/>
    <result property="password" column="password"/>
    <association property="userDetail" javaType="UserDetail">
        <id property="id" column="detail_id"/>
        <result property="realname" column="realname"/>
        <result property="address" column="address"/>
    </association>
</resultMap>

<select id="selectUserUsingResultMap" resultMap="userResultMap" parameterType="Integer">
    select 
        A.id as user_id,
        A.username as username,
        A.password as password,
        B.id as detail_id,
        B.realname as realname,
        B.address as address
    from tb_user A
        inner join tb_user_detail B on A.detail_id = B.id
    where A.id = #{id}
</select>

查询:

SqlSession session = sqlSessionFactory.openSession();
try {
    User user = session.selectOne("selectUserUsingResultMap", 1);
    System.out.println("username :" + user.getUsername());
    System.out.println("realname :" + user.getUserDetail().getRealname());
    session.commit();
} catch (Exception e) {
    session.rollback();
    e.printStackTrace();
} finally {
    session.close();
}

上述映射文件 <select> 用 inner join 内联接建立一个关联查询,在对应 <resultMap> 中使用 <association> 映射这种关联关系

<association> 中属性 javaType 指明对应 javaBean 的类

上面的映射文件也可以写成如下,这种写法使得 UserDetail 结果映射可以重用

<resultMap id="userResultMap" type="User">
    <id property="id" column="user_id"/>
    <result property="username" column="username"/>
    <result property="password" column="password"/>
    <association property="userDetail" column="detail_id" javaType="UserDetail" resultMap="authorResultMap">
    </association>
</resultMap>

<resultMap id="authorResultMap" type="UserDetail">
  <id property="id" column="detail_id"/>
  <result property="realname" column="realname"/>
  <result property="address" column="address"/>
</resultMap>

<select id="selectUserUsingResultMap" resultMap="userResultMap" parameterType="Integer">
    select 
        A.id as user_id,
        A.username as username,
        A.password as password,
        B.id as detail_id,
        B.realname as realname,
        B.address as address
    from tb_user A
        inner join tb_user_detail B on A.detail_id = B.id
    where A.id = #{id}
</select>

PS:在进行联合查询时,必须确保没有重名的字段,如果有,则必须用** as **进行区分

属性 描述
property 映射到列结果的字段或属性。如果匹配的是存在的,和给定名称相同的 property JavaBeans 的属性, 那么就会使用。 否则 MyBatis 将会寻找给定名称的字段。 这两种情形你可以使用通常点式的复杂属性导航。比如,你可以这样映射 一 些 东 西 :“ username ”, 或 者 映 射 到 一 些 复 杂 的 东 西 : “address.street.number” 。
javaType 一个 Java 类的完全限定名,或一个类型别名(参考上面内建类型别名的列 表) 。如果你映射到一个 JavaBean,MyBatis 通常可以断定类型。然而,如 javaType 果你映射到的是 HashMap,那么你应该明确地指定 javaType 来保证所需的 行为。
jdbcType 在这个表格之前的所支持的 JDBC 类型列表中的类型。JDBC 类型是仅仅 需要对插入, 更新和删除操作可能为空的列进行处理。这是 JDBC 的需要, jdbcType 而不是 MyBatis 的。如果你直接使用 JDBC 编程,你需要指定这个类型-但 仅仅对可能为空的值。
typeHandler 我们在前面讨论过默认的类型处理器。使用这个属性,你可以覆盖默认的 typeHandler 类型处理器。 这个属性值是类的完全限定名或者是一个类型处理器的实现, 或者是类型别名。
resultMap 这是结果映射的 ID,可以映射关联的嵌套结果到一个合适的对象图中。这 是一种替代方法来调用另外一个查询语句。这允许你联合多个表来合成到 resultMap 一个单独的结果集。这样的结果集可能包含重复,数据的重复组需要被分 解,合理映射到一个嵌套的对象图。为了使它变得容易,MyBatis 让你“链 接”结果映射,来处理嵌套结果。一个例子会很容易来仿照,这个表格后 面也有一个示例。
columnPrefix 当连接多表时,你将不得不使用列别名来避免ResultSet中的重复列名。指定columnPrefix允许你映射列名到一个外部的结果集中。 请看后面的例子。
notNullColumn 默认情况下,子对象仅在至少一个列映射到其属性非空时才创建。 通过对这个属性指定非空的列将改变默认行为,这样做之后Mybatis将仅在这些列非空时才创建一个子对象。 可以指定多个列名,使用逗号分隔。默认值:未设置(unset)。
autoMapping 如果使用了,当映射结果到当前属性时,Mybatis将启用或者禁用自动映射。 该属性覆盖全局的自动映射行为。 注意它对外部结果集无影响,所以在select or resultMap属性中这个是毫无意义的。 默认值:未设置(unset)。

<association> 的属性1

属性 描述
property 映射到列结果的字段或属性。如果匹配的是存在的,和给定名称相同的 property JavaBeans 的属性, 那么就会使用。 否则 MyBatis 将会寻找给定名称的字段。 这两种情形你可以使用通常点式的复杂属性导航。比如,你可以这样映射 一 些 东 西 :“ username ”, 或 者 映 射 到 一 些 复 杂 的 东 西 : “address.street.number” 。
javaType 一个 Java 类的完全限定名,或一个类型别名(参考上面内建类型别名的列 表) 。如果你映射到一个 JavaBean,MyBatis 通常可以断定类型。然而,如 javaType 果你映射到的是 HashMap,那么你应该明确地指定 javaType 来保证所需的 行为。
jdbcType 在这个表格之前的所支持的 JDBC 类型列表中的类型。JDBC 类型是仅仅 需要对插入, 更新和删除操作可能为空的列进行处理。这是 JDBC 的需要, jdbcType 而不是 MyBatis 的。如果你直接使用 JDBC 编程,你需要指定这个类型-但 仅仅对可能为空的值。
typeHandler 我们在前面讨论过默认的类型处理器。使用这个属性,你可以覆盖默认的 typeHandler 类型处理器。 这个属性值是类的完全限定名或者是一个类型处理器的实现, 或者是类型别名。
resultMap 这是结果映射的 ID,可以映射关联的嵌套结果到一个合适的对象图中。这 是一种替代方法来调用另外一个查询语句。这允许你联合多个表来合成到 resultMap 一个单独的结果集。这样的结果集可能包含重复,数据的重复组需要被分 解,合理映射到一个嵌套的对象图。为了使它变得容易,MyBatis 让你“链 接”结果映射,来处理嵌套结果。一个例子会很容易来仿照,这个表格后 面也有一个示例。
columnPrefix 当连接多表时,你将不得不使用列别名来避免ResultSet中的重复列名。指定columnPrefix允许你映射列名到一个外部的结果集中。 请看后面的例子。
notNullColumn 默认情况下,子对象仅在至少一个列映射到其属性非空时才创建。 通过对这个属性指定非空的列将改变默认行为,这样做之后Mybatis将仅在这些列非空时才创建一个子对象。 可以指定多个列名,使用逗号分隔。默认值:未设置(unset)。
autoMapping 如果使用了,当映射结果到当前属性时,Mybatis将启用或者禁用自动映射。 该属性覆盖全局的自动映射行为。 注意它对外部结果集无影响,所以在select or resultMap属性中这个是毫无意义的。 默认值:未设置(unset)。

子查询(嵌套查询)

上面的联合查询,可以用子查询来实现,即先查询 tb_user 表,再根据 tb_user 表中字段 detail_id 的值再查询 tb_user_detail

上述查询在映射文件中为:

<resultMap type="User" id="userResultMap2">
    <!-- 其他属性自动映射,可省略 -->
    <association property="userDetail" column="detail_id" javaType="UserDetail" select="selectUserDetail">          
    </association>
</resultMap>

<select id="selectUserDetail" resultType="UserDetail">
    select * from tb_user_detail where id = #{id}
</select>

<select id="selectUserBySubQuery" resultMap="userResultMap2" parameterType="int">
    select * from tb_user where id = #{id}
</select>
属性 描述
column 来自数据库的类名,或重命名的列标签。这和通常传递给 resultSet.getString(columnName)方法的字符串是相同的。 column 注 意 : 要 处 理 复 合 主 键 , 你 可 以 指 定 多 个 列 名 通 过 column= ” {prop1=col1,prop2=col2} ” 这种语法来传递给嵌套查询语 句。这会引起 prop1 和 prop2 以参数对象形式来设置给目标嵌套查询语句。
select 另外一个映射语句的 ID,可以加载这个属性映射需要的复杂类型。获取的 在列属性中指定的列的值将被传递给目标 select 语句作为参数。表格后面 有一个详细的示例。 select 注 意 : 要 处 理 复 合 主 键 , 你 可 以 指 定 多 个 列 名 通 过 column= ” {prop1=col1,prop2=col2} ” 这种语法来传递给嵌套查询语 句。这会引起 prop1 和 prop2 以参数对象形式来设置给目标嵌套查询语句。
fetchType 可选的。有效值为 lazy和eager。 如果使用了,它将取代全局配置参数lazyLoadingEnabled。

<association> 的属性2

属性 描述
column 来自数据库的类名,或重命名的列标签。这和通常传递给 resultSet.getString(columnName)方法的字符串是相同的。 column 注 意 : 要 处 理 复 合 主 键 , 你 可 以 指 定 多 个 列 名 通 过 column= ” {prop1=col1,prop2=col2} ” 这种语法来传递给嵌套查询语 句。这会引起 prop1 和 prop2 以参数对象形式来设置给目标嵌套查询语句。
select 另外一个映射语句的 ID,可以加载这个属性映射需要的复杂类型。获取的 在列属性中指定的列的值将被传递给目标 select 语句作为参数。表格后面 有一个详细的示例。 select 注 意 : 要 处 理 复 合 主 键 , 你 可 以 指 定 多 个 列 名 通 过 column= ” {prop1=col1,prop2=col2} ” 这种语法来传递给嵌套查询语 句。这会引起 prop1 和 prop2 以参数对象形式来设置给目标嵌套查询语句。
fetchType 可选的。有效值为 lazy和eager。 如果使用了,它将取代全局配置参数lazyLoadingEnabled。

PS:子查询与联合查询的比较见附录 - 子查询与联合查询的比较

集合查询

假设除了前面的 tb_user 表外还有另外一张表:tb_item ,表示用户的所拥有的物品,表字段为:id, user_id, item_name。

我们可以在程序中添加 JavaBean :

public class Item {
    private int id;
    private User user;
    private String itemName;
    
    // 省略 getter 和 setter
}

并修改 User :

public class User {

    private Integer id;
    private String username;
    private String password;
    private UserDetail userDetail;
    // 新添加的属性
    private List<Item> items;
    
    // 省略 getter 和 setter
}

则在映射文件 UserMapper.xml 中添加:

<resultMap id="userWithCollectionMap" type="User">
    <id property="id" column="user_id" />
    <result property="username" column="username" />
    <result property="password" column="password" />
    <association property="userDetail" javaType="UserDetail">
        <id property="id" column="detail_id" />
        <result property="realname" column="realname" />
        <result property="address" column="address" />
    </association>
    <collection property="items" javaType="ArrayList" ofType="Item">
        <id property="id" column="item_id" />
        <result property="itemName" column="item_name" />
    </collection>
</resultMap>

<select id="selectUserWithCollection" resultMap="userWithCollectionMap"
    parameterType="int">
    select
    A.id as user_id,
    A.username,
    A.password,
    B.id as detail_id,
    B.realname,
    B.address,
    C.id as item_id,
    C.item_name
    from tb_user A
    inner join tb_user_detail B on A.detail_id = B.id
    inner join tb_item C on A.id = C.user_id
    where A.id = #{id}
</select>

同理,该映射文件的 <association<collection 可借助 resultMap 属性分离出另外的 <resultMap> 实现重用

查询:

SqlSession session = sqlSessionFactory.openSession();
try {
    User user = session.selectOne("selectUserWithCollection", 1);
    System.out.println("username :" + user.getUsername());
    System.out.println("realname :" + user.getUserDetail().getRealname());
    for (Item item : user.getItems()) {
        System.out.println("item"+ item.getId() +" :" + item.getItemName());
    }
    session.commit();
} catch (Exception e) {
    session.rollback();
    e.printStackTrace();
} finally {
    session.close();
}

上面的映射文件也可写成如下,即使用子查询的形式

<resultMap id="userWithCollectionMap2" type="User">
    <id property="id" column="id" />
    <result property="username" column="username" />
    <result property="password" column="password" />
    <association property="userDetail" column="detail_id" javaType="UserDetail" select="userWithCollectionMap2Sub1" />
    <collection property="items" javaType="ArrayList" column="id" ofType="Item" select="userWithCollectionMap2Sub2"/>
</resultMap>

<select id="userWithCollectionMap2Sub1" resultType="UserDetail">
    select * from tb_user_detail where id = #{id}
</select>

<select id="userWithCollectionMap2Sub2" resultType="Item">
    select id,user_id,item_name as itemName from tb_item where user_id = #{id}
</select>

<select id="selectUserWithCollection" resultMap="userWithCollectionMap2" parameterType="int">
    select * from tb_user where id = #{id}
</select>

鉴别器

缓存

MyBatis 包含一个非常强大的查询缓存特性,它可以非常方便地配置和定制

默认情况下是没有开启缓存的,除了局部的 session 缓存,可以增强变现而且处理循环依赖也是必须的

要开启二级缓存,首先确保在 mybatis XML配置文件的 <settings> 元素中

<setting name="cacheEnabled" value="true" />

为默认的开始状态。

其次,需要在 SQL 映射文件中添加一行:

<cache/>

它的效果有:

  • 映射语句文件中的所有 select 语句将会被缓存。
  • 映射语句文件中的所有 insert,update 和 delete 语句会刷新缓存。
  • 缓存会使用 Least Recently Used(LRU,最近最少使用的)算法来收回。
  • 根据时间表(比如 no Flush Interval,没有刷新间隔), 缓存不会以任何时间顺序来刷新。
  • 缓存会存储列表集合或对象(无论查询方法返回什么)的 1024 个引用。
  • 缓存会被视为是 read/write (可读/可写)的缓存,意味着对象检索不是共享的,而且可以安全地被调用者修改,而不干扰其他调用者或线程所做的潜在修改。

所有的这些属性都可以通过缓存元素的属性来修改。比如:

<cache
  eviction="FIFO"
  flushInterval="60000"
  size="512"
  readOnly="true"/>

这个更高级的配置创建了一个 FIFO 缓存,并每隔 60 秒刷新,存数结果对象或列表的 512 个引用,而且返回的对象被认为是只读的,因此在不同线程中的调用者之间修改它们会导致冲突。

可用的收回策略有:

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

flushInterval(刷新间隔)可以被设置为任意的正整数,而且它们代表一个合理的毫秒 形式的时间段。默认情况是不设置,也就是没有刷新间隔,缓存仅仅调用语句时刷新。

size(引用数目)可以被设置为任意正整数,要记住你缓存的对象数目和你运行环境的 可用内存资源数目。默认值是 1024。

readOnly(只读)属性可以被设置为 true 或 false。只读的缓存会给所有调用者返回缓 存对象的相同实例。因此这些对象不能被修改。这提供了很重要的性能优势。可读写的缓存 会返回缓存对象的拷贝(通过序列化) 。这会慢一些,但是安全,因此默认是 false。

第三,在具体的SQL语句处指定使用缓存( 属性 useCache ),默认是开启的

<select id="selectCount" useCache="true">
...
</select>

自定义缓存

参照缓存

动态 SQL

MyBatis 的强大特性之一便是它的动态 SQL。利用动态 SQL 这一特性可以彻底摆脱根据不同条件拼接 SQL 语句这种痛苦

if

动态 SQL 通常要做的事情是有条件地包含 where 子句的一部分。比如:

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

如果没有传入“title”,则 <if>所包含的 SQL 语句不会添加到最后的查询 SQL 语句中

choose, when, otherwise

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

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

上面表示提供了“title”就按“title”查找,提供了“author”就按“author”查找,若两者都没有提供,就返回所有符合条件的BLOG

where, set, trim

现在考虑回到“if”示例,这次我们将“ACTIVE = 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>
</select>

这种情况下,如果两个条件都不符合,则 SQL 语句多出了一个 WHERE;如果 state 不符合 title 符合,则多出了一个 AND ,导致查询失败

上述查询使用 <where> 元素:

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

<where> 元素知道只有在一个以上的if条件有值的情况下才去插入“WHERE”子句。而且,若最后的内容是“AND”或“OR”开头的,where 元素也知道如何将他们去除

类似的 <set> 元素可以被用于动态包含需要更新的列,而舍去其他的,同时也会消除无关的逗号:

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

<trim> 元素可用于自定义需要的规则,如 <where> 元素等价于:

<trim prefix="WHERE" prefixOverrides="AND |OR ">
  ... 
</trim>

<set> 元素等价于:

<trim prefix="SET" suffixOverrides=",">
  ...
</trim>

<trim> 元素的属性有:

  • prefix
  • suffix
  • prefixOverrides
  • suffixOverrides

foreach

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

<select id="selectPostIn" resultType="domain.blog.Post">
  SELECT *
  FROM POST P
  WHERE ID in
  <foreach item="item" index="index" collection="list" open="(" separator="," close=")">
        #{item}
  </foreach>
</select>

<foreach> 元素的属性有:

  • item 表示集合中每一个元素进行迭代时的别名,
  • index 指定一个名字,用于表示在迭代过程中,每次迭代到的位置,
  • open 表示该语句以什么开始,
  • separator 表示在每次进行迭代之间以什么符号作为分隔符,
  • close 表示以什么结束。
  • collection
    当使用可迭代对象或者数组时,index是当前迭代的次数,item的值是本次迭代获取的元素

bind

Multi-db vendor support

动态 SQL 中可插拔的脚本语言

其它

参数

形如 #{id} 的符号为预处理语句参数,通过 JDBC,这样的一个参数在 SQL 中会由一个“?”来标识,并被传递到一个新的预处理语句中

虽然像 MyBatis 的剩余部分一样,参数的 javaType 通常可以从参数对象中来去确定,前提是只要对象不是一个 HashMap参数,但亦可明确指定一个数据类型:

#{id,javaType=int,jdbcType=INTEGER}

也可以指定一个特殊的类型处理器类(或所指定的对应的别名):

#{id,javaType=int,jdbcType=INTEGER,typeHandler=MyTypeHandler}

对于数值类型,还有一个小数保留位数的设置,来确定小数点后保留的位数:

#{height,javaType=double,jdbcType=NUMERIC,numericScale=2}

mode 属性允许你指定 IN,OUT 或 INOUT 参数。如果参数为 OUT 或 INOUT,参数对象属性的真实值将会被改变,就像你在获取输出参数时所期望的那样。如果 mode 为 OUT(或 INOUT),而且 jdbcType 为 CURSOR(也就是 Oracle 的 REFCURSOR),你必须指定一个 resultMap 来映射结果集到参数类型。要注意这里的 javaType 属性是可选的,如果左边的空白是 jdbcType 的 CURSOR 类型,它会自动地被设置为结果集。

#{department, mode=OUT, jdbcType=CURSOR, javaType=ResultSet, resultMap=departmentResultMap}

MyBatis 也支持很多高级的数据类型,比如结构体,但是当注册 out 参数时你必须告诉它语句类型名称

#{middleInitial, mode=OUT, jdbcType=STRUCT, jdbcTypeName=MY_TYPE, resultMap=departmentResultMap}

尽管所有这些强大的选项很多时候你只简单指定属性名,其他的事情 MyBatis 会自己去推断,最多你需要为可能为空的列名指定 jdbcType

#{firstName}
#{middleInitial,jdbcType=VARCHAR}
#{lastName}

定义可重用的 SQL 代码段 —— sql

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

<sql id="userColumns"> 
    ${alias}.id,${alias}.username,${alias}.password 
</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>

属性值可以用于包含的refid属性或者包含的字句里面的属性值

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

JavaBean 属性名与数据库字段的对应 —— 自动映射

当自动映射查询结果时,MyBatis会获取sql返回的列名并在java类中查找相同名字的属性(忽略大小写)

如果没有被手工映射,则将被自动映射。自动映射处理完毕后手工映射才会被处理

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

在 mybatis 的 XML配置文件中的 <settings> 元素有一个 autoMappingBehavior 的设置项,它有三种自动映射等级:

  • NONE - 禁用自动映射。仅设置手动映射属性。
  • PARTIAL - 将自动映射结果除了那些有内部定义内嵌结果映射的(joins).
  • FULL - 自动映射所有。

默认值是 PARTIAL。当使用FULL时,自动映射会在处理join结果时执行,并且join取得若干相同行的不同实体数据,因此这可能导致非预期的映射。
以前面的数据库表 tb_usertb_user_detail 为例,这两张表都有一个字段名为 id

<resultMap id="userResultMap" type="User">
    <association property="userDetail" resultMap="authorResultMap">
    </association>
</resultMap>

<resultMap id="authorResultMap" type="UserDetail">
</resultMap>

<select id="selectUserUsingResultMap" resultMap="userResultMap" parameterType="Integer">
    select 
        A.id,
        A.username,
        A.password,
        B.id,
        B.realname,
        B.address
    from tb_user A
        inner join tb_user_detail B on A.detail_id = B.id
    where A.id = #{id}
</select>

在映射等级为 FULL 的情况下,id 也会映射到 UserDetail 中名为 id 的属性。所以需要谨慎使用 FULL

附录

jdbcType支持的 JDBC 类型

BIT FLOAT CHAR TIMESTAMP OTHER UNDEFINED
TINYINT REAL VARCHAR BINARY BLOB NVARCHAR
SMALLINT DOUBLE LONGVARCHAR VARBINARY CLOB NCHAR
INTEGER NUMERIC DATE LONGVARBINARY BOOLEAN NCLOB
BIGINT DECIMAL TIME NULL CURSOR ARRAY

<constructor> 的子元素 <idArg> & <arg> 属性

属性 描述
column 来自数据库的类名,或重命名的列标签。这和通常传递给 resultSet.getString(columnName)方法的字符串是相同的。
javaType 一个 Java 类的完全限定名,或一个类型别名(参考上面内建类型别名的列表)。 如果你映射到一个 JavaBean,MyBatis 通常可以断定类型。然而,如 果你映射到的是 HashMap,那么你应该明确地指定 javaType 来保证所需的 行为。
jdbcType 在这个表格之前的所支持的 JDBC 类型列表中的类型。JDBC 类型是仅仅 需要对插入, 更新和删除操作可能为空的列进行处理。这是 JDBC 的需要, jdbcType 而不是 MyBatis 的。如果你直接使用 JDBC 编程,你需要指定这个类型-但 仅仅对可能为空的值。
typeHandler 我们在前面讨论过默认的类型处理器。使用这个属性,你可以覆盖默认的 类型处理器。 这个属性值是类的完全限定名或者是一个类型处理器的实现, 或者是类型别名。
select 用于加载复杂类型属性的映射语句的ID,从column中检索出来的数据,将作为此select语句的参数。具体请参考Association标签。
resultMap ResultMap的ID,可以将嵌套的结果集映射到一个合适的对象树中,功能和select属性相似,它可以实现将多表连接操作的结果映射成一个单一的ResultSet。这样的ResultSet将会将包含重复或部分数据重复的结果集正确的映射到嵌套的对象树中。为了实现它, MyBatis允许你 “串联” ResultMap,以便解决嵌套结果集的问题。想了解更多内容,请参考下面的Association元素。
name 构造器参数的名字. 使用 name 属性允许你可不用按构造方法顺序放置子元素

子查询与联合查询的比较

联合查询:一次性查询,占用资源较大
子查询的问题就是 “N+1 查询问题”。概括地讲,N+1 查询问题可以是这样引起的:

  • 你执行了一个单独的 SQL 语句来获取结果列表(就是“+1”)。
  • 对返回的每条记录,你执行了一个查询语句来为每个加载细节(就是“N”)

但是 MyBatis 的延迟加载在不需要迅速迭代来访问嵌套的数据时可以分散这些语句同时运行的消耗

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 203,098评论 5 476
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,213评论 2 380
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 149,960评论 0 336
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,519评论 1 273
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,512评论 5 364
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,533评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,914评论 3 395
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,574评论 0 256
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,804评论 1 296
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,563评论 2 319
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,644评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,350评论 4 318
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,933评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,908评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,146评论 1 259
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 42,847评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,361评论 2 342

推荐阅读更多精彩内容