mybatis大文本字段速度 查寻字段是否包含数组里的其中一个

Mybatis最入门---动态查询(foreach)
本文,我们来介绍使用Mybatis提供的标签实现我们某些循环增改删差的需求。官方文档中的内容过于简陋,于是,博主筛选出比较全面讲述foreach用法的的内容,并且配有例子。希望各位看官能够手动敲一遍下面的例子,达到快速学习的目的。
准备工作:
a.操作 :win7 x64
b.基本软件:MySQL,Mybatis,SQLyog
--------------------------------------------------------------------------------------------------------------------------------------------------------
我们先来叙述中可以供我们使用的属性有哪些:
循环体中的具体对象。支持属性的点路径访问,如item.age,item.info.details。
具体说明:在list和数组中是其中的对象,在map中是value。
该参数为必选。
collection
要做foreach的对象,作为入参时,List对象默认用list代替作为键,数组对象有array代替作为键,Map对象没有默认的键。
当然在作为入参时可以使用@Param(&keyName&)来设置键,设置keyName后,list,array将会失效。 除了入参这种情况外,还有一种作为参数对象的某个字段的时候。举个例子:
如果User有属性List ids。入参是User对象,那么这个collection = &ids&
如果User有属性I其中Ids是个对象,Ids有个属性L入参是User对象,那么collection = &ids.id&
上面只是举例,具体collection等于什么,就看你想对那个元素做循环。
该参数为必选
元素之间的分隔符,例如在in()的时候,separator=&,&会自动在元素中间用&,&隔开,避免手动输入逗号导致sql错误,如in(1,2,)这样。该参数可选。
foreach代码的开始符号,一般设置为&(&和close=&)&合用。常用在in(),values()时。该参数可选。
foreach代码的关闭符号,一般设置为&)&和open=&(&合用。常用在in(),values()时。该参数可选。
在list和数组中,index是元素的序号,在map中,index是元素的key,该参数可选。【这个具体用法见下文】
--------------------------------------------------------------------------------------------------------------------------------------------------------
【本文我们将给出完整的工程代码,请读者一定在本地运行一遍,加深理解】
1.首先,创建我们的Spring09工程,工程结构图如下:
2.pom文件内容如下:
org.mybatis
mysql-connector-java
3.配置文件jdbc.properties内容如下:
jdbc.driverClassName=com.mysql.jdbc.Driver
jdbc.url=jdbc:mysql://localhost:3306/mybatis
jdbc.username=root
jdbc.password=1234
4.日志配置文件log4j.properties内容如下:
log4j.rootLogger=debug,stdout
log4j.appender.stdout=org.apache.log4j.ConsoleAppender
log4j.appender.stdout.layout=org.apache.log4j.PatternLayout
log4j.appender.stdout.layout.ConversionPattern=%5p [%t] - %m%n
5.mybatis-config.xml配置文件内容如下:
6.UserInfo.java文件内容如下:
@SuppressWarnings(&serial&)
public class UserInfo implements Serializable {
//set,get,构造函数,toString,请自行补充
7.user.java文件内容如下:
@SuppressWarnings(&serial&)
public class User implements Serializable{
//set,get,构造函数,toString,请自行补充
8.SqlSessionFactoryUtil.java内容如下:
package com.csdn.ingo.
import java.io.InputS
import org.apache.ibatis.io.R
import org.apache.ibatis.session.SqlS
import org.apache.ibatis.session.SqlSessionF
import org.apache.ibatis.session.SqlSessionFactoryB
public class SqlSessionFactoryUtil {
private static SqlSessionFactory sqlSessionF
public static SqlSessionFactory getSqlSessionFactory(){
if(sqlSessionFactory==null){
InputStream inputStream=
inputStream=Resources.getResourceAsStream(&mybatis-config.xml&);
sqlSessionFactory=new SqlSessionFactoryBuilder().build(inputStream);
}catch(Exception e){
e.printStackTrace();
return sqlSessionF
public static SqlSession openSession(){
return getSqlSessionFactory().openSession();
--------------------------------------------------------------------------------------------------------------------------------
准备工作结束,马上开始我们的讲解,请睁大眼见哦!
--------------------------------------------------------------------------------------------------------------------------------
一.参数为数组Array。
1.UserTest的单元测试方法为:
public void testForEachArray() {
String[] sa = new String[]{&admin&,&customer&,&customer2&};
UserInfoDao userInfo = sqlSession.getMapper(UserInfoDao.class);
List UIList = userInfo.selectUserInfoByForEachArray(sa);
for (UserInfo ui : UIList) {
System.out.println(ui.toString());
} catch (Exception e) {
e.printStackTrace();
2.UserInfoDao.java的内容如下:【注意这里接口的形参为数组类型】
public interface UserInfoDao {
List selectUserInfoByForEachArray(String[] sa);
3.UserInfoMapper.xml的内容如下:
a。这里的if标签能够判断出数组是否为空。但是没有判断出数组中的内容是否为空。即,当数组中有内容时或者数组为null时,该SQL语句能够正常执行,但是如果数组为空的话,if判断的结果为真,但foreach执行0次。这种情况下,Mybatis会组装出1条错误的sql语句。换句话说这里if是多余的。
b。这里collection属性配置为&array&
c。index在这条语句中未使用,所以是可以缺省的。移除也不会引起错误
d。open,close只会在开始与结尾出现一次。
e。separator会使用配置的&,&来每次间隔标签内的内容。
f。#{item}中的item必须与中的item属性的值&item&保持一致。
------------------------------
4.运行单元测试方法,应该能够看到如下输出:【各位看官可以自行变化数组内容,观察控制台输出】
-------------------------------------------------------------------------------------------------------------------------------------
二。参数为List
1.在UserTest中新增单元测试方法:
public void testForEachList() {
List userlist = new ArrayList();
userlist.add(&admin&);
userlist.add(&customer&);
userlist.add(&customer2&);
UserInfoDao userInfo = sqlSession.getMapper(UserInfoDao.class);
List UIList = userInfo.selectUserInfoByForEachList(userlist);
for (UserInfo ui : UIList) {
System.out.println(ui.toString());
} catch (Exception e) {
e.printStackTrace();
2.在UserInfoDao.java增加接口内容如下:
List selectUserInfoByForEachList(List sl);
3.在UserInfoDaoMapper.xml中,增加如下内容:
a.与上面类似:这里的if标签能够判断出List是否为空。但是没有判断出List中的内容是否为空。即,当List中有内容时或者List为null时,该SQL语句能够正常执行,但是如果List为空的话,if判断的结果为真,但foreach执行0次。这种情况下,Mybatis会组装出1条错误的sql语句。换句话说这里if是多余的。
b。这里collection属性配置为&list&
c。index在这条语句中未使用,所以是可以缺省的。移除也不会引起错误
d。其他内容请参考上面array中的解释
-------------------------------
4.运行单元测试方法,应该能够看到如下输出:【各位看官可以自行变化数组内容,观察控制台输出】
-------------------------------------------------------------------------------------------------------------------------------------
三。参数为Map
1.在UserTest中新增单元测试方法:
public void testForEachMap() {
Map map = new HashMap();
List userlist = new ArrayList();
userlist.add(&admin&);
map.put(&userids&, userlist);
UserInfoDao userInfo = sqlSession.getMapper(UserInfoDao.class);
List UIList = userInfo.selectUserInfoByForEach(map);
for (UserInfo ui : UIList) {
System.out.println(ui.toString());
} catch (Exception e) {
e.printStackTrace();
2.在UserInfoDao.java增加接口内容如下:
List selectUserInfoByForEach(Map map);
3.在UserInfoDaoMapper.xml中,增加如下内容:
a.与上面类似:这里的if标签能够判断出map中否包含userids这个key,其对应的value可以任意。但是没有判断出value中的内容是否为空。即,当value中有内容时或者key不存在时,该SQL语句能够正常执行,但是如果value为空的话,if判断的结果为真,但foreach执行0次。这种情况下,Mybatis会组装出1条错误的sql语句。换句话说这里if是多余的。
b.这里collection属性配置为map中list对应的key的值,而不是value或者&list&
c.各位看官请注意这里的#{ParamsId}是与属性item中的ParamsId对应的
----------------------------------
4.运行单元测试方法,应该能够看到如下输出:【各位看官可以自行变化数组内容,观察控制台输出】
-------------------------------------------------------------------------------------------------------------------------------------
四。特别的index
1.在UserTest中新增单元测试方法:
public void testForeachIndex() {
Map map = new HashMap();
Map user=new HashMap();
user.put(&1&, &aaa&);
user.put(&2&, &bbb&);
user.put(&3&, &ccc&);
user.put(&4&, &ddd&);
user.put(&5&, &eee&);
map.put(&map&, user);
UserDao userDao = sqlSession.getMapper(UserDao.class);
int re = userDao.insertUserByForEachIndex(map);
if(re==1){
System.out.println(&success&);
sqlSession.commit();
} catch (Exception e) {
e.printStackTrace();
2.在UserDao.java增加接口内容如下:
public interface UserDao {
int insertUserByForEachIndex(Map map);
3.在UserDaoMapper.xml中,增加如下内容:
insert into sysuser (id,password) values
(#{id},#{item})
博主这里找了官方文档,其给出的解释是:当使用可迭代对象或者数组时,index是当前迭代的次数,item的值是本次迭代获取的元素。当使用字典(或者Map.Entry对象的集合)时,index是键,item是值。
这里我们又参考其他博文中给出的解释,设计上面的示例,供各位看官学习。
作为示例,请读者特别留心SQL语句中所诠释的官方文档的意思。即,index对应key,item对应value。
鉴于篇幅的关系,更多用法就请各位举一反三吧!
----------------------------------
4.运行单元测试方法,应该能够看到如下输出:
-------------------------------------------------------------------------------------------------------------------------------------
至此,Mybatis最入门---动态查询(foreach)结束
特别备注:
关于的用法需要具体应用场景和大量的练习,才能运用自如,请各位客官开心的敲代码吧!在XML 中支持的几种标签:  o if  o choose、when、otherwise  o where  o set  o trim   o foreach
OGNL 表达式
1. el or e22. el and e23. el == e2 或 el eq e24. el != e2 或 el neq e25. el lt e2 :小于6. el lte e2 :小于等于,其他表示为gt(大于)、gte(大于等于)7. el + e2 、el - e2 、e1 * e2 、e 1 / e2 、el % e28. !e 或 not e :非,取反9. e.method(args) : 调用对象方法JO. e.property: 对象属性值11. el[e2]:按索引取值(List、数组和Map)12. @class@method(args):调用类的静态方法13. @class@field:调用类的静态字段值map['userName']或map.userName来获取map中key为userName的值。
& if test= "@tk.mybatis.util.StringUtil@isNotEmpty(userName)"&   and user_name like concat('%', #{userName}, '%')&/if&其中StringUtil 类如下:public class StringUtil {   public static boolean isEmpty(String str) {
    return str == null || str.length() == 0;   }
  public static boolean isNotEmpty(String str) {
   return !isEmpty(str) ;   }}
if 标签有一个必填的属性test, test 的属性值是一个符合OGNL 要求的判断表达式,表达式的结果可以是true 或false ,除此之外所有的非0 值都为true ,只有0 为false。
条件查询1、判断条件property != null 或property == null,适用于任何类型的字段,用于判断属性值是否为空。2、判断条件property != '' 或 property == '', 仅适用于String 类型的宇段,用于判断是否为空字符串。3、判断条件list != null and list.size()&0,判断一个集合是否为空。4、当有多个判断条件时,使用and 或or 进行连接,嵌套的判断可以使用小括号分组,and 相当于Java 中的与(&&),or 相当于Java 中的或(||)。条件查询&select id=""& select * from sys_user where 1 = 1
&if test= "userName != null and userName != '' "&
  and user_name like concat('%', #{userName}, '%')
&if test = "userEmail != null and userEmail !='' " &
  and user_email = #{userEmail}
&/if&&/select&1 = 1 有两个作用,作用1:防止报错;作用2:全表查询。
条件更新现在要实现这样一个需求,只更新有变化的字段。更新的时候不能将原来有值但没有发生变化的字段更新为空或null。通过if标签可以实现这种动态列更新。&update id=""& update sys_user set
&if test="userName != null and userName !='' "&
  user_name= #{userName},
&if test="userPassword != null and userPassword !='' "&
  user_password= #{userPassword},
&if test="userEmail != null and userEmail != '' "&
  user_email = #{userEmail},
  id = #{id} where id = #{id}&/update&注意:where 关键字前面的id = #{id},防止报错。1、如果全部条件都为空或null,sql就是 --& update sys_user set where id = #{id}。2、查询条件只有一个不是null 也不是空(假设是userName),sql就是 --& update sys_user set user_name= #{userName}, where id= #{id}
条件插入在数据库表中插入数据的时候,如果某一列的参数值不为空,就使用传入的值,如果传入参数为空,就使用数据库中的默认值,而不使用传入的空值。使用if也可以实现这种动态插入列的功能。&insert id="" useGeneratedKeys="" keyProperty=""& insert into sys_user(
user_name, user_password,
&if test ="userEmail != null and userEmail !='' "&
  user_email,
user_info, head_img, create_time) values(
#{userName}, #{userPassword},
&if test="userEmail != null and userEmail !='' "&
  #{userEmail},
#{userInfo}, #{headImg,jdbcType=BLOB},
#{createTime, jdbcType = TIMESTAMP})&/insert&
if标签提供了基本的条件判断,但是它无法实现if...else if...else...的逻辑,要想实现这样的逻辑,就需要用到choose when otherwise标签。choose元素中包含when和otherwise两个标签,一个choose中至少有一个when,有0个或者l个otherwise。
choose when when when 是if...else if...else if的关系,只会走其中的一条路,假如第一个when返回true,那么即使第二个第三个都为true,条件里也不会有第二个和第三个。choose when 和下面的where 相比,没有剔除第一个条件前面的and或or的功能。&select id= "" resultType="test.mybatis.simple.model.SysUser"&select id , user_name, user_password , user_email, user_info, head_img, create_time from sys_userwhere 1 = 1&choose&  &when test = "id != null"&    and id= #{id}  &/when&  &when test= "userName != null and userName !='' "&    and user_name = #{userName}  &/when&  &otherwise&    and 1 = 2  &/otherwise&&/choose&&/select&
where标签的作用:
1、当if条件都不满足的时候,where元素中没有内容,SQL中不会出现where;2、紧跟在where元素后面的第一个查询条件前面如果以and 或 or开头,那么剔除;3、where 标签,或where if 标签组合,没有给条件前面添加and 或 or 的功能,所以and或or一定要自己手动添加。
set标签的作用:
1、如果该标签包含的元素中有内容,就插入一个set,如果set元素中没有内容,不插入set标签,出现SQL错误,所以为了避免错误产生,类似id=#{id}这样必然存在的赋值仍然有保留的必要。从这一点来看,set标签并没有解决全部的问题,使用时仍然需要注意。2、如果最后一个条件以逗号结尾,就将这个逗号剔除;其它条件的逗号不会加也不会减。
where和set标签的功能都可以用trim标签来实现,并且在底层就是通过TrimSqlNode实现的。where标签对应trim的实现如下:&trim prefix="WHERE" prefixOverrides="AND |OR " &...&/trim&这里的AND和OR后面的空格不能省略,为了避免匹配到andes、orders等单词。实际的prefixeOverrides 包含"AND"、"OR"、"AND\n"、"OR\n "、"AND\r"、"OR\r"、"AND\t"、"OR\t",不仅仅是上面提到的两个带空格的前缀。set标签对应的trim实现如下。&trim prefix="SET" suffixOverrides="," &...&/trim&trim 标签有如下属性。prefix:当trim 元素内包含内容时,会给内容增加prefix 指定的前缀。suffix:当trim 元素内包含内容时,会给内容增加suffix 指定的后缀。prefixOverrides:当trim 元素内包含内容时,会把内容中匹配的前缀字符串去掉。suffixOverrides:当trim 元素内包含内容时,会把内容中匹配的后缀字符串去掉。
foreach可以对数组、Map或实现了iterable接口(如List、Set)的对象进行遍历。数组在处理时会转换为List对象,因此foreach遍历的对象可以分为两大类:Iterable类型和Map类型。collection:必填,值为要迭代循环的属性名。这个属性值的情况有很多。item:变量名,值为从迭代对象中取出的每一个值,当迭代的对象是Map类型时,这个值为Map的value。index:索引的属性名,在集合数组情况下值为当前索引值,当迭代的对象是Map类型时,这个值为Map的key。open:整个循环内容开头的字符串。close:整个循环内容结尾的字符串。separator:每次循环的分隔符。
collection的属性设置方法:以下代码是DefaultSqlSession 中的方法,也是默认情况下的处理逻辑: private Object wrapCollection(final Object object) {
  if (object instanceof Collection) {
    StrictMap&Object& map = new StrictMap&Object&();
    map.put("collection", object);
      if (object instanceof List) {
        map.put("list", object);
        
      }
  } else if (object != null && object.getClass().isArray()) {
    StrictMap&Object& map = new StrictMap&Object&();
    map.put("array", object);
   }当参数类型为集合的时候,默认会转换为Map 类型,并添加一个key 为"collection",如果参数类型是List 集合,那么就继续添加一个key 为"list",这样,当collection="list"时,就能得到这个集合,并对它进行循环操作。当参数类型为数组的时候,也会转换成Map类型,默认的key为"array",使用数组参数时,就需要把foreach标签中的collection属性值设置为array。
List --& list 或者 collectionset
--& collection数组 --& arraymap
--& _parameter推荐使用@Param 来指定参数的名字,这时collection 就设置为通过@Param 注解指定的名字。
SQL语句中有时会使用IN关键字,例如idin(1,2,3)。可以使用${ids}方式直接获取值,但这种写法不能防止SQL注入,想避免SQL注入就需要用#{}的方式,这时就要配合使用foreach标签来满足需求。foreach 实现in 集合或数组List&SysUser& selectByIdList(List&Long& idList);&select id= "selectByIdList" resultType= "test.mybatis.simple.model.SysUser"& select id,user_name,user_password,user_email,user_Info,head_img,create_time
from sys_user
where id in
&foreach collection= "list" open="("
close=")" separator= "," item= "id" index = "index"&
&/foreach&&/select&
foreach 实现批量插入如果数据库支持批量插入,就可以通过foreach 来实现。批量插入是SQL-92 新增的特性,目前支持的数据库有DB2, SQL Server 2008 及以上版本、PostgreSQL8.2及以上版本、MySQL、SQLite 3.7.11及以上版本、H2 。批量插入的语法如下。INSERT INTO tablename (column-a, [column-b, ... ]) VALUES ('value-la', ['value-lb', ...]), ('value-2a', ['value-2b', ...]), ...int insertList(List&SysUser& userList); &insert id="insertList"& insert into sys_user(   user_name , user_password, user_email, user_info, head_img, create_time) values
&foreach collection ="list" item= "user" separator=","&
    #{user.userName}, #{user.userPassword}, #{user.userEmail},
    #{user.userlnfo}, #{user.headlmg, jdbcType=BLOB} ,
    #{user.createTime, jdbcType=TIMESTAMP})
&/foreach&&/insert&MyBatis支持批量新增回写主键值的功能(该功能由本书作者提交),这个功能首先要求数据库主键值为自增类型, 同时还要求该数据库提供的JDBC 驱动可以支持返回批量插入的主键值(JDBC 提供了接口,但并不是所有数据库都完美实现了该接口),因此到目前为止,可以完美支持该功能的仅有MySQL 数据库。由于SQLServer 数据库官方提供的JDBC只能返回最后一个插入数据的主键值,所以不能支持该功能。如果要在MySQL 中实现批量插入返回自增主键值, 只需要在原来代码基础上进行如下修改即可。注意,mybatis版本要是3.3.1或以上。&insert id= "insertList" useGeneratedKeys ="true" keyProperty="id"&
foreach 实现动态UPDATEint updateByMap(Map&String, Object& map);&update id=""&   update sys_user
&foreach collection="_parameter" item="val" index="key" separator=","&
  ${key} = #{val}
&/foreach&
where id = #{id}&/update&
阅读(...) 评论()博客分类:
Mybatis 的物理分页是应用中的一个难点,特别是配合检索和排序功能叠加时更是如此。
我在最近的项目中开发了这个通用分页器,过程中参考了站内不少好文章,新年第一天,特此发文回馈网站。
特别鸣谢 paginator项目 ( ) ,阅读源码帮助很大。
项目框架是 SpringMVC+Mybatis, 需求是想采用自定义的分页标签,同时,要尽量少的影响业务程序开发的。如果你已经使用了JS框架( 如:Ext,EasyUi等)自带的分页机能,是属于前端分页,不在本文讨论范围。
【关于问题】
大多数分页器会使用在查询页面,要考虑以下问题:
1)分页时是要随时带有最近一次查询条件
2)不能影响现有的sql,类似aop的效果
3)mybatis提供了通用的拦截接口,要选择适当的拦截方式和时点
4)尽量少的影响现有service等接口
【关于依赖库】
Google Guava
作为基础工具包
Commons JXPath
用于对象查询
(1/23日版改善后,不再需要)
向前台传送Json格式数据转换用
【关于适用数据库】
现在只适用mysql
(如果需要用在其他数据库可参考 paginator的Dialect部分,改动都不大)
首先是Page类,比较简单,保存分页相关的所有信息,涉及到分页算法。虽然“其貌不扬”,但很重要。后面会看到这个page类对象会以“信使”的身份出现在全部与分页相关的地方。
* 封装分页数据
import java.util.L
import java.util.M
import org.codehaus.jackson.map.ObjectM
import org.slf4j.L
import org.slf4j.LoggerF
import com.google.common.base.J
import com.google.common.collect.L
import com.google.common.collect.M
public class Page {
private static final Logger logger = LoggerFactory.getLogger(Page.class);
private static ObjectMapper mapper = new ObjectMapper();
public static String DEFAULT_PAGESIZE = "10";
private int pageNo;
//当前页码
private int pageS
//每页行数
private int totalR
//总记录数
private int totalP
private Map&String, String&
//查询条件
private Map&String, List&String&& paramL
//数组查询条件
private String searchU
private String pageNoD
//可以显示的页号(分隔符"|",总页数变更时更新)
private Page() {
pageNo = 1;
pageSize = Integer.valueOf(DEFAULT_PAGESIZE);
totalRecord = 0;
totalPage = 0;
params = Maps.newHashMap();
paramLists = Maps.newHashMap();
searchUrl = "";
pageNoDisp = "";
public static Page newBuilder(int pageNo, int pageSize, String url){
Page page = new Page();
page.setPageNo(pageNo);
page.setPageSize(pageSize);
page.setSearchUrl(url);
* 查询条件转JSON
public String getParaJson(){
Map&String, Object& map = Maps.newHashMap();
for (String key : params.keySet()){
if ( params.get(key) != null
map.put(key, params.get(key));
String json="";
json = mapper.writeValueAsString(map);
} catch (Exception e) {
logger.error("转换JSON失败", params, e);
* 数组查询条件转JSON
public String getParaListJson(){
Map&String, Object& map = Maps.newHashMap();
for (String key : paramLists.keySet()){
List&String& lists = paramLists.get(key);
if ( lists != null && lists.size()&0 ){
map.put(key, lists);
String json="";
json = mapper.writeValueAsString(map);
} catch (Exception e) {
logger.error("转换JSON失败", params, e);
* 总件数变化时,更新总页数并计算显示样式
private void refreshPage(){
//总页数计算
totalPage = totalRecord%pageSize==0 ? totalRecord/pageSize : (totalRecord/pageSize + 1);
//防止超出最末页(浏览途中数据被删除的情况)
if ( pageNo & totalPage && totalPage!=0){
pageNo = totalP
pageNoDisp = computeDisplayStyleAndPage();
* 计算页号显示样式
这里实现以下的分页样式("[]"代表当前页号),可根据项目需求调整
[1],2,3,4,5,6,7,8..12,13
1,2..5,6,[7],8,9..12,13
1,2..6,7,8,9,10,11,12,[13]
private String computeDisplayStyleAndPage(){
List&Integer& pageDisplays = Lists.newArrayList();
if ( totalPage &= 11 ){
for (int i=1; i&=totalP i++){
pageDisplays.add(i);
}else if ( pageNo & 7 ){
for (int i=1; i&=8; i++){
pageDisplays.add(i);
pageDisplays.add(0);// 0 表示 省略部分(下同)
pageDisplays.add(totalPage-1);
pageDisplays.add(totalPage);
}else if ( pageNo& totalPage-6 ){
pageDisplays.add(1);
pageDisplays.add(2);
pageDisplays.add(0);
for (int i=totalPage-7; i&=totalP i++){
pageDisplays.add(i);
pageDisplays.add(1);
pageDisplays.add(2);
pageDisplays.add(0);
for (int i=pageNo-2; i&=pageNo+2; i++){
pageDisplays.add(i);
pageDisplays.add(0);
pageDisplays.add(totalPage-1);
pageDisplays.add(totalPage);
return Joiner.on("|").join(pageDisplays.toArray());
public int getPageNo() {
return pageNo;
public void setPageNo(int pageNo) {
this.pageNo = pageNo;
public int getPageSize() {
return pageS
public void setPageSize(int pageSize) {
this.pageSize = pageS
public int getTotalRecord() {
return totalR
public void setTotalRecord(int totalRecord) {
this.totalRecord = totalR
refreshPage();
public int getTotalPage() {
return totalP
public void setTotalPage(int totalPage) {
this.totalPage = totalP
public Map&String, String& getParams() {
public void setParams(Map&String, String& params) {
this.params =
public Map&String, List&String&& getParamLists() {
return paramL
public void setParamLists(Map&String, List&String&& paramLists) {
this.paramLists = paramL
public String getSearchUrl() {
return searchU
public void setSearchUrl(String searchUrl) {
this.searchUrl = searchU
public String getPageNoDisp() {
return pageNoD
public void setPageNoDisp(String pageNoDisp) {
this.pageNoDisp = pageNoD
然后是最核心的拦截器了。涉及到了mybatis的核心功能,期间阅读大量mybatis源码几经修改重构,辛苦自不必说。
核心思想是将拦截到的select语句,改装成select count(*)语句,执行之得到,总数据数。再根据page中的当前页号算出limit值,拼接到select语句后。
为简化代码使用了Commons JXPath 包,做对象查询。
* 分页用拦截器
import java.sql.C
import java.sql.PreparedS
import java.sql.ResultS
import java.util.P
import org.apache.commons.jxpath.JXPathC
import org.apache.commons.jxpath.JXPathNotFoundE
import org.apache.ibatis.executor.E
import org.apache.ibatis.executor.parameter.DefaultParameterH
import org.apache.ibatis.mapping.BoundS
import org.apache.ibatis.mapping.MappedS
import org.apache.ibatis.mapping.MappedStatement.B
import org.apache.ibatis.mapping.ParameterM
import org.apache.ibatis.mapping.SqlS
import org.apache.ibatis.plugin.I
import org.apache.ibatis.plugin.I
import org.apache.ibatis.plugin.I
import org.apache.ibatis.plugin.P
import org.apache.ibatis.plugin.S
import org.apache.ibatis.session.ResultH
import org.apache.ibatis.session.RowB
@Intercepts({@Signature(type=Executor.class,method="query",args={ MappedStatement.class, Object.class, RowBounds.class, ResultHandler.class })})
public class PageInterceptor implements Interceptor{
public Object intercept(Invocation invocation) throws Throwable {
//当前环境 MappedStatement,BoundSql,及sql取得
MappedStatement mappedStatement=(MappedStatement)invocation.getArgs()[0];
Object parameter = invocation.getArgs()[1];
BoundSql boundSql = mappedStatement.getBoundSql(parameter);
String originalSql = boundSql.getSql().trim();
Object parameterObject = boundSql.getParameterObject();
//Page对象获取,“信使”到达拦截器!
Page page = searchPageWithXpath(boundSql.getParameterObject(),".","page","*/page");
if(page!=null ){
//Page对象存在的场合,开始分页处理
String countSql = getCountSql(originalSql);
Connection connection=mappedStatement.getConfiguration().getEnvironment().getDataSource().getConnection()
PreparedStatement countStmt = connection.prepareStatement(countSql);
BoundSql countBS = copyFromBoundSql(mappedStatement, boundSql, countSql);
DefaultParameterHandler parameterHandler = new DefaultParameterHandler(mappedStatement, parameterObject, countBS);
parameterHandler.setParameters(countStmt);
ResultSet rs = countStmt.executeQuery();
int totpage=0;
if (rs.next()) {
totpage = rs.getInt(1);
rs.close();
countStmt.close();
connection.close();
//分页计算
page.setTotalRecord(totpage);
//对原始Sql追加limit
int offset = (page.getPageNo() - 1) * page.getPageSize();
StringBuffer sb = new StringBuffer();
sb.append(originalSql).append(" limit ").append(offset).append(",").append(page.getPageSize());
BoundSql newBoundSql = copyFromBoundSql(mappedStatement, boundSql, sb.toString());
MappedStatement newMs = copyFromMappedStatement(mappedStatement,new BoundSqlSqlSource(newBoundSql));
invocation.getArgs()[0]= newMs;
return invocation.proceed();
* 根据给定的xpath查询Page对象
private Page searchPageWithXpath(Object o,String... xpaths) {
JXPathContext context = JXPathContext.newContext(o);
for(String xpath : xpaths){
result = context.selectSingleNode(xpath);
} catch (JXPathNotFoundException e) {
if ( result instanceof Page ){
return (Page)
* 复制MappedStatement对象
private MappedStatement copyFromMappedStatement(MappedStatement ms,SqlSource newSqlSource) {
Builder builder = new Builder(ms.getConfiguration(),ms.getId(),newSqlSource,ms.getSqlCommandType());
builder.resource(ms.getResource());
builder.fetchSize(ms.getFetchSize());
builder.statementType(ms.getStatementType());
builder.keyGenerator(ms.getKeyGenerator());
builder.keyProperty(ms.getKeyProperty());
builder.timeout(ms.getTimeout());
builder.parameterMap(ms.getParameterMap());
builder.resultMaps(ms.getResultMaps());
builder.resultSetType(ms.getResultSetType());
builder.cache(ms.getCache());
builder.flushCacheRequired(ms.isFlushCacheRequired());
builder.useCache(ms.isUseCache());
return builder.build();
* 复制BoundSql对象
private BoundSql copyFromBoundSql(MappedStatement ms, BoundSql boundSql, String sql) {
BoundSql newBoundSql = new BoundSql(ms.getConfiguration(),sql, boundSql.getParameterMappings(), boundSql.getParameterObject());
for (ParameterMapping mapping : boundSql.getParameterMappings()) {
String prop = mapping.getProperty();
if (boundSql.hasAdditionalParameter(prop)) {
newBoundSql.setAdditionalParameter(prop, boundSql.getAdditionalParameter(prop));
return newBoundS
* 根据原Sql语句获取对应的查询总记录数的Sql语句
private String getCountSql(String sql) {
return "SELECT COUNT(*) FROM (" + sql + ") aliasForPage";
public class BoundSqlSqlSource implements SqlSource {
BoundSql boundS
public BoundSqlSqlSource(BoundSql boundSql) {
this.boundSql = boundS
public BoundSql getBoundSql(Object parameterObject) {
return boundS
public Object plugin(Object arg0) {
return Plugin.wrap(arg0, this);
public void setProperties(Properties arg0) {
到展示层终于可以轻松些了,使用了文件标签来简化前台开发。
采用临时表单提交,CSS使用了Bootstrap。
&%@tag pageEncoding="UTF-8"%&
&%@ attribute name="page" type="cn.com.intasect.ots.common.utils.Page" required="true"%&
&%@ taglib prefix="c" uri="http://java.sun.com/jsp/jstl/core"%&
int current =
page.getPageNo();
int begin = 1;
int end = page.getTotalPage();
request.setAttribute("current", current);
request.setAttribute("begin", begin);
request.setAttribute("end", end);
request.setAttribute("pList", page.getPageNoDisp());
&script type="text/javascript"&
var paras = '&%=page.getParaJson()%&';
var paraJson = eval('(' + paras + ')');
//将提交参数转换为JSON
var paraLists = '&%=page.getParaListJson()%&';
var paraListJson = eval('(' + paraLists + ')');
function pageClick( pNo ){
paraJson["pageNo"] = pNo;
paraJson["pageSize"] = "&%=page.getPageSize()%&";
var jsPost = function(action, values, valueLists) {
var id = Math.random();
document.write('&form id="post' + id + '" name="post'+ id +'" action="' + action + '" method="post"&');
for (var key in values) {
document.write('&input type="hidden" name="' + key + '" value="' + values[key] + '" /&');
for (var key2 in valueLists) {
for (var index in valueLists[key2]) {
document.write('&input type="hidden" name="' + key2 + '" value="' + valueLists[key2][index] + '" /&');
document.write('&/form&');
document.getElementById('post' + id).submit();
//发送POST
jsPost("&%=page.getSearchUrl()%&", paraJson, paraListJson);
&div class="page-pull-right"&
&% if (current!=1 && end!=0){%&
&button class="btn btn-default btn-sm" onclick="pageClick(1)"&首页&/button&
&button class="btn btn-default btn-sm" onclick="pageClick(${current-1})"&前页&/button&
&%}else{%&
&button class="btn btn-default btn-sm" &首页&/button&
&button class="btn btn-default btn-sm" &前页&/button&
&c:forTokens items="${pList}" delims="|" var="pNo"&
&c:choose&
&c:when test="${pNo == 0}"&
&label style="font-size: 10 width: 20 text-align:"&ooo&/label&
&c:when test="${pNo != current}"&
&button class="btn btn-default btn-sm" onclick="pageClick(${pNo})"&${pNo}&/button&
&c:otherwise&
&button class="btn btn-primary btn-sm" style="font-weight:"&${pNo}&/button&
&/c:otherwise&
&/c:choose&
&/c:forTokens&
&% if (current&end && end!=0){%&
&button class="btn btn-default btn-sm" onclick="pageClick(${current+1})"&后页&/button&
&button class="btn btn-default btn-sm" onclick="pageClick(${end})"&末页&/button&
&%}else{%&
&button class="btn btn-default btn-sm"&后页&/button&
&button class="btn btn-default btn-sm"&末页&/button&
注意“信使”在这里使出了浑身解数,7个主要的get方法全部用上了。
page.getPageNo()
//当前页号
page.getTotalPage()
page.getPageNoDisp()
//可以显示的页号
page.getParaJson()
//查询条件
page.getParaListJson()
//数组查询条件
page.getPageSize()
//每页行数
page.getSearchUrl()
//Url地址(作为action名称)
到这里三个核心模块完成了。然后是拦截器的注册。
【拦截器的注册】
需要在mybatis-config.xml 中加入拦截器的配置
&plugin interceptor="cn.com.dingding.common.utils.PageInterceptor"&
&/plugins&
【相关代码修改】
首先是后台代码的修改,Controller层由于涉及到查询条件,需要修改的内容较多。
1)入参需增加 pageNo,pageSize 两个参数
2)根据pageNo,pageSize 及你的相对url构造page对象。(
3)最重要的是将你的其他入参(查询条件)保存到page中
4)Service层的方法需要带着page这个对象(最终目的是传递到sql执行的入参,让拦截器识别出该sql需要分页,同时传递页号)
5)将page对象传回Mode中
@RequestMapping(value = "/user/users")
public String list(
@ModelAttribute("name") String name,
@ModelAttribute("levelId") String levelId,
@ModelAttribute("subjectId") String subjectId,
Model model) {
model.addAttribute("users",userService.selectByNameLevelSubject(
name, levelId, subjectId));
return USER_LIST_JSP;
@RequestMapping(value = "/user/users")
public String list(
@RequestParam(required = false, defaultValue = "1") int pageNo,
@RequestParam(required = false, defaultValue = "5") int pageSize,
@ModelAttribute("name") String name,
@ModelAttribute("levelId") String levelId,
@ModelAttribute("subjectId") String subjectId,
Model model) {
// 这里是“信使”诞生之地,一出生就加载了很多重要信息!
Page page = Page.newBuilder(pageNo, pageSize, "users");
page.getParams().put("name", name);
//这里再保存查询条件
page.getParams().put("levelId", levelId);
page.getParams().put("subjectId", subjectId);
model.addAttribute("users",userService.selectByNameLevelSubject(
name, levelId, subjectId, page));
model.addAttribute("page", page);
//这里将page返回前台
return USER_LIST_JSP;
注意pageSize的缺省值决定该分页的每页数据行数 ,实际项目更通用的方式是使用配置文件指定。
拦截器可以自动识别在Map或Bean中的Page对象。
如果使用Bean需要在里面增加一个page项目,Map则比较简单,以下是例子。
public List&UserDTO& selectByNameLevelSubject(String name, String levelId, String subjectId, Page page) {
Map&String, Object& map = Maps.newHashMap();
levelId = DEFAULT_SELECTED.equals(levelId)?null: levelId;
subjectId = DEFAULT_SELECTED.equals(subjectId)?null: subjectId;
if (name != null && name.isEmpty()){
map.put("name", name);
map.put("levelId", levelId);
map.put("subjectId", subjectId);
map.put("page", page);
//MAP的话加这一句就OK
return userMapper.selectByNameLevelSubject(map);
前台页面方面,由于使用了标签,在适当的位置加一句就够了。
&tags:page page="${page}"/&
“信使”page在这里进入标签,让分页按钮最终展现。
至此,无需修改一句sql,完成分页自动化。
【效果图】
现在回过头来看下最开始提出的几个问题:
1)分页时是要随时带有最近一次查询条件
回答:在改造Controller层时,通过将提交参数设置到 Page对象的 Map&String, String& params(单个基本型参数) 和 Map&String, List&String&& paramLists(数组基本型)解决。
顺便提一下,例子中没有涉及参数是Bean的情况,实际应用中应该比较常见。简单的方法是将Bean转换层Map后加入到params。
2)不能影响现有的sql,类似aop的效果
回答:利用Mybatis提供了 Interceptor 接口,拦截后改头换面去的件数并计算limit值,自然能神不知鬼不觉。
3)mybatis提供了通用的拦截接口,要选择适当的拦截方式和时点
回答:@Signature(method = "query", type = Executor.class, args = {
MappedStatement.class, Object.class, RowBounds.class,
ResultHandler.class }) 只拦截查询语句,其他增删改查不会影响。
4)尽量少的影响现有service等接口
回答:这个自认为本方案做的还不够好,主要是Controller层改造上,感觉代码量还比较大。如果有有识者知道更好的方案还请多指教。
【遗留问题】
1)一个“明显”的性能问题,是每次检索前都要去 select count(*)一次。在很多时候(数据变化不是特别敏感的场景)是不必要的。调整也不难,先Controller参数增加一个 totalRecord 总记录数 ,在稍加修改一下Page相关代码即可。
2)要排序怎么办?本文并未讨论排序,但是方法是类似的。以上面代码为基础,可以较容易地实现一个通用的排序标签。
===================================== 分割线 (1/8)=======================================
对于Controller层需要将入参传入Page对象的问题已经进行了改善,思路是自动从HttpServletRequest 类中提取入残,减低了分页代码的侵入性,详细参看文章
===================================== 分割线 (1/23)=======================================
再次改善,使用ThreadLocal类封装Page对象,让Service层等无需传Page对象,减小了侵入性。拦截器也省去了查找Page对象的动作,性能也同时改善。整体代码改动不大。
===================================== 分割线 (2/21)=======================================
今天比较闲,顺便聊下这个分页的最终版,当然从来只有不断变化的需求,没有完美的方案,这里所说的最终版其实是一个优化后的“零侵入”的方案。为避免代码混乱还是只介绍思路。在上一个版本(1/23版)基础上有两点改动:
一是增加一个配置文件,按Url 配置初始的每页行数。如下面这样(pagesize 指的是每页行数):
&pager url="/user/users" pagesize="10" /&
二是增加一个过滤器,并将剩下的位于Control类中 唯一侵入性的分页相关代码移入过滤器。发现当前的 Url
在配置文件中有匹配是就构造Page对象,并加入到Response中。
使用最终版后,对于开发者需要分页时,只要在配置文件中加一行,并在前端页面上加一个分页标签即可,其他代码,SQL等都不需要任何改动,可以说简化到了极限。
【技术列表】
总结下最终方案用到的技术:
Mybatis 提供的拦截器接口实现(实现分页sql自动 select count 及limit 拼接)
Servlet过滤器+ThreadLocal
(生成线程共享的Page对象)
(实现前端共通的分页效果)
临时表单提交 (减少页面体积)
【其他分页方案比较】
时下比较成熟的 JPA 的分页方案,(主要应用在 Hibernate + Spring Data 的场合),主要切入点在DAO层,而Controller等各层接口依然需要带着pageNumber,pageSize 这些的参数,另外框架开发者还要掌握一些必须的辅助类,如:
org.springframework.data.repository.PagingAndSortingRepository
可分页DAO基类
org.springframework.data.domain.Page
抽取结果封装类
org.springframework.data.domain.Pageable
分页信息类
比较来看 本方案 做到了分页与业务逻辑的完全解耦,开发者无需关注分页,全部通过配置实现。通过这个例子也可以反映出Mybatis在底层开发上有其独特的优势。
【备选方案】
最后再闲扯下,上面的最终案是基于 Url 配置的,其实也可以基于方法加自定义注解来做。这样配置文件省了,但是要增加一个注解解析类。注解中参数 为初始的每页行数。估计注解fans会喜欢,如下面的样子:
@RequestMapping(value = "/user/users")
@Pagination(size=10)
public String list(
同样与过滤器配合使用,只是注解本身多少还是有“侵入性”。在初始行数基本不会变更时,这个比较直观的方案也是不错的选择。大家自行决定吧。
浏览 214580
有jar包maven地址就好了,或者直接上个实例其中需要的jar包在pom.xml中的配置&dependency& &groupId&com.google.guava&/groupId& &artifactId&guava&/artifactId& &version&18.0&/version&&/dependency&&dependency& &groupId&commons-jxpath&/groupId& &artifactId&commons-jxpath&/artifactId& &version&1.3&/version&&/dependency&
Smart_咚咚 写道这种方式不能通过链接访问。。post提交 我看看能改不好吧& 没啥问题& 关键是好像把所有select都拦截了。。不过估计你不定什么时候才上奇怪 我看了下代码& 好像没这个问题啊& 我自己找找吧
这种方式不能通过链接访问。。post提交 我看看能改不好吧& 没啥问题& 关键是好像把所有select都拦截了。。不过估计你不定什么时候才上
不明白为什么要引入Guava库,一个2m多的jar包,而程序中也只是使用Guava创建集合,是不是有些大材小用了。
引用引用引用引用引用[u][i][b][b][b]引用引用[list]
[*][*][*][*][*][img][*][*][*][*][*][flash=200,200][url][img][list][*][*][*][*][*][*][*][*][*][*][*][/list][/img][/url][/flash][*][*][*][*][*][/img]
[*][*][*][*][/list][/b][/b][/b][/i][/u][*][*][/list][*][/list][/list][/list][/img][/url][/url]
lisciple 写道model.addAttribute("page", page);&&&&&&&&&&&& //这里将page返回前台& 请问你最后的版本中从threadlocal拿到page还是放在model里面返回吗?能不能把这行也消灭掉?page的生成及放入model都在过滤器中,对业务代码无侵入,没有消灭的必要了吧。过滤器中的request貌似不能返回请求吧? 这个request是过来的路,不是回去的路吧?您能具体说说嘛?
model.addAttribute("page", page);&&&&&&&&&&&& //这里将page返回前台& 请问你最后的版本中从threadlocal拿到page还是放在model里面返回吗?能不能把这行也消灭掉?page的生成及放入model都在过滤器中,对业务代码无侵入,没有消灭的必要了吧。
& 上一页 1
duanhengbin
浏览: 338941 次
来自: 成都
如果传值进来是int findChaSanFang(@Para ...
searchPageWithXpath,这个方法是做什么用的啊 ...
额滴神啊 搞个分页这复杂..
&div class=&quote_title ...
&div class=&quote_title ...
(window.slotbydup=window.slotbydup || []).push({
id: '4773203',
container: s,
size: '200,200',
display: 'inlay-fix'}

我要回帖

更多关于 mybatis字段映射 的文章

更多推荐

版权声明:文章内容来源于网络,版权归原作者所有,如有侵权请点击这里与我们联系,我们将及时删除。

点击添加站长微信