在前幾篇,讀者已經用 Mongory 的 matcher tree 跑過一輪
今天把重點拉回到「日常可用的 API 與整合體驗」:Query builder 鏈式介面、Converters(資料/鍵/值/條件),以及如何在 Rails 中一鍵完成初始化與客製化
require 'mongory'
Mongory.enable_symbol_snippets!
Mongory.register(Array)
# 模擬資料
records = [
  { 'name' => 'Ann',  'age' => 19, 'status' => 'active',  'tags' => [{ 'name' => 'ruby',  'priority' => 6 }] },
  { 'name' => 'Ben',  'age' => 22, 'status' => 'pending', 'tags' => [{ 'name' => 'rails', 'priority' => 3 }] },
  { 'name' => 'Cody', 'age' => 17, 'status' => 'active',  'tags' => [{ 'name' => 'ruby',  'priority' => 8 }] }
]
# 建構 query
q = records.mongory
  .where(:age.gte => 18)
  .any_of({ :status => 'active' }, { :name.regex => /^A/ })
  .in(:status => %w[active pending])
q.explain
q.each { |r| p r['name'] }
where:加入條件any_of:語義為 $or(會包在 $and 架構內,維持可預期的邏輯合併)in/nin:集合條件糖衣(等價於把值包成 $in/$nin)limit(n):立即生效、縮小後續運算集合Converters 讓 Mongory 在面對不同資料型別、鍵表達方式與條件來源時仍能夠表現一致
Mongory 預設提供四個面向:
"a.b" → {"a" => {"b" => ...}}
這些 converter 會在 matcher tree 建置或比對過程中被正確調用,讓條件與資料都落在「可比較」的標準層
以 ActiveRecord 舉例:
Mongory.register(ActiveRecord::Relation)
users = User.where(active: true) # 走資料庫索引
# 接著用 Mongory 做應用層二次過濾(非索引欄位或複雜陣列條件)
q = users.mongory
  .where(:last_login.gte => 7.days.ago)
  .where(:tags.elem_match => { :name => 'ruby', :priority.gt => 5 })
q.each { |u| p u.id }
Mongoid 已有整合模組(載入 mongory/mongoid),會註冊必要的轉換,讓 Criteria 走到 Mongory 時鍵/值/資料都能順利比對
rails g mongory:install
會產生 config/initializers/mongory.rb,內容如下:
Mongory.configure do |mc|
  # 啟用 symbol 運算子語法糖
  mc.enable_symbol_snippets!
  # 註冊常見集合類別(可在 Array、ActiveRecord::Relation 等等直接呼叫 .mongory)
  mc.register(Array)
  mc.register(ActiveRecord::Relation) # 如果使用 Active record
  mc.register(Mongoid::Criteria) # 如果使用 Mongoid
  mc.register(Sequel::Dataset) # 如果使用 Sequal
  # 這裡配置 Converters(依專案需求增補)
  mc.data_converter.configure do |dc|
    dc.register(ActiveRecord::Base, :attributes) # 使用 Active record
    dc.register(Mongoid::Document, :as_document) # 使用 Mongoid
    dc.register(BSON::ObjectId, :to_s) # 使用 Mongoid
    dc.register(Sequel::Model) { values.transform_keys(&:to_s) } # 使用 Sequal
  end
  mc.condition_converter.configure do |cc|
    # 條件轉換器底下分兩種,分別對應鍵轉換與值轉換
    cc.key_converter.configure do |kc|
      # 鍵轉換需要你提供能夠接收一個參數的 method 或 block
      # Example:
      # kc.register(MyKeyObject, :trans_to_string_key_pair)
      # kc.register(MyEnumKey, ->(val) { { "my_enum" => val.to_s } })
    end
    cc.value_converter.configure do |vc|
      # Example:
      # vc.register(MyCollectionType) { map { |v| vc.convert(v) } }
      # vc.register(MyWrapperType) { unwrap_and_return_value }
      vc.register(BSON::ObjectId, :to_s) # 使用 Mongoid
    end
  end
end
# 如果你的專案不用 Rails ,無法配合 rails g 的指令,那這邊可以直接 copy 拿去用 :p
rails g mongory:matcher by_proc
會建立對應檔案與註冊片段(也可自動更新 initializer)
示例:
class ByProcMatcher < Mongory::Matchers::AbstractMatcher
  def match(data)
    @condition.call(data)
  end
  def check_validity!
    return if @condition.is_a?(Proc)
    raise TypeError, "$byProc needs a proc."
  end
end
Mongory::Matchers.register(:by_proc, '$byProc', ByProcMatcher)
# 如果有啟用語法糖,之後的 query 就可以直接使用 Symbol#by_proc
records.mongory.where(:name.by_proc => method(:name_valid?).to_proc)
symbol snippets 或是建立自訂 matcher 前先確定命名不會覆蓋到既有方法
class UsersController < ApplicationController
  def index
    scope = User.where(active: true)
    filtered = scope.mongory
      .where(:age.gte => 18)
      .any_of({ :status => 'active' }, { :tags.elem_match => { :name => 'ruby' } })
      .limit(50)
    render json: filtered.map { |u| { id: u.id, name: u.name } }
  end
end
下一篇將回到觀測性:explain 與 trace 的心法、輸出閱讀,以及如何用它們縮短除錯時間