SlideShare a Scribd company logo
1 of 85
Download to read offline
COSCUP 2022/7/31
Rails 老司機帶⾶
ihower@aihao.tw
About me
• 張⽂鈿 a.k.a. ihower

• https://ihower.tw

• Rails 實戰聖經 作者

• Ruby developer since 2006

• 從 Rails 1.1 ⽤到 Rails 7 的老司機

• 愛好資訊科技 

• https://aihao.tw
關於這場 talk
• 這是⼀場進階的 Rails 演講

• 適合有 Rails 專案開發經驗的朋友聆聽

• 充滿寫 code 的主觀意⾒(偏⾒?),非 Best Practices 教學

• 我的專案開發背景

• 約5⼈左右的 Rails Developer ⼯程師開發團隊 (無前後端分離)

• ⽬前⼿上的專案時長都在五年以上 (從 Rails 3 ⼀路升級上來)
Agenda
• 關於 OOP (12min)

• 關於前端⽅案 (9min)

• 關於效能: N+1 queries (6min)

• 關於 Gem
fi
le 版號 (3min)
關於 OOP 物件導向
Application Developer 的必備知識
Case Study: 留⾔系統
(重構前)
• 可以針對不同主題的留⾔

• 使⽤了 Polymorphic association 設計

• Comment 

• commentable_type: string

• commentable_id: integer

• 還有個 thread 分類: [:unclassi
fi
ed, :ask, :share, :ask_manager]
class Comment < ApplicationRecord
def notify_users
case commentable_type
when 'Unit'
# ...
when 'User'
# ...
when 'Question'
# ...
when 'Comment'
# ...
when 'Answer'
# ....
else
# ....
end
end
end
def render_comment_subject
if commentable_type == 'Unit' && thread == 'ask'
#...
elsif comment.commentable_type == 'Unit' && comment.thread == 'share'
#...
elsif comment.commentable_type == 'Unit' && comment.thread == 'ask_manager'
#...
elsif comment.commentable_type == 'User'
#...
elsif comment.commentable_type == 'Question' && comment.thread == 'ask'
#...
elsif comment.commentable_type == 'Question' && comment.thread == 'share'
#...
elsif comment.commentable_type == 'Question' && comment.thread == 'ask_manager'
#...
elsif comment.commentable_type == 'Answer' && comment.thread == 'ask'
#...
elsif comment.commentable_type == 'Answer' && comment.thread == 'share'
#...
elsif comment.commentable_type == 'Answer' && comment.thread == 'shaask_managerre'
#...
else
#...
end
end
基本 OO 解法⽅向:
Replace Conditional with Polymorphism
⽤多型取代條件式
常⾒解法:
Replace Type Code with Subclasses
• Rails 內建就⽀持 Single Table Inheritance (STI) 

• 適合 type 不會變更的場景
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
當 STI 不適⽤時,

這也是 Composition over inheritance 中最常⾒的技巧
什麼是 Strategy Pattern ?
• 有⼀個 method 負責挑選要⽤哪⼀個策略,然後 new Strategy 物件

• 可以是根據某⼀個 type 屬性

• 也可以是某種判斷

• 不同策略就寫在不同 Strategy class 裡⾯

• 這些 strategy class 有相同的呼叫介⾯
Case Study: 作業指派給不同助教批改
class AssignmentsController < ApplicationController
def create
@answer = Answer.create!(answer_params)
AllocationManager.new(answer).assign_to_reviewer!
redirect_to assignment_path(@answer.assignment)
end
end
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
class NullAllocation
def assign_to_reviewer!
#...
end
end
class TaTagAllocation
def assign_to_reviewer!
#...
end
end
class RandomAllocation
def assign_to_reviewer!
#...
end
end
OO基本原則 Open–closed principle 

對擴充開放、對修改封閉
基本 OO 解法⽅向:
Replace Conditional with Polymorphism
但要⼩⼼OO嘴砲原則
有些是吵架⽤,不是有⽤的指導原則
• 當我覺得你的解法做太多事情時,搬出 YAGNI

• 當我覺得你的解法太複雜時,搬出 KISS

• 當覺得⼀個類別太肥⼤的時候,就搬出 SRP (Single Responsibility Principle)

• ⼀個 class 應有且只有⼀個理由會使其改變

• 理由?? 多⼩的理由? 

• 最後整個結構不就變成 ⼀個 class 就⼀個 public function ?
7 Design Patterns to Refactor
MVC Components in Rails
介紹 Rails 中常⽤的 Design Patterns 的知名⽂章

https://www.sitepoint.com/7-design-patterns-to-refactor-mvc-
components-in-rails/
介紹了七種 Rails 常⽤(?)的設計模式
• 1. Service Objects

• 2. Value Objects

• 3. Form Objects

• 4. Query Objects

• 5. View Objects(Serializer/Presenter)

• 6. Policy Objects

• 7. Decorators
照做肯定 over-engineering…
• 我不推薦 

• Service Objects

• Form Objects

• View Objects(Serializer/Presenter)

• 適合場景很少

• Value Objects

• Query Objects

• 可⽤

• Policy Objects

• Decorators
Service Object
最多⼈推薦使⽤的,但我認為過譽了
什麼是 Service Object
• 這裡指的 service object,是指實作 command pattern

• 有個 class 帶有唯⼀的 public method,⽅法裡⾯就是⼀連串的指令來達成⼀
件事情

• 還有⼈出個 gem 定義了這個結構

https://github.com/nebulab/simple_command
# 出⾃ https://railsbook.tw/chapters/26-organize-your-code-advanced
class BooksController < 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
# 出⾃ https://railsbook.tw/chapters/26-organize-your-code-advanced
def borrow
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
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
Q: 假設接下來要還書也要通知?
逾期也要通知?
於是就變成 function list,只是外表是 class
這樣符合SRP喔?! 每個 class 都只有單⼀責任
• app/services/

• create_account_service.rb

• create_xxx_notify.rb

• create_yyy_notify.rb

• create_zzz_notify.rb

• update_xxx

• update_xxx

• update_xxx

• update_xxx

• update_xx

• ….

• …
我認為 Service Object 是 bad smell
• Service objects 看起來像是 OOP,因為⽤了 class 來包裹

• 但是這只是非常表⾯的, 只是把 code 搬到另⼀個地⽅,並不是好的 OO

• 真正概念上有⽤的 object 是包含內在屬性和⽅法的。如果⼀個 object 只是
procedural code 的包裹,那麼實際上並沒有 OOP 帶來的好處

• 應該⽤ OO 概念去設計 PORO (Plain Old Ruby Object) model 即可

• 把相同概念的屬性和⽅法⼀起包裹 ,⽤好的⽅法命名

• ⽽不是硬套 service object pattern,每個⽅法命名都叫做 perform ⽅法
# 我的設計
def borrow
if @book && @book.is_available?
@book.borrow!
# Slack 通知
notifier = BookNotifier.new(@book)
notifier.notify_borrow!
redirect_to books_path, notice: "已完成預約(出借)!"
else
redirect_to books_path, notice: "此書⽬前無法出借!"
end
end
class BookNotifier
def initialize(book)
@book = book
end
def notify_borrow!
#...
end
def notify_return!
#...
end
def notify_delay!
#...
end
private
def content_title
#.....
end
https://www.codewithjason.com/code-without-service-objects/
Value Objects
概念有⽤,但不常需要實作
• 當⼀個物件沒有概念上的標識 (conceptual identity),⽽只在乎它的屬性時,這
個物件就可以是個 Value Object。反之則是 Entity。

• 物件A == 物件B,要怎麼判斷兩個物件相等

• 度量衡 類型的物件很適合⽤這個概念來設計

• 例如內建的 Date, Time 都是⼀種 Value Objects

• ⼀個 Rails 沒有內建,但比較常⾒的場景: Money 

• https://github.com/Shopify/money
Form Objects
可不⽤費⼼
• 若不是 ActiveRecord 物件,⽤原始的 form_tag 去組合表單就好了

• 場景不多,不太值得/不太需要特別包裹成 Object

• 網路上的範例都太簡單,都是 “聯絡我們” 表單

• 若是多 model 的 association 的場景,想要⽤⼀個 Form Object 包裹處理

• 沒有什麼好的標準範例寫法,每個⼈都寫不⼀樣,維護起來很痛苦
View Objects
也是不推
• Serializer

• API 的話,Serializer 我不太⽤,我更喜歡 Jbuilder template 的表現⼒

• Serializer 常會混雜了query code,增加 N+1 除錯難度

• Presenter

• 缺乏好的範例。⽽且我認為 View 中就⽤ @instance 跟 Helper,不需要費⼼
整理成⼀個 Presenter 物件。代碼複⽤的機會很低,獲得的回報太少。

• 可以看看概念類似的 Rails ViewComponent 比較多⼈⽤
Query Objects
少數複雜場景才需要
• ActiveRecord relations 本⾝就是⼀種 Query object 了

• 所以你做的事情其實是 Query object of Query object,真的有需要嗎?

• 只有複雜的 Query 場景需要,但是既然複雜,就表⽰可能不太會被複⽤,因此
是否值得從 controller 中剝離出來?
Policy Objects
• 就是⽤ Pundit gem 權限設計

• https://github.com/varvet/pundit

• 不過 Pundit 給的範例,會讓你以為要圍繞在 model 設計,這是不⼀定的

• 簡單的後台權限,只需要⽤ Headless policies 即可
Decorators
• 包裹 model,提供本來 helper 在做的⼯作

• ⼜叫作 View Models,可⽤ ruby 內建的 SimpleDelegator 或是 draper gem 即可

• 不影響整體架構,就是把 model 裡⾯的⽅法拆出來放另⼀個檔案⽽已。這些不會
是太關鍵的⽅法,只是為了配合 view 畫⾯⽽已
OOP 推薦資源
• Ruby物件導向設計實踐

• Practical Object-Oriented Design in Ruby: An Agile Primer

• https://www.tenlong.com.tw/products/9780321721334

• Ruby Science

• https://github.com/thoughtbot/ruby-science

• 這PDF 就包括 重構 和 設計模式

• 初學者⽤ TDD 可以幫助學習
Rails 前端⽅案
Rails 變來變去的前端架構...
• Rails 1.0: 預設⽤ Prototype.js (http://prototypejs.org/)

• Rails 1.1: RJS template 

• Rails 3: Assets pipeline (Sprockets) 和導入 Co
ff
eescript

• Rails 4: 預設換 jQuery 和 jquery-ujs (ujs 是指 Rails 的 unobtrusive scripting adapter)

• Rails 5: Turbolinks、⽀援⽤ Yarn 和 Webpack

• Rails 6: 預設換 Webpacker, Turbolinks、移除 jQuery 換 rails-ujs、但 Assets pipeline 沒拆還在

• Rails 7: Import Map, Turbo, Stimulus。預設移除了 rails-ujs

• 把 webpacker 拆掉了,建議⽤ jsbundling-rails gem(可搭配 esbuild) 和 cssbundling-rails

• Assets pipeline 還在,但 DHH 有新出 Propshaft gem (因為有⼈離職了)
animation framework, drag and
drop, Ajax controls DOM utilities,
and unit testing.
考古 Rails 1.1 使⽤的JS框架
DHH 在想什麼?
Rails 發明⼈和 Rails 前端⽅案的決策者
• 堅持想⽤ server-side rendering 中⼼思想

• 因爲夥伴 Sam Stephenson 負責前端,所以⼀開始⽤ Prototype.js,並⽤ RJS 達成⽤ Ruby 寫 JavaScript ⽅案

• 因為 Prototype.js 不⾏了, 被迫接受 jQuery。RJS 也跟著 gg

• 曾經喜歡 Co
ff
eeScript,現在直接寫 ES6 了

• 很喜歡 Turbolinks ⽅案⾄今,⼤升級兩次: Classic Turbolink -> Turbolink 5 -> Turbo 

• 因為 Assets Pipeline(Sprockets) 功能對 ES6 和 js 模組化⽀援不⾜

• 被迫接受 Webpack ⽅案,但⼜捨不得把 Assets Pipeline 拆了。於是兩者並存... 前者處理js,後者處理css/image

• Rails 7 ⼜把 Webpack 拆了

• 因爲跟夥伴 Sam Stephenson 拆夥了,正在把 Sprockets 換掉了

• Sam Stephenson 除了離開 basecamp,同時放棄所有跟 Rails 相關的 js 套件開發⼯作,包括 Hotwire, Stimulus 等
考古: RJS template
Rails 1.1 的 Ruby JavaScript
#/app/views/rjs1/index.rhtml
<%= form_remote_tag :url => { :action =>’note’ }, :html => {:id=>’note-form’ } %>
<%= text_field_tag ‘note’,nil,:size => 40 %>
<%= submit_tag ‘submit’ %>
<%= end_form_tag %>
#/app/views/rjs1/note.rjs
page.insert_html :bottom, ‘notes’, :partial =>’note’
page.visual_effect :highlight, ‘note’
page.form.reset ‘note-form’
 考古: RJS ⽀援七種 DOM 操作
這是 rjs template code,是不是跟15年後的 Turbo Steams 很像?

都說不⽤寫 javascript
page.insert_html :bottom, 'dom_id', :partial => 'content'
page.insert_html :top, 'dom_id', :partial => 'content'
page.insert_html :before, 'dom_id', :partial => 'content'
page.insert_html :after, 'dom_id', :partial => 'content'

page.replace 'dom_id', :partial => 'content'
page.replace_html 'dom_id', :partial => 'content'
page.remove 'dom_id', :partial => 'content'
rails-ujs (或是 jquery-ujs) ⽅案
• RJS 隨著 Prototype.js gg 之後,Rails 放棄⽤ Ruby 產⽣ JavaScript 的⽅式

• 改成直接寫 js.erb,就直接寫 javascript 

• 但本質上是⼀樣的,由 server 後端傳 javascript 給前端執⾏

• 透過 link_to, button_to 或是 form_for 加上 remote: true,就會發送請求 script
到後端

• 後端回傳 javascript,前端執⾏
我的看法 (1)
• 我不喜歡 Turbolinks (Turbo Drive) ⽅案,因為他改變了瀏覽器預設⾏為

• 90%場景沒問題,但那 10% 有問題場景,對初學者特別困擾

• 和其他 JS library 搭配時,也經常會有相容性問題要處理
我的看法 (2) Turbo
• Turbo Frames 是個漂亮的 ajax 功能語法糖,⽤ HTML 標籤就讓你替換 partial 部分區塊

• 只需要改 HTML erb 即可,後端不⽤改就可實現 Ajax 部分區塊替換

• 跟 https://github.com/renderedtext/render_async 作⽤類似,可以 ajax 替換 partial

• Turbo Streams 

• ⽤ HTML 標籤,定義常⾒的 DOM 操作: 只是重新發明 RJS ⽽已...

• 語法換了,但是精神是⼀樣,HTTP payload 從 script 換成 html,但骨⼦裡仍是 DOM
操作指令

• RJS 有的架構限制,⼀樣存在
https://turbo.hotwired.dev/reference/streams
• 我認為 Server-side render 好⽤開發快效能⼜好,只是⼀條路不能走到⿊

• ⼤部分簡單的場景,我認為 Remote JS ⽅案是很好⽤

• 從 Rails 1.1 時代的 RJS

• rails-ujs 的使⽤相容性問題最⼩ (JS部分只是個簡單的 adapter) 

• 我的 Ajax 教材 https://ihower.tw/rails/fullstack-ajax-1.html

• Turbo Steams

• 這幾種運作本質上是⼀樣的,結構問題和能⼒限制也⼀樣

• 如果達到了 SPA 的場景需求,我會去採⽤ Vue.js 或是 React.js ⽅案
我的看法 (3) Remote JS
結構和能⼒限制 ?
• 結構問題

• 在有密集 Ajax 操作的 UI 組件中,會導致非常多零碎的 JS template….. 每個⼩⼩的 template 只為了調整畫⾯上
的 DOM

• 例如⼀樣是刪除 comment,因為前台 UI 不同,js template 就得不同

• ⽤ Hotwire 也是⼀樣,會有很多零碎的 .turbo_stream 檔案

• 能⼒限制

• 不在 rails-ujs 和 Turbo ⽀援的瀏覽器事件時 (超連結、表單等)

• 在需要 javascript template 的 SPA 場景時

• 還要需要⽤ jQuery ⾃⼰掛載瀏覽器事件送 Ajax,然後後端回傳 JSON 處理

• 或是搭配前端框架,例如 Stimulus.js, Vue.js, React.js 等
同個 comment 資料操作,不同UI準備不同template
好零碎啊....
• app/views/comments/create.js.erb

• app/views/comments/edit.js.erb

• app/views/comments/new.js.erb

• app/views/comments/update.js.erb

• app/views/comments/destroy.js.erb

• app/views/answers/comments/create.js.erb

• app/views/answers/comments/edit.js.erb

• app/views/answers/comments/new.js.erb

• app/views/answers/comments/update.js.erb

• app/views/answers/comments/destroy.js.erb

• app/views/peer_reviews/comments/create.js.erb

• app/views/peer_reviews/comments/edit.js.erb

• app/views/peer_reviews/comments/new.js.erb

• app/views/peer_reviews/comments/update.js.erb

• app/views/peer_reviews/comments/destroy.js.erb

• ……..
我的作法: 若有 SPA 需求可在 erb 上掛載 Vue.js
例如⼀個複雜的 UI widget,有⼀堆 ajax 操作
• Vue.js 可以不需要 build 就可以使⽤,HTML上掛上 CDN 就可⽤,後端框架導入超簡單

• Vue.js 有出⼀個精簡版 petite-vue, gzip 後只有 6.9K,之後有需要也⽅便轉成
Vue.js 單組件

• 缺點是沒有單組件(SFC)功能,這⼀定需要 build 程序(需要 webpack 或是 esbuild 等)

• 進⼀步可⽤ Vue.js 單組件掛載在 erb 上

• 此⽅案兼具 server-side rendering 開發快、速度也快的混和⽅案

• 需要 UI 組件時,再掛載前端組件即可
# erb view
<h2>⽂章標題</h2>
<p>⽂章內容</p>
……
<div id="comments-component">
<comments :comments="<%= @comments_data' %>"/>
</div>
這是erb,可以讓初始資料塞到
props 即可。Vue.js 不需要⽤ Ajax
拿。
# js 掛載 (需要透過 webpack 或是 esbuild 編譯)
import { createApp } from 'vue/dist/vue.esm-bundler.js';
import Comments from './components/comments.vue'
document.addEventListener('DOMContentLoaded', () => {
const comments = document.querySelector('#comments-component')
if(comments){
createApp({
components: {
Comments
}
}).mount(comments);
}
})
# Comment Vue.js 單組件 (template+js+css)(組件裡還可以包組件)
<template>
<div class="compose">
....
<ul v-for="(comment, comment_index) in comments" :key="comment.id">
<Comment :comment="comment" />
</ul>
....
</div>
</template>
<script>
import Comment from './comment.vue'
export default {
name: 'Comments',
components: {
Comment
},
props: ['comments'],
data () { },
methods: { }
}
</script>
<style lang="css" scoped>
...
</style>
參考範例
• Vue.js 2 + Webpacker

• https://medium.freecodecamp.org/how-to-pass-rails-instance-variables-into-vue-
components-7fed2a14babf

• https://5xruby.tw/posts/rails-vuejs-get-started/

• Vue.js 3 + jsbundling-rails (⽤esbuild) 

• 有空我再寫 blog

• Vite Ruby ⽅案 https://github.com/ElMassimo/vite_ruby

• Vite 是 Vue.js ⾃家出的 build ⼯具
Stimulus.js 如何?
petite-vue example
我⽤ Vue.js 改寫⼀樣的範例
<div id="hello">
<input type=“text” v-model=“name">
<button @click="greet">
Greet
</button>
<span>
{{ output }}
</span>
</div>
import { createApp } from ‘petite-vue’
createApp({
name: '',
output: '',
greet(){
this.output = `Hello, ${this.name}`
}
}).mount('#hello')
• 上述 Vue.js code 只有 stimulus 的七成...

• Stimulus 仍是傳統 DOM 操作思維

• 簡化了事件掛載,提供了⼀個 controller 架構

• Vue.js 和 React.js 是現代前端的 MVVM 和 Component 組件思維

• 前端組件有⾃⼰的狀態,DOM 隨著狀態改變

• https://zh-hant.reactjs.org/docs/thinking-in-react.html

• 我認為可以 Progressive 漸進式採⽤的 Vue.js 是比較好的選擇

• 可以先採⽤不需要 build 的 Vue.js ⽅式,這比 Stimulus.js 需要 Import Map (或 build 程序),導
入成本更低

• 之後有需要再加上 build 程序,就可以⽤單組件 Component
https://rubyonrails.org/doctrine/zh_tw
• Rails 提供你整套前端+後端的架構
• 但很多元件都是可以替換的,例如
• 前端架構可換
• 測試框架可換 (很多⼈就換了 RSpec)
我的看法(5): 前後端分離?
完全不⽤ erb 後端 view template
• 我認為 Rails 的 actionview 搭配 activerecord 是做表單的強項

• 前後端完全分離,就會失去 Rails 這個優勢

• 不會整個 app 都適合⽤ SPA 去做

• 混合⽅案才是 C/P 值最⾼

• 除非你有很好的理由: 康威定律、招聘問題(前端比較好招⼈?)
就算要前後端分離,也可以不⽤這麼分離
• cookie 還是很棒,不需要⽤ JWT

• 登入⾴⽤ rails 處理,devise 繼續⽤,就不需要重新發明整個註冊、登入、忘記密碼流程

• 登入後再進入前端 SPA ⾴⾯即可

• 若可以同網域很棒,就不需要處理 CORS 跨域問題

• ⽤ nginx 處理分流即可,例如

• example.com/* ⽤ nginx reverse-proxy 到前端 app

• example.com/api/* ⽤ nginx reverse-proxy 到後端 rails app 

• example.com/admin/* ⽤ nginx reverse-proxy 到後台 rails app
不喜歡不⼀定要跟,以拖待變!!
• 我的老專案就遲遲沒⽤ webpacker 全⾯替換 sprockets 

• 拖到 webpacker 都 gg 了, sprockets 還在

• 跳過了 webpacker 時代,直接⽤ jsbundling-rails gem(可搭配 esbuild) 了

• 未來天秤⼜會回到 MPA?

• https://nolanlawson.com/2022/05/21/the-balance-has-shifted-away-from-
spas/

• MPA ⾸屏渲染更快、Core Web Vitals 分數更⾼、SEO 更好做…..
The balance has shifted away from SPAs
https://nolanlawson.com/2022/05/21/the-balance-has-shifted-away-from-spas/
• Chrome implemented back-forward caching

• 上下⾴瀏覽器本⾝就有快取

• Shared Element Transitions

• 瀏覽器⽀援跨⾴時維持固定的元素
Rails 後端 ViewComponent ⽅案
順道⼀提
我沒採⽤
• https://github.com/github/view_component

• 我覺得不值得,如果真的有需求,不如⽤ Vue.js 或 React 的前端 component ⽅
案

• View Component 會增加改進後端性能的難度,特別是 N+1 queries 

• 後端 View Component 只有處理 erb template,⽽沒有包括 javascript 跟 css

• Javascript 跟 css 仍然是沒有組建的狀態

• view component 沒有處理 css 跟 javascript,不就是做半套嗎
Fix N+1 queries
後端最常⾒的效能問題
有些實際場景單純的 includes ⽅法解不了
# 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 嗎?
會
# 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 %>
解法
2. 預加載(Preload)資料再select
# model
def Post < 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 嗎?
會
# model
def Post < ApplicationRecord
has_many :comments
def my_comments
self.comments.select{ |x| x.user_id == self.user_id }
end
end
解法
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
class Post < ApplicationRecord
has_many :comments
end
class Comment < ApplicationRecord
belongs_to :post
end
<% Post.includes(:comments).each do |post| %>
<%= post.title %>
<% post.comments.each do |comment| %>
<%= comment.content %>
<%= comment.post.title %>
<% end %>
<% end %> 這會造成 N+1 嗎?


若inverse_of 沒作⽤就會
class Post < ApplicationRecord
has_many :comments, :foreign_key => :post_id
end
# and/or
class Comment < ApplicationRecord
belongs_to :post, :foreign_key => :post_id
end
原因: ⾃動 inverse_of 可能失效
• Rails 不總是會推導出 inverse_of 屬性,例如

• 你⼿動給 , -> { where條件 } 時

• 有 :foreign_key 參數時

• 有 :through 參數時

• 此時就必須⼿動給 inverse_of
class Post < ApplicationRecord
has_many :comments, ->{ where(....) }
end
4. JSON column type 超好⽤
• PG ⽀援、MySQL 5.7 之後也⽀援 

• 不需要 MongoDB 了、舊的 ActiveRecord Serialize ⽅法也別⽤了

• 可減少了 N+1 queries 機會

• 適合場景: 不需要正規化的資料,例如

• 儲存整個 API response data

• 額外的附屬欄位資料

• has_many 關聯資料,但是其中資料卻不需要個別拿出來使⽤的

• EAV 設計,例如客製表單
Rails 實戰聖經: Rails 後端效能
• https://ihower.tw/rails/fullstack-performance-backend.html

• 我寫的效能教材

• 有上述完整的說明
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
最後,關於 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
gem 'rails', '6.1.4.1'
# 若升級 v2 需要補設定 manifest.js
gem 'sprockets', '~> 3.7.2'
gem 'jquery-rails'
gem 'jbuilder'
gem 'kaminari'
gem 'bootstrap4-kaminari-views'
gem "pundit"
# 此專案⽤ webpack v3
gem 'webpacker', '~> 4.x'
# 因為 omniauth-wechat-oauth2 需要 omniauth 1.3.2
gem 'omniauth', '1.3.2'
# 因為 omniauth 1.3.2 被鎖在 1.3.2
gem 'omniauth-facebook', '~> 4.0.0'
# 因為 master branch 才⽀援 wechat_qiye
gem "omniauth-wechat-oauth2", :github => "NeverMin/omniauth-wechat-oauth2"
# 因為前端⽤ Bootstrap v4
gem 'bootstrap', "~> 4.6"
舉例(真正必要才指定版本)
謝謝聆聽
技術選擇⼀定有風險,套件有 upgrade 有 deprecated
選擇前應詳閱code⽂件
https://ihower.tw

More Related Content

What's hot

Inverting The Testing Pyramid
Inverting The Testing PyramidInverting The Testing Pyramid
Inverting The Testing PyramidNaresh Jain
 
MVC Seminar Presantation
MVC Seminar PresantationMVC Seminar Presantation
MVC Seminar PresantationAbhishek Yadav
 
Lambda and Stream Master class - part 1
Lambda and Stream Master class - part 1Lambda and Stream Master class - part 1
Lambda and Stream Master class - part 1José Paumard
 
Hexagonal architecture vs Functional core / Imperative shell
Hexagonal architecture vs Functional core / Imperative shellHexagonal architecture vs Functional core / Imperative shell
Hexagonal architecture vs Functional core / Imperative shellThomas Pierrain
 
Introducing Clean Architecture
Introducing Clean ArchitectureIntroducing Clean Architecture
Introducing Clean ArchitectureRoc Boronat
 
Writing automation tests with python selenium behave pageobjects
Writing automation tests with python selenium behave pageobjectsWriting automation tests with python selenium behave pageobjects
Writing automation tests with python selenium behave pageobjectsLeticia Rss
 
Java EE Introduction
Java EE IntroductionJava EE Introduction
Java EE Introductionejlp12
 
Intro to Asynchronous Javascript
Intro to Asynchronous JavascriptIntro to Asynchronous Javascript
Intro to Asynchronous JavascriptGarrett Welson
 
Software Testing
Software TestingSoftware Testing
Software TestingAndrew Wang
 
Introduction to GraphQL
Introduction to GraphQLIntroduction to GraphQL
Introduction to GraphQLAppier
 
Introduction to ReactJS
Introduction to ReactJSIntroduction to ReactJS
Introduction to ReactJSKnoldus Inc.
 
Introduction to RxJS
Introduction to RxJSIntroduction to RxJS
Introduction to RxJSBrainhub
 
Enterprise java unit-3_chapter-1-jsp
Enterprise  java unit-3_chapter-1-jspEnterprise  java unit-3_chapter-1-jsp
Enterprise java unit-3_chapter-1-jspsandeep54552
 
Nodejs - A performance que eu sempre quis ter
Nodejs - A performance que eu sempre quis terNodejs - A performance que eu sempre quis ter
Nodejs - A performance que eu sempre quis terEmerson Macedo
 

What's hot (20)

Inverting The Testing Pyramid
Inverting The Testing PyramidInverting The Testing Pyramid
Inverting The Testing Pyramid
 
MVC Seminar Presantation
MVC Seminar PresantationMVC Seminar Presantation
MVC Seminar Presantation
 
Lambda and Stream Master class - part 1
Lambda and Stream Master class - part 1Lambda and Stream Master class - part 1
Lambda and Stream Master class - part 1
 
Node ppt
Node pptNode ppt
Node ppt
 
Hexagonal architecture vs Functional core / Imperative shell
Hexagonal architecture vs Functional core / Imperative shellHexagonal architecture vs Functional core / Imperative shell
Hexagonal architecture vs Functional core / Imperative shell
 
ClassLoader Leaks
ClassLoader LeaksClassLoader Leaks
ClassLoader Leaks
 
Introducing Clean Architecture
Introducing Clean ArchitectureIntroducing Clean Architecture
Introducing Clean Architecture
 
Writing automation tests with python selenium behave pageobjects
Writing automation tests with python selenium behave pageobjectsWriting automation tests with python selenium behave pageobjects
Writing automation tests with python selenium behave pageobjects
 
Java EE Introduction
Java EE IntroductionJava EE Introduction
Java EE Introduction
 
Exploratory test
Exploratory testExploratory test
Exploratory test
 
Intro to Asynchronous Javascript
Intro to Asynchronous JavascriptIntro to Asynchronous Javascript
Intro to Asynchronous Javascript
 
Software Testing
Software TestingSoftware Testing
Software Testing
 
Introduction to GraphQL
Introduction to GraphQLIntroduction to GraphQL
Introduction to GraphQL
 
Introduction to ReactJS
Introduction to ReactJSIntroduction to ReactJS
Introduction to ReactJS
 
React & GraphQL
React & GraphQLReact & GraphQL
React & GraphQL
 
Java 8 lambda
Java 8 lambdaJava 8 lambda
Java 8 lambda
 
Clean code
Clean codeClean code
Clean code
 
Introduction to RxJS
Introduction to RxJSIntroduction to RxJS
Introduction to RxJS
 
Enterprise java unit-3_chapter-1-jsp
Enterprise  java unit-3_chapter-1-jspEnterprise  java unit-3_chapter-1-jsp
Enterprise java unit-3_chapter-1-jsp
 
Nodejs - A performance que eu sempre quis ter
Nodejs - A performance que eu sempre quis terNodejs - A performance que eu sempre quis ter
Nodejs - A performance que eu sempre quis ter
 

Similar to Ruby Rails 老司機帶飛

千呼萬喚始出來的 Java SE 7
千呼萬喚始出來的 Java SE 7千呼萬喚始出來的 Java SE 7
千呼萬喚始出來的 Java SE 7Justin Lin
 
J Ruby和Rails 让Ruby语言融入Java项目
J Ruby和Rails 让Ruby语言融入Java项目J Ruby和Rails 让Ruby语言融入Java项目
J Ruby和Rails 让Ruby语言融入Java项目George Ang
 
⼤語⾔模型 LLM 應⽤開發入⾨
⼤語⾔模型 LLM 應⽤開發入⾨⼤語⾔模型 LLM 應⽤開發入⾨
⼤語⾔模型 LLM 應⽤開發入⾨Wen-Tien Chang
 
Ruby 的快与慢
Ruby 的快与慢Ruby 的快与慢
Ruby 的快与慢vincent253
 
A brief introduction to Machine Learning
A brief introduction to Machine LearningA brief introduction to Machine Learning
A brief introduction to Machine LearningWen-Tien Chang
 
如何在 Java App 中導入 Scala
如何在 Java App 中導入 Scala如何在 Java App 中導入 Scala
如何在 Java App 中導入 Scalajavatwo2011
 
改善 Angular 開發流程:你所不知道的 Schematics 程式碼產生器
改善 Angular 開發流程:你所不知道的 Schematics 程式碼產生器改善 Angular 開發流程:你所不知道的 Schematics 程式碼產生器
改善 Angular 開發流程:你所不知道的 Schematics 程式碼產生器Chieh Kai Yang
 
Artifacts management with CI and CD
Artifacts management with CI and CDArtifacts management with CI and CD
Artifacts management with CI and CDChen-Tien Tsai
 
使用Dsl改善软件设计
使用Dsl改善软件设计使用Dsl改善软件设计
使用Dsl改善软件设计mingjin
 
View 與 Blade 樣板引擎
View 與 Blade 樣板引擎View 與 Blade 樣板引擎
View 與 Blade 樣板引擎Shengyou Fan
 
Open source的devops工具箱 公開版@coscup2016
Open source的devops工具箱 公開版@coscup2016Open source的devops工具箱 公開版@coscup2016
Open source的devops工具箱 公開版@coscup2016Kirk Chen
 
Langchain and Azure ML and Open AI
Langchain and Azure ML and Open AILangchain and Azure ML and Open AI
Langchain and Azure ML and Open AIKo Ko
 
文學通的開發心路歷程
文學通的開發心路歷程文學通的開發心路歷程
文學通的開發心路歷程建銘 廖
 
選一個框架當好朋友,讓您成為開心攻城獅
選一個框架當好朋友,讓您成為開心攻城獅選一個框架當好朋友,讓您成為開心攻城獅
選一個框架當好朋友,讓您成為開心攻城獅Shengyou Fan
 
使用 laravel 的前與後
使用 laravel 的前與後使用 laravel 的前與後
使用 laravel 的前與後Shengyou Fan
 
D2_Node在淘宝的应用实践
D2_Node在淘宝的应用实践D2_Node在淘宝的应用实践
D2_Node在淘宝的应用实践Jackson Tian
 
Redux+react js
Redux+react jsRedux+react js
Redux+react js國昭 張
 
合久必分,分久必合
合久必分,分久必合合久必分,分久必合
合久必分,分久必合Qiangning Hong
 

Similar to Ruby Rails 老司機帶飛 (20)

RSpec & TDD Tutorial
RSpec & TDD TutorialRSpec & TDD Tutorial
RSpec & TDD Tutorial
 
千呼萬喚始出來的 Java SE 7
千呼萬喚始出來的 Java SE 7千呼萬喚始出來的 Java SE 7
千呼萬喚始出來的 Java SE 7
 
J Ruby和Rails 让Ruby语言融入Java项目
J Ruby和Rails 让Ruby语言融入Java项目J Ruby和Rails 让Ruby语言融入Java项目
J Ruby和Rails 让Ruby语言融入Java项目
 
⼤語⾔模型 LLM 應⽤開發入⾨
⼤語⾔模型 LLM 應⽤開發入⾨⼤語⾔模型 LLM 應⽤開發入⾨
⼤語⾔模型 LLM 應⽤開發入⾨
 
Jasmine
JasmineJasmine
Jasmine
 
Ruby 的快与慢
Ruby 的快与慢Ruby 的快与慢
Ruby 的快与慢
 
A brief introduction to Machine Learning
A brief introduction to Machine LearningA brief introduction to Machine Learning
A brief introduction to Machine Learning
 
如何在 Java App 中導入 Scala
如何在 Java App 中導入 Scala如何在 Java App 中導入 Scala
如何在 Java App 中導入 Scala
 
改善 Angular 開發流程:你所不知道的 Schematics 程式碼產生器
改善 Angular 開發流程:你所不知道的 Schematics 程式碼產生器改善 Angular 開發流程:你所不知道的 Schematics 程式碼產生器
改善 Angular 開發流程:你所不知道的 Schematics 程式碼產生器
 
Artifacts management with CI and CD
Artifacts management with CI and CDArtifacts management with CI and CD
Artifacts management with CI and CD
 
使用Dsl改善软件设计
使用Dsl改善软件设计使用Dsl改善软件设计
使用Dsl改善软件设计
 
View 與 Blade 樣板引擎
View 與 Blade 樣板引擎View 與 Blade 樣板引擎
View 與 Blade 樣板引擎
 
Open source的devops工具箱 公開版@coscup2016
Open source的devops工具箱 公開版@coscup2016Open source的devops工具箱 公開版@coscup2016
Open source的devops工具箱 公開版@coscup2016
 
Langchain and Azure ML and Open AI
Langchain and Azure ML and Open AILangchain and Azure ML and Open AI
Langchain and Azure ML and Open AI
 
文學通的開發心路歷程
文學通的開發心路歷程文學通的開發心路歷程
文學通的開發心路歷程
 
選一個框架當好朋友,讓您成為開心攻城獅
選一個框架當好朋友,讓您成為開心攻城獅選一個框架當好朋友,讓您成為開心攻城獅
選一個框架當好朋友,讓您成為開心攻城獅
 
使用 laravel 的前與後
使用 laravel 的前與後使用 laravel 的前與後
使用 laravel 的前與後
 
D2_Node在淘宝的应用实践
D2_Node在淘宝的应用实践D2_Node在淘宝的应用实践
D2_Node在淘宝的应用实践
 
Redux+react js
Redux+react jsRedux+react js
Redux+react js
 
合久必分,分久必合
合久必分,分久必合合久必分,分久必合
合久必分,分久必合
 

More from Wen-Tien Chang

淺談 Startup 公司的軟體開發流程 v2
淺談 Startup 公司的軟體開發流程 v2淺談 Startup 公司的軟體開發流程 v2
淺談 Startup 公司的軟體開發流程 v2Wen-Tien Chang
 
RSpec on Rails Tutorial
RSpec on Rails TutorialRSpec on Rails Tutorial
RSpec on Rails TutorialWen-Tien Chang
 
ALPHAhackathon: How to collaborate
ALPHAhackathon: How to collaborateALPHAhackathon: How to collaborate
ALPHAhackathon: How to collaborateWen-Tien Chang
 
Git 版本控制系統 -- 從微觀到宏觀
Git 版本控制系統 -- 從微觀到宏觀Git 版本控制系統 -- 從微觀到宏觀
Git 版本控制系統 -- 從微觀到宏觀Wen-Tien Chang
 
Exception Handling: Designing Robust Software in Ruby (with presentation note)
Exception Handling: Designing Robust Software in Ruby (with presentation note)Exception Handling: Designing Robust Software in Ruby (with presentation note)
Exception Handling: Designing Robust Software in Ruby (with presentation note)Wen-Tien Chang
 
Exception Handling: Designing Robust Software in Ruby
Exception Handling: Designing Robust Software in RubyException Handling: Designing Robust Software in Ruby
Exception Handling: Designing Robust Software in RubyWen-Tien Chang
 
從 Classes 到 Objects: 那些 OOP 教我的事
從 Classes 到 Objects: 那些 OOP 教我的事從 Classes 到 Objects: 那些 OOP 教我的事
從 Classes 到 Objects: 那些 OOP 教我的事Wen-Tien Chang
 
Yet another introduction to Git - from the bottom up
Yet another introduction to Git - from the bottom upYet another introduction to Git - from the bottom up
Yet another introduction to Git - from the bottom upWen-Tien Chang
 
A brief introduction to Vagrant – 原來 VirtualBox 可以這樣玩
A brief introduction to Vagrant – 原來 VirtualBox 可以這樣玩A brief introduction to Vagrant – 原來 VirtualBox 可以這樣玩
A brief introduction to Vagrant – 原來 VirtualBox 可以這樣玩Wen-Tien Chang
 
Ruby 程式語言綜覽簡介
Ruby 程式語言綜覽簡介Ruby 程式語言綜覽簡介
Ruby 程式語言綜覽簡介Wen-Tien Chang
 
A brief introduction to SPDY - 邁向 HTTP/2.0
A brief introduction to SPDY - 邁向 HTTP/2.0A brief introduction to SPDY - 邁向 HTTP/2.0
A brief introduction to SPDY - 邁向 HTTP/2.0Wen-Tien Chang
 
RubyConf Taiwan 2012 Opening & Closing
RubyConf Taiwan 2012 Opening & ClosingRubyConf Taiwan 2012 Opening & Closing
RubyConf Taiwan 2012 Opening & ClosingWen-Tien Chang
 
從 Scrum 到 Kanban: 為什麼 Scrum 不適合 Lean Startup
從 Scrum 到 Kanban: 為什麼 Scrum 不適合 Lean Startup從 Scrum 到 Kanban: 為什麼 Scrum 不適合 Lean Startup
從 Scrum 到 Kanban: 為什麼 Scrum 不適合 Lean StartupWen-Tien Chang
 
那些 Functional Programming 教我的事
那些 Functional Programming 教我的事那些 Functional Programming 教我的事
那些 Functional Programming 教我的事Wen-Tien Chang
 
RubyConf Taiwan 2011 Opening & Closing
RubyConf Taiwan 2011 Opening & ClosingRubyConf Taiwan 2011 Opening & Closing
RubyConf Taiwan 2011 Opening & ClosingWen-Tien Chang
 
BDD style Unit Testing
BDD style Unit TestingBDD style Unit Testing
BDD style Unit TestingWen-Tien Chang
 
RSpec 讓你愛上寫測試
RSpec 讓你愛上寫測試RSpec 讓你愛上寫測試
RSpec 讓你愛上寫測試Wen-Tien Chang
 
Service-Oriented Design and Implement with Rails3
Service-Oriented Design and Implement with Rails3Service-Oriented Design and Implement with Rails3
Service-Oriented Design and Implement with Rails3Wen-Tien Chang
 

More from Wen-Tien Chang (20)

淺談 Startup 公司的軟體開發流程 v2
淺談 Startup 公司的軟體開發流程 v2淺談 Startup 公司的軟體開發流程 v2
淺談 Startup 公司的軟體開發流程 v2
 
RSpec on Rails Tutorial
RSpec on Rails TutorialRSpec on Rails Tutorial
RSpec on Rails Tutorial
 
ALPHAhackathon: How to collaborate
ALPHAhackathon: How to collaborateALPHAhackathon: How to collaborate
ALPHAhackathon: How to collaborate
 
Git 版本控制系統 -- 從微觀到宏觀
Git 版本控制系統 -- 從微觀到宏觀Git 版本控制系統 -- 從微觀到宏觀
Git 版本控制系統 -- 從微觀到宏觀
 
Exception Handling: Designing Robust Software in Ruby (with presentation note)
Exception Handling: Designing Robust Software in Ruby (with presentation note)Exception Handling: Designing Robust Software in Ruby (with presentation note)
Exception Handling: Designing Robust Software in Ruby (with presentation note)
 
Exception Handling: Designing Robust Software in Ruby
Exception Handling: Designing Robust Software in RubyException Handling: Designing Robust Software in Ruby
Exception Handling: Designing Robust Software in Ruby
 
從 Classes 到 Objects: 那些 OOP 教我的事
從 Classes 到 Objects: 那些 OOP 教我的事從 Classes 到 Objects: 那些 OOP 教我的事
從 Classes 到 Objects: 那些 OOP 教我的事
 
Yet another introduction to Git - from the bottom up
Yet another introduction to Git - from the bottom upYet another introduction to Git - from the bottom up
Yet another introduction to Git - from the bottom up
 
A brief introduction to Vagrant – 原來 VirtualBox 可以這樣玩
A brief introduction to Vagrant – 原來 VirtualBox 可以這樣玩A brief introduction to Vagrant – 原來 VirtualBox 可以這樣玩
A brief introduction to Vagrant – 原來 VirtualBox 可以這樣玩
 
Ruby 程式語言綜覽簡介
Ruby 程式語言綜覽簡介Ruby 程式語言綜覽簡介
Ruby 程式語言綜覽簡介
 
A brief introduction to SPDY - 邁向 HTTP/2.0
A brief introduction to SPDY - 邁向 HTTP/2.0A brief introduction to SPDY - 邁向 HTTP/2.0
A brief introduction to SPDY - 邁向 HTTP/2.0
 
RubyConf Taiwan 2012 Opening & Closing
RubyConf Taiwan 2012 Opening & ClosingRubyConf Taiwan 2012 Opening & Closing
RubyConf Taiwan 2012 Opening & Closing
 
從 Scrum 到 Kanban: 為什麼 Scrum 不適合 Lean Startup
從 Scrum 到 Kanban: 為什麼 Scrum 不適合 Lean Startup從 Scrum 到 Kanban: 為什麼 Scrum 不適合 Lean Startup
從 Scrum 到 Kanban: 為什麼 Scrum 不適合 Lean Startup
 
Git Tutorial 教學
Git Tutorial 教學Git Tutorial 教學
Git Tutorial 教學
 
那些 Functional Programming 教我的事
那些 Functional Programming 教我的事那些 Functional Programming 教我的事
那些 Functional Programming 教我的事
 
RubyConf Taiwan 2011 Opening & Closing
RubyConf Taiwan 2011 Opening & ClosingRubyConf Taiwan 2011 Opening & Closing
RubyConf Taiwan 2011 Opening & Closing
 
BDD style Unit Testing
BDD style Unit TestingBDD style Unit Testing
BDD style Unit Testing
 
RSpec 讓你愛上寫測試
RSpec 讓你愛上寫測試RSpec 讓你愛上寫測試
RSpec 讓你愛上寫測試
 
Git and Github
Git and GithubGit and Github
Git and Github
 
Service-Oriented Design and Implement with Rails3
Service-Oriented Design and Implement with Rails3Service-Oriented Design and Implement with Rails3
Service-Oriented Design and Implement with Rails3
 

Ruby Rails 老司機帶飛

  • 2. About me • 張⽂鈿 a.k.a. ihower • https://ihower.tw • Rails 實戰聖經 作者 • Ruby developer since 2006 • 從 Rails 1.1 ⽤到 Rails 7 的老司機 • 愛好資訊科技 • https://aihao.tw
  • 3. 關於這場 talk • 這是⼀場進階的 Rails 演講 • 適合有 Rails 專案開發經驗的朋友聆聽 • 充滿寫 code 的主觀意⾒(偏⾒?),非 Best Practices 教學 • 我的專案開發背景 • 約5⼈左右的 Rails Developer ⼯程師開發團隊 (無前後端分離) • ⽬前⼿上的專案時長都在五年以上 (從 Rails 3 ⼀路升級上來)
  • 4. Agenda • 關於 OOP (12min) • 關於前端⽅案 (9min) • 關於效能: N+1 queries (6min) • 關於 Gem fi le 版號 (3min)
  • 5. 關於 OOP 物件導向 Application Developer 的必備知識
  • 6. 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
  • 8. def render_comment_subject if commentable_type == 'Unit' && thread == 'ask' #... elsif comment.commentable_type == 'Unit' && comment.thread == 'share' #... elsif comment.commentable_type == 'Unit' && comment.thread == 'ask_manager' #... elsif comment.commentable_type == 'User' #... elsif comment.commentable_type == 'Question' && comment.thread == 'ask' #... elsif comment.commentable_type == 'Question' && comment.thread == 'share' #... elsif comment.commentable_type == 'Question' && comment.thread == 'ask_manager' #... elsif comment.commentable_type == 'Answer' && comment.thread == 'ask' #... elsif comment.commentable_type == 'Answer' && comment.thread == 'share' #... elsif comment.commentable_type == 'Answer' && comment.thread == 'shaask_managerre' #... else #... end end
  • 9. 基本 OO 解法⽅向: Replace Conditional with Polymorphism ⽤多型取代條件式
  • 10. 常⾒解法: Replace Type Code with 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
  • 12. 稍微進階解法 : Strategy Pattern 當 STI 不適⽤時,
 這也是 Composition over inheritance 中最常⾒的技巧
  • 13. 什麼是 Strategy Pattern ? • 有⼀個 method 負責挑選要⽤哪⼀個策略,然後 new Strategy 物件 • 可以是根據某⼀個 type 屬性 • 也可以是某種判斷 • 不同策略就寫在不同 Strategy class 裡⾯ • 這些 strategy class 有相同的呼叫介⾯
  • 14. Case Study: 作業指派給不同助教批改 class AssignmentsController < 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
  • 16. class NullAllocation def assign_to_reviewer! #... end end class TaTagAllocation def assign_to_reviewer! #... end end class RandomAllocation def assign_to_reviewer! #... end end
  • 17. OO基本原則 Open–closed principle 
 對擴充開放、對修改封閉 基本 OO 解法⽅向: Replace Conditional with Polymorphism
  • 18. 但要⼩⼼OO嘴砲原則 有些是吵架⽤,不是有⽤的指導原則 • 當我覺得你的解法做太多事情時,搬出 YAGNI • 當我覺得你的解法太複雜時,搬出 KISS • 當覺得⼀個類別太肥⼤的時候,就搬出 SRP (Single Responsibility Principle) • ⼀個 class 應有且只有⼀個理由會使其改變 • 理由?? 多⼩的理由? • 最後整個結構不就變成 ⼀個 class 就⼀個 public function ?
  • 19. 7 Design Patterns to Refactor MVC Components in Rails 介紹 Rails 中常⽤的 Design Patterns 的知名⽂章
 https://www.sitepoint.com/7-design-patterns-to-refactor-mvc- components-in-rails/
  • 20. 介紹了七種 Rails 常⽤(?)的設計模式 • 1. Service Objects • 2. Value Objects • 3. Form Objects • 4. Query Objects • 5. View Objects(Serializer/Presenter) • 6. Policy Objects • 7. Decorators
  • 21. 照做肯定 over-engineering… • 我不推薦 • Service Objects • Form Objects • View Objects(Serializer/Presenter) • 適合場景很少 • Value Objects • Query Objects • 可⽤ • Policy Objects • Decorators
  • 23. 什麼是 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 class BooksController < 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 def borrow 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
  • 28. 於是就變成 function list,只是外表是 class 這樣符合SRP喔?! 每個 class 都只有單⼀責任 • app/services/ • create_account_service.rb • create_xxx_notify.rb • create_yyy_notify.rb • create_zzz_notify.rb • update_xxx • update_xxx • update_xxx • update_xxx • update_xx • …. • …
  • 29. 我認為 Service Object 是 bad smell • Service objects 看起來像是 OOP,因為⽤了 class 來包裹 • 但是這只是非常表⾯的, 只是把 code 搬到另⼀個地⽅,並不是好的 OO • 真正概念上有⽤的 object 是包含內在屬性和⽅法的。如果⼀個 object 只是 procedural code 的包裹,那麼實際上並沒有 OOP 帶來的好處 • 應該⽤ OO 概念去設計 PORO (Plain Old Ruby Object) model 即可 • 把相同概念的屬性和⽅法⼀起包裹 ,⽤好的⽅法命名 • ⽽不是硬套 service object pattern,每個⽅法命名都叫做 perform ⽅法
  • 30. # 我的設計 def borrow if @book && @book.is_available? @book.borrow! # Slack 通知 notifier = BookNotifier.new(@book) notifier.notify_borrow! redirect_to books_path, notice: "已完成預約(出借)!" else redirect_to books_path, notice: "此書⽬前無法出借!" end end
  • 31. class BookNotifier def initialize(book) @book = book end def notify_borrow! #... end def notify_return! #... end def notify_delay! #... end private def content_title #..... end
  • 33. Value Objects 概念有⽤,但不常需要實作 • 當⼀個物件沒有概念上的標識 (conceptual identity),⽽只在乎它的屬性時,這 個物件就可以是個 Value Object。反之則是 Entity。 • 物件A == 物件B,要怎麼判斷兩個物件相等 • 度量衡 類型的物件很適合⽤這個概念來設計 • 例如內建的 Date, Time 都是⼀種 Value Objects • ⼀個 Rails 沒有內建,但比較常⾒的場景: Money • https://github.com/Shopify/money
  • 34. Form Objects 可不⽤費⼼ • 若不是 ActiveRecord 物件,⽤原始的 form_tag 去組合表單就好了 • 場景不多,不太值得/不太需要特別包裹成 Object • 網路上的範例都太簡單,都是 “聯絡我們” 表單 • 若是多 model 的 association 的場景,想要⽤⼀個 Form Object 包裹處理 • 沒有什麼好的標準範例寫法,每個⼈都寫不⼀樣,維護起來很痛苦
  • 35. View Objects 也是不推 • Serializer • API 的話,Serializer 我不太⽤,我更喜歡 Jbuilder template 的表現⼒ • Serializer 常會混雜了query code,增加 N+1 除錯難度 • Presenter • 缺乏好的範例。⽽且我認為 View 中就⽤ @instance 跟 Helper,不需要費⼼ 整理成⼀個 Presenter 物件。代碼複⽤的機會很低,獲得的回報太少。 • 可以看看概念類似的 Rails ViewComponent 比較多⼈⽤
  • 36. Query Objects 少數複雜場景才需要 • ActiveRecord relations 本⾝就是⼀種 Query object 了 • 所以你做的事情其實是 Query object of Query object,真的有需要嗎? • 只有複雜的 Query 場景需要,但是既然複雜,就表⽰可能不太會被複⽤,因此 是否值得從 controller 中剝離出來?
  • 37. Policy Objects • 就是⽤ Pundit gem 權限設計 • https://github.com/varvet/pundit • 不過 Pundit 給的範例,會讓你以為要圍繞在 model 設計,這是不⼀定的 • 簡單的後台權限,只需要⽤ Headless policies 即可
  • 38. Decorators • 包裹 model,提供本來 helper 在做的⼯作 • ⼜叫作 View Models,可⽤ ruby 內建的 SimpleDelegator 或是 draper gem 即可 • 不影響整體架構,就是把 model 裡⾯的⽅法拆出來放另⼀個檔案⽽已。這些不會 是太關鍵的⽅法,只是為了配合 view 畫⾯⽽已
  • 39. OOP 推薦資源 • Ruby物件導向設計實踐 • Practical Object-Oriented Design in Ruby: An Agile Primer • https://www.tenlong.com.tw/products/9780321721334 • Ruby Science • https://github.com/thoughtbot/ruby-science • 這PDF 就包括 重構 和 設計模式 • 初學者⽤ TDD 可以幫助學習
  • 41. Rails 變來變去的前端架構... • Rails 1.0: 預設⽤ Prototype.js (http://prototypejs.org/) • Rails 1.1: RJS template • Rails 3: Assets pipeline (Sprockets) 和導入 Co ff eescript • Rails 4: 預設換 jQuery 和 jquery-ujs (ujs 是指 Rails 的 unobtrusive scripting adapter) • Rails 5: Turbolinks、⽀援⽤ Yarn 和 Webpack • Rails 6: 預設換 Webpacker, Turbolinks、移除 jQuery 換 rails-ujs、但 Assets pipeline 沒拆還在 • Rails 7: Import Map, Turbo, Stimulus。預設移除了 rails-ujs • 把 webpacker 拆掉了,建議⽤ jsbundling-rails gem(可搭配 esbuild) 和 cssbundling-rails • Assets pipeline 還在,但 DHH 有新出 Propshaft gem (因為有⼈離職了)
  • 42. animation framework, drag and drop, Ajax controls DOM utilities, and unit testing. 考古 Rails 1.1 使⽤的JS框架
  • 43. DHH 在想什麼? Rails 發明⼈和 Rails 前端⽅案的決策者 • 堅持想⽤ server-side rendering 中⼼思想 • 因爲夥伴 Sam Stephenson 負責前端,所以⼀開始⽤ Prototype.js,並⽤ RJS 達成⽤ Ruby 寫 JavaScript ⽅案 • 因為 Prototype.js 不⾏了, 被迫接受 jQuery。RJS 也跟著 gg • 曾經喜歡 Co ff eeScript,現在直接寫 ES6 了 • 很喜歡 Turbolinks ⽅案⾄今,⼤升級兩次: Classic Turbolink -> Turbolink 5 -> Turbo • 因為 Assets Pipeline(Sprockets) 功能對 ES6 和 js 模組化⽀援不⾜ • 被迫接受 Webpack ⽅案,但⼜捨不得把 Assets Pipeline 拆了。於是兩者並存... 前者處理js,後者處理css/image • Rails 7 ⼜把 Webpack 拆了 • 因爲跟夥伴 Sam Stephenson 拆夥了,正在把 Sprockets 換掉了 • Sam Stephenson 除了離開 basecamp,同時放棄所有跟 Rails 相關的 js 套件開發⼯作,包括 Hotwire, Stimulus 等
  • 44. 考古: RJS template Rails 1.1 的 Ruby JavaScript #/app/views/rjs1/index.rhtml <%= form_remote_tag :url => { :action =>’note’ }, :html => {:id=>’note-form’ } %> <%= text_field_tag ‘note’,nil,:size => 40 %> <%= submit_tag ‘submit’ %> <%= end_form_tag %> #/app/views/rjs1/note.rjs page.insert_html :bottom, ‘notes’, :partial =>’note’ page.visual_effect :highlight, ‘note’ page.form.reset ‘note-form’
  • 45.  考古: RJS ⽀援七種 DOM 操作 這是 rjs template code,是不是跟15年後的 Turbo Steams 很像?
 都說不⽤寫 javascript page.insert_html :bottom, 'dom_id', :partial => 'content' page.insert_html :top, 'dom_id', :partial => 'content' page.insert_html :before, 'dom_id', :partial => 'content' page.insert_html :after, 'dom_id', :partial => 'content'
 page.replace 'dom_id', :partial => 'content' page.replace_html 'dom_id', :partial => 'content' page.remove 'dom_id', :partial => 'content'
  • 46. rails-ujs (或是 jquery-ujs) ⽅案 • RJS 隨著 Prototype.js gg 之後,Rails 放棄⽤ Ruby 產⽣ JavaScript 的⽅式 • 改成直接寫 js.erb,就直接寫 javascript • 但本質上是⼀樣的,由 server 後端傳 javascript 給前端執⾏ • 透過 link_to, button_to 或是 form_for 加上 remote: true,就會發送請求 script 到後端 • 後端回傳 javascript,前端執⾏
  • 47. 我的看法 (1) • 我不喜歡 Turbolinks (Turbo Drive) ⽅案,因為他改變了瀏覽器預設⾏為 • 90%場景沒問題,但那 10% 有問題場景,對初學者特別困擾 • 和其他 JS library 搭配時,也經常會有相容性問題要處理
  • 48. 我的看法 (2) Turbo • Turbo Frames 是個漂亮的 ajax 功能語法糖,⽤ HTML 標籤就讓你替換 partial 部分區塊 • 只需要改 HTML erb 即可,後端不⽤改就可實現 Ajax 部分區塊替換 • 跟 https://github.com/renderedtext/render_async 作⽤類似,可以 ajax 替換 partial • Turbo Streams • ⽤ HTML 標籤,定義常⾒的 DOM 操作: 只是重新發明 RJS ⽽已... • 語法換了,但是精神是⼀樣,HTTP payload 從 script 換成 html,但骨⼦裡仍是 DOM 操作指令 • RJS 有的架構限制,⼀樣存在
  • 50. • 我認為 Server-side render 好⽤開發快效能⼜好,只是⼀條路不能走到⿊ • ⼤部分簡單的場景,我認為 Remote JS ⽅案是很好⽤ • 從 Rails 1.1 時代的 RJS • rails-ujs 的使⽤相容性問題最⼩ (JS部分只是個簡單的 adapter) • 我的 Ajax 教材 https://ihower.tw/rails/fullstack-ajax-1.html • Turbo Steams • 這幾種運作本質上是⼀樣的,結構問題和能⼒限制也⼀樣 • 如果達到了 SPA 的場景需求,我會去採⽤ Vue.js 或是 React.js ⽅案 我的看法 (3) Remote JS
  • 51. 結構和能⼒限制 ? • 結構問題 • 在有密集 Ajax 操作的 UI 組件中,會導致非常多零碎的 JS template….. 每個⼩⼩的 template 只為了調整畫⾯上 的 DOM • 例如⼀樣是刪除 comment,因為前台 UI 不同,js template 就得不同 • ⽤ Hotwire 也是⼀樣,會有很多零碎的 .turbo_stream 檔案 • 能⼒限制 • 不在 rails-ujs 和 Turbo ⽀援的瀏覽器事件時 (超連結、表單等) • 在需要 javascript template 的 SPA 場景時 • 還要需要⽤ jQuery ⾃⼰掛載瀏覽器事件送 Ajax,然後後端回傳 JSON 處理 • 或是搭配前端框架,例如 Stimulus.js, Vue.js, React.js 等
  • 52. 同個 comment 資料操作,不同UI準備不同template 好零碎啊.... • app/views/comments/create.js.erb • app/views/comments/edit.js.erb • app/views/comments/new.js.erb • app/views/comments/update.js.erb • app/views/comments/destroy.js.erb • app/views/answers/comments/create.js.erb • app/views/answers/comments/edit.js.erb • app/views/answers/comments/new.js.erb • app/views/answers/comments/update.js.erb • app/views/answers/comments/destroy.js.erb • app/views/peer_reviews/comments/create.js.erb • app/views/peer_reviews/comments/edit.js.erb • app/views/peer_reviews/comments/new.js.erb • app/views/peer_reviews/comments/update.js.erb • app/views/peer_reviews/comments/destroy.js.erb • ……..
  • 53. 我的作法: 若有 SPA 需求可在 erb 上掛載 Vue.js 例如⼀個複雜的 UI widget,有⼀堆 ajax 操作 • Vue.js 可以不需要 build 就可以使⽤,HTML上掛上 CDN 就可⽤,後端框架導入超簡單 • Vue.js 有出⼀個精簡版 petite-vue, gzip 後只有 6.9K,之後有需要也⽅便轉成 Vue.js 單組件 • 缺點是沒有單組件(SFC)功能,這⼀定需要 build 程序(需要 webpack 或是 esbuild 等) • 進⼀步可⽤ Vue.js 單組件掛載在 erb 上 • 此⽅案兼具 server-side rendering 開發快、速度也快的混和⽅案 • 需要 UI 組件時,再掛載前端組件即可
  • 54.
  • 55. # erb view <h2>⽂章標題</h2> <p>⽂章內容</p> …… <div id="comments-component"> <comments :comments="<%= @comments_data' %>"/> </div> 這是erb,可以讓初始資料塞到 props 即可。Vue.js 不需要⽤ Ajax 拿。
  • 56. # js 掛載 (需要透過 webpack 或是 esbuild 編譯) import { createApp } from 'vue/dist/vue.esm-bundler.js'; import Comments from './components/comments.vue' document.addEventListener('DOMContentLoaded', () => { const comments = document.querySelector('#comments-component') if(comments){ createApp({ components: { Comments } }).mount(comments); } })
  • 57. # Comment Vue.js 單組件 (template+js+css)(組件裡還可以包組件) <template> <div class="compose"> .... <ul v-for="(comment, comment_index) in comments" :key="comment.id"> <Comment :comment="comment" /> </ul> .... </div> </template> <script> import Comment from './comment.vue' export default { name: 'Comments', components: { Comment }, props: ['comments'], data () { }, methods: { } } </script> <style lang="css" scoped> ... </style>
  • 58. 參考範例 • Vue.js 2 + Webpacker • https://medium.freecodecamp.org/how-to-pass-rails-instance-variables-into-vue- components-7fed2a14babf • https://5xruby.tw/posts/rails-vuejs-get-started/ • Vue.js 3 + jsbundling-rails (⽤esbuild) • 有空我再寫 blog • Vite Ruby ⽅案 https://github.com/ElMassimo/vite_ruby • Vite 是 Vue.js ⾃家出的 build ⼯具
  • 60.
  • 61. petite-vue example 我⽤ Vue.js 改寫⼀樣的範例 <div id="hello"> <input type=“text” v-model=“name"> <button @click="greet"> Greet </button> <span> {{ output }} </span> </div> import { createApp } from ‘petite-vue’ createApp({ name: '', output: '', greet(){ this.output = `Hello, ${this.name}` } }).mount('#hello')
  • 62. • 上述 Vue.js code 只有 stimulus 的七成... • Stimulus 仍是傳統 DOM 操作思維 • 簡化了事件掛載,提供了⼀個 controller 架構 • Vue.js 和 React.js 是現代前端的 MVVM 和 Component 組件思維 • 前端組件有⾃⼰的狀態,DOM 隨著狀態改變 • https://zh-hant.reactjs.org/docs/thinking-in-react.html • 我認為可以 Progressive 漸進式採⽤的 Vue.js 是比較好的選擇 • 可以先採⽤不需要 build 的 Vue.js ⽅式,這比 Stimulus.js 需要 Import Map (或 build 程序),導 入成本更低 • 之後有需要再加上 build 程序,就可以⽤單組件 Component
  • 63. https://rubyonrails.org/doctrine/zh_tw • Rails 提供你整套前端+後端的架構 • 但很多元件都是可以替換的,例如 • 前端架構可換 • 測試框架可換 (很多⼈就換了 RSpec)
  • 64. 我的看法(5): 前後端分離? 完全不⽤ erb 後端 view template • 我認為 Rails 的 actionview 搭配 activerecord 是做表單的強項 • 前後端完全分離,就會失去 Rails 這個優勢 • 不會整個 app 都適合⽤ SPA 去做 • 混合⽅案才是 C/P 值最⾼ • 除非你有很好的理由: 康威定律、招聘問題(前端比較好招⼈?)
  • 65. 就算要前後端分離,也可以不⽤這麼分離 • cookie 還是很棒,不需要⽤ JWT • 登入⾴⽤ rails 處理,devise 繼續⽤,就不需要重新發明整個註冊、登入、忘記密碼流程 • 登入後再進入前端 SPA ⾴⾯即可 • 若可以同網域很棒,就不需要處理 CORS 跨域問題 • ⽤ nginx 處理分流即可,例如 • example.com/* ⽤ nginx reverse-proxy 到前端 app • example.com/api/* ⽤ nginx reverse-proxy 到後端 rails app • example.com/admin/* ⽤ nginx reverse-proxy 到後台 rails app
  • 66. 不喜歡不⼀定要跟,以拖待變!! • 我的老專案就遲遲沒⽤ webpacker 全⾯替換 sprockets • 拖到 webpacker 都 gg 了, sprockets 還在 • 跳過了 webpacker 時代,直接⽤ jsbundling-rails gem(可搭配 esbuild) 了 • 未來天秤⼜會回到 MPA? • https://nolanlawson.com/2022/05/21/the-balance-has-shifted-away-from- spas/ • MPA ⾸屏渲染更快、Core Web Vitals 分數更⾼、SEO 更好做…..
  • 67. The balance has shifted away from SPAs https://nolanlawson.com/2022/05/21/the-balance-has-shifted-away-from-spas/ • Chrome implemented back-forward caching • 上下⾴瀏覽器本⾝就有快取 • Shared Element Transitions • 瀏覽器⽀援跨⾴時維持固定的元素
  • 68. Rails 後端 ViewComponent ⽅案 順道⼀提
  • 69. 我沒採⽤ • https://github.com/github/view_component • 我覺得不值得,如果真的有需求,不如⽤ Vue.js 或 React 的前端 component ⽅ 案 • View Component 會增加改進後端性能的難度,特別是 N+1 queries • 後端 View Component 只有處理 erb template,⽽沒有包括 javascript 跟 css • Javascript 跟 css 仍然是沒有組建的狀態 • view component 沒有處理 css 跟 javascript,不就是做半套嗎
  • 71. # 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 def Post < 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
  • 79. 原因: ⾃動 inverse_of 可能失效 • Rails 不總是會推導出 inverse_of 屬性,例如 • 你⼿動給 , -> { where條件 } 時 • 有 :foreign_key 參數時 • 有 :through 參數時 • 此時就必須⼿動給 inverse_of class Post < ApplicationRecord has_many :comments, ->{ where(....) } end
  • 80. 4. JSON column type 超好⽤ • PG ⽀援、MySQL 5.7 之後也⽀援 • 不需要 MongoDB 了、舊的 ActiveRecord Serialize ⽅法也別⽤了 • 可減少了 N+1 queries 機會 • 適合場景: 不需要正規化的資料,例如 • 儲存整個 API response data • 額外的附屬欄位資料 • has_many 關聯資料,但是其中資料卻不需要個別拿出來使⽤的 • EAV 設計,例如客製表單
  • 81. Rails 實戰聖經: Rails 後端效能 • https://ihower.tw/rails/fullstack-performance-backend.html • 我寫的效能教材 • 有上述完整的說明
  • 82. 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
  • 84. gem 'rails', '6.1.4.1' # 若升級 v2 需要補設定 manifest.js gem 'sprockets', '~> 3.7.2' gem 'jquery-rails' gem 'jbuilder' gem 'kaminari' gem 'bootstrap4-kaminari-views' gem "pundit" # 此專案⽤ webpack v3 gem 'webpacker', '~> 4.x' # 因為 omniauth-wechat-oauth2 需要 omniauth 1.3.2 gem 'omniauth', '1.3.2' # 因為 omniauth 1.3.2 被鎖在 1.3.2 gem 'omniauth-facebook', '~> 4.0.0' # 因為 master branch 才⽀援 wechat_qiye gem "omniauth-wechat-oauth2", :github => "NeverMin/omniauth-wechat-oauth2" # 因為前端⽤ Bootstrap v4 gem 'bootstrap', "~> 4.6" 舉例(真正必要才指定版本)
  • 85. 謝謝聆聽 技術選擇⼀定有風險,套件有 upgrade 有 deprecated 選擇前應詳閱code⽂件 https://ihower.tw