Khi viết API Flask, gần như endpoint nào cũng cần lấy một bản ghi theo ID: xem chi tiết user, sửa bài viết, xóa đơn hàng. Flask-SQLAlchemy 3.x có sẵn ba hàm get_or_404(), first_or_404() và one_or_404() — lấy được thì trả object, không có thì tự abort 404, khỏi viết if ... is None: abort(404) lặp đi lặp lại. Ba hàm này dùng được ngay khi bạn đã cài Flask-SQLAlchemy 3.0 trở lên.

Vấn đề

Cách quen thuộc nhất là dùng db.session.get() rồi tự kiểm tra None:

from flask import abort
@app.get("/user/<int:user_id>")
def get_user(user_id):
user = db.session.get(User, user_id)
if user is None:
abort(404)
return {"id": user.id, "name": user.name}

Đoạn kiểm tra chỉ có 2 dòng, nhưng nó lặp lại ở mọi endpoint detail, update, delete. Chỉ cần quên kiểm tra một chỗ, code sẽ crash với AttributeError và client nhận về lỗi 500 thay vì 404 đúng nghĩa. Với truy vấn theo điều kiện còn dài dòng hơn: phải execute(select(...)), .scalars().first(), rồi lại kiểm tra None rồi mới abort(404).

Giải pháp

Lấy theo khóa chính thì dùng db.get_or_404() — một dòng thay cho cả khối kiểm tra:

@app.get("/user/<int:user_id>")
def get_user(user_id):
user = db.get_or_404(User, user_id)
return {"id": user.id, "name": user.name}

Hàm này gọi session.get() rồi tự abort(404) khi kết quả là None. Lưu ý: hàm nằm trên đối tượng db, không phải trên db.session.

Lấy theo điều kiện thì có hai lựa chọn, đều nhận vào một câu select():

from sqlalchemy import select
# Lay ban ghi dau tien khop dieu kien, khong co thi 404
user = db.first_or_404(select(User).where(User.name == "mai"))
# Yeu cau dung mot ket qua duy nhat, khong co thi 404
user = db.one_or_404(select(User).where(User.email == "mai@example.com"))

Chọn first_or_404() khi điều kiện có thể khớp nhiều bản ghi và bạn chỉ cần bản đầu tiên. Chọn one_or_404() khi điều kiện phải duy nhất, ví dụ cột email có unique — nếu có hơn một kết quả, nó báo lỗi 500 thay vì im lặng trả bản đầu tiên.

Lưu ý khi nâng cấp: Flask-SQLAlchemy 3.0 đã bỏ hẳn Model.query, nên cách cũ User.query.get_or_404(id) không còn chạy nữa. Hãy chuyển sang db.get_or_404() cùng lúc với việc chuyển truy vấn sang kiểu db.select().

Áp dụng vào dự án của bạn

Rà soát các endpoint detail, update và delete trong app của bạn và thay dần các khối if ... is None: abort(404) bằng ba hàm trên. Mỗi chỗ thay chỉ mất một phút nhưng code ngắn lại, đọc xuôi hơn và không còn nguy cơ quên kiểm tra None. Nếu API của bạn trả JSON, hãy kết hợp với errorhandler(404) để trang lỗi 404 mặc định của Flask cũng trả về JSON thống nhất thay vì trang HTML.

Bài viết liên quan

Câu hỏi thường gặp

get_or_404() khác gì db.session.get()?

db.session.get() trả về None khi không tìm thấy bản ghi, còn db.get_or_404() tự gọi abort(404) trong trường hợp đó. Hai hàm giống nhau về cách lấy dữ liệu, chỉ khác ở cách xử lý khi không có kết quả.

Có dùng được với truy vấn select() phức tạp không?

Được, nhưng phải dùng first_or_404() hoặc one_or_404() vì chúng nhận vào một câu select(). Bạn viết điều kiện phức tạp thế nào cũng được, miễn là kết quả cuối cùng là một select() hợp lệ.

Muốn trả JSON thay vì trang 404 mặc định thì làm sao?

Đăng ký errorhandler(404) trên app để bắt mọi lỗi 404 và trả JSON theo chuẩn của API. Ba hàm *_or_404 chỉ lo việc abort, còn hình thức phản hồi 404 là do error handler quyết định.

one_or_404() và first_or_404() nên chọn cái nào?

Điều kiện trên cột unique như email thì dùng one_or_404() để bắt lỗi dữ liệu trùng. Điều kiện có thể khớp nhiều bản ghi, ví dụ lọc theo trạng thái, thì dùng first_or_404() để lấy bản đầu tiên.

Flask-SQLAlchemy 2.x dùng được ba hàm này không?

Không. Ba hàm này chỉ có từ Flask-SQLAlchemy 3.0 trở lên. Nếu dự án còn ở 2.x, hãy nâng cấp lên 3.x — hoặc tự viết một helper nhỏ gọi abort(404) khi kết quả là None.

💬 Bạn đã thử cách này trong dự án của mình chưa? Nếu gặp lỗi hoặc có cách làm gọn hơn, để lại bình luận bên dưới — mình đọc hết và sẽ bổ sung những ý hay vào bài.

Nguồn tham khảo