TarantalkでSmalltalkからDBを操る

# はじめに

[Tarantalk紹介](http://qiita.com/mumez/items/0a288295b9ad3550b3c2)の続きです。今回はDBMSとしての[Tarantool](https://tarantool.org)を使ってみます。

この記事用に[Tarantalk](https://github.com/mumez/Tarantalk)を更新してしまったので、前回からの方は、以下を"do it"するようにしてください。最新になります。(`get`しているところがポイントです。)

```smalltalk
Metacello new
    smalltalkhubUser: 'Pharo' project: 'MetaRepoForPharo50';
    configuration: 'Tarantalk';
    version: #stable;
    get; load.
```

# Tarantoolのデータモデル

TarantoolはいわゆるNoSQLに分類されるDBです。とはいえKVSほどフラットでも単純でもなく、いくぶん構造化されています。具体的には、タプルの集合をspaceという単位に分けた形で格納します。

タプルとは、Smalltalk的には単なる配列です。要素数はタプルごとに異なっていても構いませんし、要素の型が混在していても問題ありません。

ただし実際には検索のため、「n番目の要素は常に文字列」といった想定をして格納することが多いでしょう。Tarantoolでは、タプル中の任意の要素(の集合)に対し、HASH型やTREE型などの検索用インデックスを付与することができ、これにより高速な検索を実現しています。

より詳しくはこちらを参照してください。
<https://tarantool.org/doc/book/box/data_model.html>

# spaceの作成

では論よりコードということで、spaceを作成しタプルを入れてみましょう。

前回と同じくコンソールからTarantoolを起動し、`box.cfg{listen = 3301}`とします。

とりあえずWebブラウザのブックマーク情報でも入れてみることにします。'bookmarks'という名前のspaceを作ります。

```smalltalk
tarantalk := TrTarantalk connect: 'taran:talk@localhost:3301'.
space := tarantalk ensureSpaceNamed: 'bookmarks'.
```

"Print it"するとspaceオブジェクトが返ってみました。

![ensureSpace.png](https://qiita-image-store.s3.amazonaws.com/0/61787/d9e0ed4d-40d3-626c-7d73-660c81903e1a.png)

`ensureSpaceNamed:`により、'bookmarks'という名前のspaceがあれば取得され、なければ新規で作られます。
毎回存在チェックをしても良いのですが、面倒なのでTarantalkではこうしたensureXXXのメソッドをよく使います。

# インデックスの作成

できたspaceに早速データを入れてみましょう。と言いたいところですが、その前にインデックスを作成します。

```smalltalk
primaryIndex := space ensurePrimaryIndexSetting: [ :options | options tree; partsTypes: #(#string)].
```

![primaryIndex.png](https://qiita-image-store.s3.amazonaws.com/0/61787/4e981f1c-b1e9-a783-43bf-a43f3642ba7f.png)

実はプライマリとなるインデックスがないとタプルを格納できません。spaceに`ensurePrimaryIndexSetting:`を送ります。
オプションをブロックの中で指定していますが、タプルの第1番目の要素の型を文字列にし、TREEのインデックスを作成するという意味です。

Tarantoolでは、インデックスの種類として、TREE、HASH、BITSET、RTREEを提供しています。よく使うのはTREEかHASHでしょう。何も指定しないとTREEになります。
要素の型はデフォルトでは`unsigned`です。今回は文字列を入れたいので`partsTypes: #(#string)`としています。

# タプルを入れてみる

では今度こそデータを入れてみましょう。`insert:`を使います。

```smalltalk
space insert: #('Tarantool' 'https://tarantool.org' 'Tarantool本家サイト').
space insert: #('Pharo books' 'http://files.pharo.org/books/' 'Pharoのオンライン書籍').
space insert: #('Pharo' 'http://pharo.org' 'Pharo本家サイト').
space insert: #('Smalltalkユーザ会' 'http://www.smalltalk-users.jp' 'Smalltalkユーザのためのハブサイト' '勉強会も毎月やっています').
```

`'Smalltalkユーザ会'`だけ余計な情報が入っていて要素が一つ多いのですが、問題なく格納できています。

spaceのサイズを取得してみましょう。

```space size.```

"Print it"で`4`が返ります。

![size.png](https://qiita-image-store.s3.amazonaws.com/0/61787/709d44c2-dad3-dc59-0aba-f2a617b1913a.png)

# タプルを取り出す

取り出すにはインデックスに対し、select系のメソッドを使います。

```smalltalk
primaryIndex selectAll.
```

全件がこんな感じで返ってきます。アルファベット順に並んでいるのは、TREEのインデックスを使っているからです。

```smalltalk
#(#('Pharo' 'http://pharo.org' 'Pharo本家サイト') #('Pharo books' 'http://files.pharo.org/books/' 'Pharoのオンライン書籍') #('Smalltalkユーザ会' 'http://www.smalltalk-users.jp' 'Smalltalkユーザのためのハブサイト' '勉強会も毎月やっています') #('Tarantool' 'https://tarantool.org' 'Tarantool本家サイト'))
```

![selectAll のコピー.png](https://qiita-image-store.s3.amazonaws.com/0/61787/f25184b8-0528-1cb7-e366-492a4dbb3380.png)

検索キーを指定してみましょう。`selectHaving:`を使います。

```smalltalk
primaryIndex selectHaving: #('Pharo').
```

先頭要素が`'Pharo'`に一致したタプルのみが返ります。

```smalltalk
#(#('Pharo' 'http://pharo.org' 'Pharo本家サイト'))
```

![selectHaving.png](https://qiita-image-store.s3.amazonaws.com/0/61787/cb4e5d06-66d7-902b-ea67-99fc21ad15ac.png)

今後は範囲を指定してみます。`select:having:`で比較用のオペレータとキーを指定します。

```smalltalk
primaryIndex select: #>= having: #('Smalltalkユーザ会').
```

`'Smalltalkユーザ会'`以降のタプルが取得できました。

```smalltalk
#(#('Smalltalkユーザ会' 'http://www.smalltalk-users.jp' 'Smalltalkユーザのためのハブサイト' '勉強会も毎月やっています') #('Tarantool' 'https://tarantool.org' 'Tarantool本家サイト'))
```

![selectIteIteHaving.png](https://qiita-image-store.s3.amazonaws.com/0/61787/93b359d0-4bbd-be32-6536-47ad19ed7db5.png)

spaceを空にするには`truncate`を使います。

```smalltalk
space truncate.
```

`selectAll`しても空の配列が返ってくるだけとなりました。なお`space drop`とするとspaceそのものを削除できます。

![truncatedSelect.png](https://qiita-image-store.s3.amazonaws.com/0/61787/b0bf0ebb-3111-ec1f-629b-c99709ea670b.png)

# タグ情報DBを作ってみる

もう少し実用っぽい例として、タグ情報の管理を考えてみます。

| プログラミング言語 | タグ |
|-----------|------------|:------------:|
| Lua | dynamic |
| Lua | embeddable |
| Smalltalk | dynamic |
| Smalltalk | object-oriented |
| Smalltalk | ... |

のようなイメージで、任意のタグをプログラミング言語に付与でき、タグを集計できるようにします。

まず`'tags'`という名前でspaceを新たに定義しましょう。

```smalltalk
tagSpace := tarantalk ensureSpaceNamed: 'tags'.
```

次がインデックスの作成です。ここが工夫のしどころになります。今回は `#('Smalltalk' 'object-oriented')` のようなタプルを重複して登録したくないので、プライマリインデックスをタプルの両要素に対象にして作成します。

```smalltalk
primaryIndex := tagSpace ensurePrimaryIndexSetting: [:opts | opts partsTypes: #(#string #string)].
```

さらに、特定のタグが付与されている言語を検索したいので、タグ要素に対してセカンダリインデックスも用意します。

```smalltalk
secondaryIndex := tagSpace ensureSecondaryIndexSetting: [:opts | opts isUnique: false; partsTypes: #(#string)].
```

`'dynamic'`といったタグは、いろいろな言語に付与できなくては困りますので、`isUnique: false`によりユニーク属性を解除しています。

では、データを投入してみましょう。

```smalltalk
tagSpace insert: #('Smalltalk' 'pure').
tagSpace insert: #('Smalltalk' 'dynamic').
tagSpace insert: #('Smalltalk' 'object-oriented').
tagSpace insert: #('Smalltalk' 'pharo').
tagSpace insert: #('Smalltalk' 'cool').
tagSpace insert: #('Lua' 'table').
tagSpace insert: #('Lua' 'dynamic').
tagSpace insert: #('Lua' 'embeddable').
tagSpace insert: #('Lua' 'tarantool').
tagSpace insert: #('Lua' 'cool').
```

Smalltalkにどんなタグが付与されているかを見るには以下のようにします。

```smalltalk
primaryIndex selectHaving: #('Smalltalk').
```

![smalltalkTags.png](https://qiita-image-store.s3.amazonaws.com/0/61787/ae49fb92-1bbe-6d81-48e0-747e01b50794.png)

タグ部分のみを取り出すには```collect:```してあげれば良いでしょう。

```smalltalk
(primaryIndex selectHaving: #('Smalltalk')) collect: #last.
```

以下のようになりました。

```smalltalk
#('cool' 'dynamic' 'object-oriented' 'pharo' 'pure')
```

![smalltalkTags2.png](https://qiita-image-store.s3.amazonaws.com/0/61787/3265067b-86a9-4edc-b7cc-2b3549be4075.png)

今度は`'dynamic'`タグのついた言語一覧を取り出してみます。`secondaryIndex`の方を使います。

```smalltalk
secondaryIndex selectHaving: #('dynamic').
```

SmalltalkもLuaも動的です。

```smalltalk
#(#('Smalltalk' 'dynamic') #('Lua' 'dynamic'))
```

![dynLangs.png](https://qiita-image-store.s3.amazonaws.com/0/61787/d7ae5cac-08cd-b08a-aca4-1c06a718261b.png)

動的言語の数を集計してみます。

```smalltalk
secondaryIndex countHaving: #('dynamic').
```
`2`が返ってきますね。

![countHaving.png](https://qiita-image-store.s3.amazonaws.com/0/61787/31b3d429-a083-3a6f-8a81-fa1a6e8a4ec1.png)

# タグランキングDBを作ってみる

タグクラウドのようなものを作るには、さらに各タグがどれだけ使われているかといった情報を保持する必要があります。
`'tag_ranks'`という新たなspaceを作ってみましょう。以下のようなイメージです。

| タグ | スコア |
|-----------|------------|:------------:|
| pure | 30 |
| cool | 23 |
| ... | ... |


タグの名前順による並び替えなどは考えていないので、今回はHASHのプライマリインデックスにします。

```smalltalk
tagRankSpace := tarantalk ensureSpaceNamed: 'tag_ranks'.
tagRankSpace ensurePrimaryHashIndex.
```

セカンダリインデックスの方は、スコアとして0から増えていく数を想定しているため`unsigned`型にしました。

```smalltalk
scoreIndex := tagRankSpace ensureSecondaryIndexSetting: [:opts | opts isUnique: false; partsTypes: #(#unsigned)].
```

適当にスコアを上げます。なお、ランキング集計というのはよくある処理なので、Tarantalkでは`at:plus:`という便利メソッドが用意してあります。

```smalltalk
tagRankSpace at: 'pure' plus: 1.
tagRankSpace at: 'cool' plus: 1.
```

横着して、先ほどの'tags'のspaceの情報からタグを取り出してスコアに反映させることにします。

```smalltalk
tags := (tagSpace primaryIndex selectAll) collect: #last.
tags do: [:each | tagRankSpace at: each plus: 1].
```

スコアが2以上の物を取り出してみましょう。

```smalltalk
scoreIndex select: #>= having: #(2).
```

どうやらそれらしい結果が返ってきました。

```smalltalk
#(#('pure' 2) #('dynamic' 2) #('cool' 3))
```

![rankingSelect.png](https://qiita-image-store.s3.amazonaws.com/0/61787/7a98a521-9b87-aa3f-a2f1-9864d71ad657.png)

# まとめ

Tarantalkを永続化の目的で使ってみる例でした。Tarantoolは基本的にオンメモリにデータを保持しますが、write ahead log方式のため、突然プロセスが落ちたとしてもデータは復活するので安心です。

もう少し複雑な処理を行わせるには、Luaで関数を定義し、ストアドファンクションとしてTarantalkから`call:`で起動するということをします。そうするとLuaの実行環境＋DBMSとしてのTarantoolの力をフルに活用できることになるのですが、それはまたの機会ということで。