MyBatisがストリーミングクエリを実装する方法


基本概念
フロー・クエリーとは、クエリーが成功した後、セットを返すのではなく、反復器を返し、反復器からクエリー結果を取得するたびに適用することを意味します.ストリームクエリの利点は、メモリの使用量を削減できることです.
フロー・クエリーがない場合、データベースから1000万件のレコードを取得し、十分なメモリがない場合は、ページング・クエリーをページングしなければなりません.ページング・クエリーの効率はテーブル設計に依存し、設計が悪い場合は効率的なページング・クエリーを実行できません.したがって、フロー・クエリーは、データベース・アクセス・フレームワークに必要な機能です.
フロー・クエリーのプロセスでは、データベース接続は開いたままになります.したがって、フロー・クエリーを実行すると、データベース・アクセス・フレームワークはデータベース接続を閉じる責任を負いません.データを取得した後、自分で閉じる必要があります.
MyBatisフロークエリーインタフェース
MyBatisは、org.apache.ibatis.cursor.Cursorおよびjava.io.Closeableインタフェースを継承するjava.lang.Iterableというインタフェースクラスをストリームクエリに提供している.
  • Cursorは閉じることができます.
  • Cursorは遍歴可能です.

  • これに加えて、Cursorは3つの方法を提供しています.
  • isOpen():データを取得する前に、Cursorオブジェクトが開いているかどうかを判断するために使用される.Cursorが開いている場合にのみデータを取得できます.
  • isConsumed():クエリー結果がすべて完了したかどうかを判断するために使用されます.
  • getCurrentIndex():取得したデータ数を返す
  • Cursorは反復インタフェースを実現しているので、実際の使用では、Cursorからデータを取得するのは簡単です.
    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つの方法である.