搜尋
首頁後端開發Python教學FastAPI、Pydantic、PsycopgPython Web API 的三位一體

第 1 部分:討論

進入FastAPI

首先,對標題持保留態度。

如果我今天從頭開始進行 Python Web API 開發,我可能會更仔細地關注 LiteStar,在我看來,它的架構更好,並且具有更好的專案治理結構。

但是我們有 FastAPI,而且它不會很快消失。我將它用於許多個人和專業項目,並且仍然喜歡它的簡單性。

有關 FastAPI 設計模式的指南,請查看此頁面。

FastAPI, Pydantic, Psycopgthe holy trinity for Python web APIs

檢索資料庫數據

儘管 FastAPI 在實際的「API」部分中非常出色,但對我來說一直存在一個不確定性:如何最好地存取資料庫,特別是如果我們還需要處理地理空間資料類型。

讓我們回顧一下我們的選項。

註1:我們在這裡只對異步庫感興趣,因為FastAPI是ASGI。

註 2:我只會討論連接到 PostgreSQL,儘管部分討論仍然與其他資料庫相關。

FastAPI, Pydantic, Psycopgthe holy trinity for Python web APIs

編碼簡單 |複雜設計:ORM

處理資料庫連線並將資料庫表中的資料解析為 Python 物件。

  • SQLAlchemy2:Python ORM 世界中最大的競爭者。就我個人而言,我真的不喜歡這種語法,但每個都有自己的語法。

  • TortoiseORM:我個人很喜歡這個受 Django 啟發的非同步 ORM;乾淨又好用。

  • 替代ORM:有很多,如peewee、PonyORM等

中間立場:查詢建構器

沒有資料庫連線。只需從基於 Python 的查詢輸出原始 SQL 並將其傳遞給資料庫驅動程式。

  • SQLAlchemy Core:核心 SQL 查詢產生器,沒有對應到物件部分。還有一個基於此構建的更高級別的 ORM,稱為資料庫,看起來非常不錯。不過,我確實想知道該專案的開發程度如何。

  • PyPika:我對這個不太了解。

簡單的設計:資料庫驅動程式

  • asyncpg:這是 Postgres 的黃金標準非同步資料庫驅動程序,是最早上市且效能最高的驅動程式之一。雖然所有其他驅動程式都使用 C 庫 libpq 與 Postgres 交互,但 MagicStack 選擇重寫自己的自訂實現,也偏離了 Python DBAPI 規範。如果性能是您的主要標準,那麼 asyncpg 可能是最好的選擇。

  • psycopg3:psycopg2 顯然是 Python/Postgres 的同步資料庫驅動程式世界之王。 psycopg3(更名為 psycopg)是該函式庫的下一個完全非同步迭代。近年來,這個庫確實發揮了自己的作用,我希望進一步討論它。請參閱作者關於 psycopg3 早期的有趣部落格。

請注意,這裡顯然圍繞 ORM、查詢建構器與原始 SQL 進行了更廣泛、更概念化的討論。我不會在這裡介紹這個。

重複模型

Pydantic 與 FastAPI 捆綁在一起,非常適合建模、驗證和序列化 API 回應。

如果我們決定使用 ORM 從資料庫中檢索數據,那麼保持兩組資料庫模型同步是不是有點低效? (一個用於 ORM,另一個用於 Pydantic)?

如果我們可以使用 Pydantic 來建模資料庫不是很好嗎?

這正是 FastAPI 的創建者試圖透過 SQLModel 函式庫解決的問題。

雖然這很可能是問題的一個很好的解決方案,但我有一些擔憂:

  • 這個計畫會像FastAPI一樣遭受單一維護者症候群嗎?

  • 這仍然是一個相當年輕的專案和概念,文件並不出色。

  • 它本質上與 Pydantic 和 SQLAlchemy 緊密相關,這意味著遷移將非常困難。

  • 對於更複雜的查詢,可能需要下拉到下面的 SQLAlchemy。

回到基礎

這麼多選擇!分析癱瘓。

FastAPI, Pydantic, Psycopgthe holy trinity for Python web APIs

當存在不確定性時,我會遵循以下原則:保持簡單。

SQL 發明於 50 年前,並且仍然是任何開發人員需要學習的關鍵技能。對於大多數用例來說,它的語法始終易於掌握且編寫起來並不復雜(對於鐵桿 ORM 用戶來說,嘗試一下,您可能會感到驚訝)。

天啊,現在我們甚至可以使用開源 LLM 來產生(大部分有效)SQL 查詢並節省您的打字時間。

雖然 ORM 和查詢建構器可能會來來去去,但資料庫驅動程式可能更一致。最初的 psycopg2 函式庫是近 20 年前編寫的,並且仍在全球生產中積極使用。

將 Psycopg 與 Pydantic 模型結合使用

正如所討論的,雖然 psycopg 的性能可能不如 asyncpg(儘管這種理論性能對現實世界的影響是有爭議的),但 psycopg 專注於易用性和熟悉的 API。

對我來說殺手級功能是行工廠。

此功能允許您將返回的資料庫資料映射到任何 Python 對象,包括標準 lib 資料類、來自偉大 attrs 庫的模型,當然還有 Pydantic 模型!

對我來說,這是最好的折衷方法:原始 SQL 的終極靈活性,以及​​ Pydantic 的驗證/類型安全功能來對資料庫進行建模。 Psycopg 也處理諸如變數輸入衛生之類的事情,以避免 SQL 注入。

應該指出的是,asyncpg 還可以處理到 Pydantic 模型的映射,但更多的是作為一種解決方法,而不是內建功能。有關詳細信息,請參閱此問題線程。我也不知道這種方法是否能與其他建模庫很好地配合。

正如我上面提到的,我通常使用地理空間資料:一個經常被 ORM 和查詢建構器忽略的領域。放棄原始 SQL 使我能夠解析和解解析地理空間數據,因為我需要純 Python 中更可接受的類型。請參閱我關於此主題的相關文章。

第 2 部分:用法範例

建立資料庫表

這裡我們使用原始 SQL 建立一個名為 user 的簡單資料庫表。

我也會考慮只使用 SQL 處理資料庫建立和遷移,但這是另一篇文章的主題。

init_db.sql

CREATE TYPE public.userrole AS ENUM (
    'READ_ONLY',
    'STANDARD',
    'ADMIN'
);

CREATE TABLE public.users (
    id integer NOT NULL,
    username character varying,
    role public.userrole NOT NULL DEFAULT 'STANDARD',
    profile_img character varying,
    email_address character varying,
    is_email_verified boolean DEFAULT false,
    registered_at timestamp with time zone DEFAULT now()
);

使用 Pydantic 為您的資料庫建模

這裡我們建立一個名為 DbUser 的模型:

db_models.py

from typing import Optional
from enum import Enum
from datetime import datetime
from pydantic import BaseModel
from pydantic.functional_validators import field_validator
from geojson_pydantic import Feature

class UserRole(str, Enum):
    """Types of user, mapped to database enum userrole."""

    READ_ONLY = "READ_ONLY"
    STANDARD = "STANDARD"
    ADMIN = "ADMIN"

class DbUser(BaseModel):
    """Table users."""

    id: int
    username: str
    role: Optional[UserRole] = UserRole.STANDARD
    profile_img: Optional[str] = None
    email_address: Optional[str] = None
    is_email_verified: bool = False
    registered_at: Optional[datetime]
    # This is a geospatial type I will handle in the SQL
    favourite_place: Optional[dict]

    # DB computed fields (handled in the SQL)
    total_users: Optional[int] = None

    # This example isn't very realistic, but you get the idea
    @field_validator("is_email_verified", mode="before")
    @classmethod
    def i_want_my_ints_as_bools(cls, value: int) -> bool:
        """Example of a validator to convert data type."""
        return bool(value)

這裡我們得到了 Pydantic 的類型安全性和驗證。

當從資料庫中提取資料時,我們可以向該模型添加任何形式的驗證或資料轉換。

使用 FastAPI 設定 Psycopg

我們使用 psycopg_pool 建立一個池化資料庫連接:

db.py

from fastapi import Request
from psycopg import Connection
from psycopg_pool import AsyncConnectionPool

# You should be using environment variables in a settings file here
from app.config import settings


def get_db_connection_pool() -> AsyncConnectionPool:
    """Get the connection pool for psycopg.

    NOTE the pool connection is opened in the FastAPI server startup (lifespan).

    Also note this is also a sync `def`, as it only returns a context manager.
    """
    return AsyncConnectionPool(
        conninfo=settings.DB_URL.unicode_string(), open=False
    )


async def db_conn(request: Request) -> Connection:
    """Get a connection from the psycopg pool.

    Info on connections vs cursors:
    https://www.psycopg.org/psycopg3/docs/advanced/async.html

    Here we are getting a connection from the pool, which will be returned
    after the session ends / endpoint finishes processing.

    In summary:
    - Connection is created on endpoint call.
    - Cursors are used to execute commands throughout endpoint.
      Note it is possible to create multiple cursors from the connection,
      but all will be executed in the same db 'transaction'.
    - Connection is closed on endpoint finish.
    """
    async with request.app.state.db_pool.connection() as conn:
        yield conn

接下來我們在FastAPI生命週期事件中開啟連線池:

main.py

from contextlib import asynccontextmanager
from fastapi import FastAPI

from .db import get_db_connection_pool

@asynccontextmanager
async def lifespan(app: FastAPI):
    """FastAPI startup/shutdown event."""
    # For this demo I use print, but please use logging!
    print("Starting up FastAPI server.")

    # Create a pooled db connection and make available in app state
    # NOTE we can access 'request.app.state.db_pool' in endpoints
    app.state.db_pool = get_db_connection_pool()
    await app.state.db_pool.open()

    yield

    # Shutdown events
    print("Shutting down FastAPI server.")
    # Here we make sure to close the connection pool
    await app.state.db_pool.close()

現在,當您的 FastAPI 應用程式啟動時,您應該有一個開啟的連線池,準備從內部端點取得連線。

Pydantic 模型的輔助方法

為 Pydantic 模型添加一些方法以實現常用功能會很有用:取得一個使用者、所有使用者、建立使用者、更新使用者、刪除使用者。

但首先我們應該建立一些 Pydantic 模型,用於輸入驗證(建立新使用者)和輸出序列化(透過 API 的 JSON 回應)。

user_schemas.py

from typing import Annotated
from pydantic import BaseModel, Field
from pydantic.functional_validators import field_validator
from geojson_pydantic import FeatureCollection, Feature, MultiPolygon, Polygon
from .db_models import DbUser

class UserIn(DbUser):
    """User details for insert into DB."""

    # Exclude fields not required for input
    id: Annotated[int, Field(exclude=True)] = None
    favourite_place: Optional[Feature]

    @field_validator("favourite_place", mode="before")
    @classmethod
    def parse_input_geojson(
        cls,
        value: FeatureCollection | Feature | MultiPolygon | Polygon,
    ) -> Optional[Polygon]:
        """Parse any format geojson into a single Polygon."""
        if value is None:
            return None
        # NOTE I don't include this helper function for brevity
        featcol = normalise_to_single_geom_featcol(value)
        return featcol.get("features")[0].get("geometry")

class UserOut(DbUser):
    """User details for insert into DB."""

    # Ensure it's parsed as a Polygon geojson from db object
    favourite_place: Polygon

    # More logic to append computed values

然後我們可以定義我們的輔助方法:one、all、create:

db_models.py

...previous imports
from typing import Self, Optional
from fastapi.exceptions import HTTPException
from psycopg import Connection
from psycopg.rows import class_row

from .user_schemas import UserIn

class DbUser(BaseModel):
    """Table users."""

    ...the fields

    @classmethod
    async def one(cls, db: Connection, user_id: int) -> Self:
        """Get a user by ID.

        NOTE how the favourite_place field is converted in the db to geojson.
        """
        async with db.cursor(row_factory=class_row(cls)) as cur:
            sql = """
                SELECT
                    u.*,
                    ST_AsGeoJSON(favourite_place)::jsonb AS favourite_place,
                    (SELECT COUNT(*) FROM users) AS total_users
                FROM users u
                WHERE
                    u.id = %(user_id)s
                GROUP BY u.id;
            """

            await cur.execute(
                sql,
                {"user_id": user_id},
            )

            db_project = await cur.fetchone()
            if not db_project:
                raise KeyError(f"User ({user_identifier}) not found.")

            return db_project

    @classmethod
    async def all(
        cls, db: Connection, skip: int = 0, limit: int = 100
    ) -> Optional[list[Self]]:
        """Fetch all users."""
        async with db.cursor(row_factory=class_row(cls)) as cur:
            await cur.execute(
                """
                SELECT
                    *,
                    ST_AsGeoJSON(favourite_place)::jsonb
                FROM users
                OFFSET %(offset)s
                LIMIT %(limit)s;
                """,
                {"offset": skip, "limit": limit},
            )
            return await cur.fetchall()

    @classmethod
    async def create(
        cls,
        db: Connection,
        user_in: UserIn,
    ) -> Optional[Self]:
        """Create a new user."""

        # Omit defaults and empty values from the model
        model_dump = user_in.model_dump(exclude_none=True, exclude_default=True)
        columns = ", ".join(model_dump.keys())
        value_placeholders = ", ".join(f"%({key})s" for key in model_dump.keys())

        sql = f"""
            INSERT INTO users
                ({columns})
            VALUES
                ({value_placeholders})
            RETURNING *;
        """


        async with db.cursor(row_factory=class_row(cls)) as cur:
            await cur.execute(sql, model_dump)
            new_user = await cur.fetchone()

            if new_user is None:
                msg = f"Unknown SQL error for data: {model_dump}"
                print(f"Failed user creation: {model_dump}")
                raise HTTPException(status_code=500, detail=msg)

        return new_user

用法

routes.py

from typing import Annotated
from fastapi import Depends, HTTPException
from psycopg import Connection

from .main import app
from .db import db_conn
from .models import DbUser
from .user_schemas import UserIn, UserOut

@app.post("/", response_model=UserOut)
async def create_user(
    user_info: UserIn,
    db: Annotated[Connection, Depends(db_conn)],
):
    """Create a new user.

    Here the input is parsed and validated by UserIn
    then the output is parsed and validated by UserOut
    returning the user json data.
    """

    new_user = await DbUser.create(db, user_info)
    if not new_user:
        raise HTTPException(
            status_code=422,
            detail="User creation failed.",
        )

    return new_user

    # NOTE within an endpoint we can also use
    # DbUser.one(db, user_id) and DbUser.all(db)

這是我在我維護的專案 FMTM 中開始使用的方法,FMTM 是為世界各地社區收集現場資料的工具。

在此處查看完整的程式碼庫。
如果您覺得這有用的話 ⭐!

目前就這些!我希望這對那裡的人有幫助?

以上是FastAPI、Pydantic、PsycopgPython Web API 的三位一體的詳細內容。更多資訊請關注PHP中文網其他相關文章!

陳述
本文內容由網友自願投稿,版權歸原作者所有。本站不承擔相應的法律責任。如發現涉嫌抄襲或侵權的內容,請聯絡admin@php.cn
如何解決Linux終端中查看Python版本時遇到的權限問題?如何解決Linux終端中查看Python版本時遇到的權限問題?Apr 01, 2025 pm 05:09 PM

Linux終端中查看Python版本時遇到權限問題的解決方法當你在Linux終端中嘗試查看Python的版本時,輸入python...

我如何使用美麗的湯來解析HTML?我如何使用美麗的湯來解析HTML?Mar 10, 2025 pm 06:54 PM

本文解釋瞭如何使用美麗的湯庫來解析html。 它詳細介紹了常見方法,例如find(),find_all(),select()和get_text(),以用於數據提取,處理不同的HTML結構和錯誤以及替代方案(SEL)

如何使用TensorFlow或Pytorch進行深度學習?如何使用TensorFlow或Pytorch進行深度學習?Mar 10, 2025 pm 06:52 PM

本文比較了Tensorflow和Pytorch的深度學習。 它詳細介紹了所涉及的步驟:數據準備,模型構建,培訓,評估和部署。 框架之間的關鍵差異,特別是關於計算刻度的

Python中的數學模塊:統計Python中的數學模塊:統計Mar 09, 2025 am 11:40 AM

Python的statistics模塊提供強大的數據統計分析功能,幫助我們快速理解數據整體特徵,例如生物統計學和商業分析等領域。無需逐個查看數據點,只需查看均值或方差等統計量,即可發現原始數據中可能被忽略的趨勢和特徵,並更輕鬆、有效地比較大型數據集。 本教程將介紹如何計算平均值和衡量數據集的離散程度。除非另有說明,本模塊中的所有函數都支持使用mean()函數計算平均值,而非簡單的求和平均。 也可使用浮點數。 import random import statistics from fracti

哪些流行的Python庫及其用途?哪些流行的Python庫及其用途?Mar 21, 2025 pm 06:46 PM

本文討論了諸如Numpy,Pandas,Matplotlib,Scikit-Learn,Tensorflow,Tensorflow,Django,Blask和請求等流行的Python庫,並詳細介紹了它們在科學計算,數據分析,可視化,機器學習,網絡開發和H中的用途

如何使用Python創建命令行接口(CLI)?如何使用Python創建命令行接口(CLI)?Mar 10, 2025 pm 06:48 PM

本文指導Python開發人員構建命令行界面(CLIS)。 它使用Typer,Click和ArgParse等庫詳細介紹,強調輸入/輸出處理,並促進用戶友好的設計模式,以提高CLI可用性。

在Python中如何高效地將一個DataFrame的整列複製到另一個結構不同的DataFrame中?在Python中如何高效地將一個DataFrame的整列複製到另一個結構不同的DataFrame中?Apr 01, 2025 pm 11:15 PM

在使用Python的pandas庫時,如何在兩個結構不同的DataFrame之間進行整列複製是一個常見的問題。假設我們有兩個Dat...

解釋Python中虛擬環境的目的。解釋Python中虛擬環境的目的。Mar 19, 2025 pm 02:27 PM

文章討論了虛擬環境在Python中的作用,重點是管理項目依賴性並避免衝突。它詳細介紹了他們在改善項目管理和減少依賴問題方面的創建,激活和利益。

See all articles

熱AI工具

Undresser.AI Undress

Undresser.AI Undress

人工智慧驅動的應用程序,用於創建逼真的裸體照片

AI Clothes Remover

AI Clothes Remover

用於從照片中去除衣服的線上人工智慧工具。

Undress AI Tool

Undress AI Tool

免費脫衣圖片

Clothoff.io

Clothoff.io

AI脫衣器

AI Hentai Generator

AI Hentai Generator

免費產生 AI 無盡。

熱門文章

R.E.P.O.能量晶體解釋及其做什麼(黃色晶體)
3 週前By尊渡假赌尊渡假赌尊渡假赌
R.E.P.O.最佳圖形設置
3 週前By尊渡假赌尊渡假赌尊渡假赌
R.E.P.O.如果您聽不到任何人,如何修復音頻
3 週前By尊渡假赌尊渡假赌尊渡假赌

熱工具

記事本++7.3.1

記事本++7.3.1

好用且免費的程式碼編輯器

PhpStorm Mac 版本

PhpStorm Mac 版本

最新(2018.2.1 )專業的PHP整合開發工具

SublimeText3 Mac版

SublimeText3 Mac版

神級程式碼編輯軟體(SublimeText3)

EditPlus 中文破解版

EditPlus 中文破解版

體積小,語法高亮,不支援程式碼提示功能

mPDF

mPDF

mPDF是一個PHP庫,可以從UTF-8編碼的HTML產生PDF檔案。原作者Ian Back編寫mPDF以從他的網站上「即時」輸出PDF文件,並處理不同的語言。與原始腳本如HTML2FPDF相比,它的速度較慢,並且在使用Unicode字體時產生的檔案較大,但支援CSS樣式等,並進行了大量增強。支援幾乎所有語言,包括RTL(阿拉伯語和希伯來語)和CJK(中日韓)。支援嵌套的區塊級元素(如P、DIV),