mybatis(三)配置mapper.xml 的基本操作

Posted on 2020-04-12 22:43  FLGB  阅读(2665)  评论(1编辑  收藏  举报

参考:https://www.cnblogs.com/wuzhenzhao/p/11101555.html

XML 映射文件

  本文参考mybatis中文官网进行学习总结:http://www.mybatis.org/mybatis-3/zh/sqlmap-xml.html

  MyBatis 的真正强大在于它的映射语句,这是它的魔力所在。由于它的异常强大,映射器的 XML 文件就显得相对简单。如果拿它跟具有相同功能的 JDBC 代码进行对比,你会立即发现省掉了将近 95% 的代码。MyBatis 为聚焦于 SQL 而构建,以尽可能地为你减少麻烦。

  SQL 映射文件只有很少的几个顶级元素(按照应被定义的顺序列出):

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

select

  查询语句是 MyBatis 中最常用的元素之一,光能把数据存到数据库中价值并不大,只有还能重新取出来才有用,多数应用也都是查询比修改要频繁。对每个插入、更新或删除操作,通常间隔多个查询操作。这是 MyBatis 的基本原则之一,也是将焦点和努力放在查询和结果映射的原因。简单查询的 select 元素是非常简单的。比如:

复制代码
<resultMap id="BaseResultMap" type="blog">
     <id column="bid" property="bid" jdbcType="INTEGER"/>
     <result column="name" property="name" jdbcType="VARCHAR"/>
     <result column="author_id" property="authorId" jdbcType="INTEGER"/>
</resultMap>
<!--****************************************************************************************-->
<!--简单查询-->
<select id="selectBlogById" resultMap="BaseResultMap" statementType="PREPARED" useCache="false">
     select * from blog where bid = #{bid}
</select>
复制代码

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

// 近似的 JDBC 代码,非 MyBatis 代码...
String selectBlogById= "SELECT * FROM BLOG WHERE BID=?";
PreparedStatement ps = conn.prepareStatement(selectBlogById);
ps.setInt(1,bid);

  当然,使用 JDBC 意味着需要更多的代码来提取结果并将它们映射到对象实例中,而这就是 MyBatis 节省你时间的地方。select 元素允许你配置很多属性来配置每条语句的作用细节。

复制代码
<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 最强大的特性,如果你对其理解透彻,许多复杂映射的情形都能迎刃而解。可以使用 resultMap 或 resultType,但不能同时使用。
flushCache 将其设置为 true 后,只要语句被调用,都会导致本地缓存和二级缓存被清空,默认值:false。
useCache 将其设置为 true 后,将会导致本条语句的结果被二级缓存缓存起来,默认值:对 select 元素为 true。
timeout 这个设置是在抛出异常之前,驱动程序等待数据库返回请求结果的秒数。默认值为未设置(unset)(依赖驱动)。
fetchSize 这是一个给驱动的提示,尝试让驱动程序每次批量返回的结果行数和这个设置值相等。 默认值为未设置(unset)(依赖驱动)。
statementType STATEMENT,PREPARED 或 CALLABLE 中的一个。这会让 MyBatis 分别使用 Statement,PreparedStatement 或 CallableStatement,默认值:PREPARED。
resultSetType FORWARD_ONLY,SCROLL_SENSITIVE, SCROLL_INSENSITIVE 或 DEFAULT(等价于 unset) 中的一个,默认值为 unset (依赖驱动)。
databaseId 如果配置了数据库厂商标识(databaseIdProvider),MyBatis 会加载所有的不带 databaseId 或匹配当前 databaseId 的语句;如果带或者不带的语句都有,则不带的会被忽略。
resultOrdered 这个设置仅针对嵌套结果 select 语句适用:如果为 true,就是假设包含了嵌套结果集或是分组,这样的话当返回一个主结果行的时候,就不会发生有对前面结果集的引用的情况。 这就使得在获取嵌套的结果集的时候不至于导致内存不够用。默认值:false
resultSets 这个设置仅对多结果集的情况适用。它将列出语句执行后返回的结果集并给每个结果集一个名称,名称是逗号分隔的。

 insert, update 和 delete:

  数据变更语句 insert,update 和 delete 的实现非常接近:

复制代码
<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 可以通过类型处理器推断出具体传入语句的参数,默认值为未设置(unset)。
parameterMap 这是引用外部 parameterMap 的已经被废弃的方法。请使用内联参数映射和 parameterType 属性。
flushCache 将其设置为 true 后,只要语句被调用,都会导致本地缓存和二级缓存被清空,默认值:true(对于 insert、update 和 delete 语句)。
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 的语句;如果带或者不带的语句都有,则不带的会被忽略。

下面就是 insert,update 和 delete 语句的示例:

复制代码
<insert id="insertAuthor">
  insert into Author (id,username,password,email,bio)
  values (#{id},#{username},#{password},#{email},#{bio})
</insert>

<update id="updateAuthor">
  update Author set
    username = #{username},
    password = #{password},
    email = #{email},
    bio = #{bio}
  where id = #{id}
</update>

<delete id="deleteAuthor">
  delete from Author where id = #{id}
</delete>
复制代码

  如前所述,插入语句的配置规则更加丰富,在插入语句里面有一些额外的属性和子元素用来处理主键的生成,而且有多种生成方式。首先,如果你的数据库支持自动生成主键的字段(比如 MySQL 和 SQL Server),那么你可以设置 useGeneratedKeys=”true”,然后再把 keyProperty 设置到目标属性上就 OK 了。如果你的数据库还支持多行插入, 你也可以传入一个 Blog数组或集合,并返回自动生成的主键。

复制代码
<!--批量插入-->
<insert id="insertBlogs" useGeneratedKeys="true"
            keyProperty="bid">
     insert into blog (name, author_id) values
     <foreach item="item" collection="list" separator=",">
         (#{item.name}, #{item.authorId})
     </foreach>
</insert>
复制代码

sql

  这个元素可以被用来定义可重用的 SQL 代码段,这些 SQL 代码可以被包含在其他语句中。它可以(在加载的时候)被静态地设置参数。 在不同的包含语句中可以设置不同的值到参数占位符上。mybatis中sql标签与include标签进行配合,灵活的查询需要的数据。

复制代码
<sql id="ref">
    bid,name,authorId
</sql>

<select id="selectbyId" resultMap="BaseResultMap">
    select
    <include refid="ref"/>
    from
    blog where bid = #{bid}
</select>
复制代码

  sql标签中id属性对应include标签中的refid属性。通过include标签将sql片段和原sql片段进行拼接成一个完成的sql语句进行执行。include标签中还可以用property标签,用以指定自定义属性。

复制代码
<select id="selectbyId" resultMap="BaseResultMap">
    select
    <include refid="ref">
        <property name="abc" value="bid"/>
    </include>
    from
    blog where bid = #{bid}
</select>
复制代码

  此时,可以在sql标签中取出对应设置的自定义属性中的值,例如接上代码例子:

复制代码
<sql id="ref">
    ${abc},name,authorId
</sql>

<select id="selectbyId" resultMap="BaseResultMap">
    select
    <include refid="ref">
        <property name="abc" value="bid"/>
    </include>
    from
    blog where bid = #{bid}
</select>
复制代码

   在sql标签中通过${}取出对应include标签中设置的属性值。

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

  • 嵌套 Select 查询:通过执行另外一个 SQL 映射语句来加载期望的复杂类型。
  • 嵌套结果映射:使用嵌套的结果映射来处理连接结果的重复子集。

关联的嵌套 Select 查询

复制代码
<!-- 另一种联合查询(一对一)的实现,但是这种方式有“N+1”的问题 -->
    <resultMap id="BlogWithAuthorQueryMap" type="com.wuzz.demo.associate.BlogAndAuthor">
        <id column="bid" property="bid" jdbcType="INTEGER"/>
        <result column="name" property="name" jdbcType="VARCHAR"/>
        <association property="author" javaType="com.wuzz.demo.entity.Author"
                     column="author_id" select="selectAuthor"/>
    </resultMap>

    <!-- 嵌套查询 -->
    <select id="selectAuthor" parameterType="int" resultType="com.wuzz.demo.entity.Author">
        select author_id authorId, author_name authorName
        from author where author_id = #{authorId}
    </select>

    <!-- 根据文章查询作者,一对一,嵌套查询,存在N+1问题,可通过开启延迟加载解决 -->
    <select id="selectBlogWithAuthorQuery" resultMap="BlogWithAuthorQueryMap" >
        select b.bid, b.name, b.author_id, a.author_id , a.author_name
        from blog b
        left join author a
        on b.author_id=a.author_id
        where b.bid = #{bid, jdbcType=INTEGER}
    </select>
复制代码

  就是这么简单。我们有两个 select 查询语句:一个用来加载博客(Blog),另外一个用来加载作者(Author),而且博客的结果映射描述了应该使用 selectAuthor 语句加载它的 author 属性。其它所有的属性将会被自动加载,只要它们的列名和属性名相匹配。这种方式虽然很简单,但在大型数据集或大型数据表上表现不佳。这个问题被称为“N+1 查询问题”。 概括地讲,N+1 查询问题是这样子的:

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

  这个问题会导致成百上千的 SQL 语句被执行。有时候,我们不希望产生这样的后果。MyBatis 能够对这样的查询进行延迟加载,因此可以将大量语句同时运行的开销分散开来。mybatis.configuration.lazy-loading-enabled=true 可以开启延时加载 mybatis.configuration.aggressive-lazy-loading=true 可以指定哪些方法调用查询, 然而,如果你加载记录列表之后立刻就遍历列表以获取嵌套的数据,就会触发所有的延迟加载查询,性能可能会变得很糟糕。所以还有另外一种方法。

关联的嵌套结果映射

复制代码
<!-- 根据文章查询作者,一对一查询的结果,嵌套查询 -->
    <resultMap id="BlogWithAuthorResultMap" type="com.wuzz.demo.associate.BlogAndAuthor">
        <id column="bid" property="bid" jdbcType="INTEGER"/>
        <result column="name" property="name" jdbcType="VARCHAR"/>
        <!-- 联合查询,将author的属性映射到ResultMap -->
        <association property="author" javaType="com.wuzz.demo.entity.Author">
            <id column="author_id" property="authorId"/>
            <result column="author_name" property="authorName"/>
        </association>
    </resultMap>
    <!-- 根据文章查询作者,一对一,嵌套结果,无N+1问题 -->
    <select id="selectBlogWithAuthorResult" resultMap="BlogWithAuthorResultMap" >
        select b.bid, b.name, b.author_id, a.author_id , a.author_name
        from blog b,author a
        where b.author_id=a.author_id and b.bid = #{bid, jdbcType=INTEGER}
    </select>
复制代码

   查询文章带评论的结果(一对多)映射:

复制代码
<!--  查询文章带评论的结果(一对多) -->
    <resultMap id="BlogWithCommentMap" type="com.wuzz.demo.associate.BlogAndComment" extends="BaseResultMap" >
        <collection property="comment" ofType="com.wuzz.demo.entity.Comment">
            <id column="comment_id" property="commentId" />
            <result column="content" property="content" />
            <result column="bid" property="bid" />
        </collection>
    </resultMap>
    <!-- 根据文章查询评论,一对多 -->
    <select id="selectBlogWithCommentById" resultMap="BlogWithCommentMap" >
        select b.bid, b.name, b.author_id , c.comment_id , c.content,c.bid
        from blog b, comment c
        where b.bid = c.bid
        and b.bid = #{bid}
    </select>
复制代码

  按作者查询文章评论的结果(多对多):

复制代码
<!--  按作者查询文章评论的结果(多对多) -->
    <resultMap id="AuthorWithBlogMap" type="com.wuzz.demo.associate.AuthorAndBlog" >
        <id column="author_id" property="authorId" jdbcType="INTEGER"/>
        <result column="author_name" property="authorName" jdbcType="VARCHAR"/>
        <collection property="blog" ofType="com.wuzz.demo.associate.BlogAndComment">
            <id column="bid" property="bid" />
            <result column="name" property="name" />
            <result column="author_id" property="authorId" />
            <collection property="comment" ofType="com.wuzz.demo.entity.Comment">
                <id column="comment_id" property="commentId" />
                <result column="content" property="content" />
                <result column="bid" property="bid" />
            </collection>
        </collection>
    </resultMap>

    <!-- 根据作者文章评论,多对多 -->
    <select id="selectAuthorWithBlog" resultMap="AuthorWithBlogMap" >
        select b.bid, b.name, a.author_id , a.author_name , c.comment_id , c.content,c.bid
        from blog b, author a, comment c
        where b.author_id = a.author_id and b.bid = c.bid
    </select>
复制代码

动态 SQL

  MyBatis 的强大特性之一便是它的动态 SQL。如果你有使用 JDBC 或其它类似框架的经验,你就能体会到根据不同条件拼接 SQL 语句的痛苦。例如拼接时要确保不能忘记添加必要的空格,还要注意去掉列表最后一个列名的逗号。利用动态 SQL 这一特性可以彻底摆脱这种痛苦。虽然在以前使用动态 SQL 并非一件易事,但正是 MyBatis 提供了可以被用在任意 SQL 映射语句中的强大的动态 SQL 语言得以改进这种情形。动态 SQL 元素和 JSTL 或基于类似 XML 的文本处理器相似。在 MyBatis 之前的版本中,有很多元素需要花时间了解。MyBatis 3 大大精简了元素种类,现在只需学习原来一半的元素便可。MyBatis 采用功能强大的基于 OGNL 的表达式来淘汰其它大部分元素。

  1. if
  2. choose (when, otherwise)
  3. trim (where, set)
  4. foreach

  其中  choose再实际开发中应用的较少,我们这里就其他3个标签进行测试

复制代码
<!--动态sql-->
    <select id="selectBlogById2" resultMap="BaseResultMap" statementType="PREPARED" useCache="false"
        parameterType="blog">
        select * from blog
        <trim prefix="WHERE" prefixOverrides="AND |OR ">
            <if test="bid != null and bid !='' ">
                bid = #{bid}
            </if>
            <if test="name != null and name !='' ">
                AND name = #{name}
            </if>
            <if test="authorId != null and authorId != ''">
                AND author_id = #{author_id}
            </if>
        </trim>
    </select>
复制代码

   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>

  

批量操作
(spring-mybatis 工程单元测试目录,MapperTest 类)
我们在生产的项目中会有一些批量操作的场景,比如导入文件批量处理数据的情况
(批量新增商户、批量修改商户信息),当数据量非常大,比如超过几万条的时候,在
Java 代码中循环发送 SQL 到数据库执行肯定是不现实的,因为这个意味着要跟数据库创
建几万次会话,即使我们使用了数据库连接池技术,对于数据库服务器来说也是不堪重
负的。
在 MyBatis 里面是支持批量的操作的,包括批量的插入、更新、删除。我们可以直
接传入一个 List、Set、Map 或者数组,配合动态 SQL 的标签,MyBatis 会自动帮我们
生成语法正确的 SQL 语句。
比如我们来看两个例子,批量插入和批量更新。
批量插入
批量插入的语法是这样的,只要在 values 后面增加插入的值就可以了。
insert into tbl_emp (emp_id, emp_name, gender,email, d_id) values ( ?,?,?,?,? ) , ( ?,?,?,?,? ) , ( ?,?,?,?,? ) , ( ?,?,?,?,? ) ,
( ?,?,?,?,? ) , ( ?,?,?,?,? ) , ( ?,?,?,?,? ) , ( ?,?,?,?,? ) , ( ?,?,?,?,? ) , ( ?,?,?,?,? )
 
在 Mapper 文件里面,我们使用 foreach 标签拼接 values 部分的语句:
<!-- 批量插入 -->
<insert id="batchInsert" parameterType="java.util.List" useGeneratedKeys="true">
<selectKey resultType="long" keyProperty="id" order="AFTER">
SELECT LAST_INSERT_ID()
</selectKey>
insert into tbl_emp (emp_id, emp_name, gender,email, d_id)
values
<foreach collection="list" item="emps" index="index"
separator=",">
( #{emps.empId},#{emps.empName},#{emps.gender},#{emps.email},#{emps.dId} )
</foreach>
</insert>
Java 代码里面,直接传入一个 List 类型的参数。
我们来测试一下。效率要比循环发送 SQL 执行要高得多。最关键的地方就在于减少
了跟数据库交互的次数,并且避免了开启和结束事务的时间消耗。
批量更新
批量更新的语法是这样的,通过 case when,来匹配 id 相关的字段值。
update tbl_emp set
emp_name =
case emp_id
when ? then ?
when ? then ?
when ? then ? end ,
gender =
case emp_id
when ? then ?
when ? then ?
when ? then ? end ,
email =
case emp_id
when ? then ?
when ? then ?
when ? then ? end
where emp_id in ( ? , ? , ? )
所以在 Mapper 文件里面最关键的就是 case when 和 where 的配置。
需要注意一下 open 属性和 separator 属性。
<update id="updateBatch">
update tbl_emp set
emp_name =
<foreach collection="list" item="emps" index="index" separator=" " open="case emp_id"
close="end">
when #{emps.empId} then #{emps.empName}
</foreach>
,gender =
<foreach collection="list" item="emps" index="index" separator=" " open="case emp_id"
close="end">
when #{emps.empId} then #{emps.gender}
</foreach>
,email =
<foreach collection="list" item="emps" index="index" separator=" " open="case emp_id"
close="end">
when #{emps.empId} then #{emps.email}
</foreach>
where emp_id in
<foreach collection="list" item="emps" index="index" separator="," open="("
close=")">
#{emps.empId}
</foreach>
</update>
批量删除也是类似的。
Batch Executor
当然 MyBatis 的动态标签的批量操作也是存在一定的缺点的,比如数据量特别大的
时候,拼接出来的 SQL 语句过大。
MySQL 的服务端对于接收的数据包有大小限制,max_allowed_packet 默认是
4M,需要修改默认配置才可以解决这个问题。
Caused by: com.mysql.jdbc.PacketTooBigException: Packet for query is too large (7188967 >
4194304). You can change this value on the server by setting the max_allowed_packet' variable.
在我们的全局配置文件中,可以配置默认的 Executor 的类型。其中有一种
BatchExecutor。
 
<setting name="defaultExecutorType" value="BATCH" />
也可以在创建会话的时候指定执行器类型:
SqlSession session = sqlSessionFactory.openSession(ExecutorType.BATCH);
BatchExecutor 底层是对 JDBC ps.addBatch()的封装,原理是攒一批 SQL 以后再发
送(参考 standalone - 单元测试目录 JdbcTest.java – testJdbcBatch())。
问题:三种执行器的区别是什么?Simple、Reuse、Batch
嵌套(关联)查询/ N+1 / 延迟加载
我们在查询业务数据的时候经常会遇到跨表关联查询的情况,比如查询员工就会关
联部门(一对一),查询成绩就会关联课程(一对一),查询订单就会关联商品(一对
多),等等。

 

 

我们映射结果有两个标签,一个是 resultType,一个是 resultMap。
resultType 是 select 标签的一个属性,适用于返回 JDK 类型(比如 Integer、String
等等)和实体类。这种情况下结果集的列和实体类的属性可以直接映射。如果返回的字
 
段无法直接映射,就要用 resultMap 来建立映射关系。
对于关联查询的这种情况,通常不能用 resultType 来映射。用 resultMap 映射,要
么就是修改 dto(Data Transfer Object),在里面增加字段,这个会导致增加很多无关
的字段。要么就是引用关联的对象,比如 Blog 里面包含了一个 Author 对象,这种情况
下就要用到关联查询(association,或者嵌套查询),MyBatis 可以帮我们自动做结果
的映射。
一对一的关联查询有两种配置方式:
1、嵌套结果:
(mybatis-standalone - MyBatisTest - testSelectBlogWithAuthorResult ())
<!-- 根据文章查询作者,一对一查询的结果,嵌套查询 -->
<resultMap id="BlogWithAuthorResultMap"
type="com.gupaoedu.domain.associate.BlogAndAuthor">
<id column="bid" property="bid" jdbcType="INTEGER"/>
<result column="name" property="name" jdbcType="VARCHAR"/>
<!-- 联合查询,将 author 的属性映射到 ResultMap -->
<association property="author" javaType="com.gupaoedu.domain.Author">
<id column="author_id" property="authorId"/>
<result column="author_name" property="authorName"/>
</association>
</resultMap>
2、嵌套查询:
(mybatis-standalone - MyBatisTest - testSelectBlogWithAuthorQuery ())
<!-- 另一种联合查询 (一对一)的实现,但是这种方式有“N+1”的问题 -->
<resultMap id="BlogWithAuthorQueryMap" type="com.gupaoedu.domain.associate.BlogAndAuthor">
<id column="bid" property="bid" jdbcType="INTEGER"/>
<result column="name" property="name" jdbcType="VARCHAR"/>
<association property="author" javaType="com.gupaoedu.domain.Author"
column="author_id" select="selectAuthor"/> <!-- selectAuthor 定义在下面-->
</resultMap>
<!-- 嵌套查询 -->
 
<select id="selectAuthor" parameterType="int" resultType="com.gupaoedu.domain.Author">
select author_id authorId, author_name authorName
from author where author_id = #{authorId}
</select>
其中第二种方式:嵌套查询,由于是分两次查询,当我们查询了员工信息之后,会
再发送一条 SQL 到数据库查询部门信息。
我们只执行了一次查询员工信息的 SQL(所谓的 1),如果返回了 N 条记录,就会
再发送 N 条到数据库查询部门信息(所谓的 N),这个就是我们所说的 N+1 的问题。
这样会白白地浪费我们的应用和数据库的性能。
如果我们用了嵌套查询的方式,怎么解决这个问题?能不能等到使用部门信息的时
候再去查询?这个就是我们所说的延迟加载,或者叫懒加载。
在 MyBatis 里面可以通过开启延迟加载的开关来解决这个问题。
在 settings 标签里面可以配置:
<!--延迟加载的全局开关。当开启时,所有关联对象都会延迟加载。默认 false -->
<setting name="lazyLoadingEnabled" value="true"/>
<!--当开启时,任何方法的调用都会加载该对象的所有属性。默认 false,可通过 select 标签的
fetchType 来覆盖-->
<setting name="aggressiveLazyLoading" value="false"/>
<!-- Mybatis 创建具有延迟加载能力的对象所用到的代理工具,默认 JAVASSIST -->
<setting name="proxyFactory" value="CGLIB" />
lazyLoadingEnabled 决定了是否延迟加载。
aggressiveLazyLoading 决定了是不是对象的所有方法都会触发查询。
先来测试一下(也可以改成查询列表):
1、没有开启延迟加载的开关,会连续发送两次查询;
2 、 开 启 了 延 迟 加 载 的 开 关 , 调 用 blog.getAuthor() 以 及 默 认 的
(equals,clone,hashCode,toString)时才会发起第二次查询,其他方法并不会触发查
 
询,比如 blog.getName();
3、如果开启了 aggressiveLazyLoading=true,其他方法也会触发查询,比如
blog.getName()。
问题:为什么可以做到延迟加载?blog.getAuthor(),只是一个获取属性的方法,
里面并没有连接数据库的代码,为什么会触发对数据库的查询呢?
我怀疑:blog 根本不是 Blog 对象,而是被人动过了手脚!
把这个对象打印出来看看:
System.out.println(blog.getClass());
果然不对:
class com.gupaoedu.domain.associate.BlogAndAuthor_$$_jvst70_0
这个类的名字后面有 jvst,是 JAVASSIST 的缩写。原来到这里带延迟加载功能的对
象 blog 已经变成了一个代理对象,那到底什么时候变成代理对象的?我们后面在看源码
的时候再去分析,这个也先留一个作业给大家。
【问题】当开启了延迟加载的开关,对象是怎么变成代理对象的?
DefaultResultSetHandler.createResultObject()
既然是代理对象,那么必须要有一种创建代理对象的方法。我们有哪些实现动态代
理的方式?
这个就是为什么 settings 里面提供了一个 ProxyFactory 属性。MyBatis 默认使用
JAVASSIST 创建代理对象。也可以改为 CGLIB,这时需要引入 CGLIB 的包。
【问题】CGLIB 和 JAVASSIST 区别是什么?
 
测试一下,我们把默认的 JAVASSIST 修改为 CGLIB,再打印这个对象。
【问题】
1、resultType 和 resultMap 的区别?
2、collection 和 association 的区别?
MBG 与 Example
https://github.com/mybatis/generator
我们在项目中使用 MyBaits 的时候,针对需要操作的一张表,需要创建实体类、
Mapper 映射器、Mapper 接口,里面又有很多的字段和方法的配置,这部分的工作是
非常繁琐的。而大部分时候我们对于表的操作是相同的,比如根据主键查询、根据 Map
查询、单条插入、批量插入、根据主键删除等等等等。当我们的表很多的时候,意味着
有大量的重复工作。所以有没有一种办法,可以根据我们的表,自动生成实体类、Mapper
映射器、Mapper 接口,里面包含了我们需要用到的这些基本方法和 SQL 呢?
给大家看一个类文件(DBToJavaVO.java),这个是我以前到一个公司的时候,项
目里面用的 Hibernate+Oracle,当时是我第一次用 Hibernate。我发现 PO 类和 VO 类
的格式基本上都是一样的,属性跟表字段一一对应,主要有几个区别:
1、 类里面的属性都是表里面的字段,然后定义 getter()、setter()方法;
2、 数据库字段的下划线命名,要改成驼峰命名。
3、 PO 上面要加@Table、@Id、@Column 的注解;VO 不用;
4、 数据库的常见类型,要改成 Java 类型,比如 varchar,要对应成 Java 的 String
类型。
当时我就灵机一动,能不能写一个根据数据库的表自动生成 PO 和 VO 类的工具呢?
 
这样新建表的时候就不用一个一个写字段,一个一个加注解了。于是我就写了这个类。
后来我才知道有个东西叫 Hibernate 逆向工程。
MyBatis 也提供了一个这样的东西,叫做 MyBatis Generator,简称 MBG。我们只
需要修改一个配置文件,使用相关的 jar 包命令或者 Java 代码就可以帮助我们生成实体
类、映射器和接口文件。不知道用 MyBatis 的同学有没有跟当年的我一样,还是实体类
的一个一个字段,接口的一个一个方法,映射器的一条一条 SQL 去写的。
MBG 的配置文件里面有一个 Example 的开关,这个东西用来构造复杂的筛选条件
的,换句话说就是根据我们的代码去生成 where 条件(类似于 Tom 老师的自动生成
where 条件的方式)。
原理:在实体类中包含了两个有继承关系的 Criteria,用其中自动生成的方法来构建
查询条件。把这个包含了 Criteria 的实体类作为参数传到查询参数中,在解析 Mapper
映射器的时候会转换成 SQL 条件。
(mybatis-standalone 工程:
com.gupaoedu.domain.BlogExample
com.gupaoedu.BlogExampleTest)
BlogExample 里面包含了一个两个 Criteria:
 
实例:查询 bid=1 的 Blog,通过创建一个 Criteria 去构建查询条件:
BlogMapper mapper = session.getMapper(BlogMapper.class);
BlogExample example = new BlogExample();
BlogExample.Criteria criteria = example.createCriteria();
criteria.andBidEqualTo(1);
List<Blog> list = mapper.selectByExample(example);
生成的语句:
select 'true' as QUERYID, bid, name, author_id from blog WHERE ( bid = ? )
翻页
在写存储过程的年代,翻页也是一件很难调试的事情,我们要实现数据不多不少准
确地返回,需要大量的调试和修改。但是如果自己手写过分页,就能清楚分页的原理。
逻辑翻页与物理翻页
在我们查询数据库的操作中,有两种翻页方式,一种是逻辑翻页(假分页),一种
是物理翻页(真分页)。逻辑翻页的原理是把所有数据查出来,在内存中删选数据。 物
理翻页是真正的翻页,比如 MySQL 使用 limit 语句,Oracle 使用 rownum 语句,SQL
Server 使用 top 语句。
逻辑翻页
MyBatis 里面有一个逻辑分页对象 RowBounds,里面主要有两个属性,offset 和
limit(从第几条开始,查询多少条)。
我们可以在 Mapper 接口的方法上加上这个参数,不需要修改 xml 里面的 SQL 语句。
public List<Blog> selectBlogList(RowBounds rowBounds);
使用:mybatis-standalone- MyBatisTest-testSelectByRowBounds()
 
int start = 10; // offset,从第几行开始查询
int pageSize = 5; // limit,查询多少条
RowBounds rb = new RowBounds(start, pageSize);
List<Blog> list = mapper.selectBlogList(rb);
for(Blog b :list){
System.out.println(b);
}
它的底层其实是对 ResultSet 的处理。它会舍弃掉前面 offset 条数据,然后再取剩
下的数据的 limit 条。
// DefaultResultSetHandler.java
private void handleRowValuesForSimpleResultMap(ResultSetWrapper rsw, ResultMap resultMap,
ResultHandler<?> resultHandler, RowBounds rowBounds, ResultMapping parentMapping) throws
SQLException {
DefaultResultContext<Object> resultContext = new DefaultResultContext();
ResultSet resultSet = rsw.getResultSet();
this.skipRows(resultSet, rowBounds);
while(this.shouldProcessMoreRows(resultContext, rowBounds) && !resultSet.isClosed() &&
resultSet.next()) {
ResultMap discriminatedResultMap = this.resolveDiscriminatedResultMap(resultSet,
resultMap, (String)null);
Object rowValue = this.getRowValue(rsw, discriminatedResultMap, (String)null);
this.storeObject(resultHandler, resultContext, rowValue, parentMapping, resultSet);
}
}
很明显,如果数据量大的话,这种翻页方式效率会很低(跟查询到内存中再使用
subList(start,end)没什么区别)。所以我们要用到物理翻页。
物理翻页
物理翻页是真正的翻页,它是通过数据库支持的语句来翻页。
第一种简单的办法就是传入参数(或者包装一个 page 对象),在 SQL 语句中翻页。
<select id="selectBlogPage" parameterType="map" resultMap="BaseResultMap">
 
select * from blog limit #{curIndex} , #{pageSize}
</select>
第一个问题是我们要在 Java 代码里面去计算起止序号;第二个问题是:每个需要翻
页的 Statement 都要编写 limit 语句,会造成 Mapper 映射器里面很多代码冗余。
那我们就需要一种通用的方式,不需要去修改配置的任何一条 SQL 语句,只要在我
们需要翻页的地方封装一下翻页对象就可以了。
我们最常用的做法就是使用翻页的插件,这个是基于 MyBatis 的拦截器实现的,比
如 PageHelper。
// pageSize 每一页几条
PageHelper.startPage(pn, 10);
List<Employee> emps = employeeService.getAll();
// navigatePages 导航页码数
PageInfo page = new PageInfo(emps, 10);
return Msg.success().add("pageInfo", page);
PageHelper 是通过 MyBatis 的拦截器实现的,插件的具体原理我们后面的课再分
析。简单地来说,它会根据 PageHelper 的参数,改写我们的 SQL 语句。比如 MySQL
会生成 limit 语句,Oracle 会生成 rownum 语句,SQL Server 会生成 top 语句。
通用 Mapper
问题:当我们的表字段发生变化的时候,我们需要修改实体类和 Mapper 文件定义
的字段和方法。如果是增量维护,那么一个个文件去修改。如果是全量替换,我们还要
去对比用 MBG 生成的文件。字段变动一次就要修改一次,维护起来非常麻烦。
解决这个问题,我们有两种思路。
第 一 个 , 因 为 MyBatis 的 Mapper 是 支 持 继 承 的 ( 见 :
 
https://github.com/mybatis/mybatis-3/issues/35 ) 。 所 以 我 们 可 以 把 我 们 的
Mapper.xml 和 Mapper 接口都分成两个文件。一个是 MBG 生成的,这部分是固定不变
的。然后创建 DAO 类继承生成的接口,变化的部分就在 DAO 里面维护。
mybatis-standalone 工程:
public interface BlogMapperExt extends BlogMapper {
public Blog selectBlogByName(String name);
}
<?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.gupaoedu.mapper.BlogMapperExt">
<!-- 只能继承 statement,不能继承 sql、resultMap 等标签 -->
<resultMap id="BaseResultMap" type="com.gupaoedu.domain.Blog">
<id column="bid" property="bid" jdbcType="INTEGER"/>
<result column="name" property="name" jdbcType="VARCHAR"/>
<result column="author_id" property="authorId" jdbcType="INTEGER"/>
</resultMap>
<!-- 在 parent xml 和 child xml 的 statement id 相同的情况下,会使用 child xml 的 statement
id -->
<select id="selectBlogByName" resultMap="BaseResultMap" statementType="PREPARED">
select * from blog where name = #{name}
</select>
</mapper>
mybatis-config.xml 里面也要扫描:
<mappers>
<mapper resource="BlogMapper.xml"/>
<mapper resource="BlogMapperExt.xml"/>
</mappers>
所以以后只要修改 Ext 的文件就可以了。这么做有一个缺点,就是文件会增多。
思考:既然针对每张表生成的基本方法都是一样的,也就是公共的方法部分代码都
是一样的,我们能不能把这部分合并成一个文件,让它支持泛型呢?
 
太聪明了,当然可以!
编写一个支持泛型的通用接口,比如叫 GPBaseMapper<T>,把实体类作为参数传
入。这个接口里面定义了大量的增删改查的基础方法,这些方法都是支持泛型的。
自 定 义 的 Mapper 接 口 继 承 该 通 用 接 口 , 例 如 BlogMapper extends
GPBaseMapper<Blog>,自动获得对实体类的操作方法。遇到没有的方法,我们依然
可以在我们自己的 Mapper 里面编写。
我们能想到的解决方案,早就有人做了这个事了,这个东西就叫做通用 Mapper。
https://github.com/abel533/Mapper/wiki
用途:主要解决单表的增删改查问题,并不适用于多表关联查询的场景。
除了配置文件变动的问题之外,通用 Mapper 还可以解决:
1、 每个 Mapper 接口中大量的重复方法的定义;
2、 屏蔽数据库的差异;
3、 提供批量操作的方法;
4、 实现分页。
通用 Mapper 和 PageHelper 作者是同一个人(刘增辉)。
使用方式:在 Spring 中使用时,引入 jar 包,替换 applicationContext.xml 中的
sqlSessionFactory 和 configure。
<bean class="tk.mybatis.spring.mapper.MapperScannerConfigurer">
<property name="basePackage" value="com.gupaoedu.crud.dao"/>
</bean>
 
MyBatis-Plus
https://mybatis.plus/guide
MyBatis-Plus 是原生 MyBatis 的一个增强工具,可以在使用原生 MyBatis 的所有
功能的基础上,使用 plus 特有的功能。
MyBatis-Plus 的核心功能:
通用 CRUD:定义好 Mapper 接口后,只需要继承 BaseMapper<T> 接口即
可获得通用的增删改查功能,无需编写任何接口方法与配置文件。
条件构造器:通过 EntityWrapper<T>(实体包装类),可以用于拼接 SQL 语
句,并且支持排序、分组查询等复杂的 SQL。 
 

Copyright © 2024 FLGB
Powered by .NET 9.0 on Kubernetes