勤怠開発課のy_konnoです。
暇があるとNoSQLを漁りだす癖があるんですが、今回はRethinkDBが気になったので試してみたので書いてみようと思います。
NoSQLとは
まず先にNoSQLについて軽く触れておきましょう。
NoSQLとはデータを関係テーブルとは異なる何らかの方法で保存するタイプのデータベースです。
キーバリュー、ドキュメント、グラフなどがNoSQLでよく用いられます。
NoSQLは特定のデータモデルに特化した設計がなされています。
このためNoSQLを用いるとより柔軟なデータ設計ができたり、パフォーマンスを高めることが容易になったり、簡単にスケールアウトする構成をとれたりします。
代表的なNoSQLプロダクトは以下のとおりです。
- NoSQLプロダクト例
- キーバリュー:Redis
- ドキュメント:MongoDB
- グラフ:Neo4j
RethinkDBとは
RethinkDBはNoSQLの一つでオープンソースのドキュメント指向データベースです。
当初RethinkDB社が開発やサポートを行っていたようですが、紆余曲折を経て現在はLinux Foundationに寄贈され、開発が継続されているようです。
データの操作はRethinkDB query language(ReQL)によって行います。
複数のReQLをチェーンしたり、遅延実行することが可能です。
大きな特徴の一つとして、更新結果をリアルタイムでPUSHする機構があります。
これにより変更をポーリングすること無く連続的に取得する事が可能となっています。
その他にもクラスタリングやMap Reduceなどの大規模向けの機能や、テーブルのJOIN、複数の2次インデックスのサポートなど非常に多岐にわたる機能が提供されています。
公式のクライアントはJavaScript、Python、Ruby、Javaの4つが用意されています。
他の処理系であってもコミュティによる開発が行われています。
セットアップ
サーバーはWindows、Mac、Linuxいずれの環境にも対応していますが、公式のDockerイメージが用意されているのでそちらを利用します。
本稿執筆時点でのバージョンは2.4.1を利用しています。
https://hub.docker.com/_/rethinkdb
docker pull rethinkdb
Pullが完了したら起動します。
RethinkDBにおける通信のポートは28015なので割当てます。
また、Webの管理コンソールも同梱されているのでついでに利用できるようにしておきます。
そちらのポートは8080です。
docker run -d -p 8080:8080 -p 28015:28015 --name rethink1 rethinkdb
RethinkDBのWeb管理コンソールはブラウザからlocalhost:8080でアクセスできます。
テーブルの確認・作成・削除やドキュメントの確認などの操作を行えます。
ひとまず、これでRethinkDBの利用準備が整いました。
基本的な使い方
まずは、RethinkDBの基本的な使い方を見てみます。
ここでは一通りCRUDをしてみようと思います。
クライアントはJavaです(JDKは11想定)依存関係の追加方法はそれぞれ以下になります。
・Gradle
dependencies {
implementation 'com.rethinkdb:rethinkdb-driver:2.4.4'
}
<dependencies> <dependency> <groupId>com.rethinkdb</groupId> <artifactId>rethinkdb-driver</artifactId> <version>2.4.4</version> </dependency> </dependencies>
接続
まずはRethinkDBへのコネクションを生成します。
RethinkDB r = RethinkDB.r; Connection connection = r.connection() .hostname("localhost") .port(28015) .connect();
これだけです。
RethinkDBのインスタンスの生成時の名称(r
)に若干ぞわぞわしますが、それ以外は至ってわかりやすいかと思います。
なお、コネクションの取得時にタイムアウトや認証系の情報も設定できます。
以降は、このコネクションを使ってINSERTやSELECTなどの操作を行っていきます。
DB作成・テーブル作成
RethinkDBでは、一般的なRDBのようにデータベースとテーブルがあります。Dockerのコンテナ作成時に、デフォルトでtest
というデータベースを作成してくれるようです。
仮にtest
データベースが無い場合は、以下で生成できます。
r.dbCreate("test").run(connection);
今回は用意されたtest
データベース上に、sales
というテーブルを作成してみます。
r.db("test") .tableCreate("sales") .run(connection);
DB
コマンドでtest
データベースを選択し、そこにtableCreate
コマンドでsales
を指定、あとはrun
コマンドでRethinkDBサーバーへコマンドを送信するという流れです。
すでにsales
テーブルが存在する場合は、ReqlOpFailedError
例外が発生します。
なお、CREATE IF NOT EXISTS
のような便利機能はないので、同等のことをやる場合はtableList
コマンドでテーブルをリストアップし、存在しなければtableCreate
を実行するという手順を踏む必要があります。
INSERT
テーブルができたので、データを追加してみます。
一気に3件のデータを入れてみます。
List<Object> insertData = r.array(r.hashMap("name", "apple") .with("price", 100) .with("quantity", 5) .with("payment", "cash"), r.hashMap("name", "banana") .with("price", 200) .with("quantity", 3) .with("payment", "cash"), r.hashMap("name", "melon") .with("price", 5000) .with("quantity", 1) .with("payment", "credit") ); r.table("sales") .insert(insertData) .run(connection);
実行後、Web管理コンソールで確認すると以下のようなデータが入っています。
id | name | payment | price | quantity |
---|---|---|---|---|
29f94218-1ef9-4c3b-9dfb-1f92a231b9e8 | apple | cash | 100 | 5 |
1fbf055e-48ba-4061-9001-98ecf8053c1f | banana | cash | 200 | 3 |
d94a7dba-4d33-482b-ae0d-2164557469b3 | melon | credit | 5000 | 1 |
INSERT時にはidを指定していませんが、これはPKをテーブル作成時に指定しなかったために作成されているデフォルトのカラムになります。
値はUUIDが自動的に挿入されるので、上記の値は一例です。
UPDATE
次にUPDATEをしてみます。
先程INSERTしたmelonのドキュメントを変更してみます。
r.table("sales") .get("9769473a-eeb5-4b7f-b877-218cb6942796") .update( r.hashMap("price", 10000) .with("quantity", 10) ) .run(connection);
特定のドキュメントだけを更新するにはまず、get
で対象のIDを指定してUPDATEする対象を制限する必要があります。
あとはUPDATEしたい項目だけ、新しい値を指定すればOKです。
get
をしない場合は、テーブル内のすべてのドキュメントを一括で更新する挙動になります。
また、ID指定ではなく特定の条件でUPDATEする場合は以下のようにfilter
を使います。
r.table("sales") .get("9769473a-eeb5-4b7f-b877-218cb6942796") .update( r.hashMap("price", 10000) .with("quantity", 10) ) .run(connection);
SELECT
次にSELECTをしてみます。
UPDATEと同じく、全件取得か特定のドキュメントを取得するかで若干処理が変わります。
まずは全件取得をしてみます。こちらは非常にシンプルです。
・全件取得
Result<Object> selectAllResults = r.table("sales").run(connection); selectAllResults.forEach(e -> /* Do something. */);
単純にテーブルを指定してrun
するだけです。
戻り値のResult<Object>
を回せば、各ドキュメント内容を個別に処理できます。
もちろん、Streamにも対応しています。
次は、特定の項目だけフィルタして取得してみます。
・特定の項目だけ取得
Result<Object> selectFilteredResult = r.table("sales") .filter(e -> e.g("payment").eq("cash")) .run(connection); selectFilteredResult.forEach(e -> /* Do something. */);
filter
に条件を指定します。
ここでいうfilter
はJavaのStreamのものではなく、ReQL独自のものです。
上記の例では、payment
がcash
に合致するものだけを取得するようにしています。
取得後のイテレーションも全件取得と同じです。
なお、UPDATEのときのように、GETでPKを指定してドキュメントを取得することも可能です。
DELETE
最後にDELETEをしてみます。まずは簡単な全件削除から。
・全件削除
r.table("sales").delete().run(connection);
テーブルを指定して、delete
を呼ぶだけという非常に簡素で直感的な作りです。
挙動としてはTRUNCATEみたいなものですね。
次に条件にマッチしたドキュメントだけ削除してみます。
・特定の項目だけ削除
r.table("sales") .filter(e -> e.g("price").gt(200)) .delete() .run(connection);
やはりここでも、filter
を使って条件を指定します。
上記ではprice
が200超のものだけが削除されます。
Change Feeds
さて、ここまで基本的な操作をみてきましたが、やりたいのはそんなことじゃなくて、リアルタイム機能の中核であるChangeFeedsです。
Change Feedsは、RethinkDBにおけるリアルタイム機能の中核をなすものです。
テーブルやドキュメント、あるいは特定のクエリの結果などの変更を、その都度クライアントが受け取ることができます。
ほぼすべてのReQLクエリがChange Feedsの処理対象にできるようです。
早速、変更を受け取る側のコードを組んでみましょう。
・Change Feeds(サブスクライブ側)
Result<Object> changeFeeds = r.table("sales").changes().run(connection); for (Object change : changeFeeds) { // Do something with change feeds. }
そう、たったこれだけです。
あとはループの中でやりたいことを記述すれば終わりなのです。
上記の例では、sales
テーブルに何らかの変更が入る都度ループ内の処理が実行されます。
このループ部分はそのままだと永久に終わらないので、ループの後かつ外に記述した処理はそのままでは実行されません。
この手の機能を使う場合、通常は稼働する限り変更を受け取って処理しようとするのでそれで問題はありませんが、任意のタイミングで止めたいようなケースでは何らかの方法でループを抜け出すように手を加える必要があります。
いじってみた感想
ここまで、RethinkDBの基本的な使い方とChangeFeedsについてみてきました。
ReQLは癖がなく記述しやすい印象を受けました。
あと、ChangeFeedsによる変更の受け取りが極めて楽なことには驚きました。
NoSQLは数も多くそれぞれ得手不得手が分かれるので、うまいことハマるユースケースがあったら採用してみたいところですね。
エンジニア中途採用サイト
ラクスでは、エンジニア・デザイナーの中途採用を積極的に行っております!
ご興味ありましたら是非ご確認をお願いします。
https://career-recruit.rakus.co.jp/career_engineer/カジュアル面談お申込みフォーム
どの職種に応募すれば良いかわからないという方は、カジュアル面談も随時行っております。
以下フォームよりお申込みください。
rakus.hubspotpagebuilder.comイベント情報
会社の雰囲気を知りたい方は、毎週開催しているイベントにご参加ください!
rakus.connpass.com