CassandraのクライアントAPI Hectorを触ってみた。

前回に引き続き、6割仕事4割趣味なタスクでCassandraを扱う必要が出てきて、どうせなら生のAPIじゃなくてHectorを使ってみようかと思ったのでメモ。
日本語文献がないどころか英語でも詳細な解説が少ないので、説明不足な部分もあるかと思います。というかAPIJavadocが説明不足すぐる。
以下、参考資料

Hector GitHub

rantav/hector · GitHub

資料

Hector Client for Apache Cassandra (pdf)

プロジェクトの作り方

ライブラリを揃えるのも面倒なのでmavenプロジェクトにします。
Hectorのバージョンは0.7.0-22を使用します。

pom.xml
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
	xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
	<modelVersion>4.0.0</modelVersion>
	<groupId>jp.gr.java_conf.ka_ka_xyz</groupId>
	<artifactId>HectorExample</artifactId>
	<version>0.0.1-SNAPSHOT</version>
	<packaging>jar</packaging>
	<repositories>
		<!-- Hector用リポジトリ -->
		<repository>
			<id>riptano</id>
			<name>riptano</name>
			<url>http://mvn.riptano.com/content/repositories/public/</url>
		</repository>
	</repositories>
	<dependencies>
		<dependency>
			<groupId>me.prettyprint</groupId>
			<artifactId>hector-core</artifactId>
			<version>0.7.0-22</version>
			<scope>compile</scope>
		</dependency>
		<!-- 他のslf4jログ実装でも問題ないはずです-->
		<dependency>
			<groupId>org.slf4j</groupId>
			<artifactId>slf4j-simple</artifactId>
			<version>1.5.11</version>
		</dependency>

		<dependency>
			<groupId>org.springframework</groupId>
			<artifactId>spring-beans</artifactId>
			<version>3.0.6.RELEASE</version>
			<scope>compile</scope>
		</dependency>
		<dependency>
			<groupId>org.springframework</groupId>
			<artifactId>spring-context</artifactId>
			<version>3.0.6.RELEASE</version>
			<scope>compile</scope>
		</dependency>
		<dependency>
			<groupId>org.springframework</groupId>
			<artifactId>spring-test</artifactId>
			<version>3.0.6.RELEASE</version>
			<scope>test</scope>
		</dependency>
		<!-- JUnit 4.3系だとコンパイルエラーとなるので注意-->
		<dependency>
			<groupId>junit</groupId>
			<artifactId>junit</artifactId>
			<scope>test</scope>
			<version>4.6</version>
		</dependency>
	</dependencies>
	<build>
		<plugins>
			<plugin>
				<groupId>org.apache.maven.plugins</groupId>
				<artifactId>maven-compiler-plugin</artifactId>
				<configuration>
					<source>1.6</source>
					<target>1.6</target>
					<encoding>utf-8</encoding>
				</configuration>
			</plugin>
		</plugins>
	</build>
</project>
依存性注入

接続設定(接続先サーバー情報、Cassandraクラスター名、一貫性ポリシー)については、Springを使ってXMLから依存性を注入します。
下記の内容で"applicationContext.xml"を作成し、クラスパス直下(src/test/resources等)に置きます。

<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
    xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:context="http://www.springframework.org/schema/context"
    xsi:schemaLocation="http://www.springframework.org/schema/beans
           http://www.springframework.org/schema/beans/spring-beans-3.0.xsd
           http://www.springframework.org/schema/context
           http://www.springframework.org/schema/context/spring-context-3.0.xsd">
    <context:annotation-config/>
    <bean id="cassandraHostConfigurator" class="me.prettyprint.cassandra.service.CassandraHostConfigurator">
        <constructor-arg value="${hostname}:${portnum}"/>
    </bean>

    <bean id="cluster" class="me.prettyprint.cassandra.service.ThriftCluster">
        <constructor-arg value="TestCluster"/>
        <constructor-arg ref="cassandraHostConfigurator"/>
    </bean>

    <bean id="consistencyLevelPolicy" class="me.prettyprint.cassandra.model.ConfigurableConsistencyLevel"> 
        <property name="defaultReadConsistencyLevel" value="ONE"/>
    </bean>
</beans>

ファイル中の${hostname}、${portnum}はそれぞれ使用するCassandra環境のものに置き換えてください。

テストクラス

とりあえず、APIがどのような動作をするのか試してみるためのテストクラスを作成します。(このテストクラスの目的は、取り敢えずサックリ動かすことであり、動作仕様の定義という意味はありません)

今のところクエリ発行のみ。更新系メソッドについては後述の予定です。

import javax.annotation.Resource;

import junit.framework.TestCase;

import org.junit.Test;
import org.junit.runner.RunWith;
import org.springframework.test.context.ContextConfiguration;
import org.springframework.test.context.junit4.SpringJUnit4ClassRunner;

import me.prettyprint.cassandra.serializers.IntegerSerializer;
import me.prettyprint.cassandra.serializers.StringSerializer;
import me.prettyprint.cassandra.service.ThriftCluster;
import me.prettyprint.hector.api.Keyspace;
import me.prettyprint.hector.api.Serializer;
import me.prettyprint.hector.api.beans.ColumnSlice;
import me.prettyprint.hector.api.beans.HColumn;
import me.prettyprint.hector.api.beans.OrderedRows;
import me.prettyprint.hector.api.beans.Row;
import me.prettyprint.hector.api.beans.Rows;
import me.prettyprint.hector.api.factory.HFactory;
import me.prettyprint.hector.api.query.ColumnQuery;
import me.prettyprint.hector.api.query.MultigetSliceQuery;
import me.prettyprint.hector.api.query.QueryResult;
import me.prettyprint.hector.api.query.RangeSlicesQuery;
import me.prettyprint.hector.api.query.SliceQuery;

@RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration(locations = "/applicationContext.xml")
public class HectorExample extends TestCase {

	/**
	 * クエリ作成時のデータ型指定。他にIntegerSerializer等が用意されています。
	 * */
	private static final Serializer<String> ss = StringSerializer.get();

	/**
	 * キースペース名
	 * */
	private static final String KEYSPACE = "keyspace";
	
	/**
	 * 接続設定についてはSpringで注入
	 * */
	@Resource(name = "cluster")
	private ThriftCluster cluster;

	/**
	 * 特定のカラムについての情報を取得するためのクエリ
	 * */
	@Test
	public void testColumnQueryExample() {

		Keyspace keyspaceOperator = HFactory.createKeyspace(KEYSPACE,
				cluster);

		//クエリインスタンスの作成
		ColumnQuery<String, String, String> columnQuery = HFactory
				.createColumnQuery(keyspaceOperator, ss, ss, ss);
		columnQuery
			//カラムファミリーを指定
			.setColumnFamily("cfamily")
			//Row Keyを指定
			.setKey("keyid")
			//カラム名を指定
			.setName("column_name");
		//結果の取得
		QueryResult<HColumn<String, String>> result = columnQuery.execute();
		System.out.println(result.get().toString());
	}
	
	/**
	 * 特定のローに存在する複数のカラムについての情報を取得するためのクエリ
	 * */
	@Test
	public void testSliceQueryExample() {

		Keyspace keyspaceOperator = HFactory.createKeyspace(KEYSPACE,
				cluster);

		//クエリインスタンスの作成
		SliceQuery<String, String, String> sliceQuery = HFactory.createSliceQuery(
				keyspaceOperator, ss, ss, ss);
		
		sliceQuery
			//カラムファミリの指定
			.setColumnFamily("cfamily")
			//Row Keyを指定
			.setKey("keyid")
			/*カラム名の範囲を指定。
			 * 特定のプレフィックスが付いたカラムのみを検索する場合は
			 * 第一引数にプレフィックスを指定。
			 * 下記の例では無指定。*/
			.setRange("", "", false, 100);
		QueryResult<ColumnSlice<String, String>> results = sliceQuery.execute();
		ColumnSlice<String, String> col = results.get();
		System.out.println(col.toString());
	}

	/**
	 * 複数のロー(キー名で範囲指定)に存在する複数のカラムについての情報を取得するためのクエリ。
	 * OrderPreservingPartitionerを使用しない場合、Row Keyの順序はランダムになるので注意。
	 * */
	@Test
	public void testRangeSlicesQueryExample() {

		Keyspace keyspaceOperator = HFactory.createKeyspace(KEYSPACE,
				cluster);
		RangeSlicesQuery<String, String, String> rsQuery = HFactory
				.createRangeSlicesQuery(keyspaceOperator, ss, ss, ss);
		rsQuery
			//カラムファミリの指定
			.setColumnFamily("cfamily")
			//Row Keyの範囲を指定します
			.setKeys("from_keyid", "to_keyid")
			/*カラム名の範囲を指定。
			 * 特定のプレフィックスが付いたカラムのみを検索する場合は
			 * 第一引数にプレフィックスを指定。
			 * 下記の例では無指定。*/
			.setRange("", "", false, 3)
			//取得するRow数を制限
			.setRowCount(10);
		OrderedRows<String, String, String> results = rsQuery.execute().get();
		for (Row<String, String, String> row : results) {
			System.out.println(row.toString());

		}
	}
	
	/**
	 * 複数のロー(キー名を明示)に存在する複数のカラムについての情報を取得するためのクエリ
	 * */
	@Test
	public void testMultigetSliceQuery() {

		Keyspace keyspaceOperator = HFactory.createKeyspace(KEYSPACE,
				cluster);

		MultigetSliceQuery<String, String, String> msQuery = HFactory
				.createMultigetSliceQuery(keyspaceOperator, ss, ss, ss);
		msQuery
			//カラムファミリの指定
			.setColumnFamily("cfamily")
			//検索対象とするRowのキー名を列挙
			.setKeys("key1", "key2", "key3", "key4")
			/*カラム名の範囲を指定。
			 * 特定のプレフィックスが付いたカラムのみを検索する場合は
			 * 第一引数にプレフィックスを指定。
			 * 下記の例では無指定。*/
			.setRange("", "", false, 3);
		Rows<String, String, String> results = msQuery.execute().get();
		for (Row<String, String, String> row : results) {
			System.out.println(row.toString());
		}
	}
}

setRangeメソッドの第3、第4引数の意味は不明・・・・
追記: CassandraのColumnFamilyは、RDBのテーブルのように細かく砕いて正規化することは推奨されない(そもそも、正規化というのはあくまでテーブル間に関係性を定義できたり、Joinクエリを発行できる"リレーショナル"DBに特化した考え方)。つまり、一つのColumnFamilyに大量のColumnがぶら下がることを前提としているので、キーの範囲や取得件数を示して一括取得するAPIが有ると。納得。