17
12

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 5 years have passed since last update.

HanamiAdvent Calendar 2017

Day 17

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

Last updated at Posted at 2017-12-16

ROM(Ruby Object Mapper)とは

ROM(Ruby Object Mapper)はActiveModelに代表されるようなRubyのORマッパーの1つです。

EntityとRepositoryが分離されていることが特徴で、Hanamiフレームワークで採用されています。

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

準備

gem

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

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テーブルが作成できるのでこれを使っていきます。

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を継承したクラスの中に定義します。

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

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

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

Create

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は下記のように行います。

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

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

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は以下のようになります。

user_repo.delete(user.id)

Read

Read用のメソッドqueryby_idUserRepoに定義しましょう。

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

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

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でした。これは独自クラスに変更することができます。

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インスタンスにしてくれます。

試して見ます。

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クラスはこのように定義します。

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メソッドでテーブルのカラムと主キーを定義します。

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

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

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

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

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

使ってみます。

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

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

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

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

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でリレーションに対してアソシエーションを定義しています。

使ってみましょう。

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を使おうとしても失敗します。

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 を含める場合は以下のようになります。

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

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

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の形でアクセスしたいです。

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

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にはまだ説明していない機能がたくさんありますが、まだ著者も分からないことだらけです。できればもうちょっと調べて追記するなり新しく記事を書くなりしたいです。

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

17
12
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
17
12

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?