October 6, 2023

Viiisit [Ruby on Rails] - Active Record Query (上)!

#ruby on rails#active record

繼續來探究 Active Record,前幾篇談論資料的關聯性,這次我們來點在 Active Record 怎麼抓取資料!

前情提要

大家還記得當時提及 Active Record 的好處嗎?
如果沒印象,可以回頭看看:Day 19 - 理解 Ruby on Rails,ORM 與 Active Record 是什麼?

Active Record 是具體的 ORM 實現,提供一種方式來定義和操作 Model,隱藏了資料庫操作的細節,允許開發人員使用物件導向語法來處理資料。

一般我們可以使用原始的 SQL 查詢尋找資料庫記錄,但在 Active Record 裡可以用他提供的方法來操作,
現在讓我們來看看在 Active Record 是如何搜尋資料的吧!

如何抓到資料!

Retrieving a Single Object 找尋單一物件

Active Record 提供了多種不同的方式來找尋單一物件(資料表中的一筆記錄)。
方法:find, find_by, first, last, take

  1. find

    • Active Record: 使用 find 方法,根據 主鍵 (id) 來查找單一記錄。
      1
      user = User.find(1)
    • SQL 語法: 查找主鍵值為 1 的記錄。
      1
      SELECT * FROM users WHERE id = 1;
  2. find_by

    • Active Record: 使用 find_by 方法,根據指定的條件查找第一個匹配的記錄。
      1
      user = User.find_by(username: 'viii')
    • SQL 語法: 查找用戶名為 ‘viii’ 的第一條記錄。
      1
      SELECT * FROM users WHERE username = 'viii' LIMIT 1;

    find_by!find_by 非常相似,都是用來查找符合條件的記錄。但是,find_by! 在找不到匹配的記錄時會引發 ActiveRecord::RecordNotFound 的錯誤訊息,而 find_by 僅返回 nil。這可以用來強制確保查詢會找到一條記錄,並在未找到時引發錯誤訊息。

  3. first

    • Active Record: 使用 first 方法,檢索資料表中的第一條記錄。
      1
      first_user = User.first
    • SQL 語法: 檢索資料表中的第一條記錄。
      1
      SELECT * FROM users LIMIT 1;
  4. last

    • Active Record: 使用 last 方法,檢索資料表中的最後一條記錄。
      1
      last_user = User.last
    • SQL 語法: 檢索資料表中的最後一條記錄。
      1
      SELECT * FROM users ORDER BY id DESC LIMIT 1;
  5. take

    • Active Record: 使用 take 方法,取得資料表中的一條記錄。
      1
      user = User.take
    • SQL 語法: 檢索資料表中的一條記錄。
      1
      SELECT * FROM users LIMIT 1;

Retrieving Multiple Objects in Batches 批次找尋物件

當取得多筆資料時,很直覺覺得可以使用 all 接著 .each 方法,但是!這樣可能會導致一次將整個資料表的資料提取出來,並存放在記憶體中,這樣的做法當資料量很大時,很容易超過記憶體的負荷。

1
2
3
4
# 將整個資料表的資料全部加載到記憶體中
User.all.each do |user|
puts user.name
end

為了避免這些問題,Active Record 提供 find_eachfind_in_batches 方法,可以批次處理記錄,
而不是一次性將所有記錄載入記憶體,以便更有效地處理大量數據。這樣可以減輕記憶體負荷並提高效能。
方法:find_each, find_in_batches

  1. find_each
    find_each 按批次檢索記錄,並遍歷紀錄。這對於處理大量記錄非常有用,因為每次只加載一批記錄,而不是全部加載到內存中。

    1
    2
    3
    4
    5
      # 檢索所有用戶記錄,每次處理 1000 條記錄
    User.find_each(batch_size: 1000) do |user|
    # 在這裡對每個用戶記錄進行操作
    puts user.name
    end

    Options for find_each:batch_size, :start, :finish, :error_on_ignore, :order

    • :batch_size:指定每個批次中要檢索的記錄數量,用於控制每次處理的記錄數。
    • :start:配置序列的第一個 ID,可用於中斷的批次處理,指定從哪個 ID 開始。
    • :finish:配置序列的最後一個 ID,可用於檢索特定範圍內的記錄。
    • :error_on_ignore:用於控制當查詢操作遇到問題時應該發生什麼。如果在發現問題時立即中止操作,可以設置為 true;如果操作繼續進行,即使有問題也不中斷,可以保持默認值 false。
    • :order:指定主鍵 (id) 的順序,可以是升序 (:asc) 或降序 (:desc),默認為升序。
  2. find_in_batches
    find_in_batches 方法類似於 find_each,按批次檢索記錄,但不提供遍歷功能。返回的是一個包含每批記錄的集合,可以透過這個集合並處理每批記錄。

    1
    2
    3
    4
    5
    6
    7
    # 檢索所有用戶記錄,每次處理 1000 條記錄
    User.find_in_batches(batch_size: 1000) do |batch|
    # 在這裡處理每批記錄
    batch.each do |user|
    puts user.name
    end
    end

    Options for find_in_batches:batch_size, :start, :finish, :error_on_ignore

這兩種方法都有助於減少大量數據查詢時的性能問題,特別是當記錄數量很龐大時,透過分批處理,可以更有效地管理記憶體使用,從而提高應用程式的效能。

Conditions 條件搜尋

where 方法接收一個條件可以使用 字串、陣列、物件(key : value)作為參數,並返回所有符合條件的查詢對象,是一個 ActiveRecord 查詢集合(Relation)。
如果找不到紀錄,會是一個空的 ActiveRecord 查詢結果 [ ]
方法: where

  1. Pure String Conditions 純字串條件
    用戶可以通過輸入關鍵字來搜索產品的名稱:

    1
    2
    keyword = params[:keyword] # 使用者輸入的搜索關鍵字
    Product.where("name LIKE '%#{keyword}%'") # 不推薦的寫法,容易有 SQL injection 的風險

    將使用者輸入的 keyword 直接插入到 SQL 字符中,這樣的做法可能導致 SQL injection!
    SQL injection 是一個安全漏洞,攻擊者可以通過在 SQL 查詢中插入惡意代碼,從而對數據庫進行未授權的操作。

    如何防止 SQL injection?

    在 Rails 中,防止 SQL injection 的方式可以透過使用 Array Conditions(陣列條件)

  2. Array Conditions 陣列條件

    1
    Product.where("name LIKE ?", params[:keyword]) # 推薦的寫法,使用 Array Conditions

    陣列的第一個元素是一個 SQL 條件字串 "name LIKE ?",可以包含在 SQL 查詢的 WHERE 子句中。這個字串中可以包含佔位符,通常用問號 ? 來表示,Active Record 會將 ? 換成 params[:keyword] 做查詢,確保資料被安全地處理,以防止任何 SQL injection。

  3. Placeholder Conditions
    Placeholder conditions 具有類似於使用問號 ? 的 params 替換特性,這種風格通常用於傳遞參數值,以防止 SQL 注入攻擊。
    除了使用問號之外,還可以在查詢條件字串中指定 keys/values 用 Hash 方式!這種方式的好處是,若條件中有許多參數,這種寫法不僅提高了可讀性,傳遞起來也更方便。

    用戶可以搜索擁有特定名稱和年齡的用戶,使用 Placeholder conditions 來構建查詢條件:

    1
    2
    3
    4
    5
    6
    7
    conditions = "name = :user_name AND age > :min_age"
    values = { user_name: "viii", min_age: 30 }

    User.where(conditions, values)

    # 寫在一起
    User.where("name = :user_name AND age > :min_age", { user_name: "viii", min_age: 30 })

    儘管條件參數會自動轉義以防止 SQL 注入,但 SQL LIKE 在遇到 SQL Wildcards (SQL 萬用字元),即 %_ 下,不會轉義,這可能會導致意外行為。例如:

    1
    Book.where("title LIKE ?", params[:title] + "%")

    可以通過 sanitize_sql_like 來解決:
    1
    Book.where("title LIKE ?", Book.sanitize_sql_like(params[:title]) + "%")

  4. Hash Conditions

    前情提要:
    Only equality, range, and subset checking are possible with Hash conditions.
    只有 Equality (相等性)、Range (範圍)、Subset (子集) 可用這種形式來寫條件。

    • Equality (相等性)

      • 查找名字是 “viii” 的用戶。
        1
        User.where(name: "viii")
        生成的 SQL 查詢:SELECT * FROM users WHERE (users.name = 'viii')
      • 使用字串 'name' 指定為 “viii”。
        1
        User.where('name' => "viii")
      • 使用關聯模型的實例作為值,並使用關聯名作為鍵,以查找符合特定關聯的記錄。
        假設 User 有一個 belongs_to 關係,指向 Country 模型,
        並且想查找居住在美國的用戶。
        1
        2
        country = Country.find_by(name: "USA")
        User.where(country: country)
      • tuple-like 序組構造可用於處理具有複合主鍵的表格。
        假設 User 表具有複合主鍵,由 country_iduser_id 兩個列組成,
        可以查找具有特定組合的用戶。
        1
        User.where([:country_id, :user_id] => [[1, 101], [2, 202]])
    • Range (範圍)

      • 查詢在一個特定範圍內的記錄。
        1
        Product.where(price: 10..50) # 返回所有價格在 10 到 50 之間的產品記錄。
        生成的 SQL 查詢:SELECT * FROM products WHERE (products.price BETWEEN 10 AND 50)
    • Subset (子集)

      • 可以一次查找多個特定值的記錄
        查找名字為 “Alice”、”Bob” 和 “Charlie” 的用戶:
        1
        User.where(name: ["Alice", "Bob", "Charlie"])
        生成的 SQL 查詢:SELECT * FROM users WHERE (users.name IN ('Alice', 'Bob', 'Charlie'))
  5. NOT, OR, AND Conditions
    Active Record 提供了多種方法來構建 SQL 查詢的 NOTORAND

    • NOT
      • where.not
        1
        User.where.not(name: "John")
        生成的 SQL 查詢:SELECT * FROM users WHERE NOT (users.name = 'John')
    • OR
      • or
        1
        User.where(name: "Alice").or(User.where(name: "Bob"))
        生成的 SQL 查詢:SELECT * FROM users WHERE (users.name = 'Alice' OR users.name = 'Bob')
    • AND
      • 使用多個 where 條件,多個條件之間是 AND 關係。
        1
        User.where(name: "Alice", age: 25)
        生成的 SQL 查詢:SELECT * FROM users WHERE (users.name = 'Alice' AND users.age = 25)
  6. Ordering

    1
    2
    User.order(age: :asc) # 升冪排序
    User.order(age: :desc) # 降冪排序
    • 多次使用 order
      1
      User.order(:name).order(age: :desc)
      生成的 SQL 查詢:SELECT * FROM users ORDER BY users.name ASC, users.age DESC
  7. Selecting Specific Fields
    可以指定想要從資料庫中檢索的特定欄位,而不是檢索整個資料表,達到效能優化
    方法:select, distinct

    • 假設有一個 User 模型,包含了 nameemailage ,但只想查 nameemail
      1
      User.select(:name, :email)
      生成的 SQL 查詢:SELECT name, email FROM users

    要小心使用 select。因為實體化出來的物件僅有所選欄位。
    如果試圖存取不存在的欄位,會得到 ActiveModel::MissingAttributeError 異常:
    ActiveModel::MissingAttributeError: missing attribute: <attribute>

    • 如果想要僅選擇某個欄位中每個唯一值對應的一條記錄,可以使用 distinct
      1
      User.select(:name).distinct
      生成的 SQL 查詢:SELECT DISTINCT name FROM users
  8. Limit(限制)Offset(偏移)
    LimitOffset 是用於在 SQL 查詢中控制結果集大小和選擇的兩個重要參數。

    • Limit(限制)Limit 用於限制查詢結果集的大小,指定了要返回的記錄數量。
    • Offset(偏移)Offset 用於指定從查詢結果集的開頭位置開始返回記錄。允許跳過前幾條記錄,以便從指定位置開始檢索記錄。

    繼續用 User 模型來講解,想查詢所有用戶的名字,但每次僅返回前五個名字,並且跳過前十個名字,這樣你可以從第十一個名字開始繼續查詢,這就可以使用 limitoffset 選項來實現這個目的:

    1
    User.select(:name).limit(5).offset(10)

    這個查詢將選擇 User 表中的 name 字段,然後使用 limit(5) 來指定返回前五個名字,使用 offset(10) 來指定從第十一個名字開始返回。
    生成的 SQL 查詢:SELECT name FROM users LIMIT 5 OFFSET 10

    這兩個通常在分頁(pagination)中使用,在列表中顯示一部分記錄,然後允許用戶查看更多的記錄。

Brief Summary


參考資料:
Active Record Query Interface
Active Record 查詢
PJCHENder - [Rails] Active Record Query(SQL Query & Model 資料查詢)