Help us understand the problem. What is going on with this article?

Rails/Laravel使いに送るドメインモデル~アクティブレコードの功罪~

みなさん、こんにちは!RailsやLaravel使ってますか? ActiveRecord(LaravelではEloquent)ってめっちゃ便利ですね。ただ便利ゆえにActiveRecord以外の存在を知らない人がいるので、メリット・デメリットをまとめてみました。最終的にはドメインモデル入門になっています。

最初にRailsやLaravelから入った人(つまり僕)にありがちなのですが、ActiveRecordがどのようなものか理解せずに実装するため、ActiveRecordなのにロジックがないことがあります。また、ActiveRecordパターン以外を知らないのでActiveRecordのメリット・デメリットを理解してません。そこでActiveRecordがどのようなものかを説明していきたいと思います。

ただ、一年ほどRailsのコードに触れていないので、もし書き方がおかしかったら容赦なく突っ込んでくださし。また個人の見解が多分に含まれているので、皆さんの思うところがあるかもしれません。その時は、ガンガン言ってください。

注意

Railsのよさは結合度の高さによる実装の速さです。RailsはActiveRecordを前提としています。後半に紹介するPOROsRepositoryはRailsWayから外れたものです。この記事はRailsにRepository層を設けることを勧めているわけではなく、このような考え方もあるよという記事です。

もしDDD前提の設計をしたいのであればHanamiというフレームワークがおすすめです。
https://magazine.rubyist.net/articles/0056/0056-hanami.html

ActiveRecordとは

マーチン・ファウラーという方が書いた「Pattern of Enterprise Application Architecture (PofEAA)」という本に、

データベーステーブルまたはビューの行をラップし、データベースアクセスをカプセル化してデータにドメインロジックを追加するオブジェクト

と書かれています。ここからActiveRecordの役割が3つあることがわかります。

  • データベースアクセス
  • テーブルの行に対応するデータの保持
  • ドメインロジックをもつ

データベースアクセスは

ruby
user = new User(params)
user.save

のようにモデル自体にsavecreateを持つことです。

テーブルの行に対応するデータの保持は

id name
1 ハト太郎
2 ハム助

とテーブルがある場合、

ruby
user = User.find(1)
puts(user.id)  #ハト太郎

のように行単位で属性を保持するインスタンスを生成できます。

ドメインロジックをもつは

例えば、20歳以下であれば料金が半額とする場合はコントローラーに

ruby
if user.age <= 20 then
# 料金半額
end

とするのではなく、モデルにロジックを追加して

ruby
class User < ApplicationRecord

  中略

  def isPriceHalf
    self.age <= 20
  end

end 
ruby
if user.isPriceHalf then
  # 料金半額
end

というふうにユーザーに関するロジック(ドメインロジックといいます)をユーザーモデルにカプセルしてしまうことです。重要なのは自身のデータを用いたインスタンスメソッドを実装することです。

歴史

そもそもアクティブレコードはドメインモデルの一つです。ドメインモデルはかんたんに説明するとオブジェクト指向にのっとり、データとそれに付随するロジックをクラスに閉じ込めたものになります。なぜそうするのが良いかというと、ロジックがデータと一緒にあることで、コードの重複が防げるからです。ここらへんは「現場で役立つシステム設計の原則(増田享)」という本に詳しく載っています。

データに付随するロジックをコントローラ層やサービス層に書くのは自由ですが、一応そういう考え方もあることを知っておいてください。

ActiveRecordのメリット・デメリット

メリット

  • テーブルと1対1にモデルが存在するので、テーブル設計が終わったあとすんなりと実装に入れる
  • データと永続化メソッドが1つのモデルにあるので、実装スピードが早い

デメリット

  • ツールありき。自ら実装するのは割と手間(RailsとLaravelは標準装備なのでこれはデメリットではない)。
  • テーブルと1対1にモデルが存在するので、モデルがテーブルに引っ張られる。例えばテーブルを変更するとモデルに影響を与えるので、テーブルとモデルの結合度が高い。
  • ツールを利用した場合、モデルの継承(extend)を利用する事が多く1つしかできない継承を消費してしまう(Rubyはmixinという機能があるのであまり問題にならないかもしれない)。

ちなみにPofEAAには次のように書かれています。

アクティブレコードの最大のメリットは、シンプルな構造である。アクティブレコードの構築は容易であり、また理解しやすい。最大の問題は、アクティブレコードが有効であるのが、アクティブレコードオブジェクトがデータベーステーブルと直接対応している(同一構造スキーム)場合だけという点である。

これはテーブルと1対1のモデル設計のメリット・デメリットですね。ほかにデメリットとして

ビジネスロジックが複雑な場合には、オブジェクトの直接的な関係、コレクション、継承などを使用したいとまず考えるだろう。しかし、これらの部品は簡単にはアクティブレコードにマッピングできず、また、断片的に追加すると状況はより複雑になる。以上の理由からデータマッパーの使用を考えるようになる。

と書かれています。オブジェクト指向とリレーショナル・データベースは同一のものではありません。例えばrubyでの配列

ruby
array = ["a", "b", "c']

をデータベースに保存するときにどのように保存すればよいでしょうか? リレーショナルデータベースはデータの横持ちが苦手なので、縦持ちにしてテーブルに保存するかもしれません。

また、CarクラスとCarクラスを継承するHybridCarクラスのデータを保存することを考えると、それぞれを保存するのはどうすればよいでしょうか?

ruby
class HybridCar < Car

  def doSomething 
  end

end 

おそらく、一般的にはtype属性を加えて継承を表現するかもしれません。様々な方法があるかもしれませんが、NoSQLとは違ってリレーショナルデータベースはこのような継承を表現するのが苦手です(苦手であってできないことはない)。アクティブレコードはリレーショナルデータベースのテーブルと密結合なので、テーブルが苦手な表現はアクティブレコードも苦手です(くどいができないことはない)。

Plain Old Ruby Objects(POROs)について

いままでActiveRecordしか使ったことのない方は、ぜひ他のモデルパターンを知ってほしいです。これから紹介するのはPOROsRepositoryです。もしデータベースとロジックの分離をしたいのであれば、こちらのパターンはおすすめです。

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パターンは本来インターフェイスを用いて実装することが多いです。今回はクラスとして実装しています。

ruby
class DogRepository

  def self.findDogById(id)
    # ActiveRecordもしくはQueryBuilderによって実装
  end

  def self.save(dog)
    # ActiveRecordもしくはQueryBuilderによって実装
  end

end
ruby
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が担当します。モデルは永続化については気にしなくて良いのです。

m-dove
ハトです。社会課題の解決に理系からのアプローチをしようと試みてるこのごろです。
Why not register and get more from Qiita?
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away
Comments
No comments
Sign up for free and join this conversation.
If you already have a Qiita account
Why do not you register as a user and use Qiita more conveniently?
You need to log in to use this function. Qiita can be used more conveniently after logging in.
You seem to be reading articles frequently this month. Qiita can be used more conveniently after logging in.
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away
ユーザーは見つかりませんでした