返回文章列表

SQLAlchemy 資料函式庫查詢操作與技巧

本文介紹如何使用 SQLAlchemy 的 2.0 查詢風格進行資料函式庫操作,包含查詢、過濾、排序、聚合函式使用及結果處理技巧。從建立資料函式庫連線、定義模型到執行查詢,涵蓋了 SQLAlchemy 的核心用法,並提供程式碼範例說明如何有效地與資料函式庫互動,以及如何使用 ORM

資料函式庫 Python

SQLAlchemy 1.4 版後引入新的 2.0 查詢風格,簡化了資料函式庫操作流程。使用 Python 互動式環境搭配 SQLAlchemy,可以更直觀地進行資料函式庫查詢。首先,匯入必要的引擎、模型和會話物件,建立會話後,即可使用 select() 函式搭配模型類別查詢所有資料。透過 execute() 方法執行查詢,並使用 all()first()one() 等方法處理結果。此外,scalars() 方法適用於單值結果,並可結合其他方法簡化查詢。在實際應用中,通常使用迴圈遍歷結果集。

資料函式庫操作與查詢

在前面的章節中,我們已經成功地將產品資料匯入資料函式庫中。現在,是時候來學習如何對資料函式庫進行查詢操作了。如果你曾經使用過 SQLAlchemy,那麼你可能會注意到,從版本 1.4 開始,SQLAlchemy 對 ORM 查詢的構建方式進行了重大的更改。雖然舊的查詢實作仍然可用,但在本文中,我們將只使用新的查詢風格。SQLAlchemy 檔案將這種新的查詢風格稱為「2.0 查詢風格」,但這種查詢風格也可以在 1.4 版本中使用,只要在建立引擎和會話物件時設定 future=True 選項即可。

開啟 Python 互動式環境

最簡單的方式來操作新的資料函式庫是開啟一個 Python 互動式環境,在那裡你可以互動式地發出查詢。首先,你需要匯入引擎物件、模型類別和會話類別:

>>> from db import Session
>>> from models import Product

接著,你可以建立一個會話:

>>> session = Session()

這是一種不使用上下文管理器的建立會話的方式。在應用程式中,上下文管理器是一種非常方便的方式,但在 Python 互動式環境中,直接建立會話物件會更好。

查詢定義

關係型資料函式庫使用 SELECT 關鍵字來實作查詢。SQLAlchemy 提供了一個 select() 函式,具有類別似的功能。最簡單的查詢是傳回表中所有元素的查詢。以下是如何定義一個檢索資料函式庫中所有產品的查詢:

>>> from sqlalchemy import select
>>> q = select(Product)

select() 函式接受需要檢索的專案作為引數。當傳遞一個模型類別作為引數時,SQLAlchemy ORM 將檢索模型的所有屬性,並透明地傳回 Python 物件。

檢視 SQL 程式碼

有時,瞭解將要傳送到資料函式庫的 SQL 程式碼是非常有用的。與 SQLAlchemy 查詢物件相關聯的 SQL 程式碼可以在列印查詢時檢視:

>>> print(q)
SELECT products.id, products.name, products.manufacturer, products.year,
products.country, products.cpu
FROM products

在這裡,你可以看到如何將 Product 類別轉換為一個檢索表中所有屬性的 SELECT 陳述式。

執行查詢

在建立查詢物件之後,需要將其交給會話,會話將透過引擎維護的連線將其傳送到資料函式庫驅動程式執行。最通用的方式是使用會話的 execute() 方法:

>>> r = session.execute(q)
>>> list(r)
[(Product(1, "Acorn Atom"),), (Product(2, "BBC Micro"),), ..., (Product(149, "GEM 1000"),)]

execute() 方法傳回一個結果物件。這是一個可迭代的物件,可以檢索查詢結果。在上面的例子中,可迭代物件被轉換為列表,以強制檢索和顯示所有結果。

結果處理方法

結果物件具有多種方法,可以用來檢索查詢結果:

  • all(): 傳回所有結果行。
  • first(): 傳回第一個結果行,如果沒有結果,則傳回 None。如果有多於一行的結果,其他行將被丟棄。
  • one(): 傳回第一個且唯一的結果行,如果沒有結果或有多於一個結果,則引發異常。
  • one_or_none(): 傳回第一個且唯一的結果行,如果沒有結果,則傳回 None。如果有多於一個結果,則引發異常。

使用 scalars() 方法

如果你知道每個結果行只包含一個值,那麼你可以使用 scalars() 方法來執行查詢:

>>> session.scalars(q).all()
[Product(1, "Acorn Atom"), Product(2, "BBC Micro"), ..., Product(149, "GEM 1000")]

使用 scalars(),SQLAlchemy 傳回一個不同的「結果」物件,它只迭代每個結果行的第一個值。如果查詢傳回每個結果行的多個值,那麼其他值將被丟棄。

結果遍歷

在應用程式中,通常會在 for 迴圈中遍歷結果:

>>> r = session.execute(q)
>>> for row in r:
...     print(row)
(Product(1, "Acorn Atom"),)
(Product(2, "BBC Micro"),)
...
(Product(149, "GEM 1000"),)

注意每個結果的結構。SQLAlchemy 將每個結果作為元組傳回,因為查詢有時會傳回每個行的多個值。

簡化查詢方法

為了方便起見,SQLAlchemy 提供了一些額外的查詢執行方法,它們結合了 scalars() 和其他方法。例如:

  • scalar(q) 等同於 scalars(q).first(),傳回第一個結果行的第一個值,如果沒有結果,則傳回 None
  • scalar_one(q) 等同於 scalars(q).one(),傳回唯一結果行的第一個值,如果沒有結果或有多於一個結果,則引發異常。
  • scalar_one_or_none(q) 等同於 scalars(q).one_or_none(),傳回唯一結果行的第一個值,如果沒有結果,則傳回 None,如果有多於一個結果,則引發異常。

查詢過濾與排序

在前面的例子中,我們已經看到了如何使用 select() 函式來檢索資料函式庫中的資料。然而,在許多情況下,我們需要對查詢結果進行過濾,以取得特定的資料子集。本文將介紹如何使用 SQLAlchemy 來實作查詢過濾。

使用 where() 方法進行過濾

SQLAlchemy 提供了 where() 方法來為查詢新增過濾條件。下面的例子展示瞭如何檢索由 Commodore 製造的產品:

>>> q = select(Product).where(Product.manufacturer == 'Commodore')
>>> session.scalars(q).all()
[Product(39, "PET"), Product(40, "VIC-20"), ..., Product(48, "Amiga")]

內容解密:

  • where() 方法用於新增查詢條件。
  • Product.manufacturer == 'Commodore' 定義了一個條件:只檢索製造商為 Commodore 的產品。

多重過濾條件

可以透過多次呼叫 where() 方法或在單次呼叫中傳入多個條件來應用多重過濾。下面的例子展示瞭如何檢索 Commodore 在 1980 年製造的產品:

>>> q = (select(Product)
...      .where(Product.manufacturer == 'Commodore')
...      .where(Product.year == 1980))

>>> q = select(Product).where(Product.manufacturer == 'Commodore', Product.year == 1980)

內容解密:

  • 多次呼叫 where() 方法與單次呼叫傳入多個條件的效果相同。
  • 多個條件之間預設使用邏輯 AND 運算子。

邏輯運算子

SQLAlchemy 提供了 or_()and_()not_() 函式來支援邏輯運算。下面的例子展示瞭如何檢索在 1970 年之前或 1990 年之後製造的產品:

>>> from sqlalchemy import or_
>>> q = select(Product).where(or_(Product.year < 1970, Product.year > 1990))

內容解密:

  • or_() 函式用於將多個條件以邏輯 OR 運算子結合。
  • 其他邏輯運算子如 and_()not_() 也可按需使用。

LIKE 運算子

LIKE 運算子可用於實作簡單的搜尋功能。下面的例子展示瞭如何檢索名稱中包含 “Sinclair” 的產品:

>>> q = select(Product).where(Product.name.like('%Sinclair%'))

內容解密:

  • like() 方法接受一個模式字串,支援 %_ 萬用字元。
  • % 用於匹配零個或多個字元,而 _ 用於匹配單個字元。

BETWEEN 運算子

可以使用 between() 方法來檢索某一範圍內的資料。下面的例子展示瞭如何檢索在 1970 年代製造的產品:

>>> q = select(Product).where(Product.year.between(1970, 1979))

內容解密:

  • between() 方法定義了一個範圍,用於篩選出落在該範圍內的資料。

查詢生成的 SQL 陳述式

可以透過列印查詢物件來檢視 SQLAlchemy 生成的 SQL 陳述式。下面的例子展示瞭如何檢視查詢的 SQL 陳述式,以及如何啟用 literal_binds 以顯示實際的引數值:

>>> print(q)
SELECT products.id, products.name, products.manufacturer, products.year,
products.country, products.cpu
FROM products
WHERE products.year BETWEEN :year_1 AND :year_2

>>> print(q.compile(compile_kwargs={'literal_binds': True}))
SELECT products.id, products.name, products.manufacturer, products.year,
products.country, products.cpu
FROM products
WHERE products.year BETWEEN 1970 AND 1979

內容解密:

  • SQLAlchemy 自動使用引數化查詢以防止 SQL 注入攻擊。
  • 可以透過 compile_kwargs={'literal_binds': True} 檢視實際的引數值,但這不應在生產環境中使用。

資料排序與檢索

在資料函式庫查詢中,結果的排序是非常重要的一環。SQLAlchemy 提供了 order_by() 方法來指定查詢結果的排序順序。以下範例展示如何使用此方法按產品名稱的字母順序檢索產品:

q = select(Product).order_by(Product.name) session.scalars(q).all() [Product(10, “464 Plus”), Product(11, “6128 Plus”), …, Product(127, “ZX Spectrum”)]

若要進行反向排序,可以在指定的欄位屬性上呼叫 desc() 方法。以下範例按產品的製造年份進行降序排序,將最新的產品排在前面:

q = select(Product).order_by(Product.year.desc()) session.scalars(q).all() [Product(6, “A7000”), Product(33, “Falcon”), …, Product(74, “Honeywell 316”)]

在許多情況下,單一的排序條件可能不足以滿足需求。例如,在上述範例中,同一年份內製造的電腦產品會以任意順序傳回。order_by() 方法可以接受多個引數,每個引數都會新增一層排序條件。以下範例對產品先按年份降序排序,然後按產品名稱升序排序:

q = select(Product).order_by(Product.year.desc(), Product.name.asc())

請注意,asc() 方法用於指定升序排序,雖然升序是預設的,但有時明確指出排序方向可以使查詢陳述式更清晰。

存取個別欄位

在前面的範例中,查詢結果都是整個資料列,對應到 ORM 的實體類別例項。雖然查詢 ORM 實體是一種常見的做法,但 select() 函式非常靈活,可以處理更細粒度的資料。

例如,應用程式可能只需要檢索某個特定的欄位。以下查詢只檢索產品的名稱:

q = select(Product.name) session.scalars(q).all() [‘Acorn Atom’, ‘BBC Micro’, …, ‘GEM 1000’]

select() 函式不限於檢索每個結果列中的單一值,也可以同時請求多個值。以下查詢取得每個產品的名稱和製造商:

q = select(Product.name, Product.manufacturer) session.execute(q).all() [(‘Acorn Atom’, ‘Acorn Computers Ltd’), (‘BBC Micro’, ‘Acorn Computers Ltd’), …]

注意,這個範例必須使用 session.execute() 來執行查詢,以便將每列結果中的值對傳回為元組。

聚合函式

select() 函式也可以與 SQL 函式一起使用,這些函式會在檢索資料時動態轉換資料。其中一個非常有用的函式是 count(),它會將所有結果列替換為結果的總數。以下範例計算資料函式庫中儲存的產品數量:

from sqlalchemy import func q = select(func.count(Product.id)) r = session.scalar(q) r 149

上述範例中使用的 count() 函式將結果列表簡化為單一值,因此使用 scalar() 方法來檢索結果。在這個例子中,使用 Product.id 作為 count() 的引數是任意的,給定 Product 類別的任何欄位屬性都會得到相同的結果,因為實際資料並不重要。

還有另一種形式的查詢,不需要選取隨機欄位來計算結果數量:

q = select(func.count()).select_from(Product) r = session.scalar(q) r 149

在第二種形式中,count() 函式沒有給定任何引數,以表示需要計算結果數量,而不指定要計數的資料。使用這種格式時,必須新增 select_from() 方法來組態查詢中使用的表格,因為 SQLAlchemy 無法從給定 select() 函式的引數中自動確定它。

另兩個有用的 SQL 函式是 min()max()。以下範例傳回資料函式庫中產品製造的最早和最晚年份:

q = select(func.min(Product.year), func.max(Product.year)) r = session.execute(q) r.first() (1969, 1995)

這個查詢必須使用 execute() 因為它檢索每列兩個值。min()max() 函式將結果列表簡化為單一行,因此沒有必要像前面的範例那樣使用 all() 來檢索結果。當預先知道只有一行結果時,使用 first()one() 方法更方便,其中後者在查詢傳回非單一結果列時會引發例外。

結果分組

目前形式的資料函式庫只有產品作為第一類別實體,但有時應用程式可能對檢索相關資料屬性(如製造商)感興趣。以下是嘗試從這個表格中取得電腦製造商列表:

q = select(Product.manufacturer).order_by(Product.manufacturer) session.scalars(q).all() [‘Acorn Computers Ltd’, ‘Acorn Computers Ltd’, …, ‘West Computer AS’]

但顯然,這個查詢有一個問題。即使查詢只檢索製造商,查詢的表格中包含的是產品,因此每個結果列對應一個產品,而擁有多個產品的製造商會被重複列出。

內容解密:

此章節主要講解了SQLAlchemy中如何進行資料排序、存取個別欄位、使用聚合函式以及結果分組。首先,我們瞭解到可以使用order_by()方法對查詢結果進行排序,並且可以透過呼叫desc()asc()方法來指定排序的方向。其次,我們學習瞭如何使用select()函式存取個別欄位,以及如何同時檢索多個欄位。此外,我們還瞭解瞭如何使用SQL函式,如count(), min(), 和 max(),來動態轉換資料。最後,我們討論了結果分組的問題,並指出在目前的資料函式庫設計中,直接從產品表檢索製造商列表會導致重複項。

# 以下是一個綜合範例,展示瞭如何使用SQLAlchemy進行資料排序、存取個別欄位和使用聚合函式
from sqlalchemy import create_engine, Column, Integer, String
from sqlalchemy.ext.declarative import declarative_base
from sqlalchemy.orm import sessionmaker

# 定義資料函式庫連線和模型
engine = create_engine('sqlite:///example.db')
Base = declarative_base()

class Product(Base):
    __tablename__ = 'products'
    id = Column(Integer, primary_key=True)
    name = Column(String)
    manufacturer = Column(String)
    year = Column(Integer)

# 建立表格和Session
Base.metadata.create_all(engine)
Session = sessionmaker(bind=engine)
session = Session()

# 新增一些示例資料
# ...

# 按名稱排序檢索產品
q = session.query(Product).order_by(Product.name)
products = q.all()
for product in products:
    print(product.name)

# 只檢索產品名稱
q = session.query(Product.name)
product_names = q.all()
for name in product_names:
    print(name)

# 使用count()函式計算產品數量
from sqlalchemy import func
q = session.query(func.count(Product.id))
count = q.scalar()
print(f"Total products: {count}")

# 使用min()和max()函式找出最早和最晚的製造年份
q = session.query(func.min(Product.year), func.max(Product.year))
result = q.first()
print(f"Earliest year: {result[0]}, Latest year: {result[1]}")

內容解密:

  1. 匯入必要的模組:首先,我們需要匯入SQLAlchemy中的必要模組,包括create_engineColumnIntegerStringdeclarative_basesessionmaker
  2. 定義資料函式庫模型:我們定義了一個名為Product的類別,它繼承自declarative_base()傳回的基礎類別,並定義了四個欄位:idnamemanufactureryear
  3. 建立資料函式庫連線和Session:我們建立了一個SQLite資料函式庫引擎,並建立了Product類別對應的表格。同時,我們建立了一個Session類別,用於與資料函式庫進行互動。
  4. 新增示例資料:雖然這裡沒有顯示具體的新增程式碼,但通常我們會透過建立Product例項並將其新增到Session中來新增資料。
  5. 按名稱排序檢索產品:我們使用session.query(Product).order_by(Product.name)來按名稱升序檢索所有產品,並遍歷結果列印出每個產品的名稱。
  6. 只檢索產品名稱:透過修改查詢只選擇Product.name,我們能夠僅檢索產品名稱。
  7. 計算產品數量:利用func.count(Product.id),我們能夠計算出資料函式庫中的產品總數。
  8. 找出最早和最晚的製造年份:透過func.min(Product.year)func.max(Product.year),我們能夠找出資料函式庫中記錄的最早和最晚的產品製造年份。