先日、「Spring WebFluxのFlux結果をJSからFetch APIで取得する」という記事を書きましたが、この方法を使って、
例えば、大量のデータをブラウザに表示させたい時に、サーバーで最初の数10件だけ処理できたら、その数10件だけ先にブラウザに表示させて、その後処理できたデータをバックグラウンドでちょろちょろ追加していく。みたいなことが簡単にできそうだなと思ったので試してみました。
ユーザーに大量のデータを要求された場合、ページングで実装するという選択肢もありますが、
こういう選択肢もあるかもねー。程度の記事です。この方法をオススメするわけではありません。
ここで試したものはGitHubにアップしています。
処理内容
100万件のデータをサーバーに要求して、結果をブラウザのグリッドに表示します。
使ってる技術要素
- サーバーサイド
- Java11
- Spring WebFlux
-
H2 Database
データベース&jdbc。ガチでDBサーバー立てるのめんどくさいのでH2でファイルDBにしました。 -
uroboroSQL
DBアクセスライブラリ。弊社のOSS。何使っても良かったのですけど、私これに慣れてるのでこれ使います。
- クライアントサイド
-
Vue.js
何使っても良かったのですけど、私がVue好きなのと、Vue CLI使うと簡単に構築出来てとても好きなので使います。 -
Cheetah Grid
グリッドライブラリ。自分が作った弊社のOSS。100万件表示できるように作ったつもりなのでこれ使います。
-
Vue.js
比較と動画
先に比較結果をお見せしておきます。
Spring WebFluxを利用した非同期リクエスト
最初に描画されるまでの時間は2,056msでした。
もう少し早いかなーと思ったのですが、H2 DatabaseでSQLクエリを発行してから1件目が返るまでに時間がかかるみたいですね。(まあH2はプロダクション用途ではないと思うので仕方ないです。)
動画は切ってしまっていますが、その後、チョロチョロデータを取得して、100万件をクライアント側に溜め込むまでに40秒ぐらいかかります。
普通の同期リクエスト
描画されるまでの時間は5,827msでした。
一度に全部持ってきてるので、100万件をクライアント側に溜め込むまでの時間という意味でも同じ5,827msです。
もっと遅くなると思っていましたが、各レコードの構築で何か処理をしているわけでもなく、1レコードの情報も少ないので意外と速かったというところでしょうか?
実装
以下で実装についても書いておきます。
Controller(サーバーサイド)
以下のような実装になっています。
@Autowired
private SqlConfig uroboroSQLConfig;
@Autowired
@Qualifier("jdbcScheduler")
private Scheduler jdbcScheduler;
@GetMapping
public Flux<Person> all() {
return Flux.<Person> create(sink -> {
try (var agent = uroboroSQLConfig.agent()) {
// テーブルをSELECTしてPersonクラスにMappingしたStreamを生成
agent.queryWith("SELECT * FROM PERSON ORDER BY ID")
.stream(Person.class)
// キャンセルされていたら終了させる
.dropWhile(p -> sink.isCancelled())
// FluxSinkにEntityを渡す
.forEach(sink::next);
if (!sink.isCancelled()) {
// 終了を通知
sink.complete();
}
}
}).subscribeOn(jdbcScheduler);
}
GitHubに上げてあるソースだとこのあたりです。
uroboroSQLConfig
というのはuroboroSQLのDBアクセスで必要なものです。本題とは関係ないので気にせず、DBアクセス用の何かなんだなーと思って空気読んでいただければと思います。
jdbcScheduler
はReactorのScheduler
インスタンスで、並列化に必要なものです。
適切なスレッド数でScheduler
を定義して、
}).subscribeOn(jdbcScheduler);
と渡すことで、DBアクセスの処理と、リクエストを返す処理を並列化します。
あとは、キャンセル処理とかもしていますが、
基本的にはDBからSELECT
してその結果をsink.next(...)
で渡していくだけです。
リクエスト(クライアントサイド)
Spring WebFluxのFlux結果をJSからFetch APIで取得するで書いた方法で、Fetch APIを使ってサーバーにリクエストします。
グリッドに表示(クライアントサイド)
以下のような実装になっています。
const records = [];
const grid = /* Cheetah Gridのインスタンス */;
let buffer = [];
//...
streamJsonForVue(this, "/api/persons", {}, rec => {
buffer.push(rec);
if (
buffer.length >= 10000 ||
(records.length < 10000 && buffer.length >= 1000) ||
(records.length < 1000 && buffer.length >= 100)
) {
records.push(...buffer);
grid.records = records;
buffer = [];
//...
}
}).then(() => {
records.push(...buffer);
grid.records = records;
//...
});
GitHubに上げてあるソースだとこのあたりです。
grid
変数にCheetah Gridのインスタンスが入っていて、records
プロパティに配列を与えると、その与えたデータが画面に表示されます。
streamJsonForVue
関数は中で、「Spring WebFluxのFlux結果をJSからFetch APIで取得する」に書いた、Fetch APIの処理をしています。
プラスで、画面が破棄された時に、Fetchを中断するとかやってますが、本題には関係ないので説明は割愛します。
streamJsonForVue
のcallbackから返ってきた、rec
変数はControllerでsink.next(...)
で渡された1レコードデータです。
1件ずつgrid
に渡してしまうと、再描画が走りまくってユーザーが画面を操作しにくくなるため、buffer
配列に溜め込んで、ある程度溜まったら(最初は100件)、gridに反映させるようにしています。
同期リクエストの方の実装
GitHubに上げてあるソースだとこのあたりとこのあたりです。
比較用に作ってみただけで、本題には関係ないので説明は割愛します。
実装した感想
私が実装してみて思ったこととしては、実装がとても簡単だったということです。
自分の知識だと、HTTPレスポンスを非同期でStreamに流すのは難しいイメージでしたが、Spring WebFlux使うととても簡単に出来たのでびっくりしています。
結果
先にも書きましたが、今回のサンプルでは、初期描画にかかる時間はそれぞれ以下の結果になりました。
- Spring WebFluxを利用した非同期リクエスト
2,056ms - 普通の同期リクエスト
5,827ms
今回のサンプルでは、思っていたより効果が出なくて残念でしたが、どうしても初期描画だけは速くしたいような場合に、もしかしたら使えるかもしれません(?)