深入浅出MyBatis技术原理与实战-学习-源码解析-MyBatis 映射器(二)

映射器

        映射器是 MyBatis 最强大的工具,也是我们使用 MyBatis 时用得最多的工具,因此熟练掌握它十分重要,MyBatis 是针对映射器构造的 SQL 构建轻量级构架,并且通过配置生成对应的 JavaBean 返回给调用者,而这些配置主要便是映射器,在 MyBatis 中你可以根据情况定义动态 SQL 来满足不同场景 的需要,它比其他的框架灵活得多,MyBatis 还支持自动绑定 JavaBean,我们只要让 SQL返回的字段名和 javaBean属性名保持一致,或者采用驼峰命名,便可以省掉这些元繁琐的映射配置。
        首先让我们明确在映射器中我们可以定义哪些元素,它们的作用是什么?

元素名称描述备注
select查询语句 ,最常用,最复杂的元素之一可以自定义参数,返回结果集等
insert插入语句执行后返回一个整数,代表插入的条数
update更新语句执行后返回一个整数,代表更新的条数
delete删除语句执行后返回一个整数,代表删除条数
parameterMap定义参数映射关系即将被删除的元素,不建义大家使用
sql允许定义一部分 SQL,然后在各个地方引用它例如,一张表列名,我们可以一次定义,在多个 SQL 语句中使用
resultMap用来描述从数据库结果集中来加载对象,它是最复杂,最强大的元素它将提供映射规则
cache给定命名空间的缓存配置----
cache-ref其他命名空间缓存配置的引用
select元素

        毫无疑问,select 元素是我们最常用也是功能最强大的 SQL 语言,select元素帮助我们从数据库中读取数据,组装数据给业务人员,执行 select 语句前,我们需要定义参数,它可以是一个简单的参数类型,例如 int,float,String,也可以是一个复杂的参数类型,例如 javaBean,Map 等,这些都是 MyBatis 接受的参数类型,执行 SQL后,MyBatis 也提供了强大的映射规则,甚至是自动映射来帮助我们把返回结果集绑定到 JavaBean 中,select 元素配置如下表所示

元素说明备注
id它和 Mapper 的命名空间组合起来是唯一的,提供给 MyBatis 调用命名空间和 id 组合起来不唯一,MyBatis 将抛出异常
parameterType你可以给出类的全命名,也可以给出类的别名,但是使用别名必需 MyBatis 内部定义或者自定义我们可以选择 JavaBean,Map 等复杂的参数类型传递给 SQL
parameterMap即将废弃的元素,我们不再讨论它
resultType定义类的全路径,在自动匹配的情况下,结果集将通过 javaBean 的规范映射或定义为 int,double,float等参数,也可以使用别名,但是要符合别名的规范,不能和 resultMap 同时使用它是我们常用的参数之一,比如我们统计总条数就可以把它的值设置为 int
resultMap它是映射集的引用,将执行强大的映射功能,我也可以以使用 resultType 或者 resultMap 其中的一个,resultMap 可以给予我们自定义的映射规则的机会它是 MyBatis 最复杂的元素之一,可以配置映射规则,级联,TypeHandler
flushCache它的作用是调用 SQL 后,是否要求 MyBatis 清空之前的查询本地缓存和二级缓存取值为 boolean,true/false,默认为 false
useCache启动二级缓存的开关,是否要求 MyBatis 将此次结果缓存取值为 boolean,true/false,默认为 true
timeout设置超时参数,等超时的时候抛出异常,单位为秒默认值是数据库厂商提供的 JDBC 驱动所设置的秒数
fetchSize获取记录的总条数设定默认值是数据库厂商提供的 JDBC 驱动所设置的条数
statementType告诉 MyBatis 使用哪个 JDBC 的 Statement 工作,取值为 STATEMENT(Statement),PREPARED(PreparedStatement) ,CallableStatement默认值为 PREPARED
resultSetType这是对 JDBC的 resultSet 接口而言,它的值包括 FORWARD_ONLY(游标允许向前访问),SCROLL_SENSITIVE(双向滚动,但不及时更新,就是如果数据库里的数据修改过,并不在 resultSet 中反应出来),SCROLL_INSTNSITIVE(双向滚动,并及时跟踪数据库更新,以便更改 resultSet 中的数据)默认值是数据库厂商提供的 JDBC驱动所设置的
databaseId它的使用请参考第3章databaseIdProvider 数据库厂商标识这部分内容提供多种数据库的支持
resultOrdered这个设置仅适用于嵌套结果集 select 语句,如果为 true,就是假设包含了嵌套结果集或者是分组了,当返回一个主结果行的时候,就不能对前面的结果结构的引用,这就确保了在获取嵌套结果集的时候不至于导致内存不够用取值为布尔值,true/false,默认值为 false
resultSet适合于多个结果集的情况,它将列出执行的 sql 后每个结果集的名称,每个名称间用逗号分隔很少使用

如下:

<select id="getUser" resultType="User" parameterType="java.lang.Long">
    select * from lz_user where id=#{id}
</select>

这样就可以使用 MyBatis 调用 SQL 了,十分简单,下面对操作步骤进行归纳概括。

  • id 标出这条 SQL
  • parameterType 定义参数类型
  • resultType 定义返回值类型

传递多个参数
更多的时候,我们需要传递多个参数给映射器,如果我们需要多个参数来查询 。

<select id="getUser" resultType="User" parameterType="map">
    select * from lz_user where id=#{id} and username="#{username}"
</select>

        这个方法虽然简单易用,但是有一个弊端:这样设置的参数使用了 Map,而 Map 需要键值对应,由于业务关联不强,你需要深入程序看代码,造成了可读性下降,MyBatis 为我们提供了更好的实现方式,它就是注解参数的形式,让我们来看看如何实现。

public User getUser(@Param("id")Long id ,@Param("username") String username);
//把映射的 XML 修改为无需定义参数类型
<select id="getUser" resultType="User" >
    select * from lz_user where id=#{id} and username="#{username}"
</select>

        当我们把参数传递给后台的时候,通过@Param 提供的名称 MyBatis 就会知道#{id}代表着 id,#{username} 代表着 username,参数的可读性大大提高,但是这引发了另一个麻烦,一个 SQL如果要有10个参数需要查询 ,如果我们用@Param方式,那么参数将十分得要,可读性依旧不高,不过 MyBatis 为我们提供了 JavaBean 的方式来解析这个问题 。

使用 javaBean 传递参数

        在参数过多的情况下,MyBatis 允许组织一个 JavaBean,通过简单的 setter 和 getter方法设置参数,这样就可以使得可读性变高了。

@Data
public Class UserParam{
    private Long id ;
    private String username;
}
public User getUser(UserParam user);
//把映射的 XML 修改为无需定义参数类型
<select id="getUser" resultType="User" >
    select * from lz_user where id=#{id} and username="#{username}"
</select>

总结:我们描述了3种传递参数的方式,下面对各种方式加以点评和总结,以利于我们在实际操作中应用。

  • 使用 Map 传递参数,因为 Map导致业务可读性的丧失,从而导致后续扩展和维护的困难,我们应该在实际应用中果断废弃这样的参数传递参数的方式。
  • 使用@Param 注解传递多个参数,这种方式的使用受到了参数个数(n) 的影响,当 n <= 5时,它最佳的传参方式,它比 JavaBean更好,因为它更加直观,当 n > 5时,多个参数将给调用带来困难。
  • 当参数的个数多于5个时,建义使用 JavaBean 的方式。

使用 resultMap 映射结果集
在某个时候,我们需要处理更加复杂的结果集,resultMap 为我们提供了这样的模式,我们需要映射器定义 resultMap,这也是我们常见的场景。

<resultMap id="BaseResultMap" type="com.spring_101_200.test_131_140.test_132_mybatis_typehandlers.User">
    <id column="id" property="id"/>
    <result column="is_delete" property="isDelete"/>
    <result column="gmt_create" property="gmtCreate"/>
</resultMap>

<select id="getUserByMap" resultMap="BaseResultMap">
    select * from lz_user where id=#{id}
</select>

解释一下 resultMap 的配置。

  • 定义了一个唯一标识(id) 为BaseResultMap的 resultMap,用 type 属性去定义它对应哪个 JavaBean(也可以使用别名)
  • 通过 id 元素定义 BaseResultMap,这个对象代表着使用哪个属性作为其主键,result 元素的普通列的映射关系,例如将 SQL 结果返回的列 id,is_delete返回给定义的 javaBean的 id 和 isDelete。
  • 这样,select 语句就不再需要使用自动映射的规则,直接使用 resultMap 属性指定BaseResultMap即可,这样 MyBatis 就会使用我们自定义映射规则 。

        resultMap 可没有你看到的这么简单,它是映射器中最为复杂的元素,它一般用于得复杂,级联这些关联的配置,在简单的情况下,我们可以使用 resultType 通过自动映射来完成,这样配置的工作量会大大减少,未来随着进一步的学习深入,我们还会讨论 resultMap 的高级应用。

insert 元素

insert元素,相对于 select 元素而言要简单得多,MyBatis 会在执行插入之后返回一个整数,以表示你进行操作后插入的记录数。insert 元素配置详解 。

属性名称描述备注
id它和 Mapper 的命名空间组合起来唯一的,作为唯一的标识提供给 MyBatis 调用如不唯一,MyBatis 将抛出异常
parameterType你可以给出类的全命名,也是一个别名,但是使用别名必需是 MyBatis 内部定义或者自定义别名,定义方法可以参考typeAlias 元素讲解我们可以选择 JavaBean,Map 等参数类型传递给 SQL
parameterMap即将废弃元素
flushCache它的作用是在调用 SQL 后,是否要求 MyBatis 清空之前的查询的本地缓存和二级缓存取 boolean 值,true/false,默认为 false
timeout设置超时参数,等超时的时候将抛出异常,单位为秒默认值是数据加厂商提供的 JDBC 驱动设置的秒数
statementType告诉 MyBatis 使用哪个 JDBC 的 Statement 工作,取值为 STATEMENT(statement),PREPARED(PreparedStatement) 和 CallableStatement默认为 PREPARED
keyProperty表示以哪个列作为属性的主键,不能和 keyProperty 同时使用设置哪个列为主键,如果是联合主键可以用逗号将其隔开
useGeneratedKeys这会命令 MyBatis 使用 JDBC的 getGeneratedKeys 方法来取出数据库内部生成的主键,例如,MySQL和 SQL Server 自动递增字段,Oracle 的序列等,但是使用它就必需要给 keyProperty 或 KeyColumn 赋值取值为 boolean,默认为 false
keyColumn指明第几列是主键,不能和 keyProperty 同时使用,只接受整形参数和 keyProperty 一样联合主键可以逗号隔开
databaseId请参考上一篇博客databaseIdProvider 数据库厂商标识这部分内容提供了多种数据库支持
lang自定义语言,可使用第三方语言,使用得较少

        虽然元素也不少,但是我们实际操作中常用的元素只有几个,并不是很难撑握,下面来看看:

<insert id="insertUser" parameterType="User">
     INSERT INTO lz_user (username, password, real_name, manager_id) VALUES (#{username},#{password},#{realName},#{managerId})
  </insert>
主键回填和自定义

        现实中还有许多我们需要处理的问题,主键自增字段,MySQL 里面主要需要根据一些特殊的规则去生成,在插入后,我们往往需要获取这个主键,以便于未来操作,而 MyBatis 提供了实现的方法。
        首先我们可以使用 keyProperty 属性指定哪个是主键字段,同时使用 useGeneratedKeys告诉 MyBatis这个主键是否使用数据库内置策略生成。

<insert id="insertUser" parameterType="User" useGeneratedKeys="true" keyProperty="id" >
    insert into lz_user(
        <if test="username != null">username, </if>
        <if test="password != null">password, </if>
        <if test="realName != null">real_name, </if>
        <if test="managerId != null">manager_id, </if>
        <if test="sex != null">sex, </if>
        <if test="sexStr != null">sex_str, </if>
        is_delete,
        gmt_create,
        gmt_modified
    )values(
        <if test="username != null">#{ username}, </if>
        <if test="password != null">#{ password}, </if>
        <if test="realName != null">#{ realName}, </if>
        <if test="managerId != null">#{ managerId}, </if>
        <if test="sex != null">#{ sex}, </if>
        <if test="sexStr != null">#{ sexStr}, </if>
        0,
        now(),
        now()
    )
</insert>

        这样我们传入的 user 对象无需设置 id的值,MyBatis 会用数据库设置进行处理,这样好处就是 MyBatis 插入的时候,它会回填 JavaBean 的 id,我们进行调试,在插入之后,它会自动填充主键,方便以后使用。

        在实际工作中,往往不是我们想像的那样简单,需要根据一些特殊的关系设置主键 id 的值,假设我们取消 id 的自增规则,我们要求,如果lz_user 表中没有记录,则我们需要将 id=1 ,否则,我们就取最大的 id加2,来设置新的主键,对于一些特殊的要求,MyBatis 也给我们提供了对应办法。

<insert id="insertUserIdAddDouble" parameterType="User" useGeneratedKeys="true" keyProperty="id" >

    <selectKey keyProperty="id" resultType="long" order="BEFORE">
        select if(max(id) is null ,1 ,max(id) + 2 ) as newId from lz_user
    </selectKey>

    insert into lz_user(
    <if test="id != null">id, </if>
    <if test="username != null">username, </if>
    <if test="password != null">password, </if>
    <if test="realName != null">real_name, </if>
    <if test="managerId != null">manager_id, </if>
    is_delete,
    gmt_create,
    gmt_modified
    )values(
    <if test="id != null">#{ id}, </if>
    <if test="username != null">#{ username}, </if>
    <if test="password != null">#{ password}, </if>
    <if test="realName != null">#{ realName}, </if>
    <if test="managerId != null">#{ managerId}, </if>
    0,
    now(),
    now()
    )
</insert>

需要注意的是,上面id 列必需写。这样,我们就能定义自己的规则来生成主键了,MyBatis 的灵活性也得到体现。

update 元素和 delete 元素

        这两个元素比较简单,所以我们讨论,和 insert 元素一样,MyBatis 执行完 update 元素和 delete 元素后返回一个整数,标出执行后影响的记录条数。

<update id="updateUserById" parameterType="User" >
    update
        lz_user
    <trim prefix="set" suffixOverrides=",">
        <if test="isDelete != null">is_delete = #{isDelete},</if>
        <if test="gmtCreate != null">gmt_create = #{gmtCreate},</if>
        <if test="username != null">username = #{username},</if>
        <if test="password != null">password = #{password},</if>
        <if test="realName != null">real_name = #{realName},</if>
        <if test="managerId != null">manager_id = #{managerId},</if>
        <if test="sex != null">sex = #{sex},</if>
        <if test="sexStr != null">sex_str = #{sexStr}</if>                
    </trim>
    ,gmt_modified = now()
    where id = #{id}
</update>

<update id="deleteUserById" parameterType="java.lang.Long">
    update lz_user set is_delete = 1 where id=#{id} limit 1  
</update>

        我们所面对的大部分场景都和这个相似,需要通过一个 JavaBean 插入一张表的记录或者根据主键删除记录,对于参数传递可参数 select 元素传递参数的例子,插入和删除执行完成 MyBatis 会返回一个整数显示更新或者删除几条记录。

参数

        正如你所看到的,我们可以传入一个简单的参数,比如 int,double等,也可以传入 JavaBean,这些我们都讨论,有时候我们需要处理一些特殊的情况,我们可以指定特定的类型,以确定使用哪个 typeHandler 处理它们,以便我们进行特殊的处理。
        #{age,javaType=int,javaType=NUMBERIC}
当我们还可以指定用哪个 typeHandler 去处理参数
        #{age,javaType=int,javaType=NUMBERIC,TypeHandler=MyTypeHandler}
此外,我们还可以对一些数值的参数设置其保存的精度 。
        #{price,javaType=double,jdbcType=NUMBERIC,numbericScale=2}
可见,MyBatis 映射器可以通过 EL的功能帮助我们所需要的多种功能,使用还是很方便的。

存储过程支持

        对于存储过程而言,存在3种参数,输入参数(IN),输出参数(OUT),输入输出参数(INOUT),MyBatis 的参数规则为其提供了良好的支持,我们通过制定 mode 属性来确定是哪一种参数,它的选项有3种:IN,OUT,INOUT,当参数设置的 OUT 或者 INOUT 的时候,正如你所希望的一样,MyBatis 会将存储过程返回的结果设置到你参数中,当你返回一个游标(也就是我们制定的 JDBCTYPE=CURSOR) 的时候,你还需要去设置 resultMap 以便 MyBatis 将存储过程的参数映射到对应的类型,这时,MyBatis 就会通过你所设置的 resultMap 自动为你设置映射结果
        #{user,mode=OUT,jdbcType=CURSOR,javaType=ResultSet,resultMap=userResultMap}
这里的 javaType 是可选的,即使你不指定它,MyBatis 也会自动检测它。
        MyBatis 还支持一些高级特性,比如我们说结构体,但是当你注册参数的时候,你就会需要去指定语句的名称(JavaTypeName),比方说下面的用法。
        #{user,mode=OUT,jdbcType=STRUCT,javaTypeName=MY_TYPE,resultMap=duserResultMap}
        在大部分情况下MyBatis 都会去推断你返回的数据类型,所以大部分的情况下,你都无法去配置参数类型和结果类型,只是可能返回为空的字段类型而已,因为 null 值,MyBatis 无法判断其类型。
        #{gmtCreate},#{id},#{realName},#{username},#{password}
        对于备注而言,可能返回值是空的,用 javaType=VARCHAR 明确告知 MyBatis,让他被 StringTypeHandler 处理即可。
        这里我们暂时不给出调试过程和方法,我们会在后面来进行使用。

特殊字符串替换处理(#和$)

        在 MyBatis 中,我们常常传递字符串,我们设置的参数#{name}在大部分的情况下MyBatis 会用创建预编译的语句,然后 MyBatis 为它设置值,而我们有个时候我们需要的是传递 Sql 语句的本身,而不是 SQL 所需要的参数,例如,在一些动态表格(有时候经常遇到根据不同的条件产生不同的动态列)中,我们要传递 SQL的列名,根据某些列进行排序 ,或者传递给列名 SQL 都是比较常见的场景,当然 MyBatis 也对这样的场景进行支持,这些都是 Hibernate难以做到的。
        例如:在程序中传递变量 columns="col1,col2,col3…"给 sql,让其组装成 SQL 语句,我们当然不想被 MyBatis 像处理普通参数一样把它设置成"col1,col2,col3…"那么 们就可以写成下面这样
        select ${columns} from lz_user

       这样 MyBatis 就不会帮我们转译 columns,而变直出,而不是作为SQL 的参数进行设置了,只有这样对 SQL 直言是不安全的,MyBatis 给你灵活性的同时也需要你自己去控制参数以保证 SQL 运转的正确性和安全性。

       SQL 元素的意义,在于我们可以定义一串 SQL 语句的组成部分,其他的语句可以通过引用来使用它,例如,你有一个 SQL 需要 select 几十个字段映射到 JavaBean 中去,我的第二条 SQL 也是这几十个字段映射到 JavaBean 中去,显然这些字段写两遍不太合适,那么就用 SQL 元素来完成,例如插入角色,查询角色列表就可以这样定义

<sql id="role_columns" >
    id,role_name,note
</sql>

<select parameterType="long" id="getUser" resultMap="userMap">
    select <include refid="role_columns"/> from t_user where id = #{id}
</select>

       这里我们用 sql 元素定义了 role_columns,它可以很方便的使用 include 元素的 refid属性进行引用,从而达到重用的功能。

< id="role_columns" >
    #{prefix}.id,#{prefix}.role_name,#{prefix}.note
</sql>

<select parameterType="long" id="getUser" resultMap="userMap">
    select <include refid="role_columns"> 
    		<property name="prefix" value="r"/>
    	</include>
    from t_user where id = #{id}
</select>

这样就可以给 MyBatis 入参,我们还可以这样给 refid 一个参数值,由程序制定的引入 SQL
<sql id=“someinclude”>
       select * from <indeclude refid="${tableName}"/>
</sql>
这样就可以实现一处定义多处引用,大大减少了工作量。

resultMap 结果集映射集

       元素里还以以下的元素。

<resultMap>
    <constructor>
        <idArg/>
        <arg/>
    </constructor>
    <id />
    <result />
    <association />
    <collection />
    <discriminator >
        <case />
    </discriminator>
</resultMap>

其中 constructor 元素用于配置构造方法,一个 POJO 可能不存在没有参数的构造方法。
这个时候,我们就可以使用 constructor 进行配置,假设角色 UserBean 不存在没有参数的构造方法,它的构造方法声明为 public User(int id ,String username),那么我们需要配置这个结果集

<resultMap ...>
    <constructor>
        <idArg column="id" javaType="int"/>
        <arg column="username" javaType="String"/>
    </constructor>
....
</resultMap>

       这样,MyBatis 就知道需要用这个构造方法来构造 POJO 了
       id 元素表示哪个列是主键,允许多个主键,多个主键则称为联合主键,result 是配置 POJO到 SQL列名的映射关系,这里的 result 和 id 两个元素都有如下表属性。

元素名称说明备注
property映射到列结果字段或属性,如果 POJO的属性匹配是存在的,和给定 SQL列名(column 元素)相同的,那么 MyBatis 就会映射到 POJO 上可以使用导航式的字段,比如访问一个学生对象(Student) 需要访问学生证(selfcard) 的发生日期(issueDate),那么我们可以写成 selfcard.issueDate
column这里对应的是 SQL的列
javaType配置 java 的类型可以特定的类完全限定名或者 MyBatis 上下文的别名
jdbcType配置数据库类型这是一个 jdbc 的类型,MyBatis 己经为我们做了限定,基本支持所有常用的数据库
typeHandler配置数据库类型这是一个 jdbc 的类型,myBatis 己经为我们做了限定,基本支持所有常用的数据库类型
typeHandler类型处理器允许你用特定的处理器来覆盖 MyBatis 默认的处理器,这就是制定 jdbcType 和 javaType 相系转化的规则

此外有 association,collection和 discriminator 这些元素,我们将在级联那里详细讨论它们的用法。

使用 map 存储结果集

一般而言,任何的 select 语句都使用 map 存储。
<select id=“findColorByNote” parameterType=“string” resultType=“map”>
        select * from lz_user where id = #{username}
</select>
        使用 map原则上是可以匹配所有的结果集的,但是使用 map 接口就意味着可读性下降,所以这不是一种推荐方式,更多的时候,我们使用 PO的方式。

使用 POJO存储结果集

        使用 map 方式就意味着可读性的丢失,POJO 是我们最常用的方式,也是我们推荐的方式,一方面我们可以使用自动映射,正如select 语句里讨论的一样,我们还可以使用 select 语句的属性 resultMap 配置映射集合,只是使用前需要配置类似的 resultMap。

<resultMap id="BaseResultMap" type="com.sina.model.entity.user.User">
    <id column="id" property="id"/>
    <result column="is_delete" property="isDelete"/>
    <result column="gmt_create" property="gmtCreate"/>
    <result column="gmt_modified" property="gmtModified"/>
    <result column="username" property="username"/>
    <result column="password" property="password"/>
    <result column="real_name" property="realName"/>
    <result column="manager_id" property="managerId"/>
    <result column="sex" property="sex"/>
    <result column="sex_str" property="sexStr"/>
</resultMap>

        resultMap 元素的属性 id 代表着这个 resultMap 的标识,type 代表着你需要映射的 POJO,我们可以使用 MyBatis 定义好的类的别名,也可以使用自定义类的全限定名。
        映射关系中,id 元素表示这个对象的主键,property 代表着 POJO 的属性名称,column 表示数据库中列名,于是 POJO就和数据库中 SQL的结果一一对应起来,接着我们的映射文件中的 select 元素里这样写,就可以使用了。

<select id="selectUserById" resultMap="resultMap" >
    select * from lz_user  where id=#{id} and is_delete = 0 limit 1 
</select>

        我们可以发现 SQL 语句的列名resultMap 是 column 是一一对应的,使用 XML配置结果集,我们可以配置 typeHandler,javaType,jdbcType,但是这条语句中配置了 resultMap 就不需要配置 resultType了。

级联

        在数据库中包含着一对多,一对一的关系,通过用户信息查找账单信息。
        select * from lz_user lu left join lz_user_bill lub on lu.id =lub.user_id where lu.id =456
        这里是将用户信息和账单信息都查询出来,我们也希望用户信息中多一个属性,List<UserBill> billList 这样就可以取出用户的时候,同时将账单信息取出 。我们将这种关系叫做级联。
        在级联中存在3种对应关系,其一,一对多的关系,如用户也账单的关系,举个例子,一家软件公司存在着许多软件工程师,公司和软件工程师就是一对一的关系,其二,一对一的关系,例如每个软件工程师都有一个编号(ID),这是它是软件保护公司的标识,它与工程师是一对一的关系,其三,多对多的关系,例如,有些公司一个角色可以对应多个用户,但是一个用户也可以兼任多个角色,通俗而言,一个人可以既是总经理,同是也是技术总监,而技术总监这个职位可以对应很多人,这就是多对多的关系。
        在实际中,多对多的关系应用不多,因为它复杂,会增加理解和关联的复杂度,推荐的方法是,用一对一关系,把它分解为双向关系,以降低关系的复杂度,简化程序,有时候,我们也需要鉴别关系,比如我们去体检,男女有别,男性和女性的体检项目并不完全一样,如果让男性去体检妇科项目,就会闹出笑话。
        所在在MyBatis 中级分为3种,association,collection和 discriminator,下面分别介绍 。

  • association ,表示一对一关系,比如中国公民和身份证是一对一关系。
  • collection,代表着一对多的关系,比如班级和学生是一对多的关系,一个班级可以有多个学生。
  • discriminator,是鉴别器,它可以根据实际选择采用哪个类作为实例,允许你根据特定的条件去关联不同的结果集,比如人有男人和女人,你可以去实例化一个人对象,但是要根据情况用男人或者用女人去实例化。

association一对一级联
在实际操作中,我需要从账单中查找到用户信息。
1.准备数据库表
CREATE TABLE lz_user (
        id bigint(20) unsigned NOT NULL AUTO_INCREMENT,
        is_delete tinyint(2) DEFAULT ‘0’,
        gmt_create datetime DEFAULT CURRENT_TIMESTAMP COMMENT ‘创建时间’,
        gmt_modified datetime DEFAULT CURRENT_TIMESTAMP,
        username varchar(32) DEFAULT NULL COMMENT ‘用户名’,
        password varchar(64) DEFAULT NULL COMMENT ‘密码’,
        real_name varchar(64) DEFAULT NULL,
        manager_id int(11) DEFAULT NULL COMMENT ‘管理员id’,
        sex int(11) DEFAULT ‘1’ COMMENT ‘性别’,
        sex_str varchar(32) DEFAULT NULL,
        PRIMARY KEY (id)
) ENGINE=InnoDB AUTO_INCREMENT=473 DEFAULT CHARSET=utf8mb4 COMMENT=‘公益口罩’;

准备账单表
CREATE TABLE lz_user_bill (
        id bigint(20) unsigned NOT NULL AUTO_INCREMENT,
        is_delete tinyint(2) DEFAULT ‘0’,
        gmt_create datetime DEFAULT CURRENT_TIMESTAMP COMMENT ‘创建时间’,
        gmt_modified datetime DEFAULT CURRENT_TIMESTAMP,
        type varchar(32) DEFAULT ‘-’ COMMENT ‘收支类型’,
        user_id int(11) DEFAULT NULL COMMENT ‘用户id’,
        manager_id int(11) DEFAULT NULL COMMENT ‘管理员id’,
        amount decimal(12,2) DEFAULT NULL,
        remark text COMMENT ‘备注’,
        bill_type varchar(256) DEFAULT NULL COMMENT ‘账单类型’,
        pay_type varchar(255) DEFAULT NULL COMMENT ‘支付方式’,
        status int(11) DEFAULT ‘0’ COMMENT ‘-1表示作费,0表示提交,1表示已经报销’,
        self_look int(11) DEFAULT ‘0’ COMMENT ‘0表示公开,1表示仅仅自己可见’,
        PRIMARY KEY (id)
) ENGINE=InnoDB AUTO_INCREMENT=62 DEFAULT CHARSET=utf8mb4 COMMENT=‘公益口罩’;

  1. 准备UserMapper.xml
<resultMap type="com.spring_101_200.test_131_140.test_133_mybatis_lazyloadingenabled_aggressivelazyloading.UserBill" id="ordersUserLazyLoading">
    <!---对订单信息进行映射配置->
    <id column="id" property="id"/>
    <result column="user_id" property="userId"/>
    <result column="amount" property="amount"/>
    <result column="type" property="type"/>
    <association property="user" javaType="com.spring_101_200.test_131_140.test_133_mybatis_lazyloadingenabled_aggressivelazyloading.User" select="findUserById" column="user_id"/>
</resultMap>

<!--查询订单关联用户-->
<select id="findUserBillLazyLoading"  parameterType="java.lang.Long"  resultMap="ordersUserLazyLoading">
   select * from lz_user_bill where id = #{id}
</select>

<!--根据Id查询用户-->
<select id="findUserById" parameterType="java.lang.Long"  resultMap="userBillResult">
    select * from lz_user lu   where lu.id =#{id}
</select>

请看上面加粗的代码,这是通过一次关联来处理问题,其中 select 元素指定 SQL 去查询,而 column 则是指定传递给 select 语句的参数,这里就是Bill 里的 user_id,当取出 UserBill 的时候,MyBatis 就会知道用下面的 SQL 取出我们需要的级联信息。

  1. 准备 POJO
@Data
public class UserBill {
    private Long id;
    private String type;
    private Long userId;
    private BigDecimal amount;
    private User user;
}
  1. 测试:
    在这里插入图片描述
            我们可以看到整个过程,先根据 user_bill 信息查找出 userBill 信息,然后再根据 user_id 查找出 user 信息,因为 user中又关联了 user_bill 信息,因些再次查询,这个,我们在后面来讲。
collection 一对多级联

        一个用户有多条账单,一条账单只对应一个用户,这就是一对多关系,在 MyBatis 中,是如何来取出一对多关联数据的呢?因为数据库表结果上面己经提到过了,这里就不再重复。

  1. 准备UserMapper.xml
<resultMap id="userBillResult" type="UserBillInfo">
    <id property="id" column="id" />
    <collection property="billList" ofType="Bill" >
        <id property="id" column="id" />
        <result property="type" column="type"></result>
        <result property="userId" column="user_id"></result>
        <result property="amount" column="amount"></result>
    </collection>
</resultMap>

<select id="selectUserBill"  parameterType="java.lang.Long" resultMap="userBillResult">
    select * from lz_user lu  left outer join lz_user_bill lub on lu.id =lub.user_id where lu.id =#{id}
</select>
  1. 准备 POJO
@Data
public class UserBillInfo {
    private Long id;
    private List billList;
}
  1. 测试
public void testGetUser() throws Exception {
    SqlSession sqlSession = sqlSessionFactory.openSession();

    UserMapper userMapper = sqlSession.getMapper(UserMapper.class);
    UserBillInfo userBillInfo = userMapper.selectUserBill(456l);
    System.out.println(JSON.toJSONString(userBillInfo));
}

4.结果
在这里插入图片描述
我们打印出了用户 id,同时根据用户 id 获取账单信息。

discriminator 鉴别器级联

        鉴别器级联是在特定的条件下去使用不同的 POJO,本例中主要是为了测试,如果账单信息的 is_delete等于1,取出用户信息,看是谁的账单信息被删除了。还是数据库表不做重复说明。

  1. 准备UserMapper.xml
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.spring_101_200.test_141_150.test_146_mybatis_discriminator.UserMapper">

    <resultMap type="com.spring_101_200.test_141_150.test_146_mybatis_discriminator.UserBill" id="ordersUserLazyLoading">
        <id column="id" property="id"/>
        <result column="user_id" property="userId"/>
        <result column="amount" property="amount"/>
        <result column="type" property="type"/>
        <discriminator javaType="int" column="is_delete">
            <case value="1" resultType="com.spring_101_200.test_141_150.test_146_mybatis_discriminator.UserBill">
                <association property="user" javaType="User" select="findUserById" column="user_id"/>
            </case>
        </discriminator>
    </resultMap>

    <select id="findUserBillLazyLoading"  parameterType="java.lang.Long"  resultMap="ordersUserLazyLoading">
       select * from lz_user_bill where id = #{id}
    </select>

    <select id="findUserById" parameterType="java.lang.Long"  resultType="User">
        select * from lz_user lu   where lu.id =#{id}
    </select>

</mapper>
  1. 准备 POJO
@Data
public class UserBill {
    private Long id;
    private Integer isDelete;
    private String type;
    private Long userId;
    private BigDecimal amount;
    private User user;
}
  1. 开始测试
public void testGetUser() throws Exception {
    SqlSession sqlSession = sqlSessionFactory.openSession();
    UserMapper userMapper = sqlSession.getMapper(UserMapper.class);
    UserBill user = userMapper.findUserBillLazyLoading(60l);
    System.out.println(JSON.toJSONString(user));
}

测试结果,当账单信息被删除时,打印出用户信息。
在这里插入图片描述

性能分析和 N+1问题

        级联优势是能够方便快捷的获取数据,比如学生和学生成绩信息往往是最常用的关联信息,这个时候级联是完全有必要的,多层关联时,建义不超过三层关联时尽量少用级联,因为不仅用处不大,而且会造成复杂度增加,不利于他从的理解和维护,同时级联时也存在一些劣势,有时候我们并不需要获取所有的数据,例如,我们对学生课程和成绩感兴趣,我就不用取出学生证和健康情况了,因为取出学生证和健康情况表不但没有意义,而且会多执行几条 SQL ,导致性能下降,我们可以使用代码去取他们,而不需要每次都加载。
        级联还有更加严重的问题,假设有表关联到 User 表,那么可以想象,我们还要增加级联关系到结果集里,那么级联关系就会变得更加复杂,如果我们采用类似默认的场景,那么一个关联我们就要多执行一次 SQL,正如我们上面的例子一样,每次取一个 User对象,那么它所有的信息将被取出来,这样会造成 SQL 执行过多而导致性能下降,这就是 N+1 问题,为了解决这个问题,我们应该考虑使用延迟加载的功能。在上一篇博客中,我己经对延迟加载做了详细的分析了,这里就不再做过多的赘述。
        之前讲的是全局设置,但是还是不太灵活的,为什么呢?因为我们不能指定哪些属性可以立即加载,哪些属性可以延迟加载,当一个功能的两个对象经常要一起使用时,我们采用即时加载更好,因为即时加载可以多条 SQL一次性发送,性能高,例如,学生和学生课程成绩,当遇到类似于健康和学生证的情况时,则用延迟加载好些,因为健康和学生请表可能不需要经常访问,这样,我们就要修改 MyBatis 的全局默认的延迟加载功能,不过不必担心,MyBatis 可以很容易的解决这些问题,因为它也有局部延迟加载的功能,我们可以在 association 和 collection元素上加入属性值 fetchType 就可了,它有两个取值范围,即 eager 和 lazy,它的默认值取决于你在配置文件中 setting 的配置,假如我们没有配置它,那么它们就是 eager,一旦你配置了它们,那么全局的变量就会被它们所覆盖,这样我们就可以灵活的指定哪些东西可以延迟加载,哪些东西不需要延迟加载,很灵活。

        也许有人对延迟加载感兴趣,延迟加载的实现原理是通过动态代理来实现的,在默认情况下,MyBatis在3.3或者以上的版本时,采用的是 JAVASSIST 的动态代理,低版本用的是 CGLIB,当然,你可以使用配置修改,有兴趣的同学可以去研究一下动态代理的相关内容,它会生成一个动态代理对象,里面保存着相关的 SQL 和参数,一旦我们使用这个代理对象的方法时,它会进入动态代理对象的代理方法里,方法里面会通过发送 SQL 和参数,就可以把对应的结果从数据库中查找出来。这便是其实现原理。

缓存cache

        缓存是互联网系统常常用到的,其特点就是将数据保存到内存中,目前流行的缓存服务器有 MongoDB ,Redis ,EHcache 等,缓存是计算机内存中保存数据 ,在读取的时候无需从磁盘中读入,因此具备快速读取和使用的特点,如果缓存例中率高,那么就可以极大的提高系统的性能,如果缓存命中率低,那么缓存就没有意义了,所以使用缓存的关键在于存储内容访问的命中率。

系统缓存 (一级缓存和二级缓存)

        MyBatis 对缓存的提供支持,但是没有配置默认的情况下,它只开启一级缓存(一级缓存只是相对于同一个 SqlSession 而言)
        所以在参数和 SQL 完全一样的情况下,我们使用同一个 SqlSession 对象调用同一个 Mapper 的方法,往往只执行一次 SQL,因为使用 SqlSession 第一次查询后,MyBatis 会将其放入缓存中,以后再查询的时候,如果没有声明需要刷新缓存,并且缓存没有超时的情况下,SqlSession 都会取出当前缓存的数据,而不会再次发送 SQL 到数据库中。
        但是如果你使用的不同的 SqlSession 对象,因为不同的 SqlSession 都是相互隔离的,所以用相同的 Mapper,参数和方法,它还是会再次发送SQL 到数据库中去执行,返回结果。
测试:

public void testGetUser() throws Exception {
    SqlSession sqlSession = sqlSessionFactory.openSession();
    UserMapper userMapper = sqlSession.getMapper(UserMapper.class);
    User user = userMapper.getUser(456l);
    System.out.println("再次查询用户");
    user = userMapper.getUser(456l);
    System.out.println(JSON.toJSONString(user));
}

在这里插入图片描述

        我们发现第一个 SqlSession 实际只发生过一次查询,而第二次查询就直接从缓存中取出了,也就是 SqlSession 层面的一级缓存,它在各个 SqlSession 是相互隔离的,为了能克服这个问题,我们往往需要配置二级缓存,使得缓存是 SqlSessionFactory 层面上是能够提供给各个 SqlSession 对象共享的。
        而 SqlSessionFactory 层面上的二级缓存是不开启的,二级缓存的开启需要进行配置,实现二级缓存的时候,MyBatis 要求返回的 POJO 必需是可序列化的,也就是要求实现 Serializable接口,配置方法很简单,只需要在映射XML 文件配置的时候就开启缓存就可以了。
<cache/>
        这样一个语句里面,很多的设置是默认的,如果我们想只是这样的配置,那就意味着。

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

添加了这个配置后,我们还必需做一件重要的事情,否则就会出现异常,这就是 MyBatis 要返回的 POJO对象要实现 Serializable接口,否则它就会抛出异常。

<cache >
    <!--eviction(收回策略)
        LRU(最近最少使用的):移除最长时间不被使用的对象,这是默认值。
        FIFO(先进先出):按对象进入缓存的顺序来移除它们。
        SOFT(软引用):移除基于垃圾回收器状态和软引用规则的对象。
        WEAK(弱引用):更积极地移除基于垃圾收集器状态和弱引用规则的对象。
    -->
    <property name="eviction" value="LRU" />
    
    <!--flushinterval(刷新间隔)
        可以被设置为任意的正整数,而且它们代表一个合理的毫秒形式的时间段。默认情况不设置,即没有刷新间隔,缓存仅仅在调用语句时刷新。-->
    <property name="flushInterval" value="6000000" />
    <!--size(引用数目)
        可以被设置为任意正整数,要记住缓存的对象数目和运行环境的可用内存资源数目。默认值是1024 。-->
    <property name="size" value="1024" />
    
    <!--readOnly(只读)
        属性可以被设置为 true 或 false。只读的缓存会给所有调用者返回缓存对象的相同实例,因此这些对象不能被修改,这提供了很重要的性能优势。
        可读写的缓存会通过序列化返回缓存对象的拷贝,这种方式会慢一些,但是安全,因此默认是 false。-->
    <property name="readOnly" value="false" />
</cache>
自定义缓存

        系统缓存是 MyBatis机器上的本地缓存,但是大型服务器上,会使用各类不同的缓存服务器,这个时候我们可以定制缓存,比如现在十分流行的 Redis 缓存,我们需要实现 MyBatis 为我们提供的 org.apache.ibatis.cache.Cache,缓存接口简介如下:
//获取缓存编号
String getid();
//保存 key 值的缓存
void putObject(Object key,Object value);
//通过 Key 获取缓存对象
Object getObject(Object key);
//通过 key 删除缓存
Object removeObject(Object key);
//清空缓存
void clear();
//获取缓存对象大小
int getSize();
//获取缓存的读写锁
ReadWriteLock getReadWriteLock();

因为每种缓存都有其特点,上面的接口需要自己去实现。

  1. 准备缓存插件
public class MybatisPlusCache implements Cache {
    // 读写锁
    private final ReadWriteLock readWriteLock = new ReentrantReadWriteLock(true);

    //这里使用了redis缓存,使用springboot自动注入
    private HashMap<String, Object> cacheMap = new HashMap<>();

    private String id;

    //是mybatis必须要求的,必写。此id是xml中的namespace的值
    public MybatisPlusCache(final String id) {
        if (id == null) {
            throw new IllegalArgumentException("未获取到缓存实例id");
        }
        this.id = id;
    }
    //返回cache的唯一名称
    @Override
    public String getId() {
        return this.id;
    }
    //缓存存值
    @Override
    public void putObject(Object key, Object value) {
        String k = id + "_" + key.toString();
        //id是namespace的值,key是方法名,value是查询的结果
        System.out.println("putObject :" + k);
        cacheMap.put(k, value);
    }

    //缓存取值
    @Override
    public Object getObject(Object key) {
        String k = id + "_" + key.toString();
        System.out.println("getObject : " + k);
        return cacheMap.get(k);
    }

    //mybatis保留方法
    @Override
    public Object removeObject(Object key) {
        return null;
    }

    //清空缓存,在增删改时会自动调用
    @Override
    public void clear() {
        cacheMap.clear();
    }

    @Override
    public int getSize() {
        int i = 0;
        for (String key : cacheMap.keySet()) {
            i++;
        }
        return i;
    }

    @Override
    public ReadWriteLock getReadWriteLock() {
        return this.readWriteLock;
    }

    public boolean equals(Object o) {
        if (getId() == null) throw new CacheException("Cache instances require an ID.");
        if (this == o) return true;
        if (!(o instanceof Cache)) return false;

        Cache otherCache = (Cache) o;
        return getId().equals(otherCache.getId());
    }

    public int hashCode() {
        if (getId() == null) throw new CacheException("Cache instances require an ID.");
        return getId().hashCode();
    }
}

上述插件,我只是举个例子,如果想用 Redis,MongoDB,自己去相应的方法中实现即可。

  1. 配置 cache
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.spring_101_200.test_141_150.test_143_mybatis_cachenamespace_xml.UserMapper" >

    <cache type="com.spring_101_200.test_141_150.test_143_mybatis_cachenamespace_xml.MybatisPlusCache">
        <!--eviction(收回策略)
            LRU(最近最少使用的):移除最长时间不被使用的对象,这是默认值。
            FIFO(先进先出):按对象进入缓存的顺序来移除它们。
            SOFT(软引用):移除基于垃圾回收器状态和软引用规则的对象。
            WEAK(弱引用):更积极地移除基于垃圾收集器状态和弱引用规则的对象。
        -->
        <property name="eviction" value="LRU" />
        <!--flushinterval(刷新间隔)
            可以被设置为任意的正整数,而且它们代表一个合理的毫秒形式的时间段。默认情况不设置,即没有刷新间隔,缓存仅仅在调用语句时刷新。-->
        <property name="flushInterval" value="6000000" />
        <!--size(引用数目)
            可以被设置为任意正整数,要记住缓存的对象数目和运行环境的可用内存资源数目。默认值是1024 。-->
        <property name="size" value="1024" />
        <!--readOnly(只读)
            属性可以被设置为 true 或 false。只读的缓存会给所有调用者返回缓存对象的相同实例,因此这些对象不能被修改,这提供了很重要的性能优势。
            可读写的缓存会通过序列化返回缓存对象的拷贝,这种方式会慢一些,但是安全,因此默认是 false。-->
        <property name="readOnly" value="false" />
    </cache>

    <select id="getUser" parameterType="java.lang.Long" resultType="com.spring_101_200.test_141_150.test_143_mybatis_cachenamespace_xml.User">
            select * from lz_user where id=#{id}
    </select>
</mapper>

上面加粗字体,就是引入自定义缓存插件。

  1. 测试
public void test() throws Exception {
    SqlSession sqlSession = sqlSessionFactory.openSession();
    UserMapper userMapper = sqlSession.getMapper(UserMapper.class);
    User user = userMapper.getUser(456l);
    //一定要commit,不然查询出来结果不会被 put
    sqlSession.commit();
    System.out.println("======================");
    user = userMapper.getUser(456l);
    System.out.println(JSON.toJSONString(user));
}

4.结果如下:
在这里插入图片描述

共用缓存

        想在不同的 Mapper 中共用一个二级缓存。

  1. 准备自定义MybatisPlusCache
public class MybatisPlusCache implements Cache {
    // 读写锁
    private final ReadWriteLock readWriteLock = new ReentrantReadWriteLock(true);

    //这里使用了redis缓存,使用springboot自动注入
    private HashMap<String, Object> cacheMap = new HashMap<>();

    private String id;

    //是mybatis必须要求的,必写。此id是xml中的namespace的值
    public MybatisPlusCache(final String id) {
        if (id == null) {
            throw new IllegalArgumentException("未获取到缓存实例id");
        }
        this.id = id;
    }
    //返回cache的唯一名称
    @Override
    public String getId() {
        return this.id;
    }
    //缓存存值
    @Override
    public void putObject(Object key, Object value) {
        String k = id + "_" + key.toString();
        //id是namespace的值,key是方法名,value是查询的结果
        System.out.println("putObject id:           " + id);
        System.out.println("putObject key:           " + key);
        String myK  = getKey(key);
        if(StringUtils.isNotBlank(myK)){
            System.out.println("putObject myK:      " + myK     );
            k = myK;
        }
        cacheMap.put(k, value);
    }

    //缓存取值
    @Override
    public Object getObject(Object key) {
        String k = id + "_" + key.toString();
        System.out.println("getObject id:           " + id);
        System.out.println("getObject key:           " + key);
        String myK  = getKey(key);
        if(StringUtils.isNotBlank(myK)){
            System.out.println("getObject myK:      " + myK     );
            k = myK;
        }
        Object obj =  cacheMap.get(k);
        if(obj !=null){
            System.out.println(JSON.toJSONString(obj));
        }
        return obj;
    }
    public String getKey(Object key){
        if(key instanceof CacheKey){
            String keys [] = key.toString().split(":");
            return keys[keys.length-2] + keys[keys.length-1];
        }
        return null;
    }

    //mybatis保留方法
    @Override
    public Object removeObject(Object key) {
        return null;
    }

    //清空缓存,在增删改时会自动调用
    @Override
    public void clear() {
        cacheMap.clear();
    }

    @Override
    public int getSize() {
        int i = 0;
        for (String key : cacheMap.keySet()) {
            i++;
        }
        return i;
    }

    @Override
    public ReadWriteLock getReadWriteLock() {
        return this.readWriteLock;
    }



    public boolean equals(Object o) {
        if (getId() == null) throw new CacheException("Cache instances require an ID.");
        if (this == o) return true;
        if (!(o instanceof Cache)) return false;

        Cache otherCache = (Cache) o;
        return getId().equals(otherCache.getId());
    }

    public int hashCode() {
        if (getId() == null) throw new CacheException("Cache instances require an ID.");
        return getId().hashCode();
    }

}

        这个缓存类和上一个不一样的地方,就是getObject 和 putObject 存储的 key 不一样,这里的 key 是 sql + 参数的形式,也就是如果两个 XML 文件中,只要查询的 SQL 一样,同时查询的条件一样,就使用缓存中数据返回。

  1. 准备 UserMapper.xml
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.spring_101_200.test_141_150.test_145_mybatis_cachenamespaceref_xml.UserMapper" >


    <cache type="com.spring_101_200.test_141_150.test_145_mybatis_cachenamespaceref_xml.MybatisPlusCache">
        <!--eviction(收回策略)
            LRU(最近最少使用的):移除最长时间不被使用的对象,这是默认值。
            FIFO(先进先出):按对象进入缓存的顺序来移除它们。
            SOFT(软引用):移除基于垃圾回收器状态和软引用规则的对象。
            WEAK(弱引用):更积极地移除基于垃圾收集器状态和弱引用规则的对象。
        -->
        <property name="eviction" value="LRU" />
        <!--flushinterval(刷新间隔)
            可以被设置为任意的正整数,而且它们代表一个合理的毫秒形式的时间段。默认情况不设置,即没有刷新间隔,缓存仅仅在调用语句时刷新。-->
        <property name="flushInterval" value="6000000" />
        <!--size(引用数目)
            可以被设置为任意正整数,要记住缓存的对象数目和运行环境的可用内存资源数目。默认值是1024 。-->
        <property name="size" value="1024" />
        <!--readOnly(只读)
            属性可以被设置为 true 或 false。只读的缓存会给所有调用者返回缓存对象的相同实例,因此这些对象不能被修改,这提供了很重要的性能优势。
            可读写的缓存会通过序列化返回缓存对象的拷贝,这种方式会慢一些,但是安全,因此默认是 false。-->
        <property name="readOnly" value="false" />
    </cache>
    
    <select id="getUser" parameterType="java.lang.Long" resultType="User">
            select * from lz_user where id=#{id}
    </select>

</mapper>
  1. 准备UserBillMapper.xml
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.spring_101_200.test_141_150.test_145_mybatis_cachenamespaceref_xml.UserBillMapper" >


    <cache-ref namespace="com.spring_101_200.test_141_150.test_145_mybatis_cachenamespaceref_xml.UserMapper"/>

    <select id="getUser" parameterType="java.lang.Long" resultType="User">
            select * from lz_user where id=#{id}
    </select>

</mapper>
  1. 开始测试
public void test1() throws Exception {
    SqlSession sqlSession = sqlSessionFactory.openSession();
    UserMapper userMapper = sqlSession.getMapper(UserMapper.class);
    UserBillMapper userBillMapper = sqlSession.getMapper(UserBillMapper.class);
    User user = userMapper.getUser(456l);
    System.out.println("user:" + JSON.toJSONString(user));
    sqlSession.commit();
    System.out.println("======================");
    user.setRealName("bbbbbbbb");
    User user2 = userBillMapper.getUser(456l);
    System.out.println("user2:"+JSON.toJSONString(user2));
}

在这里插入图片描述

动态 SQL

        如果使用 JDBC或者其他框架,很多时候你得根据需要去拼接 SQL,这是一个麻烦的事情,而 MyBatis 提供对 SQL 语句动态组装能力,而且它只有几个基本的元素,十分简单,大量的判断都可以在 MyBatis 的映射 XML 文件里配置,以达到许多我们需要大量代码才能实现功能,大大减少了编写代码的工作量,这体现了 MyBatis 的灵活性,调试可配置和可维护性,MyBatis 也可以注解中配置 SQL,但是由于注解中配置功能受限,对于复杂的 SQL而言可读性很差,所以使用得较少。

概述

        MyBatis 的动态 SQL包括以下几种元素。

元素作用备注
if判断语句单条件分支判断
choose(when,otherwise)相当于 java中的 case when 语句多条件分支判断
trim(where,set)辅助元素用于处理一些 SQL拼装问题
foreach循环语句在 in 语句等列举条件中常用

下面我们来讨论这些元素的用法。

if 元素

        if元素是我们最常用的判断语句,相当于 Java 中的 if 语句,它常常与 test 属性联合使用。

        在大部分情况下,if 元素使用法简单,让我们先了解它的基本用法,现在我们要根据角色名称(username)去查找用户,但是用户名是一个可填可不填的条件,不填写时候就不需要它的查询条件,这是查询常见的场景之一,if 元素提供了简易的实现方式,如下所示

<select id="getUser" resultType="User" parameterType="java.lang.Long" >
    select * from lz_user where 1 = 1 
    <if test="username !=null ">
        and username=#{username}
    </if>
</select>

        这句话的含义就是当我们将参数 username 传递给映射器中,采取构造对 username 查询,如果这个参数为空,则不要这个条件去查询,显然这样的场景在我们的实际工作中十分常见,通过 MyBatis 的条件语句我们可以节省许多的拼接 SQL 的工作,把精力集中在 XML 维护上;

chose ,when ,otherwise 元素

        java 语言中 if 语句就是一种非此即彼的关系,但是很多的时候,我们所面对的不是一种非此即彼的选择,在有些时候,我们还需要第三种选择甚至是更多的选择,也就是说我们也需要 switch…case…default 语句,而映射器的动态语句中 choose,when,otherwise元素承担了这个功能,让我们看看下面的场景。

  • 当用户名不为空时,就用用户名去查询
  • 当真实名不为空时,用真实名去查询
  • 当用户名和真实名都为空时,只要用户名不为空,就可以是想要的结果

让我们看看如何使用 choose,when,otherwise 元素去实现

 <select id="getUser" resultType="User" parameterType="java.lang.Long" >
     select * from lz_user where 1 = 1 
     <choose>
         <when test="username != null and username != ''">
             and username= #{username}
         </when>
         <when test="realName !=null and realName!=''">
             and real_name = #{realName}
         </when>
         <otherwise>
             and username is not null 
         </otherwise>
     </choose>
 </select>

        这样 MyBatis 就会根据参数的设置进行判断来动态组装SQL,以满足不同的业务要求。

trim,where,set 元素

        细心的读者会发现SQL 语句中加入一个条件"1=1",如果没有加入这个条件,可能就会报错。
        select * from lz_user where and username=#{username}

        显然会报错关于 SQL 的语法异常,而加入了1=1 这样条件又显得相当的奇怪,不过不必担心,我们可以用 where 元素去处理 SQL 以达到预期的效果,例如我们去掉条件1 = 1 只要有 where 元素就可以了。

<select id="getUser" resultType="User" parameterType="java.lang.Long" >
    select * from lz_user 
    <where>
        <if test="username !=null ">
            and username=#{username}
        </if>
    </where>
</select>

        这里当 where 元素里面条件成立,才加入 where 这个 SQL 关键字组装的 SQL 里,否则就不加入。
        有时候我们需要去掉一些特殊的 SQL 语法,比如常见的 and ,or 而使用 trim元素就可以达到我们预想的效果。

<select id="getUser" resultType="User" parameterType="java.lang.Long" >
    select * from lz_user
    <trim prefix="where" prefixOverrides="and">
        <if test="username !=null ">
            and username=#{username}
        </if>
    </trim>
</select>

        稍微解释一下,trim 元素就意味着我们需要去掉一些特殊字符串,prefix 代表着语句的前缀,而 prefixOverrides 代表着你需要去掉的那种字符,上面的写法基本与 where 是等效的。
        在 Hibernate 中我们常常需要更新某一对象,发送所有的字段给持久对象,现实中的场景常常是,我想更新一个字段,如果发送所有的属性去更新一遍,对网络带宽消耗较大,性能最佳的办法是把主键和更新字段的值传递给 SQL 更新即可,例如,角色表有一个主键和两个字段,如果一个个的去更新,需要写两条 SQL,如果有1000个字段呢?显然这种做法是不方便的,而在 Hibernate 中,我们做更新都是会用全部字段发送给 SQL的方法来避免这一情况发生。
在 MyBatis 中,我们常常可以使用 set 元素来完成这些功能。如

<update id = "updateUser" parameterType="user">
    udpate lz_user 
    <set>
        <if test="username != null and username!=''">
            username = #{username}
        </if>
        <if test="realName != null and realName !=''">
            real_name = #{realName}
        </if>
    </set>
    where id = #{id}
</update>

当然也可以这样写

<update id="updateUserById" parameterType="User" >
    update lz_user
    <trim prefix="set" suffixOverrides=",">
        <if test="isDelete != null">is_delete = #{isDelete},</if>
        <if test="gmtCreate != null">gmt_create = #{gmtCreate},</if>
        <if test="username != null">username = #{username},</if>
        <if test="sex != null">sex = #{sex},</if>
        <if test="sexStr != null">sex_str = #{sexStr}</if>                
    </trim>
    ,gmt_modified = now()
    where id = #{id}
</update>

        set 元素遇到逗号,它会把对应的逗号去掉,如果我们自己编写那将是多少次的判断呢?当我们只想更新备注,我们只需要传递备注信息和角色编号即可,而不需要再传递角色名称,MyBatis 就会根据参数的规则进行动态 SQL组装,这样便能满足要求,同时避免全部字段更新麻烦。

foreach 元素

        显然 foreach元素是一个循环语句,它的作用是遍历集合,它能够很好的支持数组和 List,Set 接口集合,对此提供遍历。
        在数据库中,数据字典经常使用的内容,比如在此用户表中,性别可以是男或女或未知。
1-男,2-女,0-未知。
        实际工作中,可能是需要查找男性和女性用户

<select id="getUser" resultType="User" parameterType="java.lang.Long" >
    select * from lz_user 
    <where>
        <foreach collection="sexList" index="index" item="sex" open="(" separator="," close=",">
            #{sex}
        </foreach>
    </where>
</select>

这里需要稍微解析一下。

  • collection 配置的 sexList 是传递进来的参数名称,它可以是一个数组或者 List,Set 等集体。
  • item配置是循环中的当前元素
  • index 配置的当前元素在集体的位置下标
  • open 和close 配置是以什么样的符号将这些集体元素包装起来的
  • separator 是各个元素的间隔符

        在 SQL 中对于in 语句我们常常使用,对于大量的数据的 in 语句需要我们特别注意,因为它会消耗大量的性能,还有一些数据库的 SQL 执行的 SQL 长度也是有限制的,所以我们有的时候还是要计算一下 collectio的长度。

test 的属性

        test 属性用于条件判断语句,它在 MyBatis 中广泛的使用,它的作用相当于判断真假,在大部分的场景中我们都用它来判断空或非空,有时候我们需要判断字符串,数字和枚举等,所以我们十分有必要讨论一下它的用法。

<select id="getUser" resultType="User" parameterType="java.lang.Long" >
    select * from lz_user where 1 = 1 
    <if test="username !=null ">
        and username=#{username}
    </if>
</select>

如果用户名不为空,则自动在 where 1 = 1 后面加上 and username=#{username}
        换句话说,这条语句判定成功了,在旧版本里面我们往往需要加入 toString(),新的版本己经解决了这个问题,同样它们可以给我们判断数值型参数,对于枚举而言,取决于你使用体积 typeHandler,这些需要3章枚举 typeHandler的介绍。

bind 元素

        bind 元素的作用是 OGNL表达式自定义一个上下文变量,这样更加方便我们使用,在我们进行模糊查询的时候,如果 MyBatis 数据库,我们常常用到的是一个 concat 用%和参数相连接,然而在 Oracle 数据库则是使用"||",这样 SQL 需要提供两种形式去实现,但是有了 bind元素,我们就完全不必使用数据库语言的,只需要使用 MyBatis 语言就可以实现所需参数相连。
        比如我们要按用户名模糊查询,我们就可以将映射文件写成这样。

<select id="getUser" resultType="User" parameterType="java.lang.String" >
    <bind name="pattern" value="'%' +_parameter + '%'"/>
    select * from lz_user where username like #{pattern}
</select>

        这里的"_parameter" 代表着就是传递进来的参数,它和通配符连接后,赋给了 pattern,我们就可以在 select 语句中使用这个变量进行模糊查询了,不管是 MySQL 数据库还是 Oracle 数据库都可以使用这样的语句来,提高了其可移植性。
        我们传递参数往往还不止一个,如果我们传递了多个参数,那是怎样使用呢?

<select id="getUser" resultType="User" parameterType="java.lang.String" >
    <bind name="pattern_username" value="'%' +username + '%'"/>
    <bind name="pattern_realName" value="'%'+realName+'%'"></bind>
    select * from lz_user where username like #{pattern_username} or realName like #{pattern_realName}
</select>

目前我们只是对 MyBatis 的使用做讲解,关于原理和源码的解析,我们放到后面的博客中去讲解。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值