在jOOQ中获取数据的多种不同方式

jOOQ的API是关于方便的,因此,像fetch() 这样的重要操作(最重要的操作?)也必须附带方便。获取数据的默认方式是这样的:

Result<Record1<String>> result =
ctx.select(BOOK.TITLE)
   .from(BOOK)
   .fetch();

for (Record1<String> record : result) {
    // ...
}

它将整个结果集取到内存中,并急切地关闭底层的JDBC资源。但是我们还有什么其他的选择呢?

可迭代的获取方式

在上面的例子中,fetch() 的调用并不是严格意义上的必要。 [ResultQuery<R>](https://www.jooq.org/javadoc/latest/org.jooq/org/jooq/ResultQuery.html)类型方便地扩展了Iterable<R>这意味着对ResultQuery.iterator() 的调用也将执行该查询。这主要可以通过两种方式实现。

外部迭代

for (Record1<String> record : ctx
    .select(BOOK.TITLE)
    .from(BOOK)
) {
    // ...
}

这特别好,因为它感觉就像PL/SQL或PL/pgSQL的FOR 循环,用于隐式游标:

FOR rec IN (SELECT book.title FROM book) LOOP
  -- ...
END LOOP;

不过这仍然要把整个结果集取到内存中,因为在Java中没有一个for-with-resources 语法,它把foreach 语法和try-with-resources 语法结合起来。

内部迭代

JDK 8增加了Iterable::forEach ,jOOQ的 [ResultQuery](https://www.jooq.org/javadoc/latest/org.jooq/org/jooq/ResultQuery.html)继承了,所以你也可以这样做。

ctx.select(BOOK.TITLE)
   .from(BOOK)
   .forEach(record -> {
       // ...
   });

两者是完全等价的。

单一记录的获取

如果你确定你只取一个单一的值,不需要具体化一个列表。只需使用以下方法之一。鉴于这个查询:

ResultQuery<Record1<String>> query = ctx
    .select(BOOK.TITLE)
    .from(BOOK)
    .where(BOOK.ID.eq(1));

你现在可以

取一个可空的记录

这就获取了一个可空的记录,也就是说,如果没有找到记录,就会产生null 。如果有一条以上的记录,就会产生一个 [TooManyRowsException](https://www.jooq.org/javadoc/latest/org.jooq/org/jooq/exception/TooManyRowsException.html)会被抛出。

Record1<String> r = query.fetchOne();

取一个可选择的记录

null 自行车棚是真实的,那么为什么在使用jOOQ的时候不让你也骑自行车呢?与上述完全等同,但使用不同的风格,是这样的。

Optional<Record1<String>> r = query.fetchOptional();

取出一条记录

如果你知道你的查询正好产生一条记录,在jOOQ的API中有一个术语 "single",意思是正好一条:

Record1<String> r = query.fetchSingle();
println(r.toString()); // NPE safe!

r.toString() NullPointerException 的调用是安全的,因为如果记录不存在,a[NoDataFoundException](https://www.jooq.org/javadoc/latest/org.jooq/org/jooq/exception/NoDataFoundException.html)会被抛出。

资源性获取

默认情况下是急切地把所有东西都取到内存中,因为这可能比JDBC默认的一直管理资源(包括嵌套集合、lobs等)对大多数应用更有用。从上面的Iterator fetching例子中可以看出,考虑到用户甚至不能通过jOOQ访问资源(默认情况下),这往往是唯一可能不产生意外资源泄露的方法。

但这并不总是 正确的选择,所以如果你的数据集很大的话,你可以在获取数据的同时保持开放底层JDBC资源。有2种主要方式。

强制性的

通过调用 [ResultQuery.fetchLazy()](https://www.jooq.org/javadoc/latest/org.jooq/org/jooq/ResultQuery.html#fetchLazy()),你就创建了一个 [Cursor<R>](https://www.jooq.org/javadoc/latest/org.jooq/org/jooq/Cursor.html),它包装了底层的JDBCResultSet ,因此,应该包含在一个try-with-resources 语句中。

try (Cursor<Record1<String>> cursor = ctx
    .select(BOOK.TITLE)
    .from(BOOK)
    .fetchLazy()
) {
    for (Record1<String> record : cursor) {
        // ...
    }
}

Cursor<R> 仍然扩展了Iterable<R> ,但你也可以从它那里手动获取记录,例如:

Record record;

while ((record = cursor.fetchNext()) != null) {
    // ...
}

功能性的

如果Stream API更像你想处理的数据,只要调用 [ResultQuery.fetchStream()](https://www.jooq.org/javadoc/latest/org.jooq/org/jooq/ResultQuery.html#fetchStream())来代替,那么(但别忘了也要用try-with-resources 来包装!):

try (Stream<Record1<String>> stream = ctx
    .select(BOOK.TITLE)
    .from(BOOK)
    .fetchStream()
) {
    stream.forEach(record -> {
        // ...
    });
}

或者,使用Stream::mapStream::reduce ,或者其他什么。遗憾的是,Stream API并不是自动关闭的。虽然这样实现API是可能的,但它的 "逃生舱",如Stream.iterator() ,仍然会阻止自动关闭的行为(至少,除非有更多的功能被引入,如AutoCloseableIterator ,或其他什么)。

所以,你必须用try-with-resources 语句打破你的流畅管道。

功能性的,但不是资源性的

当然,你总是可以先调用fetch() ,然后再调用stream,以便直接从你的内存中流转数据。如果资源性并不重要(即对性能的影响可以忽略不计,因为结果集并不大),你可以这样写:

ctx.select(BOOK.TITLE)
   .from(BOOK)
   .fetch()
   .stream()
   .forEach(record -> {
       // ...
   });

或者使用Stream::map,Stream::reduce, 或其他什么方式

采集器获取

从jOOQ 3.11版本开始,无论是 [ResultQuery::collect](https://www.jooq.org/javadoc/latest/org.jooq/org/jooq/ResultQuery.html#collect(java.util.stream.Collector))[Cursor::collect](https://www.jooq.org/javadoc/latest/org.jooq/org/jooq/Cursor.html#collect(java.util.stream.Collector))已经被添加进来了。JDKCollector API是非常强大的。它并没有得到应有的关注(在Stream API之外)。在我看来,应该有一个Iterable::collect 方法,因为在任何集合上重新使用Collector 类型是有意义的,例如:

Set<String> s = Set.of(1, 2, 3);
List<String> l = s.collect(Collectors.toList());

为什么不呢?Collector 有点像Stream API本身的对偶。这些操作不是以流水线的语法组成的,而是以嵌套的语法组成的。除此以外,至少对我来说,它感觉非常相似。

就jOOQ而言,它们非常强大。jOOQ提供了一些有用的开箱即用的收集器,在 [Records](https://www.jooq.org/javadoc/latest/org.jooq/org/jooq/Records.html).让我展示一下 [Records.intoMap()](https://www.jooq.org/javadoc/latest/org.jooq/org/jooq/Records.html#intoMap())的例子,它有这个重载:

<K,V,R extends Record2<K,V>> Collector<R,?,Map<K,V>> intoMap()

这里有趣的一点是,它捕获了一个Record2 类型的类型,作为结果映射的键和值类型。一个简单的通用技巧,以确保它只在你正好投射2列的情况下工作,比如说:

Map<Integer, String> books =
ctx.select(BOOK.ID, BOOK.TITLE)
   .from(BOOK)
   .collect(Records.intoMap());

这完全是类型安全的。你不能投射3列,或者由于所有这些泛型而投射错误的列类型。这比直接在ResultQuery API上提供的等价物更方便,在那里你必须重复投影列的表达式:

Map<Integer, String> books =
ctx.select(BOOK.ID, BOOK.TITLE)
   .from(BOOK)
   .fetchMap(BOOK.ID, BOOK.TITLE);

通过ResultQuery::collectCursor::collect API,你可以使用任何任意的收集器,包括你自己的收集器,这真的是非常强大的!这也是为什么你可以使用 。另外,它还消除了对中间的Result 数据结构的需要,所以它不必把所有的东西都取到内存中(当然,除非你的Collector 反正是这样做)。

收集器在收集MULTISET 嵌套集合时特别有用。这里已经给出了一个例子,一个嵌套的集合也被映射到这样的Map<K, V>

反应式获取

从jOOQ 3.15开始,R2DBC得到了支持。这意味着 [ResultQuery<R>](https://www.jooq.org/javadoc/latest/org.jooq/org/jooq/ResultQuery.html)现在也是一个反应式流Publisher<R> (同时支持reactive-streams API和JDK 9Flow API,以提高互操作性)。

所以,只要选择你最喜欢的反应式流API,比如reactor,然后像这样反应式地流取jOOQ的结果集:

Flux<Record1<String>> flux = Flux.from(ctx
    .select(BOOK.TITLE)
    .from(BOOK)
);

许多获取

最后但并非最不重要的是,在极少数情况下,你的查询会产生一个以上的结果集。这在SQL Server和相关的RDBMS中曾经很流行,存储过程可以产生游标。MySQL和Oracle也有这个功能。比如说:

Results results = ctx.fetch("sp_help");

for (Result<?> result : results) {
    for (Record record : result) {
        // ...
    }
}

标准的foreach 循环只会迭代结果,但你也可以使用下面的方法访问交错的行数 [Results.resultsOrRows()](https://www.jooq.org/javadoc/latest/org.jooq/org/jooq/Results.html#resultsOrRows())如果你对这个也感兴趣的话。

总结

方便和开发者的用户体验是jOOQ的API设计的核心。像任何好的集合API一样,jOOQ提供了各种可组合的基元,允许更有效地将SQL整合到你的应用程序中。

SQL只是对数据结构的描述。jOOQ帮助在JVM上以一种类型安全的方式描述该数据结构。以同样类型安全的方式进行进一步处理是很自然的,就像我们习惯于从JDK自己的集合API或第三方(如jOOλvavrstreamex等)获得的那样。

猜你喜欢

转载自juejin.im/post/7126039763089358861