Cassandraのインストール
cassandraのインストールは以下を参考に行う
Cassandraのテーブル・データの準備
keyspaceとテーブルの作成は以下。keyspace名はstoreとしている。
bookという名前でテーブルを作成し、primary keyとしてbookのidとversionをセットして、それ以外のカラムにtitle, description, authorsがある。
今回はbookのカラムにauthorsを用意しリストでauthorを持つようにしている。authorはUDT(ユーザ定義型)で定義しカラムにはname, descriptionがある。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 |
cqlsh> CREATE KEYSPACE store WITH replication = {'class': 'SimpleStrategy', 'replication_factor' : 1}; cqlsh> use store; cqlsh:store> cqlsh:store> CREATE TYPE author ( ... name text, ... description text ... ); cqlsh:store> CREATE TABLE book ( ... book_id int, ... book_version int, ... title text, ... description text, ... authors list<FROZEN <author>>, ... PRIMARY KEY (book_id, book_version) ... ); |
データの挿入は以下のように行う。
selectしてみてデータが表示されたら準備はOK。
1 2 3 4 5 6 7 8 9 10 11 12 |
cqlsh:store> INSERT INTO book (book_id, book_version, title, description, authors) VALUES (1, 1, 'book title 1', 'book description 1', ...[{name: 'author name 1-1', description: 'author description 1-1'}, {name: 'author name 1-2', description: 'author description 1-2'}]); cqlsh:store> INSERT INTO book (book_id, book_version, title, description, authors) VALUES (2, 1, 'book title 2', 'book description 2', ...[{name: 'author name 2-1', description: 'author description 2-1'}, {name: 'author name 2-2', description: 'author description 2-2'}]); cqlsh:store> select * from book; book_id | book_version | authors | description | title ---------+--------------+--------------------------------------------------------------------------------------------------------------------------------------+--------------------+-------------- 1 | 1 | [{name: 'author name 1-1', description: 'author description 1-1'}, {name: 'author name 1-2', description: 'author description 1-2'}] | book description 1 | book title 1 2 | 1 | [{name: 'author name 2-1', description: 'author description 2-1'}, {name: 'author name 2-2', description: 'author description 2-2'}] | book description 2 | book title 2 (2 rows) cqlsh:store> |
データを取得するための実装
src以下のプロジェクト構成は以下。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 |
src └── main ├── java │ └── com │ └── example │ └── springbootcassandra │ ├── SpringBootCassandraApplication.java │ ├── config │ │ └── CassandraConfig.java │ ├── controller │ │ └── BookController.java │ ├── model │ │ ├── Author.java │ │ ├── Book.java │ │ └── BookKey.java │ ├── repository │ │ └── BookRepository.java │ └── service │ └── BookService.java └── resources ├── application.yml ├── static └── templates 13 directories, 9 files |
build.gradleにcassandraを使う為のライブラリとしてspring-boot-starter-data-cassandraを追加。
1 2 3 4 5 6 7 8 9 |
dependencies { implementation 'org.springframework.boot:spring-boot-starter-web' compileOnly 'org.projectlombok:lombok' annotationProcessor 'org.springframework.boot:spring-boot-configuration-processor' annotationProcessor 'org.projectlombok:lombok' // cassandra implementation 'org.springframework.boot:spring-boot-starter-data-cassandra' } |
cassandraのテーブル、カラムと対応するクラスはmodelフォルダ以下の3つで用意する。
Book.java
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 |
package com.example.springbootcassandra.model; import lombok.Getter; import org.springframework.data.cassandra.core.mapping.Column; import org.springframework.data.cassandra.core.mapping.PrimaryKey; import java.util.List; @Getter public class Book { @PrimaryKey private BookKey bookKey; @Column private String title; @Column private String description; @Column("authors") private List<Author> authors; } |
BookKey.java
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 |
package com.example.springbootcassandra.model; import lombok.Getter; import org.springframework.data.cassandra.core.cql.PrimaryKeyType; import org.springframework.data.cassandra.core.mapping.PrimaryKeyClass; import org.springframework.data.cassandra.core.mapping.PrimaryKeyColumn; @Getter @PrimaryKeyClass public class BookKey { @PrimaryKeyColumn(name = "book_id", ordinal = 0, type = PrimaryKeyType.PARTITIONED) private int bookId; @PrimaryKeyColumn(name = "book_version", ordinal = 1, type = PrimaryKeyType.CLUSTERED) private int bookVersion; } |
Author.java
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 |
package com.example.springbootcassandra.model; import lombok.Getter; import org.springframework.data.cassandra.core.mapping.Column; import org.springframework.data.cassandra.core.mapping.UserDefinedType; @UserDefinedType @Getter public class Author { @Column("name") private String name; @Column("description") private String description; } |
Bookクラスがテーブルのbookに対応し、Primary keyをBookKeyクラスとして切り出している。
Primary keyの少なくとも1つはPrimaryKeyColumでPartition keyとして設定しなければならないのでtype = PrimaryKeyType.PARTITIONEDがある。
BookクラスのList<Author> authors;でAuthorクラスをリストで取得できるようにしている。このとき、AuthorクラスにはUserDefinedTypeのアノテーションをつけなければならない。
このクラスらがAPIを通じて取得できることを確認するためにController, Service, Repositoryクラスを以下のように用意する。
BookController.java
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 |
package com.example.springbootcassandra.controller; import com.example.springbootcassandra.model.Book; import com.example.springbootcassandra.service.BookService; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.http.HttpStatus; import org.springframework.http.ResponseEntity; import org.springframework.web.bind.annotation.PathVariable; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RequestMethod; import org.springframework.web.bind.annotation.RestController; @RestController @RequestMapping("/book") public class BookController { @Autowired BookService bookService; @RequestMapping(method = RequestMethod.GET, path = "/{bookId}/{bookVersion}") public ResponseEntity<Book> getBook(@PathVariable(value = "bookId") int bookId, @PathVariable(value = "bookVersion") int bookVersion) { return new ResponseEntity<>(bookService.getBook(bookId, bookVersion), HttpStatus.OK); } } |
BookService.java
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 |
package com.example.springbootcassandra.service; import com.example.springbootcassandra.model.Book; import com.example.springbootcassandra.repository.BookRepository; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Service; @Service public class BookService { @Autowired BookRepository bookRepository; public Book getBook(int bookId, int bookVersion) { return bookRepository.findById(bookId, bookVersion); } } |
BookRepository.java
1 2 3 4 5 6 7 8 9 10 11 12 |
package com.example.springbootcassandra.repository; import com.example.springbootcassandra.model.Book; import com.example.springbootcassandra.model.BookKey; import org.springframework.data.cassandra.repository.Query; import org.springframework.data.repository.RepositoryDefinition; @RepositoryDefinition(domainClass = Book.class, idClass = BookKey.class) public interface BookRepository { @Query("select * from book where book_id=?0 and book_version=?1") Book findById(int bookId, int bookVersion); } |
BookRepositoryのRepositoryDefinitionではdomainClassで取得するテーブルクラスを、idClassでPrimary keyの型あるいはクラスを宣言する。
最後にcassandraに繋ぐためのConfigクラスとパラメータを管理するapplication.ymlを以下のように用意する。
CassandraConfig.java
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 |
package com.example.springbootcassandra.config; import com.datastax.driver.core.*; import org.springframework.beans.factory.annotation.Value; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.context.annotation.Primary; import org.springframework.data.cassandra.config.AbstractCassandraConfiguration; import org.springframework.data.cassandra.config.CassandraClusterFactoryBean; import org.springframework.data.cassandra.config.CassandraSessionFactoryBean; import org.springframework.data.cassandra.core.CassandraAdminTemplate; import org.springframework.data.cassandra.repository.config.EnableCassandraRepositories; @Configuration @EnableCassandraRepositories( basePackages = "com.example.springbootcassandra.repository", cassandraTemplateRef = "cassandraTemplate") public class CassandraConfig extends AbstractCassandraConfiguration { @Value("${spring.data.cassandra.username}") private String username; @Value("${spring.data.cassandra.password}") private String password; @Value("${spring.data.cassandra.keyspace-name}") private String keyspaceName; @Value("${spring.data.cassandra.contact-points}") private String contactPoints; @Value("${spring.data.cassandra.port}") private int port; @Override @Primary @Bean(name = "cassandraTemplate") public CassandraAdminTemplate cassandraTemplate() { Session session = session().getObject(); return new CassandraAdminTemplate(session, cassandraConverter()); } @Override @Bean(name = "Port") public int getPort() { return port; } @Override @Bean(name = "KeySpace") protected String getKeyspaceName() { return keyspaceName; } @Bean(name = "cluster") public CassandraClusterFactoryBean cluster() { CassandraClusterFactoryBean cluster = new CassandraClusterFactoryBean(); PlainTextAuthProvider plainTextAuthProvider = new PlainTextAuthProvider(username,password); cluster.setContactPoints(contactPoints); cluster.setPort(port); cluster.setAuthProvider(plainTextAuthProvider); cluster.setQueryOptions(new QueryOptions().setConsistencyLevel(ConsistencyLevel.LOCAL_ONE)); cluster.setSocketOptions(new SocketOptions().setReadTimeoutMillis(5000)); cluster.setRetryPolicy(getRetryPolicy()); cluster.setPoolingOptions(new PoolingOptions().setMaxRequestsPerConnection(HostDistance.LOCAL, 10)); cluster.setJmxReportingEnabled(false); return cluster; } @Override @Bean(name = "Session") public CassandraSessionFactoryBean session() { CassandraSessionFactoryBean session = super.session(); session.setKeyspaceName(this.getKeyspaceName()); return session; } } |
application.yml
1 2 3 4 5 6 7 8 |
spring: data: cassandra: keyspace-name: store username: password: contact-points: localhost port: 9042 |
CassandraConfigクラスでは@Valueでapplication.ymlで宣言されたパラメータを取得し、clusterメソッド内でそれを用いてCassandraに繋ぐ為の設定をしている。
いくつか設定の説明を取り上げるとsetReadTimeoutMillis(5000)でcassandraの最大リードタイムを5秒にしている。なのでデータの読み込みに5秒以上かかったらタイムアウトエラーが起こる。
setMaxRequestsPerConnection(HostDistance.LOCAL, 10)ではcassandraへの1つのコネクションに対して同時にリクエストを投げれる数を10までとして設定している。
ここまで行ったらプロジェクトを起動して動作確認をする。
以下のようになっていればOK。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 |
$ curl http://localhost:8080/book/1/1 | jq . { "bookKey": { "bookId": 1, "bookVersion": 1 }, "title": "book title 1", "description": "book description 1", "authors": [ { "name": "author name 1-1", "description": "author description 1-1" }, { "name": "author name 1-2", "description": "author description 1-2" } ] } |
補足:QueryBuilderを用いたcqlの書き方
BookRepositoryクラスは上記ではinterfaceや@Queryを使ったやり方をしているが以下のように、QueryBuilderを使って書くこともできる。
BookRepository.java
1 2 3 4 5 6 7 8 9 10 11 12 13 |
@Component public class BookRepository { @Autowired private CassandraAdminTemplate cassandraAdminTemplate; public Book findById(int bookId, int bookVersion) { Select.Where select = QueryBuilder.select().from("book") .where(QueryBuilder.eq("book_id", bookId)) .and(QueryBuilder.eq("book_version", bookVersion)); return cassandraAdminTemplate.selectOne(select, Book.class); } } |