Cassandra×SpringBoot 奮闘記録

# この記事の概要
- Cassandraに触れたことがなかったので、構築〜SpringBootのアプリケーションから参照・更新をすることろまでをやってみたときの記録
- 環境はローカルPC（Mac）

# 参考
Cassandra: http://cassandra.apache.org/doc/latest/
SpringDataCassandra: https://spring.io/projects/spring-data-cassandra
property設定項目: https://docs.spring.io/spring-boot/docs/current/reference/html/appendix-application-properties.html#data-properties

# Cassandraの構築

インストール

```bash
# Cassandra本体
brew install cassandra
# CQL(Cassandra Query Language)
brew install cql
```

起動

```bash
cassandra -f
```

> Connection error: ('Unable to connect to any servers', {'127.0.0.1': error(61, "Tried connecting to [('127.0.0.1', 9042)]. Last error: Connection refused")})

動かん。。。
僕の場合はJAVA_HOMEがJava11のディレクトリを指しているのがいけなかったようです。。。
定義し直したら無事起動！

```
export JAVA_HOME=/Library/Java/JavaVirtualMachines/jdk1.8.0_211.jdk/Contents/Home/
cassandra -f
```

どうせならHomebrewで起動！

```
vi /usr/local/etc/cassandra/cassandra-env.sh
# 末尾に「export JAVA_HOME=/Library/Java/JavaVirtualMachines/jdk1.8.0_211.jdk/Contents/Home/」を追記

brew services start cassandra
```

接続！

```
cqlsh
```

> cqlsh localhost 9042
これと一緒

# ちょっとCassandra単体で遊んでみる

```bash
cqlsh
```

ここから再開
こまかいオプションの説明はここでは省略。。。

## KeySpaceの追加

MySQLやOracleでいうDatabaseに当たる概念

```sql
-- # keyspace追加前
DESCRIBE keyspaces
-- system_traces  system_schema  system_auth  system  system_distributed

-- # 「sample」 keyspace追加
CREATE KEYSPACE sample WITH REPLICATION = {'class':'SimpleStrategy','replication_factor':1};

-- # keyspace追加前
DESCRIBE keyspaces
-- system_schema  system_auth  system  sample  system_distributed  system_traces
```

ちなみに削除は

```sql
DROP KEYSPACE sample;
```

## Databaseの移動

```sql
USE sample
```

## テーブルの作成

ここではシンプルにKeyとValueを持つデータを想定

```sql
-- 作成
CREATE TABLE t_sample (key text PRIMARY KEY, value text);

-- 一覧確認
DESCRIBE tables;

-- テーブル定義の確認
DESCRIBE table t_sample;
```

ちなみに削除は

```sql
DROP TABLE t_sample;
```

## データ操作

先ほど作成したt_sampleテーブルを使って一通り

```sql
-- 参照（空っぽ）
select * from t_sample;

-- 参照（上と同等）
select key, value from t_sample;

-- 登録
INSERT INTO t_sample (key, value) VALUES ('key1', 'value1');

-- 登録（重複）
-- クエリは成功して、updateのような動きになっている
INSERT INTO t_sample (key, value) VALUES ('key1', 'value2');

-- 更新
UPDATE t_sample SET value = 'value3' WHERE key = 'key1';

-- 削除
DELETE FROM t_sample WHERE key = 'key1';
```

primary違反したら弾かれるんじゃなくてupdateされるのは初体験


# Springアプリケーションの構築

## project作成

```groovy:build.gradle
plugins {
    id 'org.springframework.boot' version '2.2.4.RELEASE'
    id 'io.spring.dependency-management' version '1.0.9.RELEASE'
    id 'java'
}

group = 'com.example'
version = '0.0.1-SNAPSHOT'
sourceCompatibility = '11'

repositories {
    mavenCentral()
}

dependencies {
    implementation 'org.springframework.boot:spring-boot-starter-data-cassandra'
    // webfluxは動作確認用です
    implementation 'org.springframework.boot:spring-boot-starter-webflux'
    compileOnly('org.projectlombok:lombok')
    annotationProcessor('org.projectlombok:lombok')
}
```

```yaml:application.yml
spring:
  data:
    cassandra:
      keyspace-name: sample
```

## テーブルのモデルクラスを作成

```Java:Sample.java
import lombok.Value;
import org.springframework.data.cassandra.core.mapping.PrimaryKey;
import org.springframework.data.cassandra.core.mapping.Table;

@Data
@Table("t_sample")
public class Sample {
    @PrimaryKey
    private String key;
    private String value;
}
```

## 登録・参照・削除をしてみる

```Java:Application.java
@Slf4j
@SpringBootApplication
public class Application {
    public static void main(String[] args) {
        //SpringApplication.run(Application.class);

        Cluster cluster = Cluster.builder()
                .addContactPoints("localhost")
                .withoutJMXReporting()  // 付けないとエラー
                .build();
        Session session = cluster.connect("sample");

        var template = new CassandraTemplate(session);
        var data = new Sample("key1", "value1");

        template.insert(data);
        // 全件検索
        var selected = template.select("SELECT * from t_sample", Sample.class);
        log.info("selected: {}", selected);
        // 更新
        data = new Sample("key1", "value2");
        template.update(data);
        // 条件付き検索
        selected = template.select(Query.query(Criteria.where("key").is("key1")), Sample.class);
        log.info("selected: {}", selected);
        // 削除（PK指定）
        template.deleteById("key1", Sample.class);
        // 件数取得
        var count = template.count(Sample.class);
        log.info("count: {}", count);

        // 接続クローズ
        session.close();
        cluster.close();
    }
}

```

`.withoutJMXReporting()` については記述しておかないと以下のエラーが発生した

> Exception in thread "main" java.lang.NoClassDefFoundError: com/codahale/metrics/JmxReporter

## Webアプリケーションっぽくしてみる

まずは「spring-boot-starter-data-cassandra」がどこまでBean登録してくれるのか調査

**Cluster**

- `org.springframework.boot.autoconfigure.cassandra.CassandraAutoConfiguration`がBean登録している
- `spring.data.cassandra`配下のpropertiesを設定すれば良さそう
- `spring.data.cassandra.keyspace-name=sample`とか

**Session**

- SessionというかSessionFactoryなのかな？
- `org.springframework.boot.autoconfigure.data.cassandra.CassandraDataAutoConfiguration`がBean登録している
- こちらも同様に`spring.data.cassandra`配下の設定が反映されそう

**CassandraTemplate**

- Session同様、`org.springframework.boot.autoconfigure.data.cassandra.CassandraDataAutoConfiguration`がBean登録している

結論、、、
いきなりCassandraTemplate使うでOK！

```Java:CassandraController.java
@RequiredArgsConstructor
@RestController
public class CassandraController {
    private final CassandraTemplate template;

    // curl localhost:8080
    @GetMapping("")
    public List<Sample> all() {
        return template.select("SELECT * FROM t_sample", Sample.class);
    }

    // curl localhost:8080/key1
    @GetMapping("/{key}")
    public Sample getByKey(@PathVariable String key) {
        return template.selectOneById(key, Sample.class);
    }

    // curl localhost:8080/insert -d '{"key":"key1","value":"value1"}' -X PATCH -H 'Content-Type: application/json'
    @PatchMapping("/**")
    public Sample patch(@RequestBody Sample sample) {
        return template.insert(sample);
    }

    // curl localhost:8080/key1 -X DELETE
    @DeleteMapping("/{key}")
    public Boolean delete(@PathVariable String key) {
        return template.deleteById(key, Sample.class);
    }
}
```

# Reactive対応

ReactiveCassandraTemplateを使うだけ！
返り値の型をFlux or Monoにするもの忘れずに！（一括でPublisherとしてもOK）

```Java:ReactiveCassandraController.java
@RequiredArgsConstructor
@RestController
@RequestMapping("/reactive")
public class ReactiveCassandraController {
    private final ReactiveCassandraTemplate template;

    // curl localhost:8080/reactive
    @GetMapping("")
    public Flux<Sample> all() {
        return template.select("SELECT * FROM t_sample", Sample.class);
    }

    // curl localhost:8080/reactive/key1
    @GetMapping("/{key}")
    public Mono<Sample> getByKey(@PathVariable String key) {
        return template.selectOneById(key, Sample.class);
    }

    // curl localhost:8080/reactive/insert -d '{"key":"key1","value":"value1"}' -X PATCH -H 'Content-Type: application/json'
    @PatchMapping("/**")
    public Mono<Sample> patch(@RequestBody Sample sample) {
        return template.insert(sample);
    }

    // curl localhost:8080/reactive/key1 -X DELETE
    @DeleteMapping("/{key}")
    public Mono<Boolean> delete(@PathVariable String key) {
        return template.deleteById(key, Sample.class);
    }

}
```