さーて、お待ちかねのROM.rb入門の第3回はRepositoryから。
前回の記事はRelationを説明していますので、読んでない人は参考までに。
izumisy-tech.hatenablog.com
Repository
リポジトリと聞くと文脈的にPoEAAやDDDっぽい雰囲気を感じる人が多いのではないかと思う。
どちらも、最も重要な部分はレイヤード・アーキテクチャの考え方で責務を分離することなのではないかと思うが*1、ROM.rbのリポジトリも、公式のコア・コンセプトの説明*2によるとレイヤード・アーキテクチャの考え方と近く、ドメイン・ロジックとデータアクセス・レイヤを粗結合なものにするためのboundary(境界)としての役割を担っている。
An important function of repositories is to act as a boundary between the data access logic and Application Domain logic. This boundary helps to reduce the complexity of rehydrating your entities and keeps a direct dependency on a particular datastore out of your domain.
リポジトリの重要な点は、ドメイン・レイヤからデータアクセス・レイヤの直接的な依存を追い出す(keeps a direct dependency on a particular datastore out of your domain)というところだ。
このコンセプトと対象的なアプローチはActiveRecordパターンだ。ActiveRecordパターンはあえてドメイン・モデルをデータアクセス・レイヤのデータ構造を粗結合にすることによって、インピーダンス・ミスマッチ*3を減らし、高速なアプリケーション開発を可能にしている。
だが一方で、ActiveRecordパターンでは一挙に「モデル」と呼ばれる責務がデータアクセス・レイヤとドメイン・モデルを兼ねてしまうため、どうしてもモデルが肥大化し見通しが悪くなってしまう場合が多い。この点、ROM.rbではActiveRecordと異なり、データアクセス・レイヤとドメイン・レイヤの間にboundary(境界)としてリポジトリを挟み、それぞれの責務を異なるものとして粗結合な設計を出来るようにしようとしている。
Relationとの違い
前回の記事で説明したRelationは、Repositoryがドメインレイヤに近い存在であるのに対して、どちらかといえばインフラストラクチャ・レイヤに近い存在だ。
Relations ... provide APIs for reading the data from various databases, and low-level interfaces for making changes in the databases. Relations are adapter-specific, which means that each adapter provides its own relation specialization, exposing interfaces that make it easy to leverage the features of your database.
つまり、RelationがHTTPやSQLなどのような、アプリケーション本体の外側にあるアダプタ固有(adapter-specific)の操作をAPIとして提供するのに対して、RepositoryはそれらのRelationを更にドメイン・レイヤにふさわしい形でラップし、ドメイン・モデルがRelation(アダプタ)の操作から影響を受けないようにするためのレイヤであるということだ。
それでは実際に使ってみよう。
require "rom"
require "rom/sql"
module Relations
class Users < ROM::Relation[:sql]
schema(:users) do
associations do
has_many :books
end
attribute :id, Types::Serial
attribute :name, Types::String
attribute :age, Types::Int
end
def by_pk(id)
where(id: id)
end
def adult
where{age > 20}
end
end
class Books < ROM::Relation[:sql]
schema(:books) do
associations do
belongs_to :user
end
attribute :id, Types::Serial
attribute :title, Types::Int
attribute :user_id, Types::ForeignKey(:users)
end
end
end
module Repositories
class User < ROM::Repository[:users]
commands :create
def adults
users.adult
end
def all
aggregate(:books)
end
def by_id(id)
aggregate(:books).by_pk(id).one
end
end
end
config = ROM::Configuration.new(:sql, "sqlite::memory")
config.register_relation(Relations::Users)
config.register_relation(Relations::Books)
rom = ROM.container(config)
migration = ROM::SQL.migration(rom) do
change do
create_table(:users) do
primary_key :id
string :name
integer :age
end
create_table(:books) do
primary_key :id
foreign_key :user_id, :users
string :title
end
end
end
gateway = rom.gateways[:default]
migration.apply(gateway.connection, :up)
userRepository = Repositories::User.new(rom)
userRepository.create(name: "Justine", age: 10)
userRepository.create(name: "Jessy", age: 18)
userRepository.create(name: "Michael", age: 23)
books = rom.relations[:books]
books.insert(title: "Good Book", user_id: 1)
books.insert(title: "Nice Book", user_id: 1)
users = userRepository.all
users.each { |user| p user }
single_user = userRepository.by_id(1)
p single_user
users = userRepository.adults
users.each { |user| p user }
このように、RepositoryはRelationをドメインモデル(ROM::Struct)へと変換するインターフェイスの役割を果たしていることが分かる。
加えて、たとえばRepositoryが返すドメインモデルのインスタンスを独自のクラス定義にマッピングしたくなるかもしれない。その場合にはauto_struct
をtrueに維持したうえで、ROM::Struct
を継承した独自のクラスにRepositoryの中でマッピングするよう指定できる。そのやり方に関しては、次回以降説明していこうと思う。