Spring BootでCassandraを使っている場合に、トランザクション処理を行う方法を説明します。
トランザクション処理とは
Cassandraに2つのデータを同時に挿入することを考えます。
このとき、エラーが発生した場合でも、片方のデータだけが挿入されている状態は許可したくない、ということがあります。
つまり、1つ目のデータを挿入し、2つ目のデータを挿入するところでエラーが発生した場合、1つ目のデータを削除する、ということです。
これは「トランザクション処理」と呼ばれています。
Spring BootではCassandraBatchOperationsというクラスを使うと、これを実現できます。その方法を説明します。
トランザクション処理の実装方法
ここでは銀行のシステムで、Cassandraにmoneyというテーブルが存在し、以下のようなエンティティクラス(Cassandraのレコードをマッピングするクラス)があるとします。
@Table("money")
@Value
public class Person {
// 名前
@PrimaryKey private String name;
// 預金額
private int money;
}
このレポジトリクラスは以下のようなコードです。
import org.springframework.data.cassandra.repository.CassandraRepository;
public interface MoneyRepository extends CassandraRepository<Money, String> {}
この時、例えば岡田さんが田中さんに500円送金するコードは以下のような感じになります。
CassandraBatchOperations batchOps = cassandraTemplate.batchOps();
// テーブルから情報を取得
Money okada = moneyRepository.findById("岡田");
Money tanaka = moneyRepository.findById("田中");
// 500円送金するので、岡田さんの貯金を500円減らし、田中さんの貯金を500円増やす
okada.setMoney(okada.getMoney() - 500);
tanaka.setMoney(tanaka.getMoney() + 500);
// テーブルの情報を更新
// Cassandraなので、INSERT文を実行すれば、主キーが同じレコードのUPDATEが行われる
batchOps.insert(okada);
batchOps.insert(tanaka);
batchOps.execute();
こうすると、例えば以下のように、1件目のデータの更新と2件目のデータの更新の間で例外を発生させた場合、1件目のデータの更新が取り消されます。
CassandraBatchOperations batchOps = cassandraTemplate.batchOps();
// テーブルから情報を取得
Money okada = moneyRepository.findById("岡田");
Money tanaka = moneyRepository.findById("田中");
// 500円送金するので、岡田さんの貯金を500円減らし、田中さんの貯金を500円増やす
okada.setMoney(okada.getMoney() - 500);
tanaka.setMoney(tanaka.getMoney() + 500);
// テーブルの情報を更新
// Cassandraなので、INSERT文を実行すれば、主キーが同じレコードのUPDATEが行われる
batchOps.insert(okada);
// ここで例外が発生しても、↑で500円減った岡田さんの貯金は元に戻る
throw new RuntimeException("エラーが発生しました。");
batchOps.insert(tanaka);
batchOps.execute();
Cassandraでトランザクション処理を行う場面
今回は銀行のシステムを例に取り上げましたが、そもそもの話として、こうしたトランザクション処理をしたい場合には、MySQLなどのRDBを使うべきです。
では、実際に使う場面としてどのような場面があるかというと、以下のようなケースです。
今回は例として、動物園の名前と、そこにいる動物の種類を格納するテーブルを作成するとします。
そして、ある動物園にいる動物の一覧も知りたいし、ある動物がいる動物園の一覧も知りたい、とします。
そうすると、Cassandraは主キー以外での検索ができませんから、テーブルを2つ作成することになります。
以下はそのエンティティクラスです。1つ目のテーブルは動物園名が主キーになっています。
@Table("animal_by_zoo")
@Value
public class AnimalByZoo {
// 動物園の名前
@PrimaryKey private String zoo;
// 動物の名前
private String animal;
}
2つ目のテーブルは動物名が主キーになっています。
@Table("animal_by_animal")
@Value
public class AnimalByAnimal {
// 動物園の名前
@PrimaryKey private String animal;
// 動物の名前
private String zoo;
}
このとき、データを追加するには2つのテーブルを同時に更新する必要があります。
CassandraBatchOperations batchOps = cassandraTemplate.batchOps();
batchOps.insert(new AnimalByZoo("上野動物園", "パンダ"));
batchOps.insert(new AnimalByAnimal("パンダ", "上野動物園"));
batchOps.execute();
こうすると、たとえば上野動物園にいる動物を検索することも、パンダがいる動物園を探すこともできるというわけです。
...ちなみに、こうしたケースでもMySQLなどのRDBを使った方が実装は楽です。また、Cassandraを使う場合でも、セカンダリ・インデックスを使うという手もあります。
ただし、今回説明した方法には処理が高速という利点があり、使いどころがなくもないテクニックなわけです。
もっと詳しく
今回説明したトランザクション処理については、こちらのサイトがよくまとまっています。
複雑なので解読に時間がかかるかもしれませんが、やろうとしていることは、今回の動物園の例と同じです。
つまり、主キーが異なるが格納する情報はほぼ同じ複数のテーブルに対して、データの挿入や削除を行う、といった内容となっています。