Cassandraのはじめ方─手を動かしてNoSQLを体感しよう

第5回APIの全体像とデータの投入

前回でCassandraへのアクセスするコードの基本をおさえました。今回解説するのは以下の2点です。

  • CassandraのAPIの全体像
  • Cassandraにデータを投入するコードの詳細

3つの分類からAPIの全体像をおさえる

第1回でもご紹介しましたが、CassandraのクライアントAPIはThriftによって自動生成されます。APIは非常にシンプルなものが幾つかあるだけで、覚えるのもさほど難しくはありません。本連載ではその中からよく使うものに特化してご紹介していきます。

以下にCassandraのAPIを、データ挿入系・データ検索系・認証/管理系の3つに分類してまとめてみました。まずはこれらにひと通り目を通してみてください。

データ挿入/更新/削除のAPI

データ挿入、更新、削除のAPIは以下の表のとおりです。現実的によく使う中心的なAPIはbatch_mutate、removeの2つです。この連載でも幾度と登場することになるので、まずはこれらをおさえておいてください。

表1 データ挿入、更新、削除のAPI
メソッド名説明
insertカラムファミリかスーパーカラム内に1件カラムを挿入する
batch_insert同一キー内のカラムやスーパーカラムを一括挿入する[1]
batch_mutateバッチ的に複数のカラムまたはスーパーカラムを更新する
remove特定行のデータまたはカラムファミリまたはスーパーカラムを指定して削除する

データ検索系

データ検索系の中心となるのはget_slice、get_range_slicesの2つです。特定のキーまたはキーのグループがわかっている場合にはmultiget_sliceを使うといいでしょう。検索結果が無い場合の挙動がそれぞれのAPIで多少違うので、あわせて付記しておきました。

表2 データ検索系のAPI
メソッド名説明検索結果が無い場合の挙動
get特定キーからカラムまたはスーパーカラム1つを取り出すNotFoundExceptionが発生する
get_sliceスライスされたカラムまたはスーパーカラムの集合を返す空のListを返す
multiget複数キーに対して並列に複数getをこなす値のカラムまたはスーパーカラムが空のMapを返す[2]
multiget_slice複数キーに対して複数のget_sliceを並列にこなす値のカラムまたはスーパーカラムが空のMapを返す
get_countカラムファミリまたはスーパーカラム内のカラム数を数える0を返す
get_range_sliceキーレンジでカラムのサブセットを返す空の配列を返す[3]
get_range_slicesキーレンジでカラムのサブセットを返す空の配列を返す

認証/管理系API

認証管理系のAPIで重要なのは、認証を行うloginです。loginでどのような挙動を行うかは設定により変更可能です。

運用・管理については、じつは別途nodetool、nodeprobeといったツールが準備されているので、APIよりそちらを利用するのが一般的です。しかし、プログラム経由で運用・管理を行いときのために知っておくとよいでしょう。

表3 認証/管理系API
メソッド名説明
loginキースペースごとに認証する
get_string_propertyノードやクラスタ情報をキー指定で取り出す[4]
get_string_list_propertyノードやクラスタ情報を取り出す[5]
describe_keyspacesキースペースを取り出す
describe_cluster_nameクラスタ名を取得する
describe_versionThrift APIのバージョン名を取得する
describe_ringトークンのレンジとホストアドレスのリングを表示する
describe_keyspaceキースペース内のカラムファミリ名とその属性のMapを取得する
describe_splitsHadoopの並列処理クエリ用の実験的なAPI[6]

カラムを1つ挿入する

では、Cassandraへの操作を書いていきましょう。まずはデータがないと何もならないので、データを入れてみます。前回もデータ挿入のサンプルをご紹介しましたが、Cassandraへデータを入れるには大きく分けて2つの方法があります。

  1. insertでカラムを1つづつ入れていく
  2. batch_mutateでバッチ的に一気にデータを投入する

前出の表1にはbatch_insertというAPIも記載しましたが、今後廃止予定になっているので本連載では割愛します。ひとまず、前回の振り返りでinsertでカラムを挿入するところからはじめてみましょう。

カラムを1つ挿入するにはinsertを使います。insertは以下の引数をとります。

void insert(String keyspace, 
            String key, 
            ColumnPath column_path, 
            byte[] value, 
            long timestamp, 
            ConsistencyLevel consistencyLevel)

ColumnPathでデータの格納場所を、ConsistencyLevelで一貫性レベルを指定

前回の最後にご紹介したコードの抜粋を使っておさらいしておくと、実際には以下のような形で使います。

	final String key = "sample1";
	final String columnName = "hoge";
	final String value = "sample_value";
	final long timestamp = System.currentTimeMillis();

	// ColumnPathは単一のカラムの位置を特定するためのクラス(後述)
	final ColumnPath columnPath = new ColumnPath(COLUMN_FAMILY);
	columnPath.setColumn(columnName.getBytes());

	// 1件カラムをインサート
	client.insert(KEYSPACE, key, columnPath, value.getBytes(),
			timestamp, ConsistencyLevel.ONE);

この中で、ColumnPathとConsistencyLevelの2つはまだあまり馴染みの無いと思います。くわしく見ていきましょう。

ColumnPath

ColumnPathはカラムファミリとカラム名を特定するためのクラスで、その名のとおりデータをどこに格納すればよいかを明示します。ディレクトリ構造におけるパスのような扱いと思ってもらえれば差し支えありません。そのため以下の指定を行う必要があります。

表4 ColumnPathで指定する項目
カラムファミリ名
カラム名
スーパーカラム名(スーパーカラムを使っている場合)
図1 ColumnPath
図1 ColumnPath

ConsistencyLevel

Cassandraでは全ての操作で、その操作に対する一貫性レベルをクライアントコードから与える形式をとっています。全ての操作の引数にConsistencyLevelのenumがついてくるのはそのためです。ConsistencyLevelは書き込み時と読み込み時で表5のような意味を持っています。

表5 ConsistencyLevelの一覧
ConsistencyLevel読み込み時書き込み時
ZERO使用不可何も保障しない書き込み。非同期に実行される
ONEもっとも最初に読めた1ノードのデータを返す(※7)クライアントに返すまでの間に1ノードに書き込まれることを保障する
QUORUM全ノードの内、レプリケーション数 / 2+1からデータが読めたら返す。返すデータは最新のタイムスタンプのものを返すクライアントに返すまでの間に(レプリケーション数 / 2+1)のノードに書き込まれることを保障する
DCQUORUM0.6.1ではALLと同等(※8)0.6.1ではALLと同等
DCQUORUMSYNC0.6.1ではALLと同等0.6.1ではALLと同等
ALL全ノードから読めた場合、データを返す(※9)クライアントに返すまでの間にレプリケーション数だけ書き込まれる事を保障する
ANY使用不可1度どこかのタイミングで書き込まれる事を保障する

注意すべき点は、これらの一貫性はノード数全体ではなく、storage-conf.xmlで指定したレプリケーション数によって決まる点です。具体的には<ReplicationFactor>の設定で、キースペースごとに指定します。

RDBMSのようなACIDでの強度な一貫性と異なり、CassandraはEventual Consistency(一貫性のある状態にいつか到達する)という思想に基づき設計されています。つまり、一貫性を緩める代わりに応答速度を向上させるという狙いがあります。ConsistencyLevelにバリエーションがあるのもそのためです。

スーパーカラム内の1つのカラムにデータを入れる

ではサンプルコードに戻って、スーパーカラム内の1つのカラムにデータを入れてみましょう。

スーパーカラム内のカラムにデータを入れる方法は、カラムのときとほとんど変わりません。違うのはColumnPathでどのスーパーカラムのどのカラムかを指定するようにしている点だけです。スーパーカラムとカラムは、以下のようにして指定します。

  • columnPath.setSuper_column() → スーパーカラム名を指定
  • columnPath.setColumn() → カラム名を指定
リスト1 SimpleSuperColumnInsert.java
final String key = "super_sample1";
final String superColumnName = "sample1";
final String columnName = "foo";
final String value = "super_sample_value";
final long timestamp = System.currentTimeMillis();

// ColumnPathは単一のカラムの位置を特定するためのクラス
final ColumnPath columnPath = new ColumnPath(COLUMN_FAMILY);
// スーパーカラム名を指定
columnPath.setSuper_column(superColumnName.getBytes());
// カラム名を指定
columnPath.setColumn(columnName.getBytes());

// 1件カラムをインサート
client.insert(KEYSPACE, key, columnPath, value.getBytes(),
		timestamp, ConsistencyLevel.ONE);

カラムを複数まとめて追加する

次は複数カラムをまとめて追加してみましょう。複数カラムをまとめて追加するにはbatch_mutateを使います。batch_mutateは以下の引数をとります。

  void batch_mutate(String keyspace,
                    Map<string, Map<string, List<Mutation>>> mutation_map,
                    ConsistencyLevel consistencyLevel)

batch_mutateでポイントとなるのは、真ん中の引数のmutation_mapです。ここにカラムファミリのキーとカラムファミリを指定して、Mutationというオブジェクトを保持する構造になっています。図2を見てください。

図2 MutationMapの構造
図2 MutationMapの構造

ここで出てくるMutationとは、挿入と削除を表現するためのオブジェクトのことです。Mutationは、カラムまたはスーパーカラムを保持する役目を持つColumnOrSuperColumnと、削除を表現するDeletionを持ちます。オブジェクト構造は図のMutationのようになっています。

では、3つのカラムをアップデートするコードをみてみましょう。以下は一部抜粋なので、全体はサンプルファイルのダウンロードからご確認ください。

この例ではhoge、foo、barというカラム3つをbatch_mutateを使って挿入しています。実際にカラムを追加している部分は、Mutationを作成しているところです。

リスト2 SimpleMultipleColumnsInsert.java
final String rowkey = "sample2";
final long timestamp = System.currentTimeMillis();
// ロウキーとカラムファミリのMapを作成
Map<String, Map<String, List<Mutation>>> mutationMap = newMap();
// カラムファミリのMapを作成。キーはカラムファミリ名、値はMutationのList
Map<String, List<Mutation>> columnFamilyMap = newMap();

// インサートの場合、MutationのListにはカラムかスーパーカラムを設定する
List<Mutation> mutations = newList();
// hogeというカラムキーで"hoge_value"を追加
{
	Mutation mutation = new Mutation();
	ColumnOrSuperColumn csc = new ColumnOrSuperColumn()
			.setColumn(new Column("hoge".getBytes(), "hoge_value
					.getBytes(), timestamp));
	mutation.setColumn_or_supercolumn(csc);
	mutations.add(mutation);
}
// fooというカラムキーで"foo_value"を追加
{
	Mutation mutation = new Mutation();
	ColumnOrSuperColumn csc = new ColumnOrSuperColumn()
			.setColumn(new Column("foo".getBytes(), "foo_value"
					.getBytes(), timestamp));
	mutation.setColumn_or_supercolumn(csc);
	mutations.add(mutation);
}
// barというカラムキーで"bar_value"を追加
{
	Mutation mutation = new Mutation();
	ColumnOrSuperColumn csc = new ColumnOrSuperColumn()
			.setColumn(new Column("bar".getBytes(), "bar_value"
					.getBytes(), timestamp));
	mutation.setColumn_or_supercolumn(csc);
	mutations.add(mutation);
}
columnFamilyMap.put(COLUMN_FAMILY, mutations);
mutationMap.put(rowkey, columnFamilyMap);
client.batch_mutate(KEYSPACE, mutationMap, ConsistencyLevel.ALL);

スーパーカラム内のカラムを複数まとめて追加する

では最後に、スーパーカラム内のカラムを複数まとめて追加してみましょう。

やり方はカラムを複数追加するときとあまり変わりありません。唯一違うのは、SuperColumnを作成して、Columnを複数追加するところだけです。

注意していただきたいのは、SuperColumnは名前が必須になる点です。SuperColumn#setName()でスーパーカラム名を忘れず設定しましょう。ここでは"shot_profile"という名前をつけました。

リスト3 SimpleMultipleSuperColumnsInsert.java
final String rowkey = "super_sample2";
final long timestamp = System.currentTimeMillis();
// ロウキーとカラムファミリのMapを作成
Map<String, Map<String, List<Mutation>>> mutationMap = newMap();
// カラムファミリのMapを作成。キーはカラムファミリ名、値はMutationのList
Map<String, List<Mutation>> columnFamilyMap = newMap();

// インサートの場合、MutationのListにはカラムかスーパーカラムを設定する
List<Mutation> mutations = newList();
// SuperColumnを作成し、Columnを幾つか足す
{
	// SuperColumnを作成し、Columnを幾つか追加する
	SuperColumn superColumn = new SuperColumn();
	superColumn.setName("shot_profile".getBytes());

	superColumn.addToColumns(new Column("name".getBytes(),
			"shinpei ohtani".getBytes(), timestamp));
	superColumn.addToColumns(new Column("id".getBytes(), "shot6"
			.getBytes(), timestamp));
	superColumn.addToColumns(new Column("job".getBytes(),
			"programmer".getBytes(), timestamp));
	mutations.add(new Mutation()
			.setColumn_or_supercolumn(new ColumnOrSuperColumn()
					.setSuper_column(superColumn)));
}
columnFamilyMap.put(COLUMN_FAMILY, mutations);
mutationMap.put(rowkey, columnFamilyMap);
client.batch_mutate(KEYSPACE, mutationMap, ConsistencyLevel.ALL);

今回はCassandraのデータの挿入に着目して、紹介しました。

実際に使うときは、Cassandra用のユーティリティを自前で作るなり、既存の高レベルなクライアントフレームワークなりを使うことが多くなります。ただ、プリミティブなレベルでどのようなコードになるかをイメージできるようにしておくと効率的にCassandraにアクセスできるようになるので、ひと通り知っておくといいでしょう。

なお、今回作成したサンプルプログラム全体は、以下からダウンロード可能です。

次回はデータの更新と削除について紹介します。お楽しみに。

おすすめ記事

記事・ニュース一覧