Rails で利用可能な PostgreSQL 特有の型について

この記事は Ruby on Rails  Advent Calendar の 7日目の記事です。

この記事では、Ruby on Rails がサポートしている PostgreSQL の型を紹介した上で
その中でも特にマイナーな ltree 型について詳しく紹介します。

## PostgreSQL で使える型

実は、Ruby on Rails では PostgreSQL 特有の多数の型を利用可能です。

例として、下記のような型が利用可能です。

|型 | 概要 |
|:--:|:--:|
| daterange| 日付の範囲 |
| numrange| Numeric 型の範囲 |
| tsrange| Timestamp （Ruby の DateTime相当）の範囲 |
| tstzrange|　タイムゾーンつき Timestamp の範囲 |
| int4range| integer(32bit 整数)の範囲 |
| int8range| bigint(64bit 整数)の範囲 |
| binary| 巨大なバイナリ文字列。内部的には bytea 型 |
| xml| XML 型 |
| tsvector| テキスト検索に最適化された文字列の型 |
|hstore| キー、値の組合せを意味する型 |
|inet| IPv4 アドレスまたはネットワーク |
|cidr| IPv4 ネットワーク |
|macaddr| Macアドレス |
|uuid| RFC4122 で定義された Universally Unique Identifier|
|json| JSON 型。入力されたテキストを内部的にそのまま保存する |
|jsonb| JSON 型。入力されたテキストを解析して、バイナリ形式で保存する|
|ltree| Label Tree を表現するデータ型 |
|citext| 大文字、小文字を区別しないテキスト型 |
|point| 平面における座標点。幾何データ型の１つ。|
|line|無限長の直線。幾何データ型の１つ。|
|lseg|線分。幾何データ型の１つ。|
|box|矩形。幾何データ型の１つ。|
|path|経路。幾何データ型の１つ。|
|polygon|多角形。幾何データ型の１つ。|
|circle|円。幾何データ型の１つ。|
|bit| 固定長のビット列データ型。`limit: 5` なら長さ５のビット列。|
|bit_varying| 可変長のビット列データ型。 `limit: 7` なら最大長 7 のビット列 |
|money| 貨幣金額を固定精度の小数点で格納するデータ型 |

全部を網羅的に説明すると大変ですので、上記の中で特に誰にも知られてなさそうな ltree データ型を例に紹介します。

## ltree
### ltree とは
ltree とは、階層的なラベルデータを表現するデータ型です。

下記の例では、`Top` が最上位で、 `Top.Science` `Top.Science.Astronomy` などがその子孫の
Label として存在しています。このように `.` 区切りで英数字の Label を並べることで、Label Path を表現します。


### Ruby on Rails での ltree の利用

まず、ltree を利用可能なように準備します

```
$ bin/rails dbconsole
psql> CREATE EXTENSION ltree;
```

次にテーブルを作成します

```
$ bin/rails g model test path:ltree
```

インデックスを作るように編集します。

```rb:db/migrate/20161206150631_create_tests.rb
class CreateTests < ActiveRecord::Migration[5.0]
  def change
    create_table :tests do |t|
      t.ltree :path

      t.timestamps
    end
    add_index :tests, :path, using: :gist, name: "index_tests_path_gist"
    add_index :tests, :path, using: :btree, name: "index_tests_path_btree"
  end
end
```

`t.ltree` と指定することで、`ltree` 型の列 `path` を定義しています。
今回は GiSTインデックスと B-Tree インデックスの両方を設定しているので、インデックスの名前が重複しないうに、あえて name を指定しています。

```shell-session
$ bin/rake db:migrate:up VERSION=20161206150631
Running via Spring preloader in process 27159
== 20161206150631 CreateTests: migrating ======================================
-- create_table(:tests)
   -> 0.0587s
-- add_index(:tests, :path, {:using=>:gist, :name=>"index_tests_path_gist"})
   -> 0.0049s
-- add_index(:tests, :path, {:using=>:btree, :name=>"index_tests_path_btree"})
   -> 0.0405s
== 20161206150631 CreateTests: migrated (0.1044s) =============================
```


次にレコードを追加します。

```rb
%w(
Top
Top.Science
Top.Science.Astronomy
Top.Science.Astronomy.Astrophysics
Top.Science.Astronomy.Cosmology
Top.Hobbies
Top.Hobbies.Amateurs_Astronomy
Top.Collections
Top.Collections.Pictures
Top.Collections.Pictures.Astronomy
Top.Collections.Pictures.Astronomy.Stars
Top.Collections.Pictures.Astronomy.Galaxies
Top.Collections.Pictures.Astronomy.Astronauts
).each do |path|
  Test.create(path: path)
end
```

下記は、Top.Science の子孫のみを抽出する例です。

```rb
> bin/rails console
irb> Test.where("path <@ ?", "Top.Science").map &:path
  Test Load (0.6ms)  SELECT "tests".* FROM "tests" WHERE (path <@ 'Top.Science')
=> ["Top.Science", "Top.Science.Astronomy", "Top.Science.Astronomy.Astrophysics", "Top.Science.Astronomy.Cosmology"]
```

下記は、 Astronomy を経由する場合のみを抽出する例です。

```rb
irb> Test.where("path ~ ?", "*.Astronomy.*").map &:path
  Test Load (1.2ms)  SELECT "tests".* FROM "tests" WHERE (path ~ '*.Astronomy.*')
=> ["Top.Science.Astronomy", "Top.Science.Astronomy.Astrophysics", "Top.Science.Astronomy.Cosmology", "Top.Collections.Pictures.Astronomy", "Top.Collections.Pictures.Astronomy.Stars", "Top.Collections.Pictures.Astronomy.Galaxies", "Top.Collections.Pictures.Astronomy.Astronauts"]
```

簡単に、 Astronomy を経由するレコードを一度に取得することができました。
このように ltree を使うと、階層構造を簡単に扱うことができて、便利です。

## まとめ

今回、Ruby on Rails では多数の PostgreSQL 特有の型が扱えることを紹介しました。
さらに、PostgreSQL 特有の型の中でも特にマイナーな ltree 型を例に詳しく使い方を紹介しました。
ltree 型を使うことで、階層的なデータに対して、子孫や祖先、共通の経路などの条件で、簡単に該当するレコードを取得できます。

## 参考文献
[PostgreSQL ドキュメント 付録 F. 追加で提供されるモジュール F21 ltree](https://www.postgresql.jp/document/9.4/html/ltree.html)
[Rails Guide: ActiveRecord and PostgreSQL](http://edgeguides.rubyonrails.org/active_record_postgresql.html)