---
title: "實體與倉庫 - Rails 開發實踐"
date: 2023-10-13T00:00:00+08:00
publishDate: 2023-10-13T00:00:00+08:00
lastmod: 2023-09-03T17:33:12+08:00
tags: ["經驗","心得","Rails","Rails 開發實踐"]
series: "rails-in-practice"
toc: true
permalink: "https://blog.aotoki.me/posts/2023/10/13/rails-in-practice-entity-and-repository/"
language: "zh-tw"
---


在我們實作訂閱功能的過程中，提到了像是 Entity（實體）還有 Repository（倉庫）等關鍵字，現在我們要來回顧一些這些使用的物件有怎樣的特性，在 Rails 中我們應該如何使用，才能避免預期外的問題。

<!--more-->

## 實體{#entity}

在 Rails 中我們所使用的 Model 就是 Entity，然而 ActiveRecord 同時也把這些 Model 物件賦予了 Repository 的角色，然而這並不代表他是混合的，這是因為 Ruby 語言的特性所造成的影響。

首先，對 Ruby 來說一切都被視為「物件」也因此一個類別（Class）也是一種物件，以 `User` 為例子，我們會有 `User` 類別物件和 `#<User id=1>` 的實例（Instance）兩種物件，前者是 Repository 的角色，因此會有 `User.find(1)` 的使用，而後者則是 Entity 的角色 `user.admin?`。

因為 Entity 是用於維護「狀態」的物件，我們要避免寫入資料庫這類操作，這是由 Repository 負責的任務。基於這樣的原則，我們會發現 ActiveModel 會紀錄「狀態變化」這件事情，進而提供像是 `user.changed?` 和 `user.changes` 的方法。

在大多數的狀況下，這類型的使用我們應該避免：

```ruby
class User < ApplicationRecord
  # ...
  def upgrade(role:)
    update(role: role)
  end
end
```

這種類型的操作會讓「副作用（Side Effect）」產生，像是常見的 N + 1 查詢，或者在沒有注意到的狀況下不小心改動到使用者。

```ruby
class User < ApplicationRecord
  # ...
  def upgrade(role:)
    self.role = role
  end
end

# ...
users.each { |user| user.upgrade(role: :admin) }
# 明確聲明「儲存」並且確保一起成功
User.transaction { users.each(&:save!) }
end
```

## 倉庫{#repository}

Repository 是用來獲取資料的方式，實際上只要能夠將「資料」轉換成「資訊」就可以，簡單說就是取的資料後產生一個或多個實體，因此將他認知為一個「介面（Interface）」會更加適合，舉例來說除了 ActiveRecord 可以從資料庫獲取資料外，也可以用 ActiveResource 來抓到資料。

```ruby
class Person < ActiveResource::Base
  self.site = "http://api.people.com:3000"
end
```

因為 ActiveResource 和 ActiveRecord 提供了相同的介面，像是 `.find`、`.create` 等方法，因此我們可以在拆分服務的狀況下，做出從 `class Person < ActiveRecord` 切換成 `class Person < ActiveResource::Base` 這樣子的切換，只要提供的方法還是相同，那麼就可以無痛轉移。

因此在使用上，比起單純使用 `.find` 這類基礎方法，我們可以更善用 `scope` 的機制來更清楚的描述我們的使用情境，舉例來說：

```ruby
class Subscription < ApplicationRecord
  # ...
  scope :unexpired, -> { where(expired_at: nil) }
end
```

這樣就可以用 `Subscription.unexpired` 來表示「未過期的訂閱」在 ActiveResource 的版本下，可能就會像這樣實現。

```ruby
class Subscription < ActiveResource::Base
  # ...

  def self.unexipred
    where(expired_at: nil)
  end
end
```

如此一來我們就可以在修改成外部服務時限縮重構的部分。

> 因為 Rails 比較少以這種方式拆分，因此大多數套件的介面跟 ActiveRecord 其實不太相容，在使用上還是要多評估，或者自己拆分出 Repository 而不要去依賴 ActiveModel 處理。

