Case Study: 留⾔系統
(重構前)
•可以針對不同主題的留⾔
• 使⽤了 Polymorphic association 設計
• Comment
• commentable_type: string
• commentable_id: integer
• 還有個 thread 分類: [:unclassi
fi
ed, :ask, :share, :ask_manager]
7.
class Comment <ApplicationRecord
def notify_users
case commentable_type
when 'Unit'
# ...
when 'User'
# ...
when 'Question'
# ...
when 'Comment'
# ...
when 'Answer'
# ....
else
# ....
end
end
end
常⾒解法:
Replace Type Codewith Subclasses
• Rails 內建就⽀持 Single Table Inheritance (STI)
• 適合 type 不會變更的場景
11.
class UnitShareomment <Comment
def notify_users
#...
end
def render_comment_subject
#...
end
end
class UnitAskComment < Comment
def notify_users
#...
end
def render_comment_subject
#...
end
end
什麼是 Strategy Pattern?
• 有⼀個 method 負責挑選要⽤哪⼀個策略,然後 new Strategy 物件
• 可以是根據某⼀個 type 屬性
• 也可以是某種判斷
• 不同策略就寫在不同 Strategy class 裡⾯
• 這些 strategy class 有相同的呼叫介⾯
14.
Case Study: 作業指派給不同助教批改
classAssignmentsController < ApplicationController
def create
@answer = Answer.create!(answer_params)
AllocationManager.new(answer).assign_to_reviewer!
redirect_to assignment_path(@answer.assignment)
end
end
15.
class AllocationManager
def initialize(answer)
@answer= answer
self.set_strategy
end
def set_strategy
@strategy = if answer.scored?
NoNeedAllocation.new(answer_list)
elsif answer.assignment.allocation_rules.empty?
TaTagAllocation.new(program, answer_list)
else
RandomAllocation.new(program, answer_list)
end
end
def assign_to_reviewer!
@strategy.assign_to_reviewer!
end
end
什麼是 Service Object
•這裡指的 service object,是指實作 command pattern
• 有個 class 帶有唯⼀的 public method,⽅法裡⾯就是⼀連串的指令來達成⼀
件事情
• 還有⼈出個 gem 定義了這個結構
https://github.com/nebulab/simple_command
24.
# 出⾃ https://railsbook.tw/chapters/26-organize-your-code-advanced
classBooksController < ApplicationController
# ...[略]...
def borrow
if @book && @book.is_available?
@book.borrow!
# Slack 通知
notifier = Slack::Notifier.new ENV["WEBHOOK_URL"] do
defaults channel: ENV["NotifyChannel"], username: ENV["SlackUser"]
end
notifier.ping "#{@book.title} 已出借!"
redirect_to books_path, notice: "已完成預約(出借)!"
else
redirect_to books_path, notice: "此書⽬前無法出借!"
end
end
# ...[略]...
end
25.
# 出⾃ https://railsbook.tw/chapters/26-organize-your-code-advanced
defborrow
if @book && @book.is_available?
@book.borrow!
# Slack 通知
service = SlackNotifyService.new("#{@book.title} 已出借!")
service.perform
redirect_to books_path, notice: "已完成預約(出借)!"
else
redirect_to books_path, notice: "此書⽬前無法出借!"
end
end
26.
class SlackNotifyService
def initialize(message= "")
@message = message
end
def perform
SlackAPI.notify( ::Notifier.new ENV["WEBHOOK_URL"] do
defaults channel: ENV["NotifyChannel"], username: ENV["SlackUser"]
end
notifier.ping @message unless @message.empty?
end
end
# model
class Post< ApplicationRecord
has_many :comments
end
class Comment < ApplicationRecord
belongs_to :post
scope :visible, -> { where( :status => "public") }
end
# controller
@posts = Post.includes(:comments)
# view
<% @posts.each do |post| %>
<% post.comments.visible.each{ ... } ) %>
<% end %>
1. 有 Scope 條件時
這會造成 N+1 嗎?
會
72.
# model
class Post< ApplicationRecord
has_many :comments
has_many :visible_comments, -> { visible }, :class_name => "Comment"
end
# controller
@posts = Post.includes(:visible_comments)
# view
<% @posts.each do |post| %>
<% post.visible_comments.each{ ... } %>
<% end %>
解法
73.
2. 預加載(Preload)資料再select
# model
defPost < ApplicationRecord
has_many :comments
def my_comments
self.comments.where( :user_id => self.user_id )
end
end
# controller
@posts = Post.includes(:comments)
# view
<% @posts.each do |post| %>
<% post.comments.each{ ... } %>
<% post.my_comments.each{ ... } %>
<% end %>
這會造成 N+1 嗎?
會
74.
# model
def Post< ApplicationRecord
has_many :comments
def my_comments
self.comments.select{ |x| x.user_id == self.user_id }
end
end
解法
75.
3. inverse_of 跟N+1 queries 也有關係
class Post < ApplicationRecord
has_many :comments, :inverse_of => :post
end
class Comment < ApplicationRecord
belongs_to :post, :inverse_of => :comments
end
76.
class Post <ApplicationRecord
has_many :comments
end
class Comment < ApplicationRecord
belongs_to :post
end
77.
<% Post.includes(:comments).each do|post| %>
<%= post.title %>
<% post.comments.each do |comment| %>
<%= comment.content %>
<%= comment.post.title %>
<% end %>
<% end %> 這會造成 N+1 嗎?
若inverse_of 沒作⽤就會
78.
class Post <ApplicationRecord
has_many :comments, :foreign_key => :post_id
end
# and/or
class Comment < ApplicationRecord
belongs_to :post, :foreign_key => :post_id
end
Benito Serna
• https://bhserna.com/avoid-n-plus-one-queries.html
•這 blog 是我看過研究 N+1 最深入的,例如
• 5 ways to fetch the latest-N-of-each record on Rails
• 5 ways to
fi
x the latest-comment n+1 problem
• 作者還有出⼀本⼩PDF書,值得⼀看
• https://bhserna.com/avoid-n-plus-1-queries-on-rails.html
83.
最後,關於 Gemfile 版本指定
•Gem
fi
le 裡⾯,盡量不要鎖定版本,那是 Gem
fi
le.lock 的事
• 只有真的真的需要指定版本才能動的版本,才需要在 Gem
fi
le 內指定版本
• 例如某 gem ⼀定要 '~> 3.0‘,不能⽤ 4.0,這種才需要指定版本
• 原因可能是⽬前 rails 不⽀援 4.0,或是 4.0 需要改 code 但還沒處理,或是 4.0 有 bug
• 若⼀定要⽤舊版,不能升級新版的原因要註解寫在 code 上⾯
• 如此⽤ bundle update 升級時,才會是正確的預期結果
• 那些有指定版本的 gem,也就是(⽬前)無法升級的 gem