MyBatisのCursorはバッチ処理で大量のデータを全件読み込みたい時に、Out of Memoryを防ぐために利用可能な機能です。
同様な目的で使うResultHandlerと比べて、CursorはIteratorとして扱えるため、コードがシンプルになります。
次のMapperの定義があるとします。
package com.example;
import org.apache.ibatis.annotations.Mapper;
import org.apache.ibatis.annotations.Select;
import org.apache.ibatis.cursor.Cursor;
@Mapper
public interface DepositWithdrawalMapper {
@Select("SELECT BRANCH_NAME, CUSTOMER_ID, TRANSACTION_TYPE, AMOUNT, TRANSACTION_DATE FROM DEPOSIT_WITHDRAWAL ORDER BY TRANSACTION_DATE, BRANCH_NAME")
Cursor<DepositWithdrawal> findAll();
}
CursorはIterableを継承しているため、for-each文でループ回すことができます。
try (Cursor<DepositWithdrawal> cursor = this.depositWithdrawalMapper.findAll()) {
cursor.forEach(System.out::println);
}
また、iterator()メソッドでIteratorを取得することもできます。
try (Cursor<DepositWithdrawal> cursor = this.depositWithdrawalMapper.findAll()) {
Iterator<DepositWithdrawal> iterator = cursor.iterator();
while (iterator.hasNext()) {
DepositWithdrawal data = iterator.next();
System.out.println(data);
}
}
バッチ処理では次の行のデータを見て、処理の判断をしたいことがあります。例えば、ソートされたデータで項目が切り替わったときに集計処理を行う場合などです。
SQLだけで完結できれば良いのですが、複雑な集計ロジックが絡む場合はJava側で実装する必要があります。
Iteratorには先読みのためのメソッドがないため、普通に先読みを実装する場合、先読みデータの変数を用意して、iterator()で取得したIteratorを使って次の行を読み込み、変数にセットすることになります。Cursorが一つの場合はこれでもいいですが、複数のCursorを扱い集計を行いたい場合は、状態管理のためのコードが複雑になり、バグの温床になります。
そこで、GuavaのPeekingIteratorを使うと、Iteratorには先読みのためのメソッドが追加され、コードがシンプルになります。
入出金データを日付・支店名でソートして取得し、CursorとPeekingIteratorを使い、日付・支店名ごとに入金・出金の件数と合計金額を集計して表示するコード例を示します。
Note
バッチ処理の題材とテストデータはこちらを使用しました。
package com.example;
import com.google.common.collect.Iterators;
import com.google.common.collect.PeekingIterator;
import java.math.BigInteger;
import java.text.DecimalFormat;
import java.time.LocalDate;
import java.util.Objects;
import org.apache.ibatis.cursor.Cursor;
import org.springframework.boot.CommandLineRunner;
import org.springframework.stereotype.Component;
import org.springframework.transaction.annotation.Transactional;
import org.springframework.util.Assert;
@Component
public class Demo implements CommandLineRunner {
private final DepositWithdrawalMapper depositWithdrawalMapper;
public Demo(DepositWithdrawalMapper depositWithdrawalMapper) {
this.depositWithdrawalMapper = depositWithdrawalMapper;
}
@Override
@Transactional
public void run(String... args) throws Exception {
try (Cursor<DepositWithdrawal> cursor = this.depositWithdrawalMapper.findAll()) {
PeekingIterator<DepositWithdrawal> iterator = Iterators.peekingIterator(cursor.iterator());
int depositCount = 0;
int withdrawalCount = 0;
BigInteger totalDeposit = BigInteger.ZERO;
BigInteger totalWithdrawal = BigInteger.ZERO;
System.out.println("+-----------------+--------------+---------------+-----------------+----------------+-------------------+");
System.out.println("| Transaction Date| Branch Name | Deposit Count | Withdrawal Count| Total Deposit | Total Withdrawal |");
System.out.println("+-----------------+--------------+---------------+-----------------+----------------+-------------------+");
DecimalFormat formatter = new DecimalFormat("#,###");
while (iterator.hasNext()) {
DepositWithdrawal data = iterator.next();
Assert.notNull(data, "'data' must not be null");
if (data.isWithdrawal()) {
withdrawalCount++;
totalWithdrawal = totalWithdrawal.add(data.amount());
}
else if (data.isDeposit()) {
depositCount++;
totalDeposit = totalDeposit.add(data.amount());
}
DepositWithdrawal nextData = iterator.hasNext() ? iterator.peek() : null;
boolean isBreak = isDifferentTransactionDate(data, nextData) || isDifferentBranchName(data, nextData);
if (isBreak) {
System.out.printf("| %-15s | %-12s | %13s | %15s | %14s | %17s |%n", data.transactionDate(),
data.branchName(), formatter.format(depositCount), formatter.format(withdrawalCount),
formatter.format(totalDeposit), formatter.format(totalWithdrawal));
depositCount = 0;
withdrawalCount = 0;
totalDeposit = BigInteger.ZERO;
totalWithdrawal = BigInteger.ZERO;
}
}
System.out.println("+-----------------+--------------+---------------+-----------------+----------------+-------------------+");
}
}
boolean isDifferentTransactionDate(DepositWithdrawal data1, DepositWithdrawal data2) {
LocalDate date1 = data1 != null ? data1.transactionDate() : null;
LocalDate date2 = data2 != null ? data2.transactionDate() : null;
return !Objects.equals(date1, date2);
}
boolean isDifferentBranchName(DepositWithdrawal data1, DepositWithdrawal data2) {
String s1 = data1 != null ? data1.branchName() : null;
String s2 = data2 != null ? data2.branchName() : null;
return !Objects.equals(s1, s2);
}
}
次のDDLと、
CREATE TABLE DEPOSIT_WITHDRAWAL (
BRANCH_NAME VARCHAR(8),
CUSTOMER_ID VARCHAR(4),
TRANSACTION_TYPE VARCHAR(1),
AMOUNT BIGINT,
TRANSACTION_DATE DATE
);
次のデータを用意します。
INSERT INTO DEPOSIT_WITHDRAWAL (BRANCH_NAME, CUSTOMER_ID, TRANSACTION_TYPE, AMOUNT,
TRANSACTION_DATE)
VALUES ('Saitama', '319', '1', 891240, '2011-10-01'),
('Saitama', '173', '1', 420216, '2011-10-01'),
('Chiba', '476', '0', 826811, '2011-10-01'),
('Chiba', '692', '1', 19301, '2011-10-01'),
('Tokyo', '160', '1', 220363, '2011-10-01'),
('Tokyo', '874', '0', 762613, '2011-10-01'),
('Tokyo', '88', '1', 441986, '2011-10-01'),
('Tokyo', '972', '0', 951090, '2011-10-01'),
('Tokyo', '521', '0', 277999, '2011-10-01'),
('Tokyo', '177', '0', 651350, '2011-10-01'),
('Saitama', '555', '1', 453222, '2011-10-02'),
('Chiba', '892', '1', 449405, '2011-10-02'),
('Chiba', '887', '0', 314118, '2011-10-02'),
('Chiba', '336', '1', 805071, '2011-10-02'),
('Tokyo', '850', '0', 748174, '2011-10-02'),
('Tokyo', '286', '1', 462774, '2011-10-02'),
('Tokyo', '260', '0', 606862, '2011-10-02'),
('Tokyo', '756', '0', 15399, '2011-10-02'),
('Tokyo', '770', '1', 802144, '2011-10-02'),
('Tokyo', '491', '0', 89243, '2011-10-02'),
('Saitama', '386', '1', 661562, '2011-10-03'),
('Saitama', '749', '0', 235999, '2011-10-03'),
('Saitama', '180', '0', 613690, '2011-10-03'),
('Saitama', '709', '1', 241846, '2011-10-03'),
('Chiba', '192', '1', 536581, '2011-10-03'),
('Chiba', '688', '1', 860915, '2011-10-03'),
('Tokyo', '930', '0', 374102, '2011-10-03'),
('Tokyo', '745', '0', 958636, '2011-10-03'),
('Tokyo', '241', '0', 68636, '2011-10-03'),
('Tokyo', '803', '0', 937094, '2011-10-03'),
('Saitama', '843', '1', 808066, '2011-10-04'),
('Saitama', '31', '0', 107638, '2011-10-04'),
('Saitama', '592', '1', 627890, '2011-10-04'),
('Saitama', '616', '1', 334311, '2011-10-04'),
('Saitama', '584', '0', 366661, '2011-10-04'),
('Chiba', '23', '0', 915191, '2011-10-04'),
('Chiba', '760', '0', 89511, '2011-10-04'),
('Chiba', '440', '0', 804531, '2011-10-04'),
('Chiba', '51', '1', 978903, '2011-10-04'),
('Tokyo', '56', '0', 763763, '2011-10-04'),
('Saitama', '560', '1', 763545, '2011-10-05'),
('Saitama', '27', '0', 203102, '2011-10-05'),
('Saitama', '565', '1', 285055, '2011-10-05'),
('Chiba', '510', '1', 112097, '2011-10-05'),
('Chiba', '4', '1', 871444, '2011-10-05'),
('Chiba', '554', '1', 614683, '2011-10-05'),
('Chiba', '877', '1', 529944, '2011-10-05'),
('Tokyo', '431', '0', 391908, '2011-10-05'),
('Tokyo', '191', '1', 789341, '2011-10-05'),
('Tokyo', '521', '1', 209756, '2011-10-05');
コードを実行すると、次のように日付・支店名ごとに入金・出金の件数と合計金額が集計されて表示されます。
+-----------------+--------------+---------------+-----------------+----------------+-------------------+
| Transaction Date| Branch Name | Deposit Count | Withdrawal Count| Total Deposit | Total Withdrawal |
+-----------------+--------------+---------------+-----------------+----------------+-------------------+
| 2011-10-01 | Chiba | 1 | 1 | 19,301 | 826,811 |
| 2011-10-01 | Saitama | 2 | 0 | 1,311,456 | 0 |
| 2011-10-01 | Tokyo | 2 | 4 | 662,349 | 2,643,052 |
| 2011-10-02 | Chiba | 2 | 1 | 1,254,476 | 314,118 |
| 2011-10-02 | Saitama | 1 | 0 | 453,222 | 0 |
| 2011-10-02 | Tokyo | 2 | 4 | 1,264,918 | 1,459,678 |
| 2011-10-03 | Chiba | 2 | 0 | 1,397,496 | 0 |
| 2011-10-03 | Saitama | 2 | 2 | 903,408 | 849,689 |
| 2011-10-03 | Tokyo | 0 | 4 | 0 | 2,338,468 |
| 2011-10-04 | Chiba | 1 | 3 | 978,903 | 1,809,233 |
| 2011-10-04 | Saitama | 3 | 2 | 1,770,267 | 474,299 |
| 2011-10-04 | Tokyo | 0 | 1 | 0 | 763,763 |
| 2011-10-05 | Chiba | 4 | 0 | 2,128,168 | 0 |
| 2011-10-05 | Saitama | 2 | 1 | 1,048,600 | 203,102 |
| 2011-10-05 | Tokyo | 2 | 1 | 999,097 | 391,908 |
+-----------------+--------------+---------------+-----------------+----------------+-------------------+
以上、MyBatisのCursorで行を先読みするメモでした。PeekingIteratorを使うことでシンプルに実装できました。