Mybatis ページング方法: 1. ページングに配列を使用する 最初にすべてのデータをクエリし、次にリストから必要な部分をインターセプトします。 2. SQL ステートメントを使用してページングを実行し、SQL ステートメントの後に limit paging ステートメントを追加します。 3. ページングにインターセプタを使用し、インターセプタを介して SQL ステートメントの最後に limit ステートメントを追加して、ページング クエリを実行します。 4. RowBounds を使用してページングを実装するには、すべての適格なデータを一度に取得し、メモリ内のビッグ データを操作してページング効果を実現する必要があります。
#このチュートリアルの動作環境: Windows7 システム、Java8、Dell G3 コンピューター。
Mybatis のページング方法は 2 つのカテゴリに分類できます:
1. 論理ページング
2. 物理ページング
配列を利用したページング (論理ページング)
With SQL ステートメントのヘルプ ページング (物理ページング)
インターセプター ページング (物理ページング) インターセプターを使用して、クエリする SQL ステートメントの最後にリミット ステートメントを追加します。最良の
RowBounds はページング (論理ページング) を実装します
#1. 配列を利用したページング
原則: データベース クエリ操作中に、データベース内の条件を満たすすべてのレコードが取得され、アプリケーションの一時配列に保存されます。その後、条件を満たすすべてのレコードが次の方法で取得されます。 List の subList メソッド。
実装:
まず、データベース操作のために dao 層に StudentMapper インターフェイスを作成します。以下に示すように、インターフェイス内の配列をページングするためのクエリ メソッドを定義します。
List<Student> queryStudentsByArray();
メソッドは非常に単純です。つまり、すべてのデータを取得し、リストを通じてそれを受信し、ページングを実行します。手術。
StudentMapper.xml ファイルを作成し、クエリ SQL ステートメントを作成します。
<select id="queryStudentsByArray" resultMap="studentmapper"> select * from student </select>
SQL ステートメントを作成するときに、ページング関連の操作を実行していないことがわかります。すべての学生情報はここからご覧いただけます。
次に、サービス層でデータを取得し、ページングを実装します。
IStuService インターフェイスを定義し、ページング メソッドを定義します。
List<Student> queryStudentsByArray(int currPage, int pageSize);
currPage パラメータ Data、pageSize は、各ページに表示されるデータ項目の数を表します。
IStuService インターフェイス実装クラス StuServiceIml を作成してメソッドを実装し、取得した配列を currPage と pageSize でページ分割します。
@Override public List<Student> queryStudentsByArray(int currPage, int pageSize) { List<Student> students = studentMapper.queryStudentsByArray(); // 从第几条数据开始 int firstIndex = (currPage - 1) * pageSize; // 到第几条数据结束 int lastIndex = currPage * pageSize; return students.subList(firstIndex, lastIndex); }
subList メソッドを通じて、2 つのインデックス間のすべてのデータを取得します。
最後に、コントローラーでテスト メソッドを作成します。
@ResponseBody @RequestMapping("/student/array/{currPage}/{pageSize}") public List<Student> getStudentByArray(@PathVariable("currPage") int currPage, @PathVariable("pageSize") int pageSize) { List<Student> student = StuServiceIml.queryStudentsByArray(currPage, pageSize); return student; }
ユーザーから渡された currPage と pageSize を通じて指定されたデータを取得します。
テスト:
まず、以下に示すように、ページング効果が実装される前に取得されたすべてのデータを取得します。ブラウザに
http://localhost:8080/student/student/array/1/2と入力して、ページング データをテストします。最初のページのデータを取得し、各ページに 2 つのデータを表示します。
結果は次のとおりです: 出力は 0 ~ 2 の指定されたデータであり、配列によるページング関数が成功したことを示しています。 (ここでは関連クエリを使用しているため、大量のデータが存在すると思われます)
欠点: データベースはクエリを実行してすべてのデータを返します。必要なのは、条件を満たす非常に少量のデータだけです。要求事項。データ量が少ない場合は問題ありません。データベース データの量が大きすぎる場合、各クエリはデータベースとプログラムのパフォーマンスに大きな影響を与えます。
2. ページングに SQL ステートメントを使用する配列によるページングの欠点について学習した後、配列内でページングを実行できないことがわかりました。データベースに毎回すべてのデータが取得されます。次に、プログラムで取得した大量のデータに対して二次的な操作を実行します。これにより、多くのスペースとパフォーマンスが消費されます。そこで、プログラムで処理することなく、データベース言語で条件に合致したレコードだけを直接取得したいと考えています。このとき、SQL ステートメント ページング テクノロジが登場しました。 実装: SQL ステートメントを使用してページングを実装することも非常に簡単です。これを実現するには、クエリのステートメントを変更するだけです。つまり、SQL ステートメントの後に制限ページング ステートメントを追加するだけです。
まず、次のように SQL ステートメント クエリ メソッドを StudentMapper インターフェイスに追加します。List<Student> queryStudentsBySql(Map<String,Object> data);次に、SQL ステートメントを StudentMapper.xml ファイルに記述して、limiy キーワードでページ分割します。
<select id="queryStudentsBySql" parameterType="map" resultMap="studentmapper"> select * from student limit #{currIndex} , #{pageSize} </select>次に、IStuService インターフェイスでメソッドを定義し、StuServiceIml で SQL ページングを実装します。
List<Student> queryStudentsBySql(int currPage, int pageSize);
@Override public List<Student> queryStudentsBySql(int currPage, int pageSize) { Map<String, Object> data = new HashedMap(); data.put("currIndex", (currPage-1)*pageSize); data.put("pageSize", pageSize); return studentMapper.queryStudentsBySql(data); }SQL ページング ステートメントは次のとおりです:
select * from table limitindex, pageSize;
したがって、currIndex はサービス内で計算されます。クエリされたインデックス。テスト:
http://localhost:8080/student/student/sql/1/2
と入力して、最初のページのデータを取得します。各ページに 2 つのデータを表示します。 ######結果:###从输出结果可以看出和数组分页的结果是一致的,因此sql语句的分页也是没问题的。
缺点:虽然这里实现了按需查找,每次检索得到的是指定的数据。但是每次在分页的时候都需要去编写limit语句,很冗余。而且不方便统一管理,维护性较差。所以我们希望能够有一种更方便的分页实现。
3、拦截器分页
上面提到的数组分页和sql语句分页都不是我们今天讲解的重点,今天需要实现的是利用拦截器达到分页的效果。自定义拦截器实现了拦截所有以ByPage结尾的查询语句,并且利用获取到的分页相关参数统一在sql语句后面加上limit分页的相关语句,一劳永逸。不再需要在每个语句中单独去配置分页相关的参数了。。
首先我们看一下拦截器的具体实现,在这里我们需要拦截所有以ByPage结尾的所有查询语句,因此要使用该拦截器实现分页功能,那么再定义名称的时候需要满足它拦截的规则(以ByPage结尾),如下所示:
package com.cbg.interceptor; import org.apache.ibatis.executor.Executor; import org.apache.ibatis.executor.parameter.ParameterHandler; import org.apache.ibatis.executor.resultset.ResultSetHandler; import org.apache.ibatis.executor.statement.StatementHandler; import org.apache.ibatis.mapping.MappedStatement; import org.apache.ibatis.plugin.*; import org.apache.ibatis.reflection.MetaObject; import org.apache.ibatis.reflection.SystemMetaObject; import java.sql.Connection; import java.util.Map; import java.util.Properties; /** * Created by chenboge on 2017/5/7. * <p> * Email:baigegechen@gmail.com * <p> * description: */ /** * @Intercepts 说明是一个拦截器 * @Signature 拦截器的签名 * type 拦截的类型 四大对象之一( Executor,ResultSetHandler,ParameterHandler,StatementHandler) * method 拦截的方法 * args 参数 */ @Intercepts({@Signature(type = StatementHandler.class, method = "prepare", args = {Connection.class, Integer.class})}) public class MyPageInterceptor implements Interceptor { //每页显示的条目数 private int pageSize; //当前现实的页数 private int currPage; private String dbType; @Override public Object intercept(Invocation invocation) throws Throwable { //获取StatementHandler,默认是RoutingStatementHandler StatementHandler statementHandler = (StatementHandler) invocation.getTarget(); //获取statementHandler包装类 MetaObject MetaObjectHandler = SystemMetaObject.forObject(statementHandler); //分离代理对象链 while (MetaObjectHandler.hasGetter("h")) { Object obj = MetaObjectHandler.getValue("h"); MetaObjectHandler = SystemMetaObject.forObject(obj); } while (MetaObjectHandler.hasGetter("target")) { Object obj = MetaObjectHandler.getValue("target"); MetaObjectHandler = SystemMetaObject.forObject(obj); } //获取连接对象 //Connection connection = (Connection) invocation.getArgs()[0]; //object.getValue("delegate"); 获取StatementHandler的实现类 //获取查询接口映射的相关信息 MappedStatement mappedStatement = (MappedStatement) MetaObjectHandler.getValue("delegate.mappedStatement"); String mapId = mappedStatement.getId(); //statementHandler.getBoundSql().getParameterObject(); //拦截以.ByPage结尾的请求,分页功能的统一实现 if (mapId.matches(".+ByPage$")) { //获取进行数据库操作时管理参数的handler ParameterHandler parameterHandler = (ParameterHandler) MetaObjectHandler.getValue("delegate.parameterHandler"); //获取请求时的参数 Map<String, Object> paraObject = (Map<String, Object>) parameterHandler.getParameterObject(); //也可以这样获取 //paraObject = (Map<String, Object>) statementHandler.getBoundSql().getParameterObject(); //参数名称和在service中设置到map中的名称一致 currPage = (int) paraObject.get("currPage"); pageSize = (int) paraObject.get("pageSize"); String sql = (String) MetaObjectHandler.getValue("delegate.boundSql.sql"); //也可以通过statementHandler直接获取 //sql = statementHandler.getBoundSql().getSql(); //构建分页功能的sql语句 String limitSql; sql = sql.trim(); limitSql = sql + " limit " + (currPage - 1) * pageSize + "," + pageSize; //将构建完成的分页sql语句赋值个体'delegate.boundSql.sql',偷天换日 MetaObjectHandler.setValue("delegate.boundSql.sql", limitSql); } //调用原对象的方法,进入责任链的下一级 return invocation.proceed(); } //获取代理对象 @Override public Object plugin(Object o) { //生成object对象的动态代理对象 return Plugin.wrap(o, this); } //设置代理对象的参数 @Override public void setProperties(Properties properties) { //如果项目中分页的pageSize是统一的,也可以在这里统一配置和获取,这样就不用每次请求都传递pageSize参数了。参数是在配置拦截器时配置的。 String limit1 = properties.getProperty("limit", "10"); this.pageSize = Integer.valueOf(limit1); this.dbType = properties.getProperty("dbType", "mysql"); } }
上面即是拦截器功能的实现,在intercept方法中获取到select标签和sql语句的相关信息,拦截所有以ByPage结尾的select查询,并且统一在查询语句后面添加limit分页的相关语句,统一实现分页功能。
重点详解:
StatementHandler是一个接口,而我们在代码中通过StatementHandler statementHandler = (StatementHandler) invocation.getTarget();
获取到的是StatementHandler默认的实现类RoutingStatementHandler。而RoutingStatementHandler只是一个中间代理,他不会提供具体的方法。那你可能会纳闷了,拦截器中基本上是依赖statementHandler获取各种对象和属性的,没有具体属性和方法怎么行??接着看下面代码:
private final StatementHandler delegate; public RoutingStatementHandler(Executor executor, MappedStatement ms, Object parameter, RowBounds rowBounds, ResultHandler resultHandler, BoundSql boundSql) { switch(RoutingStatementHandler.SyntheticClass_1.$SwitchMap$org$apache$ibatis$mapping$StatementType[ms.getStatementType().ordinal()]) { case 1: this.delegate = new SimpleStatementHandler(executor, ms, parameter, rowBounds, resultHandler, boundSql); break; case 2: this.delegate = new PreparedStatementHandler(executor, ms, parameter, rowBounds, resultHandler, boundSql); break; case 3: this.delegate = new CallableStatementHandler(executor, ms, parameter, rowBounds, resultHandler, boundSql); break; default: throw new ExecutorException("Unknown statement type: " + ms.getStatementType()); } }
原来它是通过不同的MappedStatement创建不同的StatementHandler实现类对象处理不同的情况。这里的到的StatementHandler实现类才是真正服务的。看到这里,你可能就会明白MappedStatement mappedStatement = (MappedStatement) MetaObjectHandler.getValue("delegate.mappedStatement");
中delegate的来源了吧。至于为什么要这么去获取,后面我们会说道。
拿到statementHandler后,我们会通过MetaObject MetaObjectHandler = SystemMetaObject.forObject(statementHandler);
去获取它的包装对象,通过包装对象去获取各种服务。
MetaObject:mybatis的一个工具类,方便我们有效的读取或修改一些重要对象的属性。四大对象(ResultSetHandler,ParameterHandler,Executor和statementHandler)提供的公共方法很少,要想直接获取里面属性的值很困难,但是可以通过MetaObject利用一些技术(内部反射实现)很轻松的读取或修改里面的数据。
接下来说说:MappedStatement mappedStatement = (MappedStatement) MetaObjectHandler.getValue("delegate.mappedStatement");
上面提到为什么要这么去获取MappedStatement对象??在RoutingStatementHandler中delegate是私有的(private final StatementHandler delegate;
),有没有共有的方法去获取。所以这里只有通过反射来获取啦。
MappedStatement是保存了xxMapper.xml中一个sql语句节点的所有信息的包装类,可以通过它获取到节点中的所有信息。在示例中我们拿到了id值,也就是方法的名称,通过名称区拦截所有需要分页的请求。
通过StatementHandler的包装类,不光能拿到MappedStatement,还可以拿到下面的数据:
public abstract class BaseStatementHandler implements StatementHandler { protected final Configuration configuration; protected final ObjectFactory objectFactory; protected final TypeHandlerRegistry typeHandlerRegistry; protected final ResultSetHandler resultSetHandler; protected final ParameterHandler parameterHandler; protected final Executor executor; protected final MappedStatement mappedStatement; protected final RowBounds rowBounds; protected BoundSql boundSql; protected BaseStatementHandler(Executor executor, MappedStatement mappedStatement, Object parameterObject, RowBounds rowBounds, ResultHandler resultHandler, BoundSql boundSql) { this.configuration = mappedStatement.getConfiguration(); this.executor = executor; this.mappedStatement = mappedStatement; this.rowBounds = rowBounds; this.typeHandlerRegistry = this.configuration.getTypeHandlerRegistry(); this.objectFactory = this.configuration.getObjectFactory(); if(boundSql == null) { this.generateKeys(parameterObject); boundSql = mappedStatement.getBoundSql(parameterObject); } this.boundSql = boundSql; this.parameterHandler = this.configuration.newParameterHandler(mappedStatement, parameterObject, boundSql); this.resultSetHandler = this.configuration.newResultSetHandler(executor, mappedStatement, rowBounds, this.parameterHandler, resultHandler, boundSql); }
上面的所有数据都可以通过反射拿到。
几个重要的参数:
Configuration:所有配置的相关信息。
ResultSetHandler:用于拦截执行结果的组装。
ParameterHandler:拦截执行Sql的参数的组装。
Executor:执行Sql的全过程,包括组装参数、组装结果和执行Sql的过程。
BoundSql:执行的Sql的相关信息。
接下来我们通过如下代码拿到请求时的map对象(反射)。
//获取进行数据库操作时管理参数的handler ParameterHandler parameterHandler = (ParameterHandler) MetaObjectHandler.getValue("delegate.parameterHandler"); //获取请求时的参数 Map<String, Object> paraObject = (Map<String, Object>) parameterHandler.getParameterObject(); //也可以这样获取 //paraObject = (Map<String, Object>) statementHandler.getBoundSql().getParameterObject();
拿到我们需要的currPage和pageSize参数后,就是组装分页查询的sql语句’limitSql‘了。
最后通过MetaObjectHandler.setValue("delegate.boundSql.sql", limitSql);
将原始的sql语句替换成我们新的分页语句,完成偷天换日的功能,接下来让代码继续执行。
编写好拦截器后,需要注册到项目中,才能发挥它的作用。在mybatis的配置文件中,添加如下代码:
<plugins> <plugin interceptor="com.cbg.interceptor.MyPageInterceptor"> <property name="limit" value="10"/> <property name="dbType" value="mysql"/> </plugin> </plugins>
如上所示,还能在里面配置一些属性,在拦截器的setProperties方法中可以获取配置好的属性值。如项目分页的pageSize参数的值固定,我们就可以配置在这里了,以后就不需要每次传入pageSize了,读取方式如下:
//读取配置的代理对象的参数 @Override public void setProperties(Properties properties) { String limit1 = properties.getProperty("limit", "10"); this.pageSize = Integer.valueOf(limit1); this.dbType = properties.getProperty("dbType", "mysql"); }
到这里,有关拦截器的相关知识就讲解的差不多了,接下来就需要测试,是否我们这样写真的有效??
首先还是添加dao层的方法和xml文件的sql语句配置,注意项目中拦截的是以ByPage结尾的请求,所以在这里,我们的方法名称也以此结尾:
方法 List<Student> queryStudentsByPage(Map<String,Object> data); xml文件的select语句 <select id="queryStudentsByPage" parameterType="map" resultMap="studentmapper"> select * from student </select>
可以看出,这里我们就不需要再去手动配置分页语句了。
接下来是service层的接口编写和实现方法:
方法: List<Student> queryStudentsByPage(int currPage,int pageSize); 实现: @Override public List<Student> queryStudentsByPage(int currPage, int pageSize) { Map<String, Object> data = new HashedMap(); data.put("currPage", currPage); data.put("pageSize", pageSize); return studentMapper.queryStudentsByPage(data); }
这里我们虽然传入了currPage和pageSize两个参数,但是在sql的xml文件中并没有使用,直接在拦截器中获取到统一使用。
最后编写controller的测试代码:
@ResponseBody @RequestMapping("/student/page/{currPage}/{pageSize}") public List<Student> getStudentByPage(@PathVariable("currPage") int currPage, @PathVariable("pageSize") int pageSize) { List<Student> student = StuServiceIml.queryStudentsByPage(currPage, pageSize); return student; }
测试:
在浏览器输入:http://localhost:8080/student/student/page/1/2
结果:
可见和上面两种分页的效果是一样的。
4、RowBounds实现分页
原理:通过RowBounds实现分页和通过数组方式分页原理差不多,都是一次获取所有符合条件的数据,然后在内存中对大数据进行操作,实现分页效果。只是数组分页需要我们自己去实现分页逻辑,这里更加简化而已。
存在问题:一次性从数据库获取的数据可能会很多,对内存的消耗很大,可能导师性能变差,甚至引发内存溢出。
适用场景:在数据量很大的情况下,建议还是适用拦截器实现分页效果。RowBounds建议在数据量相对较小的情况下使用。
简单介绍:这是代码实现上最简单的一种分页方式,只需要在dao层接口中要实现分页的方法中加入RowBounds参数,然后在service层通过offset(从第几行开始读取数据,默认值为0)和limit(要显示的记录条数,默认为java允许的最大整数:2147483647)两个参数构建出RowBounds对象,在调用dao层方法的时,将构造好的RowBounds传进去就能轻松实现分页效果了。
具体操作如下:
dao层接口方法:
//加入RowBounds参数 public List<UserBean> queryUsersByPage(String userName, RowBounds rowBounds);
然后在service层构建RowBounds,调用dao层方法:
@Override @Transactional(isolation = Isolation.READ_COMMITTED, propagation = Propagation.SUPPORTS) public List<RoleBean> queryRolesByPage(String roleName, int start, int limit) { return roleDao.queryRolesByPage(roleName, new RowBounds(start, limit)); }
RowBounds就是一个封装了offset和limit简单类,如下所示:
public class RowBounds { public static final int NO_ROW_OFFSET = 0; public static final int NO_ROW_LIMIT = 2147483647; public static final RowBounds DEFAULT = new RowBounds(); private int offset; private int limit; public RowBounds() { this.offset = 0; this.limit = 2147483647; } public RowBounds(int offset, int limit) { this.offset = offset; this.limit = limit; } public int getOffset() { return this.offset; } public int getLimit() { return this.limit; } }
只需要这两步操作,就能轻松实现分页效果了,是不是很神奇。但却不简单,内部是怎么实现的??给大家提供一个简单的思路:RowBounds分页简单原理
结论:从上面四种sql分页的实现方式可以看出,通过RowBounds实现是最简便的,但是通过拦截器的实现方式是最优的方案。只需一次编写,所有的分页方法共同使用,还可以避免多次配置时的出错机率,需要修改时也只需要修改这一个文件,一劳永逸。而且是我们自己实现的,便于我们去控制和增加一些逻辑处理,使我们在外层更简单的使用。同时也不会出现数组分页和RowBounds分页导致的性能问题。当然,具体情况可以采取不同的解决方案。数据量小时,RowBounds不失为一种好办法。但是数据量大时,实现拦截器就很有必要了。
【相关推荐:编程教学】
以上がmybatis でのページングのいくつかの方法の詳細内容です。詳細については、PHP 中国語 Web サイトの他の関連記事を参照してください。