Runner in the High

技術のことをかくこころみ

入門ROM-rb: Relation

さて、入門ROM-rbの第2回は、まずRelationから。

ROM-rbとはなんぞやという方は第1回の下の記事からどうぞ。 izumisy-tech.hatenablog.com

入門なのでいきなりSinatraRailsとどう使うか、ではなくワンソースでどういうAPIがあって、どう使うのか、というところから見ていこうと思う。ちなみに、ソースコードにコメントを書いて読み進めながら学んでいけるスタイルをConversational Tutorial*1というらしい。

Relation

ROMにおけるRelationはインフラレイヤの一部で、アダプタ(Gateway)の実装を隠蔽するインターフェースのような役割を果たす。Relationの中では、そのRelationが対象とするアダプタの操作が行える。

それでは実際に使ってみる

require "rom"
require "rom/sql"

#
# ## Relation
#
# RelationはROM.rbにおいてアダプタ固有の操作をラップする永続化レイヤの責務を扱う
# SQLiteやMySQL固有の操作はAdapterに実装されており、それらに対するインターフェースを提供する
# (余談ではあるが、rom-sqlは中のSQLビルダにSequelを使っている)

module Relations
  class Users < ROM::Relation[:sql]

    # schemaをROM::Relationの継承クラスの中で使うことでデータベースのスキーマを定義できる
    # MySQLなどのデータベースを使っていれば、`schema(infer: true)`の一文でスキーマ推論を有効化できる

    schema(:users) do
      attribute :id, Types::Int
      attribute :name, Types::String
      attribute :age, Types::Int
      attribute :has_car, Types::Bool
    end

    # インスタンスメソッドを定義することで、Relationが持つメソッドを追加できる
    # ここでは、Relationが対象としているアダプタ(例えばこのコードの場合はSQLite)
    # に定義された固有の操作が行える。whereはSQLiteアダプタの中で定義されている。

    def all
      where()
    end

    def has_car
      where(has_car: true)
    end

    # デフォルトではすべてのカラムが返り値のレコードに含まれて返されるが
    # それを変更したければdatasetを定義することができる。
    # この例の場合はhas_carを意図的にレコードに含めないようにしている。

    dataset do
      select(:id, :name, :age)
    end

  end
end

# ROMを初期化して定義したRelationを登録する。
# auto_registrationというディレクトリをまるごと登録対象にできるメソッドがあるので
# ちゃんとしたプロジェクトであればそちらをつかうのがよい。

config = ROM::Configuration.new(:sql, "sqlite::memory")
config.register_relation(Relations::Users)
rom = ROM.container(config)

# データベースのテーブルがないのでマイグレーションをする
# こちらもちゃんとしたRakeタスクをROMが用意してくれているので
# まともなプロジェクトではこのようにマイグレーションを書くことはない。

migration = ROM::SQL.migration(rom) do
  change do
    create_table(:users) do
      primary_key :id
      string :name
      integer :age
      boolean :has_car, default: false
    end
  end
end

gateway = rom.gateways[:default]
migration.apply(gateway.connection, :up)

# ROMに登録されたRelationを操作してみる。
# Relationを経由すればinsertなどのSQLite固有のメソッドも使えるが、できればRelationのメソッド
# としてラップしたほうがよい。固有のメソッド以外にもoneやallなどのビルトインメソッドもある。

users = rom.relations[:users]

users.insert(name: "Bob", age: 22, has_car: true)
users.insert(name: "Alice", age: 23)

p users.all.to_a # [{:id=>1, :name=>"Bob", :age=>22}, {:id=>2, :name=>"Alice", :age=>23}]

p users.has_car.to_a # [{:id=>1, :name=>"Bob", :age=>22}]

 上のコメントでも触れているように、RelationというのはAdapterのラッパなので、ROM::Relation継承クラスではできるだけアダプタだけが知っている操作を隠蔽するような実装にしていくのがよい。そうすることで、アプリケーションが外部のインフラレイヤと粗結合になり、変更に強くすることができる。

 たとえば、サンプルではinsertメソッドを直接Relationから読んでしまっているが、例えばHTTPアダプタなら必ずしもinsertではなくpostメソッドのようなものが生えているかもしれない。ROMを使うアプリケーションにとっては、インフラレイヤのアダプタがデータベースなのかWebAPIなのか、という点は興味の対象にはならないので、実際のアダプタの操作を抽象化して隠蔽するメソッドがRelationに生えているほうが好ましいと言える。

*1:たとえばROMの作者が書いているチュートリアルブログもこの形式になっている: https://www.icelab.com.au/notes/a-conversational-introduction-to-rom-rb