一次使用 SQLAlchemy 實現分類以及計數的業務過程

在編寫業務邏輯代碼的時候, 我不幸遇到下面的表結構(已經將主要邏輯抽離出來了):

class Category(Model):
    __tablename__ = 'category'
    # 分類ID
    id = Column(Integer, primary_key=True, autoincrement=True)
    # 分類名稱
    name = Column(String(length=255))
    
class Product(Model):
    __tablename__ = 'product'
    # 產品 ID
    id = Column(Integer, primary_key=True, autoincrement=True)
    # 產品名稱
    name = Column(String(length=255))
    # 分類 ID
    category_id = Column(Integer)

現在需要實現的業務是返回分類的列表結果:

[
    {
        "id": 1,
        "name": "分類1",
        "product_count": 1
    },
    ...
]

這是一個一對多的模型.
一般的笨拙思路就是:

data = []
categorys = Category.query.all()
for category in categorys:
    product_count = len(Product.query.filter(Product.category_id == category.id).all())
    data.append({
        'id': category.id,
        'name': category.name,
        'product_count': product_count
    })

明眼人一看就知道可以把len(Product.query.filter(Product.category_id == category.id).all())換成:

product_count = Product.query.filter(Product.category_id == category.id).count()

但是, 根據這篇文章:[Why is SQLAlchemy count() much slower than the raw query?
](https://stackoverflow.com/que... 似乎這樣寫會有更好的性能:

from sqlalchemy import func
session.query(func.count(Product.id)).filter(Product.category_id == category.id).scalar()

但是, 稍微有點經驗的人就會對上面的寫法嗤之以鼻, 因爲product_count是放在for category in categorys:裏面的, 這意味着如果categorys有成千上萬個, 就要發出成千上萬個session.query(), 而數據庫請求是在網絡上的消耗, 請求時間相對較長, 有的數據庫沒有處理好連接池, 建立連接和斷開連接又是一筆巨大的開銷, 所以 query 的請求應該越少越好. 像上面這樣把 query 放到 for 循環中顯然是不明智的選擇.
於是有了下面一個請求的版本:

result = db.session.query(Product, Category) \
    .filter(Product.category_id == Category.id)\
    .order_by(Category.id).all()
id_list = []
data = []
for product, category in result:
    if category and product:
        if category.id not in id_list:
            id_list.append(category.id)
            data.append({
                'id': category.id,
                'name': category.name,
                'product_count': 0
            })
        idx = id_list.index(category.id)
        data[idx]['product_count'] += 1  

這樣的寫法十分難看, 而且同樣沒有合理利用 SQLAlchemy 的 count 函數. 於是改成:

product_count = func.count(Product.id).label('count')
results = session.query(Category, product_count) \
    .join(Product, Product.category_id == Category.id) \
    .group_by(Category).all()
data = [
    {
        'id': category.id,
        'name': category.name,
        'product_count': porduct_count
    } for category, product_count in results]

不過這裏還有一個問題, 就是如果先添加一個Category, 而屬於這個Category下沒有Product, 那麼這個Category就不會出現在data裏面, 所以join必須改成outerjoin. 即:

results = session.query(Category, product_count) \
    .outerjoin(Product, Product.category_id == Category.id) \
    .group_by(Category).all()

需求又來了!!!
現在考慮設計Product爲僞刪除模式, 即添加一個is_deleted屬性判斷Product是否被刪除.
那麼count函數就不能簡單地count(Product.id), 而是要同時判斷Product.is_deleted是否爲真和Product是否爲None, 經過悉心研究, 發現使用func.nullif可以實現這個需求,即用下面的寫法:

product_count = func.count(func.nullif(Product.is_deleted.is_(False), False)).label('count')
results = session.query(Category, product_count) \
    .join(Product, Product.category_id == Category.id) \
    .group_by(Category).all()
data = [
    {
        'id': category.id,
        'name': category.name,
        'product_count': porduct_count
    } for category, product_count in results]

可見使用 ORM 有的時候還是需要考慮很多東西.

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章