最新国产好看的视频,伊人天堂AV在线,国产Aaaaaa视频,蜜臀视频在线观看一区,人妻av色图,密臀久久久精品影片,青青视频免费观看毛片,久草在线观看视,国产三级精品色情在线

Python?FastAPI連接操作MySQL數(shù)據(jù)庫指南

 更新時(shí)間:2026年07月09日 09:27:00   作者:copyer_xyf  
寫?FastAPI?后端時(shí),數(shù)據(jù)庫連接不是只寫一行連接串就結(jié)束了,本文將手把手教你用SQLAlchemy+PyMySQL搭建穩(wěn)定MySQL架構(gòu),可以輕松實(shí)現(xiàn)事務(wù)控制、錯(cuò)誤回滾和模塊化開發(fā),從此告別混亂的代碼結(jié)構(gòu)

寫 FastAPI 后端時(shí),數(shù)據(jù)庫連接不是只寫一行連接串就結(jié)束了。

一個(gè)能長期維護(hù)的 MySQL 接入方式,通常要同時(shí)解決這些問題:

  • Python 用什么驅(qū)動(dòng)真正連上 MySQL
  • ORM 用什么方式把 Python 類映射成數(shù)據(jù)庫表
  • 每個(gè) HTTP 請(qǐng)求如何拿到獨(dú)立的數(shù)據(jù)庫會(huì)話
  • 寫操作如何提交事務(wù),失敗時(shí)如何回滾
  • 項(xiàng)目變大后,數(shù)據(jù)庫連接、模型、路由、業(yè)務(wù)邏輯分別放在哪里

先看完整流程。

這張圖可以拆成兩條主線:

應(yīng)用啟動(dòng)

  -> 讀取 DATABASE_URL
  -> 創(chuàng)建 Engine
  -> 創(chuàng)建 SessionLocal
  -> 可選:創(chuàng)建表、寫入初始化數(shù)據(jù)

接口請(qǐng)求

  -> FastAPI Router 接收請(qǐng)求
  -> Depends 注入數(shù)據(jù)庫 Session
  -> Service 處理業(yè)務(wù)規(guī)則
  -> Repository / ORM 執(zhí)行數(shù)據(jù)庫讀寫
  -> commit 或 rollback
  -> 請(qǐng)求結(jié)束后關(guān)閉 Session

FastAPI 不直接負(fù)責(zé)連接 MySQL。它負(fù)責(zé) HTTP、參數(shù)校驗(yàn)、依賴注入和響應(yīng)返回。

真正跟 MySQL 打交道的是:

FastAPI
  -> SQLAlchemy
  -> PyMySQL
  -> MySQL

一、先把它們之間的配合講清楚

如果只記一個(gè)關(guān)系,就記這條鏈路:

用戶發(fā)起 HTTP 請(qǐng)求
  -> FastAPI 找到對(duì)應(yīng)路由函數(shù)
  -> Depends 調(diào)用 get_db 創(chuàng)建一個(gè) Session
  -> 路由函數(shù)把 Session 交給 Service
  -> Service 處理業(yè)務(wù)規(guī)則和事務(wù)
  -> Repository 使用 Session 寫 SQLAlchemy 查詢
  -> SQLAlchemy 把 ORM 查詢翻譯成 SQL
  -> PyMySQL 把 SQL 發(fā)給 MySQL
  -> MySQL 執(zhí)行 SQL 并返回結(jié)果
  -> SQLAlchemy 把結(jié)果轉(zhuǎn)換成 Python 對(duì)象
  -> FastAPI 把響應(yīng)對(duì)象轉(zhuǎn)換成 JSON 返回給前端

這套關(guān)系里,每一層都有明確分工。

FastAPI
  -> 管 HTTP,不管數(shù)據(jù)庫底層連接

Depends / get_db
  -> 管每個(gè)請(qǐng)求的數(shù)據(jù)庫 Session 生命周期

SQLAlchemy Engine
  -> 管數(shù)據(jù)庫入口和連接池

SQLAlchemy Session
  -> 管一次業(yè)務(wù)操作里的查詢、寫入、提交、回滾

SQLAlchemy ORM Model
  -> 管 Python 類和數(shù)據(jù)庫表之間的映射

PyMySQL
  -> 管 Python 進(jìn)程和 MySQL 服務(wù)之間的底層通信

MySQL
  -> 真正存儲(chǔ)和查詢數(shù)據(jù)

二、需要哪些第三方依賴

一個(gè)同步版 FastAPI + MySQL 項(xiàng)目,最少需要這些依賴:

uv add "fastapi[standard]" sqlalchemy pymysql pydantic-settings

如果使用 pip

pip install "fastapi[standard]" sqlalchemy pymysql pydantic-settings

這些包分別負(fù)責(zé)不同層級(jí)的事情。

1. FastAPI

FastAPI 是 Web 框架,負(fù)責(zé):

  • 定義 HTTP 接口
  • 解析路徑參數(shù)、查詢參數(shù)、請(qǐng)求體
  • 使用 Pydantic 做數(shù)據(jù)校驗(yàn)
  • 通過 Depends 注入依賴
  • 自動(dòng)生成 OpenAPI 文檔

這里有兩個(gè)詞先解釋清楚。

路由 指的是“某個(gè) HTTP 請(qǐng)求應(yīng)該交給哪個(gè)函數(shù)處理”。例如 GET /users/1 交給 get_user 函數(shù)處理。

依賴注入 指的是“路由函數(shù)需要什么對(duì)象,由 FastAPI 在調(diào)用函數(shù)前幫你準(zhǔn)備好”。數(shù)據(jù)庫 Session 就很適合用依賴注入,因?yàn)槊總€(gè)請(qǐng)求都需要一個(gè)獨(dú)立 Session,請(qǐng)求結(jié)束后還必須關(guān)閉。

數(shù)據(jù)庫連接進(jìn)入 FastAPI 的方式,通常不是在路由函數(shù)里手動(dòng)創(chuàng)建連接,而是通過依賴注入:

from typing import Annotated

from fastapi import Depends
from sqlalchemy.orm import Session


# SessionDep 是一個(gè)類型別名:
# 它告訴 FastAPI,只要路由參數(shù)標(biāo)注為 SessionDep,
# 就先執(zhí)行 get_db(),把得到的數(shù)據(jù)庫 Session 傳進(jìn)來。
SessionDep = Annotated[Session, Depends(get_db)]

這樣路由只聲明“我需要一個(gè)數(shù)據(jù)庫 Session”,至于這個(gè) Session 怎么創(chuàng)建、怎么關(guān)閉,交給統(tǒng)一的依賴函數(shù)處理。

2. SQLAlchemy

SQLAlchemy 是數(shù)據(jù)庫工具和 ORM。

它負(fù)責(zé):

  • 創(chuàng)建數(shù)據(jù)庫連接入口 Engine
  • 管理連接池
  • 創(chuàng)建請(qǐng)求級(jí) Session
  • 定義 ORM 模型
  • 生成 SQL
  • 執(zhí)行增刪改查
  • 管理事務(wù)提交和回滾

ORMObject Relational Mapping 的縮寫,中文常叫“對(duì)象關(guān)系映射”。

它解決的是這件事:

  • 在 Python 代碼里面操作對(duì)象
  • 在數(shù)據(jù)庫里面是操作表和行
  • ORM 負(fù)責(zé)在兩者之間做轉(zhuǎn)換

例如 Python 里有一個(gè) User 類,數(shù)據(jù)庫里有一張 users 表。ORM 會(huì)把 User.username 對(duì)應(yīng)到 users.username 字段。這樣你可以寫 Python 對(duì)象和查詢表達(dá)式,而不是在業(yè)務(wù)里到處拼 SQL 字符串。

使用 SQLAlchemy 后,業(yè)務(wù)代碼通常不直接拼 SQL 字符串,而是操作 Python 類:

# select(User) 表示查詢 users 表對(duì)應(yīng)的 User 模型
# where(...) 表示追加查詢條件
# scalar(...) 表示只取一條 ORM 對(duì)象結(jié)果
user = db.scalar(select(User).where(User.username == username))

這行代碼最后會(huì)被 SQLAlchemy 轉(zhuǎn)成 SQL,交給底層數(shù)據(jù)庫驅(qū)動(dòng)執(zhí)行。

3. PyMySQL

PyMySQL 是 MySQL 的 Python 驅(qū)動(dòng)。

它是真正負(fù)責(zé)和 MySQL 服務(wù)通信的底層庫。SQLAlchemy 自己不是 MySQL 驅(qū)動(dòng),它需要通過 DBAPI 驅(qū)動(dòng)連接具體數(shù)據(jù)庫。

驅(qū)動(dòng) 可以理解成數(shù)據(jù)庫的“適配器”。不同數(shù)據(jù)庫說話方式不一樣,MySQL、PostgreSQL、SQLite 都有自己的協(xié)議和細(xì)節(jié)。SQLAlchemy 負(fù)責(zé)生成 SQL 和管理 ORM,但真正打開網(wǎng)絡(luò)連接、登錄 MySQL、發(fā)送 SQL、讀取結(jié)果,需要驅(qū)動(dòng)來做。

DBAPI 是 Python 里數(shù)據(jù)庫驅(qū)動(dòng)遵循的一套接口規(guī)范。PyMySQL 實(shí)現(xiàn)了這套規(guī)范,所以 SQLAlchemy 可以通過它連接 MySQL。

連接 MySQL 時(shí),SQLAlchemy URL 一般寫成:

mysql+pymysql://用戶名:密碼@主機(jī):端口/數(shù)據(jù)庫名?charset=utf8mb4

這里的 pymysql 就是在告訴 SQLAlchemy:

  • 數(shù)據(jù)庫類型是 MySQL
  • 底層驅(qū)動(dòng)使用 PyMySQL

如果 MySQL 賬號(hào)使用了需要 RSA 的認(rèn)證方式,可以安裝:

uv add "pymysql[rsa]"

或者:

pip install "PyMySQL[rsa]"

普通本地開發(fā)大多數(shù)時(shí)候只需要 pymysql。

4. pydantic-settings

pydantic-settings 用來讀取環(huán)境變量和 .env 配置。

數(shù)據(jù)庫連接串、是否打印 SQL、是否自動(dòng)建表,這些都不應(yīng)該寫死在代碼里,而應(yīng)該放到環(huán)境變量中。

典型 .env

DATABASE_URL="mysql+pymysql://root:password@127.0.0.1:3306/todo_api?charset=utf8mb4"
DATABASE_ECHO=false
DATABASE_AUTO_CREATE=false

讀取配置:

from pydantic_settings import BaseSettings, SettingsConfigDict


class Settings(BaseSettings):
    # 告訴 pydantic-settings 從 .env 文件讀取配置
    # env_file_encoding 用來避免中文或特殊字符出現(xiàn)編碼問題
    model_config = SettingsConfigDict(
        env_file=".env",
        env_file_encoding="utf-8",
    )

    # 必填配置:沒有 DATABASE_URL 時(shí),應(yīng)用應(yīng)該直接啟動(dòng)失敗
    database_url: str

    # 是否打印 SQL。調(diào)試時(shí)可以改成 true,正常開發(fā)一般保持 false
    database_echo: bool = False

    # 是否在啟動(dòng)時(shí)自動(dòng)建表。本地 demo 可以 true,正式項(xiàng)目一般 false
    database_auto_create: bool = False


# 創(chuàng)建全局配置對(duì)象。其他模塊通過 settings.database_url 讀取配置
settings = Settings()

5. Alembic

Alembic 不是連接 MySQL 的必需依賴,但正式項(xiàng)目通常需要它。

它負(fù)責(zé)數(shù)據(jù)庫遷移:

uv add alembic

為什么需要遷移工具?

因?yàn)轫?xiàng)目上線后,表結(jié)構(gòu)變化不能靠 Base.metadata.create_all() 隨手創(chuàng)建。正式環(huán)境需要可追蹤、可回滾、可審查的遷移文件。

可以先記住這個(gè)區(qū)分:

本地學(xué)習(xí) / 早期 demo
  -> 可以用 create_all()

團(tuán)隊(duì)項(xiàng)目 / 正式環(huán)境
  -> 應(yīng)該用 Alembic 管理遷移

三、連接串怎么寫

MySQL 連接串的完整格式:

mysql+pymysql://用戶名:密碼@主機(jī):端口/數(shù)據(jù)庫名?charset=utf8mb4

示例:

DATABASE_URL="mysql+pymysql://root:password@127.0.0.1:3306/todo_api?charset=utf8mb4"

拆開看:

mysql+pymysql://root:password@127.0.0.1:3306/todo_api?charset=utf8mb4
                |    |        |         |    |        |
                用戶名 密碼    主機(jī)      端口 數(shù)據(jù)庫名  字符集

幾個(gè)點(diǎn)要特別注意。

第一,推薦顯式寫 mysql+pymysql://,不要只寫 mysql://。

mysql+pymysql:// 的意思更明確:MySQL 方言加 PyMySQL 驅(qū)動(dòng)。團(tuán)隊(duì)項(xiàng)目里不要依賴隱式默認(rèn)值,否則換環(huán)境時(shí)容易出現(xiàn)“本機(jī)能跑,別人機(jī)器不能跑”的問題。

第二,字符集推薦 utf8mb4。

utf8mb4 能完整支持 Unicode,包括 emoji。新項(xiàng)目沒有必要再使用 MySQL 早期的 utf8。

第三,密碼里如果有特殊字符,需要 URL 編碼。

例如密碼里有 @#、/:,連接串可能被解析錯(cuò)。更穩(wěn)的做法是密碼避免使用這些字符,或者在生成連接串時(shí)進(jìn)行 URL 編碼。

四、創(chuàng)建 Engine

Engine 是 SQLAlchemy 的數(shù)據(jù)庫入口。

可以把它理解成:Engine = 數(shù)據(jù)庫連接入口 + 連接池

這里的 連接池 指的是一組可復(fù)用的數(shù)據(jù)庫連接。

如果每次請(qǐng)求都重新連接 MySQL,再斷開連接,成本會(huì)很高。連接池會(huì)提前維護(hù)一些連接,請(qǐng)求來了就借一條,用完再還回去。這樣后端服務(wù)可以更穩(wěn)定地處理大量請(qǐng)求。

Engine 本身不是某一次查詢,也不是某一個(gè)請(qǐng)求的連接。它更像應(yīng)用級(jí)的數(shù)據(jù)庫入口,通常在應(yīng)用啟動(dòng)時(shí)創(chuàng)建一次,然后整個(gè)應(yīng)用復(fù)用。

基礎(chǔ)寫法:

from sqlalchemy import create_engine

from app.core.config import settings


engine = create_engine(
    # SQLAlchemy 根據(jù)這個(gè) URL 判斷數(shù)據(jù)庫類型、驅(qū)動(dòng)、賬號(hào)、密碼、主機(jī)和庫名
    settings.database_url,

    # echo=True 會(huì)把 SQL 打到控制臺(tái),排查問題時(shí)很有用
    echo=settings.database_echo,

    # 每次從連接池取連接前先檢查連接是否還活著
    # MySQL 長時(shí)間空閑后可能主動(dòng)斷開連接,這個(gè)參數(shù)能減少斷連錯(cuò)誤
    pool_pre_ping=True,
)

常見參數(shù):

  • echo=True:打印 SQL,適合調(diào)試
  • echo=False:不打印 SQL,適合正常開發(fā)
  • pool_pre_ping=True:從連接池取連接前先檢查連接是否可用,減少 MySQL 空閑連接斷開導(dǎo)致的問題

MySQL 服務(wù)端可能會(huì)關(guān)閉長時(shí)間空閑的連接。后端服務(wù)如果繼續(xù)拿到這條已經(jīng)失效的連接,就會(huì)在執(zhí)行 SQL 時(shí)失敗。pool_pre_ping=True 可以讓 SQLAlchemy 在使用連接前做一次輕量檢查,發(fā)現(xiàn)連接不可用就重新連接。

五、創(chuàng)建 SessionLocal

Session 是 ORM 的操作上下文。

可以把它理解成:Session = 一次數(shù)據(jù)庫讀寫上下文 + 事務(wù)邊界

Session 這個(gè)名字容易誤會(huì)。它不是瀏覽器登錄態(tài)里的 Session,也不是用戶會(huì)話。

在 SQLAlchemy 里,Session 表示一次數(shù)據(jù)庫操作上下文:它知道你查了哪些對(duì)象、改了哪些對(duì)象、哪些對(duì)象準(zhǔn)備新增、最后要不要提交事務(wù)。

SessionLocal 則是一個(gè)“Session 工廠”。它不是具體的 Session,而是用來創(chuàng)建 Session 的函數(shù)式對(duì)象。每個(gè)請(qǐng)求進(jìn)來時(shí),都通過它創(chuàng)建一個(gè)新的 Session。

創(chuàng)建 Session 工廠:

from sqlalchemy.orm import sessionmaker


SessionLocal = sessionmaker(
    # 這個(gè) Session 工廠創(chuàng)建出來的 Session,都使用前面創(chuàng)建的 engine
    bind=engine,

    # 不在查詢前自動(dòng) flush,減少隱式數(shù)據(jù)庫寫入時(shí)機(jī)
    autoflush=False,

    # 不自動(dòng)提交事務(wù)。寫操作完成后必須手動(dòng) db.commit()
    autocommit=False,

    # commit 后對(duì)象字段仍然可讀,不會(huì)立刻過期
    expire_on_commit=False,
)

參數(shù)含義:

  • bind=engine:這個(gè) Session 工廠使用哪個(gè) Engine
  • autoflush=False:不在查詢前自動(dòng)刷新變更,減少新手階段的隱式行為
  • autocommit=False:不自動(dòng)提交事務(wù),寫操作必須顯式 commit
  • expire_on_commit=False:提交后對(duì)象字段仍可直接讀取

使用方式:

from sqlalchemy import select


# with 負(fù)責(zé)在代碼塊結(jié)束后關(guān)閉 Session
# 關(guān)閉 Session 不等于關(guān)閉 MySQL 服務(wù),而是把連接還回連接池
with SessionLocal() as db:
    # select(User) 生成查詢 users 表的 SQLAlchemy 查詢對(duì)象
    # scalars(...) 表示返回 ORM 對(duì)象,而不是原始行對(duì)象
    users = db.scalars(select(User)).all()

with 退出時(shí)會(huì)關(guān)閉 Session,把連接歸還給連接池。

六、定義 ORM 模型

ORM 模型用 Python 類描述數(shù)據(jù)庫表。

模型 指的是“數(shù)據(jù)庫表在 Python 代碼里的表示”。表里每一行數(shù)據(jù),讀取出來后可以變成一個(gè) Python 對(duì)象。對(duì)象的字段,對(duì)應(yīng)數(shù)據(jù)庫表的列。

先定義統(tǒng)一的 Base:

from sqlalchemy.orm import DeclarativeBase


# 所有 ORM 模型都繼承 Base
# SQLAlchemy 會(huì)通過 Base.metadata 收集所有表結(jié)構(gòu)
class Base(DeclarativeBase):
    pass

再定義業(yè)務(wù)模型:

from datetime import datetime

from sqlalchemy import DateTime, Integer, String, func
from sqlalchemy.orm import Mapped, mapped_column

from app.db.base import Base


class User(Base):
    # __tablename__ 指定這個(gè)模型對(duì)應(yīng)數(shù)據(jù)庫里的哪張表
    __tablename__ = "users"

    # primary_key=True 表示主鍵
    # autoincrement=True 表示由 MySQL 自動(dòng)生成遞增 ID
    id: Mapped[int] = mapped_column(Integer, primary_key=True, autoincrement=True)

    # unique=True 表示數(shù)據(jù)庫層面不允許重復(fù)
    # nullable=False 表示這一列不能為空
    username: Mapped[str] = mapped_column(String(50), unique=True, nullable=False)
    email: Mapped[str] = mapped_column(String(191), unique=True, nullable=False)

    # default=1 是 Python 側(cè)默認(rèn)值。創(chuàng)建 User 時(shí)不傳 status,就默認(rèn)是 1
    status: Mapped[int] = mapped_column(Integer, nullable=False, default=1)

    # server_default=func.now() 表示由數(shù)據(jù)庫服務(wù)器生成默認(rèn)時(shí)間
    created_at: Mapped[datetime] = mapped_column(
        DateTime(timezone=True),
        server_default=func.now(),
        nullable=False,
    )

這段代碼表示:

User 類
  -> users 表

User.username
  -> users.username 字段

User.email
  -> users.email 字段

Mapped[...]mapped_column(...) 是 SQLAlchemy 2.x 推薦的聲明式寫法。類型標(biāo)注不僅給編輯器看,也幫助 ORM 更清楚地理解字段類型。

七、執(zhí)行增刪改查

1. 查詢

查詢?nèi)坑脩簦?/p>

from sqlalchemy import select
from sqlalchemy.orm import Session


def list_users(db: Session) -> list[User]:
    return list(db.scalars(select(User)).all())

按主鍵查詢:

from sqlalchemy.orm import Session


def get_user(db: Session, user_id: int) -> User | None:
    return db.get(User, user_id)

按條件查詢:

from sqlalchemy import select
from sqlalchemy.orm import Session


def get_user_by_username(db: Session, username: str) -> User | None:
    return db.scalar(select(User).where(User.username == username))

分頁查詢:

from sqlalchemy import func, select
from sqlalchemy.orm import Session


def page_users(db: Session, page: int, page_size: int) -> dict[str, object]:
    # 第 1 頁跳過 0 條,第 2 頁跳過 page_size 條
    offset = (page - 1) * page_size

    # 先查總數(shù),用來給前端顯示一共有多少條數(shù)據(jù)
    total = db.scalar(select(func.count()).select_from(User)) or 0

    # 再查當(dāng)前頁數(shù)據(jù)
    items = db.scalars(
        select(User)
        .order_by(User.id.desc())
        .offset(offset)
        .limit(page_size)
    ).all()

    return {
        "items": list(items),
        "total": total,
        "page": page,
        "page_size": page_size,
    }

2. 新增

from sqlalchemy.orm import Session


def create_user(db: Session, username: str, email: str) -> User:
    # 這里只是在 Python 內(nèi)存里創(chuàng)建 ORM 對(duì)象,還沒有寫入數(shù)據(jù)庫
    user = User(
        username=username,
        email=email,
        status=1,
    )

    # 加入當(dāng)前 Session,表示這條數(shù)據(jù)準(zhǔn)備新增
    db.add(user)

    # 提交事務(wù)后,INSERT 才會(huì)真正執(zhí)行
    db.commit()

    # 刷新對(duì)象,拿到數(shù)據(jù)庫生成的 id、created_at 等字段
    db.refresh(user)

    return user

關(guān)鍵點(diǎn):

  • db.add(user):把對(duì)象加入當(dāng)前 Session
  • db.commit():提交事務(wù),真正寫入數(shù)據(jù)庫
  • db.refresh(user):從數(shù)據(jù)庫刷新對(duì)象,拿到自增 ID、默認(rèn)值等字段

3. 更新

from sqlalchemy.orm import Session


def update_user_email(db: Session, user_id: int, email: str) -> User | None:
    # db.get 適合按主鍵查詢
    user = db.get(User, user_id)
    if user is None:
        return None

    # 修改 ORM 對(duì)象字段后,SQLAlchemy 會(huì)記錄這次變化
    user.email = email

    # commit 時(shí) SQLAlchemy 會(huì)生成 UPDATE 語句
    db.commit()
    db.refresh(user)

    return user

只要對(duì)象是從當(dāng)前 Session 查出來的,修改字段后執(zhí)行 commit(),SQLAlchemy 就會(huì)生成對(duì)應(yīng)的 UPDATE。

4. 刪除

from sqlalchemy.orm import Session


def delete_user(db: Session, user_id: int) -> bool:
    user = db.get(User, user_id)
    if user is None:
        return False

    # 標(biāo)記這個(gè) ORM 對(duì)象要?jiǎng)h除
    db.delete(user)

    # commit 時(shí) SQLAlchemy 會(huì)生成 DELETE 語句
    db.commit()

    return True

刪除數(shù)據(jù)時(shí)要考慮外鍵約束、關(guān)聯(lián)表、業(yè)務(wù)規(guī)則和是否允許物理刪除。真實(shí)項(xiàng)目里,用戶、訂單、支付記錄這類數(shù)據(jù)通常不會(huì)隨意物理刪除。

八、事務(wù)怎么處理

數(shù)據(jù)庫寫操作必須有清晰的事務(wù)邊界。

最常見的寫法是:

from sqlalchemy.orm import Session


def create_user_with_role(db: Session, user: User, role: Role) -> User:
    try:
        # 兩個(gè)新增動(dòng)作放在同一個(gè)事務(wù)里
        db.add(user)
        db.add(role)

        # 兩個(gè)對(duì)象都寫入成功,事務(wù)才提交
        db.commit()
        db.refresh(user)
        return user
    except Exception:
        # 任意一步失敗,都撤銷本次事務(wù)里的所有數(shù)據(jù)庫變更
        db.rollback()
        raise

事務(wù)的規(guī)則很簡單:

全部成功
  -> commit

中途失敗
  -> rollback

也可以使用 begin() 上下文:

# begin 會(huì)自動(dòng)管理事務(wù):
# 正常退出自動(dòng) commit,出現(xiàn)異常自動(dòng) rollback
with SessionLocal.begin() as db:
    db.add(user)
    db.add(role)

begin() 正常退出時(shí)自動(dòng)提交,發(fā)生異常時(shí)自動(dòng)回滾。

在業(yè)務(wù)接口里,更常見的是把 Session 交給 Service 或 Repository,然后在明確的業(yè)務(wù)動(dòng)作完成后提交。這樣讀起來更清楚,也方便處理業(yè)務(wù)異常。

九、在 FastAPI 中注入數(shù)據(jù)庫 Session

FastAPI 推薦用依賴注入管理請(qǐng)求級(jí)資源。

數(shù)據(jù)庫 Session 的依賴函數(shù)可以這樣寫:

from collections.abc import Generator

from sqlalchemy.orm import Session

from app.db.session import SessionLocal


def get_db() -> Generator[Session, None, None]:
    # 每個(gè)請(qǐng)求進(jìn)來時(shí),創(chuàng)建一個(gè)新的數(shù)據(jù)庫 Session
    db = SessionLocal()
    try:
        # yield 會(huì)把 db 交給路由函數(shù)使用
        # 路由函數(shù)執(zhí)行完成后,代碼會(huì)繼續(xù)走到 finally
        yield db
    finally:
        # 請(qǐng)求結(jié)束后關(guān)閉 Session,把連接還回連接池
        db.close()

這段代碼的行為是:

請(qǐng)求進(jìn)入
  -> 創(chuàng)建一個(gè) Session

路由函數(shù)執(zhí)行
  -> 使用這個(gè) Session 查詢或?qū)懭霐?shù)據(jù)庫

請(qǐng)求結(jié)束
  -> finally 關(guān)閉 Session

路由里使用:

from typing import Annotated

from fastapi import APIRouter, Depends, HTTPException
from sqlalchemy.orm import Session

from app.dependencies.database import get_db
from app.modules.users.service import UserService


# prefix 表示這個(gè) router 下的接口都以 /users 開頭
# tags 用來給 OpenAPI 文檔分組
router = APIRouter(prefix="/users", tags=["users"])

# 聲明一個(gè)可復(fù)用的數(shù)據(jù)庫依賴類型
# 以后路由參數(shù)寫 db: SessionDep,就會(huì)自動(dòng)拿到 get_db 提供的 Session
SessionDep = Annotated[Session, Depends(get_db)]


@router.get("/{user_id}")
def get_user(user_id: int, db: SessionDep) -> dict[str, object]:
    # 路由層不直接寫數(shù)據(jù)庫查詢,而是調(diào)用 Service
    user = UserService.get_user(db, user_id)
    if user is None:
        # Service 返回 None,路由層把它轉(zhuǎn)換成 HTTP 404
        raise HTTPException(status_code=404, detail="User not found")

    return {
        "id": user.id,
        "username": user.username,
        "email": user.email,
    }

這里路由層只做三件事:

  • 接收 HTTP 參數(shù)
  • 調(diào)用業(yè)務(wù)邏輯
  • 返回 HTTP 響應(yīng)

它不負(fù)責(zé)創(chuàng)建數(shù)據(jù)庫連接,也不直接堆很多 SQLAlchemy 查詢。

十、從零搭一個(gè)可維護(hù)的工程結(jié)構(gòu)

項(xiàng)目小的時(shí)候,一個(gè) main.py 就能跑。

項(xiàng)目變大后,不建議把配置、數(shù)據(jù)庫連接、模型、路由、業(yè)務(wù)邏輯都塞在一起。更穩(wěn)的結(jié)構(gòu)是:

todo-api/
├── app/
│   ├── main.py
│   ├── api/
│   │   └── v1/
│   │       └── router.py
│   ├── core/
│   │   └── config.py
│   ├── db/
│   │   ├── base.py
│   │   ├── models.py
│   │   └── session.py
│   ├── dependencies/
│   │   └── database.py
│   └── modules/
│       └── users/
│           ├── models.py
│           ├── schemas.py
│           ├── repository.py
│           ├── service.py
│           └── router.py
├── migrations/
├── tests/
├── .env
└── pyproject.toml

每一層的職責(zé)要清楚。

app/core/config.py
  -> 讀取環(huán)境變量
app/db/session.py
  -> 創(chuàng)建 Engine、SessionLocal、init_database
app/db/base.py
  -> 定義 SQLAlchemy Base
app/db/models.py
  -> 統(tǒng)一導(dǎo)入所有 ORM 模型,方便 create_all 或 Alembic 發(fā)現(xiàn)模型
app/dependencies/database.py
  -> 定義 get_db
app/modules/users/models.py
  -> 用戶表 ORM 模型
app/modules/users/schemas.py
  -> 用戶接口的請(qǐng)求體和響應(yīng)體
app/modules/users/repository.py
  -> 用戶相關(guān)數(shù)據(jù)庫讀寫
app/modules/users/service.py
  -> 用戶業(yè)務(wù)規(guī)則
app/modules/users/router.py
  -> 用戶 HTTP 接口

這個(gè)結(jié)構(gòu)的核心原則是:

  • 公共基礎(chǔ)設(shè)施放 app/core、app/db、app/dependencies
  • 業(yè)務(wù)代碼按模塊放 app/modules/{module_name}

十一、核心文件怎么寫

1. app/core/config.py

from pydantic_settings import BaseSettings, SettingsConfigDict


class Settings(BaseSettings):
    # 指定 .env 文件作為本地配置來源
    model_config = SettingsConfigDict(
        env_file=".env",
        env_file_encoding="utf-8",
    )

    # 應(yīng)用名稱,會(huì)展示在接口文檔里
    app_name: str = "Todo API"

    # MySQL 連接串,例如 mysql+pymysql://root:password@127.0.0.1:3306/todo_api
    database_url: str

    # 是否打印 SQL
    database_echo: bool = False

    # 是否啟動(dòng)時(shí)自動(dòng) create_all 建表
    database_auto_create: bool = False


# 全局配置對(duì)象。其他模塊只讀取它,不在業(yè)務(wù)代碼里手寫環(huán)境變量解析
settings = Settings()

2. app/db/base.py

from sqlalchemy.orm import DeclarativeBase


# 所有 ORM 模型共同繼承的基類
# 后續(xù) User、Todo、Role 等模型都會(huì)注冊(cè)到 Base.metadata 中
class Base(DeclarativeBase):
    pass

3. app/modules/users/models.py

from datetime import datetime

from sqlalchemy import DateTime, Integer, String, func
from sqlalchemy.orm import Mapped, mapped_column

from app.db.base import Base


class User(Base):
    # users 是數(shù)據(jù)庫里的真實(shí)表名
    __tablename__ = "users"

    # id 是主鍵,由 MySQL 自增生成
    id: Mapped[int] = mapped_column(Integer, primary_key=True, autoincrement=True)

    # username 和 email 都要求唯一且不能為空
    username: Mapped[str] = mapped_column(String(50), unique=True, nullable=False)
    email: Mapped[str] = mapped_column(String(191), unique=True, nullable=False)

    # status 可以用來表示啟用、禁用等業(yè)務(wù)狀態(tài)
    status: Mapped[int] = mapped_column(Integer, nullable=False, default=1)

    # created_at 由數(shù)據(jù)庫寫入當(dāng)前時(shí)間
    created_at: Mapped[datetime] = mapped_column(
        DateTime(timezone=True),
        server_default=func.now(),
        nullable=False,
    )

4. app/db/models.py

from app.modules.users.models import User

# 統(tǒng)一導(dǎo)出模型,方便其他地方一次性導(dǎo)入
__all__ = ["User"]

這個(gè)文件看起來簡單,但很重要。

SQLAlchemy 只有在模型類被 Python 導(dǎo)入后,Base.metadata 才知道有哪些表。集中導(dǎo)入模型,可以避免“明明寫了模型,create_all 或 Alembic 卻找不到表”的問題。

5. app/db/session.py

from sqlalchemy import create_engine
from sqlalchemy.orm import sessionmaker

from app.core.config import settings
from app.db.base import Base
from app.db import models


engine = create_engine(
    # 從配置讀取連接串,避免把賬號(hào)密碼寫死在代碼里
    settings.database_url,

    # 控制是否打印 SQL
    echo=settings.database_echo,

    # 防止 MySQL 空閑連接失效后,請(qǐng)求第一次查詢就報(bào)錯(cuò)
    pool_pre_ping=True,
)

SessionLocal = sessionmaker(
    # SessionLocal 創(chuàng)建出的 Session 都綁定到這個(gè) engine
    bind=engine,
    autoflush=False,
    autocommit=False,
    expire_on_commit=False,
)


def init_database() -> None:
    # 只在明確開啟時(shí)自動(dòng)建表
    # 正式項(xiàng)目通常使用 Alembic,不依賴 create_all
    if settings.database_auto_create:
        Base.metadata.create_all(bind=engine)

from app.db import models 不是為了在代碼里直接使用 models 變量,而是為了確保所有 ORM 模型都被導(dǎo)入,讓 Base.metadata 收集到表結(jié)構(gòu)。

6. app/dependencies/database.py

from collections.abc import Generator

from sqlalchemy.orm import Session

from app.db.session import SessionLocal


def get_db() -> Generator[Session, None, None]:
    # 為當(dāng)前請(qǐng)求創(chuàng)建 Session
    db = SessionLocal()
    try:
        # 把 Session 交給 FastAPI 路由函數(shù)
        yield db
    finally:
        # 無論接口成功還是失敗,最終都關(guān)閉 Session
        db.close()

7. app/modules/users/schemas.py

from datetime import datetime

from pydantic import BaseModel, EmailStr


class UserCreate(BaseModel):
    # 創(chuàng)建用戶時(shí),前端必須傳 username 和 email
    username: str
    email: EmailStr


class UserRead(BaseModel):
    # 返回給前端的用戶字段
    id: int
    username: str
    email: EmailStr
    status: int
    created_at: datetime

如果使用 EmailStr,需要安裝郵箱校驗(yàn)依賴:

uv add "pydantic[email]"

或者:

pip install "pydantic[email]"

如果不想加這個(gè)依賴,可以先把 EmailStr 改成普通的 str。

8. app/modules/users/repository.py

from sqlalchemy import select
from sqlalchemy.orm import Session

from app.modules.users.models import User


class UserRepository:
    @staticmethod
    def get_by_id(db: Session, user_id: int) -> User | None:
        # Repository 只關(guān)心數(shù)據(jù)庫怎么查
        return db.get(User, user_id)

    @staticmethod
    def get_by_username(db: Session, username: str) -> User | None:
        # 根據(jù)唯一用戶名查單個(gè)用戶
        return db.scalar(select(User).where(User.username == username))

    @staticmethod
    def create(db: Session, username: str, email: str) -> User:
        # 這里只 add,不 commit
        # commit 由 Service 在完整業(yè)務(wù)動(dòng)作結(jié)束后統(tǒng)一處理
        user = User(username=username, email=email)
        db.add(user)
        return user

Repository 只處理數(shù)據(jù)庫讀寫,不寫 HTTP 異常,也不關(guān)心接口怎么返回。

9. app/modules/users/service.py

from sqlalchemy.orm import Session

from app.modules.users.models import User
from app.modules.users.repository import UserRepository
from app.modules.users.schemas import UserCreate


class UserService:
    @staticmethod
    def get_user(db: Session, user_id: int) -> User | None:
        # Service 可以直接復(fù)用 Repository
        return UserRepository.get_by_id(db, user_id)

    @staticmethod
    def create_user(db: Session, data: UserCreate) -> User:
        # 業(yè)務(wù)規(guī)則:用戶名不能重復(fù)
        exists = UserRepository.get_by_username(db, data.username)
        if exists is not None:
            raise ValueError("Username already exists")

        try:
            # 先執(zhí)行數(shù)據(jù)庫新增動(dòng)作
            user = UserRepository.create(
                db,
                username=data.username,
                email=data.email,
            )

            # 業(yè)務(wù)動(dòng)作完整成功后再提交事務(wù)
            db.commit()
            db.refresh(user)
            return user
        except Exception:
            # 提交前或提交時(shí)出錯(cuò),都回滾本次事務(wù)
            db.rollback()
            raise

Service 負(fù)責(zé)業(yè)務(wù)規(guī)則和事務(wù)邊界。

比如“用戶名不能重復(fù)”屬于業(yè)務(wù)規(guī)則,應(yīng)該放在 Service。commitrollback 也更適合放在完成一個(gè)業(yè)務(wù)動(dòng)作的位置。

10. app/modules/users/router.py

from typing import Annotated

from fastapi import APIRouter, Depends, HTTPException, status
from sqlalchemy.orm import Session

from app.dependencies.database import get_db
from app.modules.users.schemas import UserCreate, UserRead
from app.modules.users.service import UserService


# 這個(gè)文件只定義 users 模塊自己的 HTTP 接口
router = APIRouter(prefix="/users", tags=["users"])
SessionDep = Annotated[Session, Depends(get_db)]


@router.get("/{user_id}", response_model=UserRead)
def get_user(user_id: int, db: SessionDep) -> UserRead:
    # 1. 路由層接收 user_id
    # 2. 把 db 和 user_id 交給 Service
    user = UserService.get_user(db, user_id)
    if user is None:
        raise HTTPException(
            status_code=status.HTTP_404_NOT_FOUND,
            detail="User not found",
        )

    # ORM 對(duì)象轉(zhuǎn)換成響應(yīng)模型,再由 FastAPI 轉(zhuǎn)成 JSON
    return UserRead.model_validate(user, from_attributes=True)


@router.post("", response_model=UserRead, status_code=status.HTTP_201_CREATED)
def create_user(data: UserCreate, db: SessionDep) -> UserRead:
    try:
        # data 已經(jīng)被 Pydantic 校驗(yàn)過,這里交給 Service 處理業(yè)務(wù)
        user = UserService.create_user(db, data)
    except ValueError as exc:
        # 業(yè)務(wù)異常在路由層轉(zhuǎn)換成 HTTP 狀態(tài)碼
        raise HTTPException(
            status_code=status.HTTP_409_CONFLICT,
            detail=str(exc),
        ) from exc

    return UserRead.model_validate(user, from_attributes=True)

路由層要薄。

它應(yīng)該關(guān)心 HTTP 語義,比如:

  • 什么路徑
  • 什么方法
  • 什么狀態(tài)碼
  • 請(qǐng)求體是什么
  • 響應(yīng)體是什么
  • 業(yè)務(wù)異常轉(zhuǎn)成什么 HTTP 錯(cuò)誤

它不應(yīng)該塞滿數(shù)據(jù)庫查詢。

11. app/api/v1/router.py

from fastapi import APIRouter

from app.modules.users.router import router as users_router


api_router = APIRouter(prefix="/api/v1")

# 把 users 模塊的接口掛到 /api/v1 下
api_router.include_router(users_router)

12. app/main.py

from collections.abc import AsyncIterator
from contextlib import asynccontextmanager

from fastapi import FastAPI

from app.api.v1.router import api_router
from app.core.config import settings
from app.db.session import init_database


@asynccontextmanager
async def lifespan(app: FastAPI) -> AsyncIterator[None]:
    # 應(yīng)用啟動(dòng)時(shí)執(zhí)行一次初始化邏輯
    init_database()

    # yield 之前是啟動(dòng)邏輯,yield 之后可以寫關(guān)閉邏輯
    yield


app = FastAPI(
    title=settings.app_name,
    # lifespan 用來管理應(yīng)用啟動(dòng)和關(guān)閉階段的邏輯
    lifespan=lifespan,
)


# 注冊(cè)總路由
app.include_router(api_router)

啟動(dòng):

uv run fastapi dev app/main.py

也可以用 Uvicorn:

uv run uvicorn app.main:app --reload

十二、一條創(chuàng)建用戶請(qǐng)求怎么走完整流程

現(xiàn)在把前面的文件串起來,看一條真實(shí)請(qǐng)求:

POST /api/v1/users
Content-Type: application/json
{
  "username": "copyer",
  "email": "copyer@example.com"
}

它在后端的執(zhí)行順序是這樣的。

1. app/main.py
   FastAPI 應(yīng)用已經(jīng) include_router(api_router)
2. app/api/v1/router.py
   /api/v1 前綴命中,總路由繼續(xù)把請(qǐng)求交給 users_router
3. app/modules/users/router.py
   POST /users 命中 create_user 路由函數(shù)
4. FastAPI 發(fā)現(xiàn)路由參數(shù)里有 db: SessionDep
   于是先執(zhí)行 get_db()
5. app/dependencies/database.py
   get_db() 通過 SessionLocal() 創(chuàng)建一個(gè)新的數(shù)據(jù)庫 Session
6. router.py
   create_user(data, db) 開始執(zhí)行
   data 是 Pydantic 校驗(yàn)后的 UserCreate
   db 是 get_db() 提供的 SQLAlchemy Session
7. service.py
   UserService.create_user(db, data) 執(zhí)行業(yè)務(wù)規(guī)則
   先檢查 username 是否重復(fù)
8. repository.py
   UserRepository.get_by_username(db, data.username)
   使用 SQLAlchemy 查詢 User 表
9. SQLAlchemy
   把 select(User).where(...) 翻譯成 SELECT SQL
10. PyMySQL
    把 SELECT SQL 發(fā)給 MySQL
11. MySQL
    執(zhí)行查詢,返回是否已有這個(gè)用戶
12. service.py
    如果用戶名不存在,調(diào)用 Repository 創(chuàng)建 User 對(duì)象
13. repository.py
    db.add(user)
    把 user 標(biāo)記為準(zhǔn)備新增,但此時(shí)還沒有真正寫入數(shù)據(jù)庫
14. service.py
    db.commit()
    提交事務(wù),SQLAlchemy 生成 INSERT SQL
15. PyMySQL
    把 INSERT SQL 發(fā)給 MySQL
16. MySQL
    寫入 users 表,生成自增 id 和默認(rèn)時(shí)間
17. service.py
    db.refresh(user)
    把數(shù)據(jù)庫生成的新字段刷新回 Python 對(duì)象
18. router.py
    把 ORM 對(duì)象轉(zhuǎn)換成 UserRead
19. FastAPI
    把 UserRead 轉(zhuǎn)成 JSON 響應(yīng)
20. get_db()
    請(qǐng)求結(jié)束,finally 執(zhí)行 db.close()
    Session 關(guān)閉,連接歸還連接池

這條鏈路里,最重要的是不要把職責(zé)混在一起。

Router
  -> 只處理 HTTP 輸入輸出

Service
  -> 處理業(yè)務(wù)規(guī)則和事務(wù)

Repository
  -> 處理數(shù)據(jù)庫讀寫細(xì)節(jié)

Session
  -> 記錄本次數(shù)據(jù)庫操作,并負(fù)責(zé) commit / rollback

Engine
  -> 提供數(shù)據(jù)庫連接入口和連接池

PyMySQL
  -> 真正和 MySQL 通信

如果創(chuàng)建用戶失敗,流程會(huì)變成:

Service 執(zhí)行中出錯(cuò)
  -> except 捕獲異常
  -> db.rollback()
  -> 撤銷本次事務(wù)里已經(jīng)準(zhǔn)備寫入的變更
  -> raise 把異常繼續(xù)拋出
  -> Router 或全局異常處理把異常轉(zhuǎn)換成 HTTP 響應(yīng)
  -> get_db finally 關(guān)閉 Session

所以 commitrollback、close 是三個(gè)不同動(dòng)作:

commit
  -> 確認(rèn)寫入數(shù)據(jù)庫

rollback
  -> 撤銷本次事務(wù)

close
  -> 關(guān)閉當(dāng)前 Session,把連接還回連接池

不要用 close 代替 rollback,也不要以為 add 之后數(shù)據(jù)已經(jīng)寫進(jìn)數(shù)據(jù)庫。真正落庫發(fā)生在 commit

十三、同步還是異步

這篇使用的是同步 SQLAlchemy + PyMySQL。

也就是說:

SQLAlchemy create_engine
  -> PyMySQL
  -> 同步數(shù)據(jù)庫調(diào)用

這種方式適合大多數(shù)入門項(xiàng)目和普通后臺(tái)系統(tǒng),代碼簡單,資料多,問題也容易排查。

如果要做全鏈路異步,依賴會(huì)變成另一套:

SQLAlchemy create_async_engine
  -> asyncmy 或 aiomysql
  -> AsyncSession
  -> async def 路由

不要把同步 Session 和異步 Engine 混在一起,也不要只因?yàn)?FastAPI 支持 async def 就強(qiáng)行把數(shù)據(jù)庫層改成異步。先把同步版本寫清楚,比一開始就堆異步概念更重要。

十四、常見錯(cuò)誤

1. 忘記提交事務(wù)

只寫:

db.add(user)

數(shù)據(jù)不會(huì)真正落庫。寫操作要執(zhí)行:

db.commit()

如果還要拿到自增 ID 或數(shù)據(jù)庫默認(rèn)值,再執(zhí)行:

db.refresh(user)

2. 異常后不回滾

寫操作失敗后,應(yīng)該:

db.rollback()

否則當(dāng)前 Session 可能處于失敗狀態(tài),后續(xù)繼續(xù)使用會(huì)報(bào)錯(cuò)。

3. 在路由里直接創(chuàng)建 Engine

不要這樣寫:

@router.get("/users")
def list_users():
    engine = create_engine(...)

Engine 應(yīng)該在應(yīng)用啟動(dòng)時(shí)創(chuàng)建一次,復(fù)用連接池。每個(gè)請(qǐng)求里只創(chuàng)建和關(guān)閉 Session。

4. 把數(shù)據(jù)庫密碼寫進(jìn)代碼

不要把連接串硬編碼在 Python 文件里。

更好的方式:

.env
  -> 本地開發(fā)配置

生產(chǎn)環(huán)境變量
  -> 生產(chǎn)配置

.env 通常不提交到 Git。

5. 模型沒有被導(dǎo)入

如果調(diào)用 Base.metadata.create_all(bind=engine) 后沒有生成表,常見原因是模型類沒有被導(dǎo)入。

解決方式是建立一個(gè)統(tǒng)一的模型導(dǎo)入文件:

from app.modules.users.models import User

__all__ = ["User"]

并在初始化數(shù)據(jù)庫前導(dǎo)入它。

以上就是Python FastAPI連接操作MySQL數(shù)據(jù)庫指南的詳細(xì)內(nèi)容,更多關(guān)于Python FastAPI連接MySQL的資料請(qǐng)關(guān)注腳本之家其它相關(guān)文章!

相關(guān)文章

  • python中如何進(jìn)行連乘計(jì)算

    python中如何進(jìn)行連乘計(jì)算

    在本篇文章里小編給大家分享的是關(guān)于python連乘計(jì)算的代碼,有興趣的朋友們可以參考學(xué)習(xí)下。
    2020-05-05
  • python 字典和列表嵌套用法詳解

    python 字典和列表嵌套用法詳解

    python中字典和列表的使用,在數(shù)據(jù)處理中應(yīng)該是最常用的,今天通過多種場景給大家分享python 字典和列表嵌套用法,感興趣的朋友一起看看吧
    2021-06-06
  • python 在指定范圍內(nèi)隨機(jī)生成不重復(fù)的n個(gè)數(shù)實(shí)例

    python 在指定范圍內(nèi)隨機(jī)生成不重復(fù)的n個(gè)數(shù)實(shí)例

    今天小編就為大家分享一篇python 在指定范圍內(nèi)隨機(jī)生成不重復(fù)的n個(gè)數(shù)實(shí)例,具有很好的參考價(jià)值,希望對(duì)大家有所幫助。一起跟隨小編過來看看吧
    2019-01-01
  • 淺析Python函數(shù)式編程

    淺析Python函數(shù)式編程

    在本篇文章中我們給大家分享了關(guān)于Python函數(shù)式編程的相關(guān)知識(shí)點(diǎn)內(nèi)容,有興趣的朋友參考下。
    2018-10-10
  • Python中隱藏的五種實(shí)用技巧分享

    Python中隱藏的五種實(shí)用技巧分享

    這篇文章主要和大家分享五個(gè)Python中隱藏的實(shí)用技巧,文中的示例代碼講解詳細(xì),對(duì)我們學(xué)習(xí)Python有一定的幫助,感興趣的小伙伴可以學(xué)習(xí)一下
    2022-05-05
  • 使用Python在Excel文檔中創(chuàng)建表格并應(yīng)用表格樣式

    使用Python在Excel文檔中創(chuàng)建表格并應(yīng)用表格樣式

    本文介紹了使用FreeSpire.XLS for Python在Excel中創(chuàng)建表格并應(yīng)用各種內(nèi)置樣式的方法,通過編程實(shí)現(xiàn)數(shù)據(jù)的自動(dòng)格式化,提升報(bào)表的專業(yè)性和效率,涵蓋淺色、中等和深色樣式系列,以及配置總計(jì)行、行條紋、列條紋和篩選功能,需要的朋友可以參考下
    2026-04-04
  • python利用opencv實(shí)現(xiàn)SIFT特征提取與匹配

    python利用opencv實(shí)現(xiàn)SIFT特征提取與匹配

    這篇文章主要為大家詳細(xì)介紹了python利用opencv實(shí)現(xiàn)SIFT特征提取與匹配,文中示例代碼介紹的非常詳細(xì),具有一定的參考價(jià)值,感興趣的小伙伴們可以參考一下
    2020-03-03
  • python 測試實(shí)現(xiàn)方法

    python 測試實(shí)現(xiàn)方法

    使用python進(jìn)行測試也足夠簡明了
    2008-12-12
  • Python合并pdf文件的工具

    Python合并pdf文件的工具

    PDF文件合并工具是非常好用可以把多個(gè)pdf文件合并成一個(gè),本文以5個(gè)pdf文件為例給大家分享具體操作方法,通過實(shí)例代碼給大家介紹的非常詳細(xì),需要的朋友參考下吧
    2021-07-07
  • Python使用defaultdict讀取文件各列的方法

    Python使用defaultdict讀取文件各列的方法

    這篇文章主要介紹了Python使用defaultdict讀取文件各列的方法,涉及Python針對(duì)文件相關(guān)讀取、遍歷操作技巧,需要的朋友可以參考下
    2017-05-05

最新評(píng)論

枣阳市| 阜平县| 郓城县| 清原| 饶河县| 丰都县| 博乐市| 大同县| 武强县| 东丽区| 外汇| 石柱| 宝清县| 乌鲁木齐市| 嘉定区| 大城县| 马关县| 筠连县| 乌兰察布市| 阜宁县| 吐鲁番市| 沙洋县| 五大连池市| 苍溪县| 瑞安市| 绿春县| 北宁市| 新宁县| 武清区| 永春县| 通道| 顺昌县| 聂荣县| 平谷区| 监利县| 宁蒗| 乐平市| 潮州市| 安平县| 东至县| 云阳县|