iT邦幫忙

2025 iThome 鐵人賽

DAY 6
0
佛心分享-SideProject30

Mongory:打造跨語言、高效能的萬用查詢引擎系列 第 7

Day 6:Query builder 與 converters:ActiveRecord / Mongoid 實戰

  • 分享至 

  • xImage
  •  

在前幾篇,讀者已經用 Mongory 的 matcher tree 跑過一輪
今天把重點拉回到「日常可用的 API 與整合體驗」:Query builder 鏈式介面、Converters(資料/鍵/值/條件),以及如何在 Rails 中一鍵完成初始化與客製化

Query builder 基本用法(鏈式、可讀)

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 的角色與配置(Data / Key / Value / Condition)

Converters 讓 Mongory 在面對不同資料型別、鍵表達方式與條件來源時仍能夠表現一致
Mongory 預設提供四個面向:

  • DataConverter:把資料對象轉成可比對的 Hash/Array 結構(例如 ActiveRecord/Mongoid 物件 → Hash)
    • 為了效能,資料對象的轉換通常用淺轉換來實作,因為資料有可能很大包,深度轉換但條件比對只需要其中幾個欄位,並不划算
  • KeyConverter:把鍵名統一(如 symbol → string;"a.b" → {"a" => {"b" => ...}}
  • ValueConverter:把值正規化(如 Time/Date/Range→ 標準可比較形式)
  • ConditionConverter:把讀者傳入的條件轉成可編譯的標準結構

這些 converter 會在 matcher tree 建置或比對過程中被正確調用,讓條件與資料都落在「可比較」的標準層

ActiveRecord / Mongoid 實戰

以 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 generator 快速示範

  1. Mongory config 初始化與持續實戰維護:
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
  1. 產生自訂 matcher:
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)

常見踩雷與建議

  • 先用 DB 取索引友善資料,再交給 Mongory 做應用層複雜過濾(不要混為替代關係)
  • 啟用 symbol snippets 或是建立自訂 matcher 前先確定命名不會覆蓋到既有方法
    • 如果方法衝突,Mongory 選擇不 overwrite,那他就不是個有效的 Mongory snippet

綜合例(AR + Mongory)

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

下一篇將回到觀測性:explaintrace 的心法、輸出閱讀,以及如何用它們縮短除錯時間

專案首頁(Ruby 版)


上一篇
筆者心裡話:在這個 AI 盛行的時代,要 Mongory 有何用?
下一篇
Day 7:Explain 與 match trace:可觀測性到位
系列文
Mongory:打造跨語言、高效能的萬用查詢引擎10
圖片
  熱門推薦
圖片
{{ item.channelVendor }} | {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言