MyBatis系列九 实用的场景(上)

  本章主要介绍一些实用的场景,让大家了解在实际工作中应该如何使用MyBatiS。 些场景包括数据库的BLOB字段的读写、批量更新、调度存储过程、分页、使用参数作为 列名、分表等内容。这些场景在大量的编码中使用,具备较强的实用价值,这些内容都是 笔者通过实战得来的,供读者们参考。

一、数据库BLOB字段读写

  对于文件的操作,在数据库中往往是通过BLOB字段进行支持的,所以我们先看看 MyBatis对Blob字段的支持。

  在第3章配置里面,我们谈到了 typeHandler,实际上MyBatis在其默认的类型处理器 中为我们提供了 BlobTypeHandler BlobByteObjectArrayTypeHandler0 其中最常用的是 BlobTypeHandler, BlobByteObjectArrayTypeHandler 是用于数据库兼容性的,并不常用。 为了方便举例讲解,我们要先建一个数据库表,如代码清单9.1所示。

 

 然后,建一个POJO与之对应,如代码清单9.2所示。

 

   最后,给出一个映射器的XML文件,如代码清单9.3所示。

 

   我们将其注册在配置文件里面,这样便可以测试代码了,如代码清单9.4所示。

 

 

 

 

  完成上面的操作就能够正确读取BLOB字段了。

  但是更多的时候我们都应该有一个文件服务器,数据库读取文件路径即可,而不把文 件写入数据库。因为一旦文件很大,那么这个方法就很容易引起内存溢出。所以这样读写 BLOB字段时要十分小心,注意使用的场景才行。

二、批量更新

   在数据库中使用批量更新有助于提高性能。在MyBatis中,我们可以修改配置文件中 settings的defaultExecutorType来制定其执行器为批量执行器,如代码清单9-5所示。

 

 

 

   当然我们也可以用Java代码来实现批量执行器的使用,如代码清单9.6所示。

 

 

   如果你是在Spring环境中使用批量执行器,也可以这样定义Spring的Bean,如代码清 9-7所示。

 

 

   批量执行需要注意的问题是,一旦使用了批量执行器,那么在默认的情况下,它在commit后才发送SQL到数据库,此时我们需要注意代码清单9-8所示的问题。

 

 

   我们运行上面的代码后,出现了下面这行代码:

 

 

   抛出空异常,然后事务回滚。

  从代码上看,我们先插入了 role到数据库,它运行了代码role2依旧为空,所以打印其 名称的时候将抛出异常,为什么会这样呢?由于我们釆用了批量的执行器,则更新数据SQL 的执行操作是要到session.commit()中才会被MyBatis发送到数据库执行的,所以在我们执 行下面的操作之前,insert在数据库中根本没有被执行,于是便出现了这句话获取一个空对 象的情况。这是我们需要注意的。

 

  而我们在getRole方法调用前并不想提交事务,因为后面可能还有其他更新的数据库语 句要执行,这个时候我们只要执行SqlSession的flushstatements方法便可以了,它的含义 是将当前缓存的SQL发送给数据库执行。于是我们按照代码清单9-9的方法修改代码。

 

 

   这样就避免了在一个事务里面插入了数据,而select查不出来的情况,代码就能运行 成功了,我们在使用批量更新的时候要特别注意这个问题。

三、调用存储过程

1、MyBatis对存储过程提供了调用功能,并且支持对游标数据的转化功能,让我们在这 里学习它们。

(1)存储过程in和out参数的使用

  MyBatis支持存储过程,并且对它们进行封装。让我们看看MyBatis是如何实现对存储 过程支持的。这里笔者采用了 Oracle数据库,我们先来新建一个存储过程,如代码清单 9.10所示。

 

 

   我们新建了一个按角色名称模糊查询的存储过程,这里的存储过程中存在一个in参数, 两个out参数。in参数是一个输入的参数,而out参数则是一个输出的参数。首先我们把模 糊查询的结果保存到countjotal这个out参数中,并且将当前日期保存在out_date这个参 数中,然后结束过程。

  我们首先定义一个POJO来反映这个存储过程的参数,如代码清单9-11所示。

 

 

   这里我们可以轻易看到POJO和存储过程参数的对应关系。那么还需要在XML映射器 中配置它们,如代码清单9.12所示。

  这里的statementType="CALLABLE"告诉MyBatis我们将用存储过程的方式去执行它。 如果不声明它,程序将会抛出异常。参数定义,mode=IN的时候为输入参数,mode=OUT 的时候为输出参数,jdbcType定义为数据库的类型。当我们这样写的时候,MyBatis会帮我 们回填result和execDate。当然也可以使用Map,但是我们不推荐那么做,因为Map将失 去业务可读性。为了测试需要请声明一下Mapper接口,如代码清单9-13所示。

 

 

 

   这样我们便能够测试一下这个接口,让我们看看测试代码,如代码清单9.14所示。

 

 

   我们这里只是传递一个roleName参数到过程中,再通过接口调度过程,最后打印一下 返回的其他属性。让我们看看测试结果。

 

 

   我们通过日志可以发现过程已经被我们调用了,而结果也正确打印出来了。这样我们 就可以轻松使用存储过程来获取我们想要的数据了。

(2)存储过程游标

  我们在9.3.1节看到了 in和out参数的使用过程,还是比较简单的,但是在存储过程中 往往还需要返回游标。MyBatis对存储过程的游标提供了一个JdbcType=CURSOR的支持, 它可以智能地把游标读到的数据通过配置的映射关系映射到某个类型的POJO上,方便了 我们的使用,让我们看看它的用法。

  这里我们使用角色名称p_role_name查询角色,但是往往查询需要考虑分页的效果, 所以新加了 p_start和p_end参数来确定从数据库的第几行到第几行,从而确定分页。而分 页还需要一个总数,我们用存储过程的。ut参数r_count记录,而查询到的具体角色用游标 ref_cur来记录,遍历它将可以得到对应查询出来的记录。编写过程如代码清单9.15所示。

 

 

 

 

 

   这里统计了满足条件的总数,并用游标打开返回满足条件的模糊查询记录。这个游标中每行的数据需要一个POJO进行保存,我们先定义游标返回的POJO,如代码清单9-16所示。

 

 

 

 

 

   显然POJO和游标的返回值是——对应的。但是返回的不单单是游标,还有另外一个 总数和其他参数。那么让我们在游标的POJO的基础上再定义一个POJO,如代码清单9-17 所示。

 

 

 

 

 

 

   这里我们是以roleList来保存游标的数据的,count代表总数。然后定义游标返回的映 射规则,如代码清单9-18所示。

 

 

   首先我们定义了一个roleMap,它能满足游标返回对POJO的映射,这样我们就在过程 游标输出参数里面定义了。

 

 

   此时MyBatis就知道游标的数据集可以依赖于roleMap定义的规则去转化为roleList列表对象。其他的参数规则与9.3.1节讲到的in和out参数规则相同。

  最后,我们定义接口,如代码清单9.19所示。

 

 

   现在测试这段代码,如代码清单9.20所示。

 

 

   运行一下代码,我们便可以得到想要的结果。

 

 

 

 

 

   一个游标便被映射成了我们想要的POJO对象返回给调用者了。

四、分表

  在大型互联网中应用表的数据会很多,为了减少单表的压力,提高性能,我们往往会 考虑分表的算法。

  在实际工作中,比如大型公司的账单表(t_bill)可能有上亿条,对于这种情况我们往 往需要进行分表处理。账单表有许多数据,我们可以把2015年的账单保存在表(t_bill_2015) 中,2016年的账单保存在表(t_bill_2016)中,未来我们还需要建2020年的表(t_bill_2020)o MyBatis允许我们把表名作为参数传递到SQL中,这样就能迅速解决这些问题了。

  假设有一个场景,用户希望知道年份和账单编号(id)以查找账单,那么我们可以知 道两个参数,即年份和id。其中年份对账单表的名称产生影响,id则是我们查询的参数。

  让我们先定义接口参数,如代码清单9.21所示。

 

 

 很普通的定义,然后我们看看映射器XML的定义代码,如代码清单9.22所示。

 

 

   $(year}的含义是直接让参数加入到SQL中。换句话说,我们可以使用这样的一个规则: 让SQL的任何部分都可以被参数改写,包括列名,以此来满足不同的需求。但是这样是危 险的,比如把year参数修改为1900,那么这条语句的SQL就变为了查询t_bill_1900,而这个表根本就不存在,这会导致发生错误。如果不是很有必要,笔者不推荐使用。对于参 数笔者还是建议使用“#{}”的形式。让我们测试代码清单9-23。

 

 

   运行一下,得到下面的结果。

 

 

   从日志我们可以看出表名参数传递完全成功。

五、分页

  MyBatis具有分页功能,它里面有一个类——RowBounds,我们可以使用RowBounds 分页。但是使用它分页有一个很严重的问题,那就是它会在一条SQL中查询所有的结果出 来,然后根据从第几条到第几条取出数据返回。如果这条SQL返回很多数据,毫无疑问, 系统就很容易抛出内存溢出的异常。因此我们需要用其他方法去处理它。这里将分别讨论 RowBounds传递参数的分页方法和使用插件的SQL分页方法。

1、RowBounds 分页

  RowBounds分页是MyBatis的内置功能,在任何的select语句中都可以使用它。我们 来掌握一下RowBounds的源码,如代码清单9-24所示。

 

 

 

 

 

   RowBounds主要定义了两个参数,offset和limit。其中,offset代表从第几行开始读取 数据,而limit则是限制返回的记录数。在默认的情况下,offset的默认值为0,而limit 则是Java所允许的最大整数(2147483647)o不过在一些大数据的场合,一次性取出大量 的数据,比方说从一张表中一次性取出上百万条记录,这对内存的消耗是很大的,性能 差不说,这么多的数据还会引起内存溢出的问题,所以在大数据的查询场景下要慎重使 用它。

  我们看一个简单的查询,通过角色名称模糊查询角色信息,如代码清单9.25所示。

 

 

   接口定义需要修改为下面的形式,如代码清单9.26所示。

 

 

   这样便可以使用这个参数了,现在让我们测试一下代码清单9-27。

  测试结果如下。

 

 

 

 

 

   显然系统限制了 5条记录,在一些不需要考虑大数据量的场景下我们可以使用它,比 较方便和简易。

  注意,虽然RowBounds分页在任何的select语句中都可以使用,但是它是在SQL査询 出所有结果的基础上截取数据的,所以在大数据量返回的SQL中并不适用。RowBounds 分页更适合在一些返回数据结果较少的查询中使用。

2、插件分页

  9.5.1节我们谈到了 RowBounds分页的不足,大数据量下会常常发生内存溢出,为了 避免这个问题,我们需要修改SQL。因此,我们往往需要提供一个插件重写SQL来进行分 页,以避免大数据量的问题。

  在编写插件之前,我们需要回顾第6章和第7章的内容,只有在掌握了 SqlSession 四大对象的运作过程和插件开发的过程,才能写出安全高效的插件。

  分页插件是MyBatis中最为经典和常用的插件,所以首先要确定拦截方法,通过第6 章的学习我们知道,SQL的预编译是在StatementHandler对象的prepare方法中进行的,因 此我们需要在此方法运行之前去创建计算总数SQL,并且通过它得到查询总条数,然后将 当前要运行的SQL改造为分页的SQL,这样就能保证SQL分页。

  为了方便分页插件的使用,这里先定义一个POJO对象,如代码清单9-28所示。

 

 

   这样就可以通过这个POJO去定义当前的页码,每页的条数,是否启用插件,是否检 测当前页码的有效性,通过这些属性可以控制插件的行为。而total和totalPage则是等待插 件回填的两个数据,通过回填的数据,调用者就可以轻易得到这条SQL运行的总数和总页 数。

  有了思路,就要去确定方法签名,MyBatis插件要求提供3个注解信息:拦截对象类型(type,只能是四大对象中的一个),方法名称(method)和方法参数(args)o由于我们 拦截的是StatementHandler对象的prepare方法,它的参数是Connnection对象’所以就可 以得到如代码清单9-29所示的分页插件签名。

 

 

   通过第7章的学习,大家都知道插件需要实现Interceptor接口,它定义了 3个方法:

  • intercept 。
  • plugin 。
  • setProperties 。

  定义分页POJO属性的一些默认值,有了默认值可以更加方便地使用分页插件。我们 可以通过插件接口所提供的setProperties(Propterties porps)方法进行设置,因此只要在分页 插件中配置这些默认值就可以了。而plugin()方法用于生成代理对象,可以使用MyBatis 方法Plugin.wrap(),至于其原理请查看第7章的内容。我们很快就可以完成plugin方法和 setProperties方法,如代码清单9-30所示。

 

 

 

 

 

   这里使用了 setProperties。方法去设置配置的参数得到默认值,然后通过Plugin.wrap() 方法去生产动态代理对象,一般而言我们都是那么使用的。

  现在我们讨论intercept方法,这是我们的重点。这里支持3种传递分页参数的方法:

    继承PageParams的POJO作为参数;

    使用注解@卩3!11传递PageParams对象;

    使用Map传递参数。

  使用其中任意一种都是支持的,稍后会给出分离分页参数的方法。

  这里需要先统计当前SQL运行可以返回的总条数。因此,我们先要构造统计总条数的SQL,然后运行它得到总条数,再通过每页多少条的pageSize进而算岀最大页数,回填之 前定义的POJOo而拿到当前运行的SQL去构建统计总条数的SQL还是比较容易的,但是 这里的难点是给构造的计算总条数SQL设置参数。这是头疼的问题,不过应该注意到它和 查询语句的参数是一致的,因此可以利用MyBatis自身提供的类来设置参数,在第6章讲 述过它是通过ParameterHandler对象完成的,因此需要构建一个新的ParameterHandler对象, 在MyBatis中默认是使用DefaultParameterHandler来实现ParameterHandler的,使用它就可 以给总条数SQL设置参数,所以先看看它的构造方法,如代码清单9.31所示。

 

 

   其中,用当前查询语句的上下文便可以得到mappedStatement和parameterobject, BoundSql则要使用统计总数的SQL。因此,在构建新的ParameterHander之前,需要构建 一个新的BoundSql,它的构造方法如代码清单9-32所示。

 

 

   configuration, parameterMappings parameterobject 都可以在当前执行查询 SQL BoundSql中获得,而我们仅仅需要修改统计的SQL而已。我们来看看intercept的实现,如 代码清单9・33所示。

 

 

 

 

 

   其中加粗的代码是需要后续讨论的方法。首先需要从代理对象中分离出真实对象,通 MetaObject绑定这个非代理对象来获取各种参数值,这是插件中常常用到的方法。让我 们看看获取真实对象的方法,如代码清单9.34所示。

 

 

   里从BoundSql中获取我们当前要执行的SQL,如果是select语句我们才进行分页处 理,否则直接通过反射执行原有的prepare方法,所以这里有一个判断的方法,如代码清单 9-35所示。

 

   这个时候需要获取分页参数。参数可以是Map对象,也可以是POJO,或者通过@Param注解。这里支持继承PageParams或者Map。这里支持继承PageParams或者Map,从映射 器的内部组成的参数规则可以知道@Param方式在MyBatis也是一种Map传参。获取分页 参数的方法,如代码清单9.36所示。

 

   判断参数是否是一个map,如果是map,则遍历map找到分页参数,如果不是map,就判断它是不是继承了 PageParams类,如果是就直接返回。一旦得到的这个分页参数为null 或者分页参数指示不启用插件,那么就直接执行原来拦截的方法返回。

 

  得到分页参数后,要获取总数。获取总数是分页插件最难的部分,但是根据之前的分 析我们也有了应对的方法,这个获取总数的方法,如代码清单9.37所示。

 

 

 

   我们从BoundSql中获取了当前需要执行的SQL,对它进行改写就可以得到我们统计的 SQL,然后使用Connection预编译。设置参数是难点,因为参数规则总数和当前要执行的 查询是一致的,所以使用MyBatis提供的ParameteHandler进行参数设置即可。在此之前我 们分析过,需要构建一个BoundSql对象,而除了计算总数的SQL,所有的参数都可以从原 来的BoundSql对象中获得。然后进一步利用MyBatis提供的DefaultParameterHandler构建 ParameterHandler对象,并使用setParameters设置参数,釆用JDBC的方式计算出总数并将 其返回,但是这里不能够关闭Connection对象,因为后面的查询还需要用到它。

  得到这个总数后将它回填到分页参数中,这样调用者就可以得到这两个在分页中很重 要的参数,如代码清单9-38所示。

 

   后,根据分页参数的设置判断是否启用检测页码正确性的处理,当当前页码大于最大页码的时候抛出异常,提示错误,如代码清单9.39所示。

 

   这里根据设置的参数,判断是需要检测当前页码的有效性,当无效的时候抛出异常, 这样MyBatis就会停止以后的工作,如果正常就继续。最后,我们修改当前SQL为分页的 SQL,如代码清单9.40所示。

 

 

 

   首先,从对象中获取当前需要执行的SQL,将其改写为分页的SQL。然后,回填到对 象中,但是改写后我们多加了两个分页参数,因此调度原有的方法(invocation.proceedQ) 后还差这两个参数没有设置。所以我们在后面再设置它,这样就可以调用原来的prepare 法对SQL进行预编译,完成了使用插件的任务,以后我们的查询都可以得到分页。

  从上面的分析看,我们只有对MyBatis的四大对象十分了解,才能编写出想要的插件, 所以第6章和第7章是这个分页插件的学习基础。注意,在MyBatis中使用插件要慎重, 因为插件将覆盖原有对象的方法,所以必须慎用插件,能够不用尽量不要用它。

 

 

posted @ 2020-09-05 21:07  跃小云  阅读(453)  评论(0编辑  收藏  举报