Successfully reported this slideshow.
Your SlideShare is downloading. ×

Ruby Rails 老司機帶飛

More Related Content

Related Books

Free with a 30 day trial from Scribd

See all

Related Audiobooks

Free with a 30 day trial from Scribd

See all

Ruby Rails 老司機帶飛

  1. 1. COSCUP 2022/7/31 Rails 老司機帶⾶ ihower@aihao.tw
  2. 2. About me • 張⽂鈿 a.k.a. ihower • https://ihower.tw • Rails 實戰聖經 作者 • Ruby developer since 2006 • 從 Rails 1.1 ⽤到 Rails 7 的老司機 • 愛好資訊科技 • https://aihao.tw
  3. 3. 關於這場 talk • 這是⼀場進階的 Rails 演講 • 適合有 Rails 專案開發經驗的朋友聆聽 • 充滿寫 code 的主觀意⾒(偏⾒?),非 Best Practices 教學 • 我的專案開發背景 • 約5⼈左右的 Rails Developer ⼯程師開發團隊 (無前後端分離) • ⽬前⼿上的專案時長都在五年以上 (從 Rails 3 ⼀路升級上來)
  4. 4. Agenda • 關於 OOP (12min) • 關於前端⽅案 (9min) • 關於效能: N+1 queries (6min) • 關於 Gem fi le 版號 (3min)
  5. 5. 關於 OOP 物件導向 Application Developer 的必備知識
  6. 6. Case Study: 留⾔系統 (重構前) • 可以針對不同主題的留⾔ • 使⽤了 Polymorphic association 設計 • Comment • commentable_type: string • commentable_id: integer • 還有個 thread 分類: [:unclassi fi ed, :ask, :share, :ask_manager]
  7. 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. 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. 9. 基本 OO 解法⽅向: Replace Conditional with Polymorphism ⽤多型取代條件式
  10. 10. 常⾒解法: Replace Type Code with Subclasses • Rails 內建就⽀持 Single Table Inheritance (STI) • 適合 type 不會變更的場景
  11. 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. 12. 稍微進階解法 : Strategy Pattern 當 STI 不適⽤時,
 這也是 Composition over inheritance 中最常⾒的技巧
  13. 13. 什麼是 Strategy Pattern ? • 有⼀個 method 負責挑選要⽤哪⼀個策略,然後 new Strategy 物件 • 可以是根據某⼀個 type 屬性 • 也可以是某種判斷 • 不同策略就寫在不同 Strategy class 裡⾯ • 這些 strategy class 有相同的呼叫介⾯
  14. 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. 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. 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. 17. OO基本原則 Open–closed principle 
 對擴充開放、對修改封閉 基本 OO 解法⽅向: Replace Conditional with Polymorphism
  18. 18. 但要⼩⼼OO嘴砲原則 有些是吵架⽤,不是有⽤的指導原則 • 當我覺得你的解法做太多事情時,搬出 YAGNI • 當我覺得你的解法太複雜時,搬出 KISS • 當覺得⼀個類別太肥⼤的時候,就搬出 SRP (Single Responsibility Principle) • ⼀個 class 應有且只有⼀個理由會使其改變 • 理由?? 多⼩的理由? • 最後整個結構不就變成 ⼀個 class 就⼀個 public function ?
  19. 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. 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. 21. 照做肯定 over-engineering… • 我不推薦 • Service Objects • Form Objects • View Objects(Serializer/Presenter) • 適合場景很少 • Value Objects • Query Objects • 可⽤ • Policy Objects • Decorators
  22. 22. Service Object 最多⼈推薦使⽤的,但我認為過譽了
  23. 23. 什麼是 Service Object • 這裡指的 service object,是指實作 command pattern • 有個 class 帶有唯⼀的 public method,⽅法裡⾯就是⼀連串的指令來達成⼀ 件事情 • 還有⼈出個 gem 定義了這個結構
 https://github.com/nebulab/simple_command
  24. 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. 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. 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
  27. 27. Q: 假設接下來要還書也要通知? 逾期也要通知?
  28. 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. 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. 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. 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
  32. 32. https://www.codewithjason.com/code-without-service-objects/
  33. 33. Value Objects 概念有⽤,但不常需要實作 • 當⼀個物件沒有概念上的標識 (conceptual identity),⽽只在乎它的屬性時,這 個物件就可以是個 Value Object。反之則是 Entity。 • 物件A == 物件B,要怎麼判斷兩個物件相等 • 度量衡 類型的物件很適合⽤這個概念來設計 • 例如內建的 Date, Time 都是⼀種 Value Objects • ⼀個 Rails 沒有內建,但比較常⾒的場景: Money • https://github.com/Shopify/money
  34. 34. Form Objects 可不⽤費⼼ • 若不是 ActiveRecord 物件,⽤原始的 form_tag 去組合表單就好了 • 場景不多,不太值得/不太需要特別包裹成 Object • 網路上的範例都太簡單,都是 “聯絡我們” 表單 • 若是多 model 的 association 的場景,想要⽤⼀個 Form Object 包裹處理 • 沒有什麼好的標準範例寫法,每個⼈都寫不⼀樣,維護起來很痛苦
  35. 35. View Objects 也是不推 • Serializer • API 的話,Serializer 我不太⽤,我更喜歡 Jbuilder template 的表現⼒ • Serializer 常會混雜了query code,增加 N+1 除錯難度 • Presenter • 缺乏好的範例。⽽且我認為 View 中就⽤ @instance 跟 Helper,不需要費⼼ 整理成⼀個 Presenter 物件。代碼複⽤的機會很低,獲得的回報太少。 • 可以看看概念類似的 Rails ViewComponent 比較多⼈⽤
  36. 36. Query Objects 少數複雜場景才需要 • ActiveRecord relations 本⾝就是⼀種 Query object 了 • 所以你做的事情其實是 Query object of Query object,真的有需要嗎? • 只有複雜的 Query 場景需要,但是既然複雜,就表⽰可能不太會被複⽤,因此 是否值得從 controller 中剝離出來?
  37. 37. Policy Objects • 就是⽤ Pundit gem 權限設計 • https://github.com/varvet/pundit • 不過 Pundit 給的範例,會讓你以為要圍繞在 model 設計,這是不⼀定的 • 簡單的後台權限,只需要⽤ Headless policies 即可
  38. 38. Decorators • 包裹 model,提供本來 helper 在做的⼯作 • ⼜叫作 View Models,可⽤ ruby 內建的 SimpleDelegator 或是 draper gem 即可 • 不影響整體架構,就是把 model 裡⾯的⽅法拆出來放另⼀個檔案⽽已。這些不會 是太關鍵的⽅法,只是為了配合 view 畫⾯⽽已
  39. 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 可以幫助學習
  40. 40. Rails 前端⽅案
  41. 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. 42. animation framework, drag and drop, Ajax controls DOM utilities, and unit testing. 考古 Rails 1.1 使⽤的JS框架
  43. 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. 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. 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. 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. 47. 我的看法 (1) • 我不喜歡 Turbolinks (Turbo Drive) ⽅案,因為他改變了瀏覽器預設⾏為 • 90%場景沒問題,但那 10% 有問題場景,對初學者特別困擾 • 和其他 JS library 搭配時,也經常會有相容性問題要處理
  48. 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 有的架構限制,⼀樣存在
  49. 49. https://turbo.hotwired.dev/reference/streams
  50. 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. 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. 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. 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. 54. # erb view <h2>⽂章標題</h2> <p>⽂章內容</p> …… <div id="comments-component"> <comments :comments="<%= @comments_data' %>"/> </div> 這是erb,可以讓初始資料塞到 props 即可。Vue.js 不需要⽤ Ajax 拿。
  55. 55. # 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); } })
  56. 56. # 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>
  57. 57. 參考範例 • 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 ⼯具
  58. 58. Stimulus.js 如何?
  59. 59. 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')
  60. 60. • 上述 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
  61. 61. https://rubyonrails.org/doctrine/zh_tw • Rails 提供你整套前端+後端的架構 • 但很多元件都是可以替換的,例如 • 前端架構可換 • 測試框架可換 (很多⼈就換了 RSpec)
  62. 62. 我的看法(5): 前後端分離? 完全不⽤ erb 後端 view template • 我認為 Rails 的 actionview 搭配 activerecord 是做表單的強項 • 前後端完全分離,就會失去 Rails 這個優勢 • 不會整個 app 都適合⽤ SPA 去做 • 混合⽅案才是 C/P 值最⾼ • 除非你有很好的理由: 康威定律、招聘問題(前端比較好招⼈?)
  63. 63. 就算要前後端分離,也可以不⽤這麼分離 • 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
  64. 64. 不喜歡不⼀定要跟,以拖待變!! • 我的老專案就遲遲沒⽤ 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 更好做…..
  65. 65. 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 • 瀏覽器⽀援跨⾴時維持固定的元素
  66. 66. Rails 後端 ViewComponent ⽅案 順道⼀提
  67. 67. 我沒採⽤ • 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,不就是做半套嗎
  68. 68. Fix N+1 queries 後端最常⾒的效能問題 有些實際場景單純的 includes ⽅法解不了
  69. 69. # 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 嗎? 會
  70. 70. # 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 %> 解法
  71. 71. 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 嗎? 會
  72. 72. # model def Post < ApplicationRecord has_many :comments def my_comments self.comments.select{ |x| x.user_id == self.user_id } end end 解法
  73. 73. 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
  74. 74. class Post < ApplicationRecord has_many :comments end class Comment < ApplicationRecord belongs_to :post end
  75. 75. <% Post.includes(:comments).each do |post| %> <%= post.title %> <% post.comments.each do |comment| %> <%= comment.content %> <%= comment.post.title %> <% end %> <% end %> 這會造成 N+1 嗎? 若inverse_of 沒作⽤就會
  76. 76. class Post < ApplicationRecord has_many :comments, :foreign_key => :post_id end # and/or class Comment < ApplicationRecord belongs_to :post, :foreign_key => :post_id end
  77. 77. 原因: ⾃動 inverse_of 可能失效 • Rails 不總是會推導出 inverse_of 屬性,例如 • 你⼿動給 , -> { where條件 } 時 • 有 :foreign_key 參數時 • 有 :through 參數時 • 此時就必須⼿動給 inverse_of class Post < ApplicationRecord has_many :comments, ->{ where(....) } end
  78. 78. 4. JSON column type 超好⽤ • PG ⽀援、MySQL 5.7 之後也⽀援 • 不需要 MongoDB 了、舊的 ActiveRecord Serialize ⽅法也別⽤了 • 可減少了 N+1 queries 機會 • 適合場景: 不需要正規化的資料,例如 • 儲存整個 API response data • 額外的附屬欄位資料 • has_many 關聯資料,但是其中資料卻不需要個別拿出來使⽤的 • EAV 設計,例如客製表單
  79. 79. Rails 實戰聖經: Rails 後端效能 • https://ihower.tw/rails/fullstack-performance-backend.html • 我寫的效能教材 • 有上述完整的說明
  80. 80. 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
  81. 81. 最後,關於 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
  82. 82. 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" 舉例(真正必要才指定版本)
  83. 83. 謝謝聆聽 技術選擇⼀定有風險,套件有 upgrade 有 deprecated 選擇前應詳閱code⽂件 https://ihower.tw

×