みなさん、こんにちは!RailsやLaravel使ってますか? ActiveRecord(LaravelではEloquent)ってめっちゃ便利ですね。ただ便利ゆえにActiveRecord以外の存在を知らない人がいるので、メリット・デメリットをまとめてみました。最終的にはドメインモデル入門になっています。
最初にRailsやLaravelから入った人(つまり僕)にありがちなのですが、ActiveRecordがどのようなものか理解せずに実装するため、ActiveRecordなのにロジックがないことがあります。また、ActiveRecordパターン以外を知らないのでActiveRecordのメリット・デメリットを理解してません。そこでActiveRecordがどのようなものかを説明していきたいと思います。
ただ、一年ほどRailsのコードに触れていないので、もし書き方がおかしかったら容赦なく突っ込んでくださし。また個人の見解が多分に含まれているので、皆さんの思うところがあるかもしれません。その時は、ガンガン言ってください。
注意
Railsのよさは結合度の高さによる実装の速さです。RailsはActiveRecordを前提としています。後半に紹介するPOROs
やRepository
はRailsWayから外れたものです。この記事はRailsにRepository層を設けることを勧めているわけではなく、このような考え方もあるよという記事です。
もしDDD前提の設計をしたいのであればHanamiというフレームワークがおすすめです。
https://magazine.rubyist.net/articles/0056/0056-hanami.html
ActiveRecordとは
マーチン・ファウラーという方が書いた「Pattern of Enterprise Application Architecture (PofEAA)」という本に、
データベーステーブルまたはビューの行をラップし、データベースアクセスをカプセル化してデータにドメインロジックを追加するオブジェクト
と書かれています。ここからActiveRecordの役割が3つあることがわかります。
- データベースアクセス
- テーブルの行に対応するデータの保持
- ドメインロジックをもつ
データベースアクセスは
user = new User(params)
user.save
のようにモデル自体にsave
やcreate
を持つことです。
テーブルの行に対応するデータの保持は
id | name |
---|---|
1 | ハト太郎 |
2 | ハム助 |
とテーブルがある場合、
user = User.find(1)
puts(user.id) #ハト太郎
のように行単位で属性を保持するインスタンスを生成できます。
ドメインロジックをもつは
例えば、20歳以下であれば料金が半額とする場合はコントローラーに
if user.age <= 20 then
# 料金半額
end
とするのではなく、モデルにロジックを追加して
class User < ApplicationRecord
中略
def isPriceHalf
self.age <= 20
end
end
if user.isPriceHalf then
# 料金半額
end
というふうにユーザーに関するロジック(ドメインロジックといいます)をユーザーモデルにカプセルしてしまうことです。重要なのは自身のデータを用いたインスタンスメソッドを実装することです。
歴史
そもそもアクティブレコードはドメインモデルの一つです。ドメインモデルはかんたんに説明するとオブジェクト指向にのっとり、データとそれに付随するロジックをクラスに閉じ込めたものになります。なぜそうするのが良いかというと、ロジックがデータと一緒にあることで、コードの重複が防げるからです。ここらへんは「現場で役立つシステム設計の原則(増田享)」という本に詳しく載っています。
データに付随するロジックをコントローラ層やサービス層に書くのは自由ですが、一応そういう考え方もあることを知っておいてください。
ActiveRecordのメリット・デメリット
メリット
- テーブルと1対1にモデルが存在するので、テーブル設計が終わったあとすんなりと実装に入れる
- データと永続化メソッドが1つのモデルにあるので、実装スピードが早い
デメリット
- ツールありき。自ら実装するのは割と手間(RailsとLaravelは標準装備なのでこれはデメリットではない)。
- テーブルと1対1にモデルが存在するので、モデルがテーブルに引っ張られる。例えばテーブルを変更するとモデルに影響を与えるので、テーブルとモデルの結合度が高い。
- ツールを利用した場合、モデルの継承(extend)を利用する事が多く1つしかできない継承を消費してしまう(Rubyはmixinという機能があるのであまり問題にならないかもしれない)。
ちなみにPofEAAには次のように書かれています。
アクティブレコードの最大のメリットは、シンプルな構造である。アクティブレコードの構築は容易であり、また理解しやすい。最大の問題は、アクティブレコードが有効であるのが、アクティブレコードオブジェクトがデータベーステーブルと直接対応している(同一構造スキーム)場合だけという点である。
これはテーブルと1対1のモデル設計のメリット・デメリットですね。ほかにデメリットとして
ビジネスロジックが複雑な場合には、オブジェクトの直接的な関係、コレクション、継承などを使用したいとまず考えるだろう。しかし、これらの部品は簡単にはアクティブレコードにマッピングできず、また、断片的に追加すると状況はより複雑になる。以上の理由からデータマッパーの使用を考えるようになる。
と書かれています。オブジェクト指向とリレーショナル・データベースは同一のものではありません。例えばrubyでの配列
array = ["a", "b", "c']
をデータベースに保存するときにどのように保存すればよいでしょうか? リレーショナルデータベースはデータの横持ちが苦手なので、縦持ちにしてテーブルに保存するかもしれません。
また、Carクラス
とCarクラスを継承するHybridCarクラス
のデータを保存することを考えると、それぞれを保存するのはどうすればよいでしょうか?
class HybridCar < Car
def doSomething
end
end
おそらく、一般的にはtype
属性を加えて継承を表現するかもしれません。様々な方法があるかもしれませんが、NoSQLとは違ってリレーショナルデータベースはこのような継承を表現するのが苦手です(苦手であってできないことはない)。アクティブレコードはリレーショナルデータベースのテーブルと密結合なので、テーブルが苦手な表現はアクティブレコードも苦手です(くどいができないことはない)。
Plain Old Ruby Objects(POROs)について
いままでActiveRecordしか使ったことのない方は、ぜひ他のモデルパターンを知ってほしいです。これから紹介するのはPOROs
とRepository
です。もしデータベースとロジックの分離をしたいのであれば、こちらのパターンはおすすめです。
Plain Old Ruby Objectsは特になんのひねりもなくただのRubyのClass
です。マーティンが単純なJavaのクラスをJavaBeansに対してPlain Old Java Objects (POJOs)と呼んだことにちなんでいます。
class Dog
def initialize(name)
@name = name
end
def doSomething
# do something
end
end
ただのRubyのClassなのでActiveRecordなどは継承していません。特定のツールに依存しないので、自分の自由に実装できます。外部API由来のモデルもDB由来のモデルもすべて同じように扱えます。継承を消費しないのでデザインパターンを適用しやすくなります。POROsはオブジェト指向を気持ちよく実装できます。
Repositoryパターンについて
RepositoryパターンはドメインモデルのIOを担当します。今回だと上記のPOROsと外部API通信やデータベースへの永続化を担当します。Repositoryという層をはさむ事によってモデルはデータベースを知る必要がありません。また、データベース以外に外部APIをモデルにマッピングすることもできます。背後にActiveRecordを使ってもQueryBuilderを使っても構いません。
注意: Repositoryパターンは本来インターフェイスを用いて実装することが多いです。今回はクラスとして実装しています。
class DogRepository
def self.findDogById(id)
# ActiveRecordもしくはQueryBuilderによって実装
end
def self.save(dog)
# ActiveRecordもしくはQueryBuilderによって実装
end
end
class DogController < ApplicationController
def show
@dog = DogRepository.findDogById(params.id)
end
def create
dog = new Dog(params)
# dog.save()ではない
DogRepository.save(dog)
end
end
Repositoryパターンを使うとモデルはActiveRecordではなく、モデル自身に永続化のメソッドがないので、dog.save()
ではなく、DogRepository.save(dog)
となっていることに注目してください。Repositoryパターンではモデルは自身のロジックに集中してデータベースへの永続化の処理はRepositoryが担当します。モデルは永続化については気にしなくて良いのです。