Type something to search...
從 Arel 看 Visitor Pattern

從 Arel 看 Visitor Pattern

  • Backend
  • 03 Dec, 2020

偶然發現,Arel 好像是用 visitor pattern,好奇心驅使之下,就來研究看看。

翻翻歷史

翻了一下 Arel 的 repo,發現它其實一開始並不是用 visitor pattern 的,是在 2.0.0 之後才重寫,如這個 History 所述。

稍微研究了一下 1.0.1 跟 目前的結構的差別,目前是將各種節點都整理在 nodes 模組下,並將 to_sql 相關的模組完全抽離自節點,放到 visitors 模組。

題外話:為了知道 Arel 在 1.0.1 的運作邏輯,我還弄了一個 Rails 3.0.0 + Arel 1.0.1 的環境,放在這裏,雖然最後也看不出什麼鬼,反正就紀錄一下 XD 要跑個 Rails 3.0.0 還真是不容易(汗

Arel#to_sql 的運作模式

先產生一段簡單的 relation,並拿到 arel:

arel = User.joins(:posts).where(posts: { active: true }).arel

這會是一個 Arel::SelectManager 物件。

arel 傳遞 to_sql 訊息後,進入點是這裏

def to_sql(engine = Table.engine)
  collector = Arel::Collectors::SQLString.new      
  collector = engine.connection.visitor.accept @ast, collector
  collector.value    
end

這邊會根據 DB engine,來決定使用哪一種 ToSql Visitor,比方說如果是用 SQLite 的話,就會是用 Arel::Visitors::SQLite 。其中,collector 是蒐集最後的 SQL 字串的容器,@ast 是一個 Arel::Nodes::SelectStatement node。

接著走進 acceptvisit

def visit(object, collector = nil)
  dispatch_method = dispatch[object.class]
  if collector            
    send dispatch_method, object, collector
  else           
    send dispatch_method, object     
  end
  ...

visit 會根據 object (node) 的 class 來決定要拿來拜訪的 method 是哪一種,比方說目前的 node 是一種 Arel::Nodes::SelectStatementdispatch_method 就會是 visit_Arel_Nodes_SelectStatement

根據不同的 node,在 Arel::Visitors::ToSql 裡面定義了各式各樣的 [visit_Arel_xxx_Node](https://github.com/rails/rails/blob/v6.0.3.4/activerecord/lib/arel/visitors/to_sql.rb)s。而不同的 DB,各自 override 了不同的 visit_Arel_xxx_Nodes 實作。例如 MySQL 的在這裡

接著進入 visit_Arel_Nodes_SelectStatementvisit_Arel_Nodes_SelectCore,這裡就是依照順序走訪 SELECT 會需要經歷的各種 node,讓 collector 一路蒐集,最後組出 SQL 字串。

def visit_Arel_Nodes_SelectCore(o, collector)
  collector << "SELECT"
  collector = collect_optimizer_hints(o, collector)
  collector = maybe_visit o.set_quantifier, collector
  collect_nodes_for o.projections, collector, " "
  if o.source && !o.source.empty?
    collector << " FROM "
    collector = visit o.source, collector
  end
  collect_nodes_for o.wheres, collector, " WHERE ", " AND "
  collect_nodes_for o.groups, collector, " GROUP BY "
  collect_nodes_for o.havings, collector, " HAVING ", " AND "
  collect_nodes_for o.windows, collector, " WINDOW "
  maybe_visit o.comment, collector
end

Arel 的其他 Visitor

除了 ToSql Visitor 之外,其實只要繼承 Arel::Visitors::Visitor,並定義自己的 visit_Arel_xxx_Nodes 也可以走訪 node,做不一樣的事。

比方說有個 Dot Visitor,他可以輸出 dot 檔,拿來給 graphviz 畫圖。例如:

File.write('example.dot', User.joins(:posts).where(posts: { active: true }).arel.to_dot)

接著在 terminal 跑(要裝 graphviz):

dot -Tpng example.dot -o outfile.png

就可以輸出這個圖,看起來很酷,其實就是整個 Arel::Nodes::SelectStatement 的結構 XD

Related Posts

bullet gem 可能干擾對使用 preload 所造成效能影響的判定
bullet gem 可能干擾對使用 preload 所造成效能影響的判定

最近遇到了一個案例是,有個使用 includes 去 preload association 的地方,相對於直接 pluck 出來,花的時間慢的異常多。研究之後發現是 bullet 的 unused_eager_loading_enable 偵測打開後,會一定程度的影響效能。 如同這位開發者在 [issue](https://github.com/flyerhzm/bullet/issu

read more
CVE-2020–15169
CVE-2020–15169

來看看這個 CVE-2020–15169 漏洞是怎麼回事 看 https://github.com/advisories/GHSA-cfjv-5498-mph5 的說明可以知道,發生的時機在於,如果你使用了 [https://guides.rubyonrails.org/i18n.html#

read more
ActiveRecord 的 default_attributes
ActiveRecord 的 default_attributes

在 Rails server 沒有重開的情況下,更動資料庫表的欄位預設值,會有意想不到的事情發生 以下是用 Rails 6.0.3.2 + MySQL 5.6 實驗 事情是這樣的 有一張 tasks 表,有個欄位 completed,預設值為 false,且不能為 NULL create_table "tasks", options: "ENGI

read more
How to Update Your Pry Prompt Setting since v0.13.0
How to Update Your Pry Prompt Setting since v0.13.0

The pry gem changed itsPry::Prompt API since v0.13.0, and deprecated setting prompt through Pry.config.prompt = [] . If you have customized this way, you should use t

read more
ArgumentError: invalid byte sequence in UTF-8
ArgumentError: invalid byte sequence in UTF-8

有次遇到這個錯誤:ArgumentError: invalid byte sequence in UTF-8,一追之下發現是遇到類似這段程式碼, "english 中文\xED\xB6\xB0".gsub(/english/, '')會錯的原因是因為 \xED\xB6\xB0 不是合法的 UTF-8 編碼。 要檢查是不是合法的編碼可以用 `valid_encod

read more
method_missing 傳遞 splat argument 會噴 SystemStackError
method_missing 傳遞 splat argument 會噴 SystemStackError

最近遇到一個因為 splat argument 導致噴 SystemStackError 的案例,研究了半天發現是使用 method_missing 才會遇到 最近工作上遇到一個案例,是呼叫某個函式會噴 SystemStackError,google 發現了類似的事情 [https://github.com/redis/redis-rb/issues/264](https://github.co

read more
Rack 的 Middleware 的執行順序
Rack 的 Middleware 的執行順序

看〈為你自己學 Ruby on Rails〉介紹 Rack 的 Middleware 執行順序看不太懂,於是就研究了一下原始碼,看看是怎麼回事 因為常常聽到 Middleware Middleware 的,但是又不太懂,於是在網路上找到龍哥寫的文章,非常簡顯易懂,跟著動手做可以知道 Rack 要怎麼使用。 但是在「Middleware 的順序?」這的章節看不太懂盤子的比喻,於是決定直接在看原始

read more
[Rails][MySQL] 使用 update_all + JOIN + LIMIT 需注意效能問題
[Rails][MySQL] 使用 update_all + JOIN + LIMIT 需注意效能問題

MySQL 使用 JOIN + update_all 時,若遇到 LIMIT 則會轉為 subquery,需注意效能問題。 Rails 在使用 update_all 時,如果有用到 JOIN,會改用 subquery 的形式改寫,但若 Adapter 為 MySQL 時就會維持,因為 MySQL 語法本身支援 JOIN + UPDATE。但若是包含 LIMIT 時,由於 MySQL 也不支援,所

read more
Ruby Concurrency 筆記
Ruby Concurrency 筆記

Ruby 3.0.0 在去年聖誕節推出,release note 提到了一些 concurrency 相關的新功能,一邊研究一邊做個紀錄 在 Ruby 3.0.0 Released 提到了 Ruby 針對 Concurrency 加入了 [`Fiber#sch

read more
使用 Arel 寫出好看的 Left Join
使用 Arel 寫出好看的 Left Join

之前想要寫有條件的 left join 都會寫成 raw SQL query 的字串,但是其實也可以用 Arel 寫出比較好看的程式碼。 假設有個 DB 的 schema 長這樣: create_table "posts", force: :cascade do |t| t.string "title" t.boolean "active" t.integer "u

read more
在 Rails 5.1 之後,使用 pluck 不保證會 DB query
在 Rails 5.1 之後,使用 pluck 不保證會 DB query

在 Rails 5.1 之後,因為這個 commit,為 pluck 的行為帶來的一點變動。 因為習慣在 Rails 4.2 開發,養成了一個直覺是使用 pluck 就是會 DB query。但是就在換

read more
好像搞懂 searchkick 的 field 到底是怎麼用的了
好像搞懂 searchkick 的 field 到底是怎麼用的了

之前在看 searchkick 的 Github README 時,一直搞不懂 field 這個單位到底是什麼,於是花了一點時間來研究, gem 本人在此: searchkick 這個 gem 是封裝了幾個 Elasticsearch 相關的 gem,讓我們用 ActiveRecord 的方式,去操作 Elasticsearch 的 API。 <LinkCard url="https://

read more
如何 preload polymorphic associations
如何 preload polymorphic associations

因為想要用 includes preload polymorphic associations,但是不同 model 後面的 association 對象不一樣時不能直接用。後來在網路上找到可以用 ActiveRecord::Associations::Preloader preload 已經初始化後的 records 來達到目的。 **備註:Rails 6 已經支援 preload p

read more
如何使用 batch-loader gem 避免 N+1 Query
如何使用 batch-loader gem 避免 N+1 Query

我一開始看 batch-loader 在 github 上的說明,一直看不懂到底該怎麼用,所以這邊紀錄一下它的用法。 這是一個基本的使用方式,可以從 accounts 從 DB 拿出對應的 users: def user_lazy(account) BatchLoader.for

read more
如何在 debug test case 時印出 DB 的 log
如何在 debug test case 時印出 DB 的 log

有時候會想利用 test case 去追蹤一些操作 DB 的行為。 像是會放 binding.pry 在要觀察的程式附近,接著只要用 ActiveRecord::Base.logger = ActiveSupport::Logger.new(STDOUT)就可以把操作 DB 的 log 印到 terminal 的畫面了。

read more
如何在 IRB/Pry/Rails Console 使用 Up/Down 鍵搜尋以前打過的命令
如何在 IRB/Pry/Rails Console 使用 Up/Down 鍵搜尋以前打過的命令

覺得 Ctrl + R 的 reverse-i-search 用的不是很習慣,google 了一下原來這樣設定就可以用 up/down 鍵搜尋了。 新增 ~/.inputrc 檔案,裡面設定: "\e[A": history-search-backward "\e[B": history-search-forward

read more
如何無痛跑 rake task
如何無痛跑 rake task

用 Rails console 開 sandbox 模式 $ rails c -s先載入環境就可以跑了 > Rails.application.load_tasks > Rake::Task['some_namespace:some_task_name'].invoke由於對 DB 的操作之後都會 rollback,所以可以拿來測試 rake

read more
如何讓 whenever 排程執行的程式,噴錯時寫到 Rollbar
如何讓 whenever 排程執行的程式,噴錯時寫到 Rollbar

串接 Rollbar 的好處是可以方便監控伺服器噴錯。但是在使用 whenever 排程時執行程式時,有辦法在噴錯時也寫到 Rollbar 嗎? 如果是使用 sidekiq,無論是使用 include Sidekiq::Worker 產生的 [worker](https://github.com/mperham/sidekiq/wiki/G

read more
設定可以跑 rails core repository 測試的開發環境
設定可以跑 rails core repository 測試的開發環境

記錄一下照著官方教學設定開發 rails core repository 的環境 以下步驟取自 https://github.com/rails/rails-dev-box:安裝 VirtualBox。Mac 如果安裝 VirtualBox 遇到權限問題,[要到 Security & Privacy 允許

read more