さて、入門ROM.rbの第2回は、まずRelationから。
ROM.rbとはなんぞやという方は第1回の下の記事からどうぞ。 izumisy-tech.hatenablog.com
入門なのでいきなりSinatraやRailsとどう使うか、ではなくワンソースでどういう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などのビルトインメソッドもある。 # RelationはROMによってシングルトンのオブジェクトにされるので、`rom.relations[:クラス名]`でアクセスできる。 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