Webエンジニアのメモ帳

技術的な話を中心に書いています。

【Spring Boot】Cassandraを使った開発でトランザクション処理を行う

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を使う場合でも、セカンダリ・インデックスを使うという手もあります。

ただし、今回説明した方法には処理が高速という利点があり、使いどころがなくもないテクニックなわけです。

もっと詳しく

今回説明したトランザクション処理については、こちらのサイトがよくまとまっています。

複雑なので解読に時間がかかるかもしれませんが、やろうとしていることは、今回の動物園の例と同じです。

つまり、主キーが異なるが格納する情報はほぼ同じ複数のテーブルに対して、データの挿入や削除を行う、といった内容となっています。