覚書:Reducing Leaky Abstractions Introduced by ActiveRecord

Reducing Leaky Abstractions Introduced by ActiveRecord(ActiveRecordがもたらす抽象化の漏れを軽減する)を写経した際の覚書です。

ruby 3.1.0p0
Rails 7.0.2.3

The Setup

rails new reducing_leaky_abstractions
cd reducing_leaky_abstractions
bin/rails g model person name
bin/rails g model post body:text person:belongs_to published:boolean
bin/rails db:migrate

モデルをフィクスチャでロードしたいので、Postモデルのauthorにforeign_keyを追加する。

class Person < ApplicationRecord
  has_many :posts
end

class Post < ApplicationRecord
  belongs_to :author, class_name: "Person", foreign_key: "person_id"
end

test/fixtures/people.yml
Personのフィクスチャを区別できるようにnameを変える。

one:
  name: person 1

two:
  name: person 2

test/fixtures/posts.yml
Postのフィクスチャを区別できるようにbodyを変える。
personをauthorに変更する。
クエリーに一致するようにpublishedをtrueにする。
distinctの有無が影響するようにtwoのauthorをoneにする。

one:
  body: post 1
  author: one
  published: true

two:
  body: post 2
  author: one
  published: true

bin/rails db:fixtures:load
bin/rails c
@newest_posts = Post.where(published: true).order(created_at: :desc).limit(10)
@published_authors = Person.distinct.joins(:posts).where(posts: { published: true })

The Pain

A new feature comes in where teammates want to enqueue posts to be published in the future.
チームメイトが将来公開する投稿をキューに入れたい、という新機能が追加された。

Shotgun Surgery(変更の分散)
何らかの修正を加える際、多くの異なるクラスに対して、多くの小さな変更を加える必要がある。

スキーマを変更する。
bin/rails g migration AddPublishedAtToPosts published_at:datetime
bin/rails g migration RemovePublishedFromPosts published:boolean
bin/rails db:migrate

test/fixtures/people.yml
Personモデルを5つ用意する。

# person_0 .. person_4
<% 5.times do |n| %>
person_<%= n %>:
  name: <%= "person #{n}" %>
<% end %>

test/fixtures/posts.yml
publishedをpublished_atに変更する。
person_0とperson_1がpostを持つ。
person_0は2件中1件、published_atに現在時刻を設定する。
person_1は5件すべて、published_atに現在時刻を設定する。

# person_0のpost
post_0_0:
  body: post 0 0
  author: person_0
  published_at: nil

post_0_1:
  body: post 0 1
  author: person_0
  published_at: <%= Time.current %>

post_0_2:
  body: post 0 2
  author: person_0
  published_at: <%= Time.current %>

# person_1のpost
<% 5.times do |n| %>
post_1_<%= n %>:
  body: <%= "post 1 #{n}" %>
  author: person_1
  published_at: <%= Time.current %>
<% end %>

# person_2以降のpostは無し

The Underlying Issue

leaky abstraction
抽象的な表現が漏れる法則

The Suggested Fix

Using Named Scopes Across Models with ActiveRecord#Merge

class Post < ApplicationRecord
  # other methods

  def self.published
    where("published_at < ?", Time.current)
  end
end

bin/rails db:fixtures:load
bin/rails c
@newest_posts = Post.published.order(created_at: :desc).limit(10)
@newest_posts.count # => 7
@published_authors = Person.distinct.joins(:posts).merge(Post.published)
@published_authors.count # => 2

Caveats and Considerations

query object

以上です。

この記事が気に入ったらサポートをしてみませんか?