MyBatis——流式查询

流式查询指的是查询成功后不是返回一个集合而是返回一个迭代器,应用每次从迭代器取一条查询结果。流式查询的好处是能够降低内存使用。

如果没有流式查询,我们想要从数据库取 1000 万条记录而又没有足够的内存时,就不得不分页查询,而分页查询效率取决于表设计,如果设计的不好,就无法执行高效的分页查询。因此流式查询是一个数据库访问框架必须具备的功能。

流式查询的过程当中,数据库连接是保持打开状态的,因此要注意的是:执行一个流式查询后,数据库访问框架就不负责关闭数据库连接了,需要应用在取完数据后自己关闭。

MyBatis 流式查询接口

MyBatis 提供了一个叫 org.apache.ibatis.cursor.Cursor 的接口类用于流式查询,这个接口继承了 java.io.Closeable 和 java.lang.Iterable 接口,由此可知:

  • Cursor 是可关闭的。实际上当关闭 Cursor 时,也一并将数据库连接关闭了;
  • Cursor 是可遍历的。

除此之外,Cursor 还提供了三个方法:

  • isOpen():用于在取数据之前判断 Cursor 对象是否是打开状态。只有当打开时 Cursor 才能取数据;
  • isConsumed():用于判断查询结果是否全部取完;
  • getCurrentIndex():返回已经获取了多少条数据。

因为 Cursor 实现了迭代器接口,因此在实际使用当中,从 Cursor 取数据非常简单:

1
2
3
4
5
try(Cursor cursor = mapper.querySomeData()) {
cursor.forEach(rowObject -> {
// ...使用 try-resource 方式可以令 Cursor 自动关闭
});
}

流式查询示例

我们举个实际例子。下面是一个 Mapper 类:

1
2
3
4
5
@Mapper
public interface FooMapper {
@Select("select * from foo limit #{limit}")
Cursor<Foo> scan(@Param("limit") int limit);
}

方法 scan() 是一个非常简单的查询。我们在定义这个方时,指定返回值为 Cursor 类型,MyBatis 就明白这个查询方法是一个流式查询。

然后我们再写一个 SpringMVC Controller 方法来调用 Mapper(无关的代码已经省略):

1
2
3
4
5
6
@GetMapping("foo/scan/0/{limit}")
public void scanFoo0(@PathVariable("limit") int limit) throws Exception {
try (Cursor<Foo> cursor = fooMapper.scan(limit)) { // 1 获取 Cursor 对象并保证它能最后关闭
cursor.forEach(foo -> {}); // 2 从 cursor 中取数据
}
}

上面的代码看上去没什么问题,但是执行scanFoo0(int)时会报错:

java.lang.IllegalStateException: A Cursor is already closed.

这是因为我们前面说了在取数据的过程中需要保持数据库连接,而 Mapper 方法通常在执行完后连接就关闭了,因此 Cusor 也一并关闭了。所以,解决这个问题的思路不复杂,保持数据库连接打开即可。

解决方案:@Transactional 注解

1
2
3
4
5
6
7
@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 -> { });
}
}

fork和clone的区别,fetch与pull的区别

fork和clone的区别,fetch与pull的区别

fork:在github页面,点击fork按钮。将别人的仓库复制一份到自己的仓库。

clone:将github中的仓库克隆到自己本地电脑中。

(1)pull request的作用

比如在仓库的主人(A)没有把我们添加为项目合作者的前提下,我们将A的某个仓库名为“a”的仓库clone到自己的电脑中,在自己的电脑进行修改,但是我们会发现我们没办法通过push将代码贡献到B中。

所以要想将你的代码贡献到B中,我们应该:

  1. 在A的仓库中fork项目a (此时我们自己的github就有一个一模一样的仓库a,但是URL不同)
  2. 将我们修改的代码push到自己github中的仓库B中
  3. pull request ,主人就会收到请求,并决定要不要接受你的代码
  4. 也可以可以申请为项目a的contributor,这样可以直接push

516rP.md.png

(2)fork了别人的项目到自己的repository之后,别人的项目更新了,我们fork的项目怎么更新?

首先fetch网上的更新到自己的项目上,然后再判断、merge。这里就涉及了下一个问题,pull和fetch有啥区别。

fetch+merge与pull效果一样。但是要多用fetch+merge,这样可以检查fetch下来的更新是否合适。pull直接包含了这两步操作,如果你觉得网上的更新没有问题,那直接pull也是可以的。


业务中的逻辑删除示例

前端代码

1
2
3
4
5
6
7
/**删除接口 */
export function deleteProject(id) {
return request({
url: '/backend/test/'+ id,
method: 'delete'
})
}

后端代码

api接口

1
2
3
4
5
@ApiOperation("测试表单删除接口")
@DeleteMapping(path = "/{id}")
public ApiResponseBody delete(@PathVariable Long id) throws Exception {
return iTestService.logicalDelete(id);
}

接口

1
ApiResponseBody logicalDelete(Long id);

实现类

1
2
3
4
5
@Override
public ApiResponseBody logicalDelete(Long id) {
testMainGyMapper.logicalDelete(id);
return ApiResponseBody.defaultSuccess();
}

MyBatis接口

1
2
@Update("update test_main_ge_y1 set delete_flag = 1 where id = #{id}")
int logicalDelete(Long id);

注意

1
2
//查询之前先对flag进行判断
criteria.andDeleteFlagEqualTo(DeleteFlagEnum.DATA_OK.getFlag());