什么是流式查询?
流式查询 指的是查询成功后不是返回一个集合而是返回一个迭代器,应用每次从迭代器取一条查询结果。流式查询的好处是能够降低内存使用。
如果没有流式查询,我们想要从数据库取 1000 万条记录而又没有足够的内存时,就不得不分页查询,而分页查询效率取决于表设计,如果设计的不好,就无法执行高效的分页查询。因此流式查询是一个数据库访问框架必须具备的功能。
⚠️流式查询的过程当中,数据库连接是保持打开状态的,因此要注意的是:执行一个流式查询后,数据库访问框架就不负责关闭数据库连接了,需要应用在取完数据后自己关闭。
MyBatis 流式查询接口
MyBatis 提供了一个叫 org.apache.ibatis.cursor.Cursor 的接口类用于流式查询,这个接口继承了 java.io.Closeable 和 java.lang.Iterable 接口,由此可知:
- Cursor 是可关闭的;
- Cursor 是可遍历的。
除此之外,Cursor 还提供了三个方法:
- isOpen():用于在取数据之前判断 Cursor 对象是否是打开状态。只有当打开时 Cursor 才能取数据;
- isConsumed():用于判断查询结果是否全部取完。
- getCurrentIndex():返回已经获取了多少条数据
talk is cheap, show me the code
step1
数据库建一张文章表,并随便插入三条数据
CREATE TABLE `tb_article` ( `id` bigint(20) NOT NULL AUTO_INCREMENT COMMENT 'ID', `create_date` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间', `update_date` timestamp NULL DEFAULT NULL ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间', `title` varchar(128) NOT NULL COMMENT '标题', `content` text NOT NULL COMMENT '内容', `status` tinyint(2) NOT NULL DEFAULT '0' COMMENT '状态', PRIMARY KEY (`id`) ) ENGINE=InnoDB AUTO_INCREMENT=4 DEFAULT CHARSET=utf8 COMMENT='文章';
INSERT INTO `tb_article` VALUES (1, '2020-12-22 19:51:28', NULL, '123', '123', 0); INSERT INTO `tb_article` VALUES (2, '2020-12-22 19:51:37', '2020-12-22 19:51:48', '456', '456', 1); INSERT INTO `tb_article` VALUES (3, '2020-12-22 19:51:44', '2020-12-22 19:51:49', '789', '789', 2);
step2
代码编写
pom.xml
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.4.1</version>
<relativePath/> <!-- lookup parent from repository -->
</parent>
<groupId>com.aeert</groupId>
<artifactId>streamquery</artifactId>
<version>0.0.1-SNAPSHOT</version>
<name>streamquery</name>
<description>Demo project for Streaming query</description>
<properties>
<java.version>1.8</java.version>
<mybatisplus.version>3.4.1</mybatisplus.version>
<mysql.version>8.0.22</mysql.version>
</properties>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<optional>true</optional>
</dependency>
<dependency>
<groupId>com.baomidou</groupId>
<artifactId>mybatis-plus-boot-starter</artifactId>
<version>${mybatisplus.version}</version>
</dependency>
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<version>${mysql.version}</version>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
<configuration>
<excludes>
<exclude>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
</exclude>
</excludes>
</configuration>
</plugin>
</plugins>
</build>
</project>
application.properties
spring.datasource.type=com.zaxxer.hikari.HikariDataSource
spring.datasource.driver-class-name=com.mysql.cj.jdbc.Driver
spring.datasource.url=jdbc:mysql://127.0.0.1:3306/streamquery?allowMultiQueries=true&useUnicode=true&characterEncoding=UTF-8&useSSL=false
spring.datasource.username=root
spring.datasource.password=root
spring.datasource.hikari.minimum-idle=5
spring.datasource.hikari.maximum-pool-size=15
spring.datasource.hikari.auto-commit=true
spring.datasource.hikari.idle-timeout=30000
spring.datasource.hikari.pool-name=DatebookHikariCP
spring.datasource.hikari.max-lifetime=1800000
spring.datasource.hikari.connection-timeout=30000
spring.datasource.hikari.connection-test-query=SELECT 1
mybatis-plus.mapper-locations=classpath*:/mapper/**/*.xml
mybatis-plus.type-aliases-package=com.aeert.streamquery.entity
mybatis-plus.global-config.db-config.id-type=auto
mybatis-plus.global-config.db-config.table-underline=true
mybatis-plus.configuration.map-underscore-to-camel-case=true
mybatis-plus.configuration.cache-enabled=false
mybatis-plus.configuration.call-setters-on-nulls=true
mybatis-plus.configuration.jdbc-type-for-null='null'
# SQL日志
mybatis-plus.configuration.log-impl=org.apache.ibatis.logging.stdout.StdOutImpl
ArticleEntity
package com.aeert.streamquery.entity;
import com.baomidou.mybatisplus.annotation.TableId;
import com.baomidou.mybatisplus.annotation.TableName;
import lombok.Data;
import java.io.Serializable;
import java.util.Date;
/**
* 文章
*
* @author l'amour solitaire
* @date 2020-12-22 19:44:32
*/
@Data
@TableName("tb_article")
public class ArticleEntity implements Serializable {
private static final long serialVersionUID = 1L;
/**
* ID
*/
@TableId
private Long id;
/**
* 创建时间
*/
private Date createDate;
/**
* 更新时间
*/
private Date updateDate;
/**
* 标题
*/
private String title;
/**
* 内容
*/
private String content;
/**
* 状态
*/
private Integer status;
}
ArticleDao
package com.aeert.streamquery.dao;
import com.aeert.streamquery.entity.ArticleEntity;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import org.apache.ibatis.annotations.Mapper;
import org.apache.ibatis.annotations.Select;
import org.apache.ibatis.cursor.Cursor;
import java.awt.*;
/**
* 文章
*
* @author l'amour solitaire
* @date 2020-12-22 19:44:32
*/
@Mapper
public interface ArticleDao extends BaseMapper<ArticleEntity> {
@Select("SELECT * FROM tb_article")
Cursor<ArticleEntity> queryByCursor();
}
ArticleService
package com.aeert.streamquery.service;
import com.aeert.streamquery.entity.ArticleEntity;
import com.baomidou.mybatisplus.extension.service.IService;
/**
* 文章
*
* @author l'amour solitaire
* @date 2020-12-22 19:44:32
*/
public interface ArticleService extends IService<ArticleEntity> {
/**
* 流式查询
**/
public void queryByCursor();
}
ArticleServiceImpl
⚠️注意这里的@Transactional,没有这个的话会抛异常 java.lang.IllegalStateException: A Cursor is already closed.
这是因为我们前面说了在取数据的过程中需要保持数据库连接,而 Mapper 方法通常在执行完后连接就关闭了,因此 Cusor 也一并关闭了。
package com.aeert.streamquery.service.impl;
import com.aeert.streamquery.dao.ArticleDao;
import com.aeert.streamquery.entity.ArticleEntity;
import com.aeert.streamquery.service.ArticleService;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import org.apache.ibatis.cursor.Cursor;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
/**
* @Author l'amour solitaire
* @Description articleService
* @Date 2020/12/22 下午7:57
**/
@Service("articleService")
public class ArticleServiceImpl extends ServiceImpl<ArticleDao, ArticleEntity> implements ArticleService {
@Override
@Transactional(rollbackFor = Exception.class)
public void queryByCursor() throws Exception {
try (Cursor<ArticleEntity> cursor = baseMapper.queryByCursor()) {
cursor.forEach(foo -> {
System.out.println(foo.getId());
});
}
}
}
所以,解决这个问题的思路不复杂,保持数据库连接打开即可。我们至少有三种方案可选。这里我们用的是方案三;
方案一:SqlSessionFactory
我们可以用 SqlSessionFactory 来手工打开数据库连接,将 Controller 方法修改如下:
@GetMapping("foo/scan/1/{limit}") public void scanFoo1(@PathVariable("limit") int limit) throws Exception { try ( SqlSession sqlSession = sqlSessionFactory.openSession(); // 1 Cursor<Foo> cursor = sqlSession.getMapper(FooMapper.class).scan(limit) // 2 ) { cursor.forEach(foo -> { }); } }
上面的代码中,1 处我们开启了一个 SqlSession (实际上也代表了一个数据库连接),并保证它最后能关闭;2 处我们使用 SqlSession 来获得 Mapper 对象。这样才能保证得到的 Cursor 对象是打开状态的。
方案二:TransactionTemplate
在 Spring 中,我们可以用 TransactionTemplate 来执行一个数据库事务,这个过程中数据库连接同样是打开的。代码如下:
@GetMapping("foo/scan/2/{limit}") public void scanFoo2(@PathVariable("limit") int limit) throws Exception { TransactionTemplate transactionTemplate = new TransactionTemplate(transactionManager); // 1 transactionTemplate.execute(status -> { // 2 try (Cursor<Foo> cursor = fooMapper.scan(limit)) { cursor.forEach(foo -> { }); } catch (IOException e) { e.printStackTrace(); } return null; }); }
上面的代码中,1 处我们创建了一个 TransactionTemplate 对象(此处 transactionManager 是怎么来的不用多解释,本文假设读者对 Spring 数据库事务的使用比较熟悉了),2 处执行数据库事务,而数据库事务的内容则是调用 Mapper 对象的流式查询。注意这里的 Mapper 对象无需通过 SqlSession 创建。
方案三:@Transactional 注解
这个本质上和方案二一样,代码如下:
@GetMapping("foo/scan/3/{limit}") @Transactional public void scanFoo3(@PathVariable("limit") int limit) throws Exception { try (Cursor<Foo> cursor = fooMapper.scan(limit)) { cursor.forEach(foo -> { }); } }
它仅仅是在原来方法上面加了个 @Transactional 注解。这个方案看上去最简洁,但请注意 Spring 框架当中注解使用的坑:只在外部调用时生效。在当前类中调用这个方法,依旧会报错。
ArticleController
package com.aeert.streamquery.controller;
import com.aeert.streamquery.service.ArticleService;
import lombok.RequiredArgsConstructor;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
/**
* 文章
*
* @author l'amour solitaire
* @date 2020-12-22 19:44:32
*/
@RestController
@RequestMapping("/article")
@RequiredArgsConstructor(onConstructor_ = @Autowired)
public class ArticleController {
private final ArticleService articleService;
/**
* 列表
*/
@GetMapping("/list")
public void list() {
articleService.queryByCursor();
}
}
StreamqueryApplication
package com.aeert.streamquery;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.transaction.annotation.EnableTransactionManagement;
/**
* @Author l'amour solitaire
* @Description TODO
* @Date 2020/12/22 下午7:47
**/
@SpringBootApplication
@EnableTransactionManagement
public class StreamqueryApplication {
public static void main(String[] args) {
SpringApplication.run(StreamqueryApplication.class, args);
}
}
step3
访问接口 http://localhost:8080/article/list 后会流式打印出所有文章ID。
欢迎咨询公众号《JAVA拾贝》,回复 流式查询 即可免费下载源码或通过 积分下载