單子條件解包(unwrap)是在建構 matcher tree 時,主動移除「只有一個子節點的聚合器」等多餘包裝,讓呼叫深度更淺、explain
更直觀、效能更穩定的一個技巧
以 {:age => {:$gt => 18}}
為例,直覺建出的樹可能是:
AND
└─ Field: "age"
└─ AND
└─ Gt: 18
因為 age 對應到的條件值{:$gt => 18}
,型別為 hash,很自然就當作聚合層處理
但這裡的內層 AND 只有一個子 matcher,沒有資訊增量,卻會增加一層呼叫與 trace
雜訊
Mongory 在建樹期會把這層拿掉,得到:
Field: "age"
└─ Gt: 18
ConditionMatcher
/ AndMatcher
/ OrMatcher
僅有一個子 matcher$elemMatch
(陣列元素匹配需要完整上下文)$every
(同上)下列概念化程式碼示意建樹過程中的解包邏輯,非完整原始碼:
def build_matcher(node)
case node
when Hash
if operator_hash?(node)
matcher = build_operator_matcher(node)
return unwrap_if_single(matcher)
else
and_matcher = build_and_from_hash(node) # 將 field:cond 攤平成 AND
return unwrap_if_single(and_matcher)
end
else
EqMatcher.new(node)
end
end
def unwrap_if_single(m)
return m if must_preserve?(m)
return m unless m.is_a?(AbstractMultiMatcher)
return m if m.matchers.size != 1
m.matchers.first
end
def must_preserve?(m)
m.is_a?(ElemMatchMatcher) || m.is_a?(EveryMatcher)
end
如想了解解包邏輯,請參考 source code enable_unwrap!
require 'mongory'
Mongory.enable_symbol_snippets!
Mongory.register(Array)
records = [
{ 'name' => 'John', 'age' => 20 },
{ 'name' => 'Ann', 'age' => 17 }
]
# 單子條件(可解包)
q1 = records.mongory.where(:age.gt => 18)
q1.explain
# 需保留語意的條件
q2 = records.mongory.where(:tags.elem_match => { :name => 'ruby' })
q2.explain
輸出:
└─ Query: {"$and"=>[{"age"=>{"$gt"=>18}}]}
└─ Field: "age" to match: {"$gt"=>18}
└─ Gt: 18
└─ Query: {"$and"=>[{"tags"=>{"$elemMatch"=>{"name"=>"ruby"}}}]}
└─ Field: "tags" to match: {"$elemMatch"=>{"name"=>"ruby"}}
└─ ElemMatch: {"name"=>"ruby"}
└─ Field: "name" to match: "ruby"
└─ Eq: "ruby"
$elemMatch
, $every
不解包:它不是單純聚合器,而是「針對陣列逐一匹配」的語意單元explain
不一致在條件深度 4 ~ 6、每層只有單一子條件的情境,解包可讓呼叫深度下降 30–60%,explain/trace
的輸出量也更精簡
在 Mongory 的 C core/Ruby 兩條路徑上,解包都能帶來可觀的呼叫層數縮減,越複雜的條件越有感
Field -> And -> <單一子>
就想到可以解包$elemMatch/$every
就想到要保留explain
檢視樹,再用 trace
逐筆驗證匹配過程下一篇:Query builder 與 converters(含 Rails generator 快速示範)