MyBatisがストリーミングクエリを実装する方法
基本概念
フロー・クエリーとは、クエリーが成功した後、セットを返すのではなく、反復器を返し、反復器からクエリー結果を取得するたびに適用することを意味します.ストリームクエリの利点は、メモリの使用量を削減できることです.
フロー・クエリーがない場合、データベースから1000万件のレコードを取得し、十分なメモリがない場合は、ページング・クエリーをページングしなければなりません.ページング・クエリーの効率はテーブル設計に依存し、設計が悪い場合は効率的なページング・クエリーを実行できません.したがって、フロー・クエリーは、データベース・アクセス・フレームワークに必要な機能です.
フロー・クエリーのプロセスでは、データベース接続は開いたままになります.したがって、フロー・クエリーを実行すると、データベース・アクセス・フレームワークはデータベース接続を閉じる責任を負いません.データを取得した後、自分で閉じる必要があります.
MyBatisフロークエリーインタフェース
MyBatisは、 Cursorは閉じることができます. Cursorは遍歴可能です.
これに加えて、Cursorは3つの方法を提供しています. Cursorは反復インタフェースを実現しているので、実際の使用では、Cursorからデータを取得するのは簡単です.
しかし、Cursorを構築するプロセスは簡単ではありません.
実際の例を挙げましょう.次はMapperクラスです.
メソッドscan()は非常に簡単なクエリーです.Mapperメソッドの戻り値がCursorタイプであることを指定することで、MyBatisはこのクエリメソッドのフロークエリを知っています.
次にSpringMVC Controllerメソッドを書いてMapperを呼び出します(関係のないコードは省略されています):
上のコードではfooMapperは@Autowiredが入っています.注記1でscanメソッドを呼び出し、Cursorオブジェクトを取得し、最後に閉じることを保証します.2箇所はcursorからデータを取ります.
上のコードは問題ないように見えますが、scanFoo 0()を実行するとエラーが表示されます.
これは、データの取得中にデータベース接続を維持する必要があると前述したためであり、Mapperメソッドは通常、実行後に接続が閉じられるため、Cusorも一緒に閉じられます.
したがって、この問題を解決する考え方は複雑ではなく、データベース接続を開くようにすればよい.少なくとも3つの選択肢があります.
方案一:SqlSessionFactory
SqlSessionFactoryを使用してデータベース接続を手動で開き、Controllerメソッドを次のように変更できます.
上記のコードでは、1つのSqlSession(実際にはデータベース接続を表しています)をオンにし、最後にオフにできることを保証します.2つのSqlSessionを使用してMapperオブジェクトを取得します.これにより、得られたCursorオブジェクトがオープン状態であることを保証できます.
シナリオ2:TransactionTemplate
Springでは、TransactionTemplateを使用してデータベーストランザクションを実行できます.このプロセスでは、データベース接続も開いています.コードは次のとおりです.
上のコードでは、1でTransactionTemplateオブジェクトを作成しました(ここでtransactionManagerがどのように来たのかはあまり説明しないが、読者がSpringデータベーストランザクションの使用に詳しいと仮定する)、2箇所でデータベーストランザクションを実行し、データベーストランザクションの内容はMapperオブジェクトを呼び出すフロークエリである.ここでのMapper対象はSqlSessionで作成する必要はないことに注意する.
シナリオ3:@Transactional注記
この本質はシナリオ2と同じで、コードは以下の通りです.
従来の方法に
以上がMyBatisフロークエリを実現する3つの方法である.
フロー・クエリーとは、クエリーが成功した後、セットを返すのではなく、反復器を返し、反復器からクエリー結果を取得するたびに適用することを意味します.ストリームクエリの利点は、メモリの使用量を削減できることです.
フロー・クエリーがない場合、データベースから1000万件のレコードを取得し、十分なメモリがない場合は、ページング・クエリーをページングしなければなりません.ページング・クエリーの効率はテーブル設計に依存し、設計が悪い場合は効率的なページング・クエリーを実行できません.したがって、フロー・クエリーは、データベース・アクセス・フレームワークに必要な機能です.
フロー・クエリーのプロセスでは、データベース接続は開いたままになります.したがって、フロー・クエリーを実行すると、データベース・アクセス・フレームワークはデータベース接続を閉じる責任を負いません.データを取得した後、自分で閉じる必要があります.
MyBatisフロークエリーインタフェース
MyBatisは、
org.apache.ibatis.cursor.Cursor
およびjava.io.Closeable
インタフェースを継承するjava.lang.Iterable
というインタフェースクラスをストリームクエリに提供している.これに加えて、Cursorは3つの方法を提供しています.
isOpen()
:データを取得する前に、Cursorオブジェクトが開いているかどうかを判断するために使用される.Cursorが開いている場合にのみデータを取得できます.isConsumed()
:クエリー結果がすべて完了したかどうかを判断するために使用されます.getCurrentIndex()
:取得したデータ数を返すcursor.forEach(rowObject -> {...});
しかし、Cursorを構築するプロセスは簡単ではありません.
実際の例を挙げましょう.次はMapperクラスです.
@Mapper
public interface FooMapper {
@Select("select * from foo limit #{limit}")
Cursor scan(@Param("limit") int limit);
}
メソッドscan()は非常に簡単なクエリーです.Mapperメソッドの戻り値がCursorタイプであることを指定することで、MyBatisはこのクエリメソッドのフロークエリを知っています.
次にSpringMVC Controllerメソッドを書いてMapperを呼び出します(関係のないコードは省略されています):
@GetMapping("foo/scan/0/{limit}")
public void scanFoo0(@PathVariable("limit") int limit) throws Exception {
try (Cursor cursor = fooMapper.scan(limit)) { // 1
cursor.forEach(foo -> {}); // 2
}
}
上のコードではfooMapperは@Autowiredが入っています.注記1でscanメソッドを呼び出し、Cursorオブジェクトを取得し、最後に閉じることを保証します.2箇所はcursorからデータを取ります.
上のコードは問題ないように見えますが、scanFoo 0()を実行するとエラーが表示されます.
java.lang.IllegalStateException: A Cursor is already closed.
これは、データの取得中にデータベース接続を維持する必要があると前述したためであり、Mapperメソッドは通常、実行後に接続が閉じられるため、Cusorも一緒に閉じられます.
したがって、この問題を解決する考え方は複雑ではなく、データベース接続を開くようにすればよい.少なくとも3つの選択肢があります.
方案一:SqlSessionFactory
SqlSessionFactoryを使用してデータベース接続を手動で開き、Controllerメソッドを次のように変更できます.
@GetMapping("foo/scan/1/{limit}")
public void scanFoo1(@PathVariable("limit") int limit) throws Exception {
try (
SqlSession sqlSession = sqlSessionFactory.openSession(); // 1
Cursor cursor =
sqlSession.getMapper(FooMapper.class).scan(limit) // 2
) {
cursor.forEach(foo -> { });
}
}
上記のコードでは、1つのSqlSession(実際にはデータベース接続を表しています)をオンにし、最後にオフにできることを保証します.2つのSqlSessionを使用してMapperオブジェクトを取得します.これにより、得られたCursorオブジェクトがオープン状態であることを保証できます.
シナリオ2: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 cursor = fooMapper.scan(limit)) {
cursor.forEach(foo -> { });
} catch (IOException e) {
e.printStackTrace();
}
return null;
});
}
上のコードでは、1でTransactionTemplateオブジェクトを作成しました(ここでtransactionManagerがどのように来たのかはあまり説明しないが、読者がSpringデータベーストランザクションの使用に詳しいと仮定する)、2箇所でデータベーストランザクションを実行し、データベーストランザクションの内容はMapperオブジェクトを呼び出すフロークエリである.ここでのMapper対象はSqlSessionで作成する必要はないことに注意する.
シナリオ3:@Transactional注記
この本質はシナリオ2と同じで、コードは以下の通りです.
@GetMapping("foo/scan/3/{limit}")
@Transactional
public void scanFoo3(@PathVariable("limit") int limit) throws Exception {
try (Cursor cursor = fooMapper.scan(limit)) {
cursor.forEach(foo -> { });
}
}
従来の方法に
@Transactional
の注釈を加えただけです.このスキームは最も簡潔に見えますが、Springフレームワークで注釈に使用されるピットは、外部呼び出し時にのみ有効であることに注意してください.現在のクラスでこのメソッドを呼び出すと、エラーが発生します.以上がMyBatisフロークエリを実現する3つの方法である.