ROM(Ruby Object Mapper)の基本的な使い方

## ROM(Ruby Object Mapper)とは

[ROM(Ruby Object Mapper)](http://rom-rb.org/)はActiveModelに代表されるようなRubyのORマッパーの1つです。

EntityとRepositoryが分離されていることが特徴で、[Hanamiフレームワーク](http://hanamirb.org/)で採用されています。

ROMの基本的な使い方を説明していきたいと思います。

## 準備
### gem

必要なgem構成は以下の通り(`pry`は任意)です。

```rb:Gemfile
# frozen_string_literal: true
source "https://rubygems.org"

ruby '2.4.2'

gem 'rom-repository', '2.0.2'
gem 'rom-sql', '2.3.0'
gem 'sqlite3', '1.3.13'

group :development do
  gem 'pry'
end
```

### テーブルの作成

以下のようにすると`users`テーブルが作成できるのでこれを使っていきます。

```rb:app.rb
require 'rom-repository'
require 'pry'

rom = ROM.container(:sql, 'sqlite::memory') do |conf|
  conf.default.create_table(:users) do
    primary_key :id
    column :name , String, null: false
    column :email, String, null: false
  end

  class Users < ROM::Relation[:sql]
    schema(infer: true)
  end

  conf.register_relation(Users)
end
```

よくわからないものがいきなりたくさん出てきてしましましが、今はこれでテーブルとRelationが作られる、と思ってください。

Relationについては後ほど説明します。

## 基本的なCreate/Update/Delete

最初に`UserRepo`クラスを作成しています。これはDBに対する操作を定義するためのクラスです。DB操作メソッドは`ROM::Repository`を継承したクラスの中に定義します。

```rb
class UserRepo < ROM::Repository[:users]
  commands :create, update: :by_pk, delete: :by_pk
end
```

上記のコードでは`UserRepo`に対して基本的なCreate/Update/Deleteメソッドを定義しています。

これを使ってみましょう。

### Create

```rb
user = user_repo.create(name: "Jane", email: "jane@doe.org")
# => #<ROM::Struct[User] id=1 name="Jane" email="jane@doe.org">

user.id   # => 1
user.name # => "Jane"
```

`UserRepo`のインスタンスに対して`#create`を投げることで、DBへレコードの保存とuserのインスタンスの作成が行われます。このように**「DB操作」と「レコードをマップしたインスタンス」が別のオブジェクトである**というのがActiveRecordなどと比較したときのROMの特徴ですので覚えておきましょう。

### Update

Updateは下記のように行います。

```rb
updated_user = user_repo.update(user.id, name: "Jane Doe")
# => #<ROM::Struct[User] id=1 name="Jane Doe" email="jane@doe.org">
```

このとき、ActiveRecordの感覚で以下のようにやってもだめです。

```rb:NG
user.name = "Jane Doe"
#=> ROM::Struct::MissingAttribute: undefined method `name=' for #<ROM::Struct::User id=1 name="Jane" email="jane@doe.org">
user.save
```

userはイミュータブルなオブジェクトで、代入するメソッドなどは持っていないので要注意です。

### Delate

Deleteは以下のようになります。

```rb
user_repo.delete(user.id)
```

## Read

Read用のメソッド`query`と`by_id`を`UserRepo`に定義しましょう。

```rb
class UserRepo < ROM::Repository[:users]
  commands :create

  def query(conditions)
    users.where(conditions).to_a
  end

  def by_id(id)
    users.by_pk(id).one!
  end
end
```

幾つかの新しいキーワードが出てきました。1つずつ確認しましょう。

- `#users` : Relation(後述)
- `#where` : Relationを条件で絞り込み。Relationを返す。
- `#by_pk` : Relationを主キーで絞り込み。Relationを返す。
- `#to_a` : RelationをStructの配列に変換
- `#one!` : RelationをStructに変換

上の例では出てきませんでしたが`#one`(`!`なし)というのもあります。
`#one`と`#one!`の違いは以下です。

- `#one` : 結果が0件or1件ならStructに変換(0件の場合はnil), 複数件の場合はerror
- `#one!` : 結果が1件ならStructに変換, 複数件or0件の場合はerror

定義したメソッドは以下のように使います。

```rb
user_repo = UserRepo.new(rom)

user = user_repo.create(name: "Jane", email: "jane@doe.org")
user = user_repo.create(name: "Jane", email: "jane@example.com")

user_repo.query(name: "Jane")
# => [#<ROM::Struct::User id=1 name="Jane" email="jane@doe.org">, #<ROM::Struct::User id=2 name="Jane" email="jane@example.com">]

user_repo.by_id(2)
# => #<ROM::Struct::User id=2 name="Jane" email="jane@example.com">
```

### 結果を独自クラスにする

さて、ここまでのReadメソッドでは戻り値がStructでした。これは独自クラスに変更することができます。

```rb
class User
  attr_reader :id, :name, :email

  def initialize(attributes)
    @id, @name, @email = attributes.values_at(:id, :name, :email)
  end
end

class UserRepo < ROM::Repository[:users]
  commands :create

  def query(conditions)
    users.where(conditions).map_to(User)
  end
end
```

`map_to(User)`というのがポイントで、これによって結果を`User`インスタンスにしてくれます。

試して見ます。

```rb
user_repo = UserRepo.new(rom)

user_repo.create(name: "Jane", email: "jane@doe.org")

user = user_repo.query(name: "Jane").first
# => #<User @email="jane@doe.org", @id=1, @name="Jane">
user.class # => User
```

ここで注目したいのは`User`クラスは何も継承もMixinもしていないPureなRubyであるということです。これによって`User`クラスを**従来のデザインパターンを使って、好きなだけ表現力豊かなモデルにすることができます**。

## Relation

さて、さんざん後回しにされていたRelationの説明です。

Relationは低レベルなCRUDのためのAPIを提供するものです。

Relationクラスはこのように定義します。

```rb
class Users < ROM::Relation[:sql]
  schema do
    attribute :id   , Types::Int
    attribute :name , Types::String
    attribute :email, Types::String

    primary_key :id
  end
end
```

`schema`メソッドでテーブルのカラムと主キーを定義します。

以下のように省略して書くこともできます。

```rb
class Users < ROM::Relation[:sql]
  schema(infer: true)
end
```

`schema(infer: true)`とすることでクラス名から推測して`users`テーブルの定義を参照してくれます。

Relationにメソッドを書いてみましょう。Relationのメソッドは必ず戻り値もRelationでなければならないというルールがあります。

```rb:app.rb
require 'rom-repository'
require 'pry'

rom = ROM.container(:sql, 'sqlite::memory') do |conf|
  conf.default.create_table(:users) do
    primary_key :id
    column :name , String, null: false
    column :email, String, null: false
  end

  class Users < ROM::Relation[:sql]
    schema(infer: true)

    def listing
      select(:name).order(:name)
    end
  end

  conf.register_relation(Users)
end

class UserRepo < ROM::Repository[:users]
  commands :create

  def listing
    users.listing.to_a
  end
end
```

使ってみます。

```rb
user_repo = UserRepo.new(rom)

user = user_repo.create(name: "Jane", email: "jane@doe.org")
user = user_repo.create(name: "Bob" , email: "bob@example.com")

user_repo.listing
# => [#<ROM::Struct::User name="Bob">, #<ROM::Struct::User name="Jane">]
```

# Associations

アソシエーションを使っていきましょう。ここからは以下のテーブルを使います。

```rb
rom = ROM.container(:sql, 'sqlite::memory') do |conf|
  conf.default.create_table(:users) do
    primary_key :id
    column :name, String, null: false
    column :email, String, null: false
  end

  conf.default.create_table(:tasks) do
    primary_key :id
    foreign_key :user_id, :users
    column :title, String, null: false
  end
```

`users`と`tasks`は1対多の関係になっています。この関係を定義しましょう。


```rb
rom = ROM.container(:sql, 'sqlite::memory') do |conf|
  conf.default.create_table(:users) do
    primary_key :id
    column :name, String, null: false
    column :email, String, null: false
  end

  conf.default.create_table(:tasks) do
    primary_key :id
    foreign_key :user_id, :users
    column :title, String, null: false
  end

  conf.relation(:users) do
    schema(infer: true) do
      associations do
        has_many :tasks
      end
    end
  end

  conf.relation(:tasks) do
    schema(infer: true) do
      associations do
        belongs_to :user
      end
    end
  end
end
```

`conf.relation`でリレーションに対してアソシエーションを定義しています。

使ってみましょう。

```rb
class UserRepo < ROM::Repository[:users]
  commands :create

  def by_id(id)
    users.by_pk(id).one!
  end
end

class TaskRepo < ROM::Repository[:tasks]
  commands :create

  def by_id(id)
    tasks.by_pk(id).one!
  end
end

user_repo = UserRepo.new(rom)
task_repo = TaskRepo.new(rom)

user = user_repo.create(name: "Jane", email: "jane@doe.org")
task_repo.create(user_id: user.id, title: "foo")
task_repo.create(user_id: user.id, title: "bar")

user = user_repo.aggregate(:tasks).one
# => #<ROM::Struct::User id=1 name="Jane" email="jane@doe.org" tasks=[#<ROM::Struct::Task id=1 user_id=1 title="foo">, #<ROM::Struct::Task id=2 user_id=1 title="bar">]>

user.tasks
# => [#<ROM::Struct::Task id=1 user_id=1 title="foo">, #<ROM::Struct::Task id=2 user_id=1 title="bar">]
```

`#aggregate`を使うことでリレーションを含んだ(`Combined`クラスの)インスタンスが生成されます。

以下のように`#aggregate`を使わずに`user`から`tasks`を使おうとしても失敗します。

```rb:NG
user = user_repo.by_id(1)
# => #<ROM::Struct::User id=1 name="Jane" email="jane@doe.org">
user.tasks
# => ROM::Struct::MissingAttribute: undefined method `tasks' for #<ROM::Struct::User id=1 name="Jane" email="jane@doe.org"> (attribute not loaded?)

from rom-sample/vendor/bundle/ruby/2.4.0/gems/rom-mapper-1.1.0/lib/rom/struct.rb:112:in `rescue in method_missing'
```

`tasks` から `user` を含める場合は以下のようになります。

```rb
task_repo.aggregate(:user).first.user
# => #<ROM::Struct::User id=1 name="Jane" email="jane@doe.org">
```

単に、ある`task`に対して`user`を取得するには単純に以下のようにします。

```rb
task = task_repo.by_id(1)
# => #<ROM::Struct::Task id=1 user_id=1 title="foo">
user_repo.by_id(task.user_id)
# => #<ROM::Struct::User id=1 name="Jane" email="jane@doe.org">
```

しかし、これはちょっと面倒くさいですね。やっぱり`task.user`の形でアクセスしたいです。

これを実現するためには以下のようなメソッドを用意します。

```rb
class TaskRepo < ROM::Repository[:tasks]
  commands :create

  def by_id(id)
    tasks.by_pk(id).one!
  end

  def by_id_with_user(id)
    tasks.by_pk(id).wrap(:user).one
  end
end

task = task_repo.by_id_with_user(1)
# => #<ROM::Struct::Task id=1 user_id=1 title="foo" user=#<ROM::Struct::User id=1 name="Jane" email="jane@doe.org">>
task.user
# => #<ROM::Struct::User id=1 name="Jane" email="jane@doe.org">
```

これでできました！

# 感想

ROM(Ruby Object Mapper)の基本的な使い方を見てきました。ROMにはまだ説明していない機能がたくさんありますが、まだ著者も分からないことだらけです。できればもうちょっと調べて追記するなり新しく記事を書くなりしたいです。

英語でもいいからもっとドキュメントが増えてほしい！