從 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。
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::SelectStatement ,dispatch_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_SelectStatement 跟 visit_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
