FastAPI(44)- 操作關係型資料庫

小菠蘿測試筆記發表於2021-10-02

ORM

  • FastAPI 可與任何資料庫和任何樣式的庫配合使用並和資料庫通訊
  • object-relational mapping 物件關係對映
  • ORM 具有在程式碼和資料庫表(關係)中的物件之間進行轉換(對映)的工具
  • 使用 ORM,通常會建立一個表示 SQL 資料表的類,該類的每個屬性都表示一個列,具有名稱和型別

 

小栗子

  • Pet 類可以表示 SQL 表 pets
  • 並且 Pet 類的每個例項物件代表資料庫中的一行資料
  • 例如,物件 orion_cat(Pet 的一個例項)可以具有屬性 orion_cat.type,用於列型別,屬性的值可以是:貓

 

專案架構

.
└── sql_app
    ├── __init__.py
    ├── curd.py
    ├── database.py
    ├── main.py
    ├── models.py
    └── schemas.py

  

前提

需要先安裝 sqlalchemy

pip install sqlalchemy

 

使用 sqlite

  • 後面的栗子,暫時跟著官網,先使用 sqlite 資料庫來演示
  • 後面有時候再通過 Mysql 來寫多一篇文章

 

database.py 程式碼

# 1、匯入 sqlalchemy 部分的包
from sqlalchemy import create_engine
from sqlalchemy.ext.declarative import declarative_base
from sqlalchemy.orm import sessionmaker

# 2、宣告 database url

SQLALCHEMY_DATABASE_URL = "sqlite:///./sql_app.db"
# SQLALCHEMY_DATABASE_URL = "postgresql://user:password@postgresserver/db"

# 3、建立 sqlalchemy 引擎
engine = create_engine(
    url=SQLALCHEMY_DATABASE_URL,
    connect_args={"check_same_thread": False}
)

# 4、建立一個 database 會話
session = sessionmaker(autocommit=False, autoflush=False, bind=engine)

# 5、返回一個 ORM Model
Base = declarative_base()

 

宣告 database 連線 url

SQLALCHEMY_DATABASE_URL = "sqlite:///./sql_app.db"
# SQLALCHEMY_DATABASE_URL = "postgresql://user:password@postgresserver/db" 

第一行是 slite 連線 url

 

其他資料庫連線 url 的寫法

# sqlite-pysqlite 庫
sqlite+pysqlite:///file_path

# mysql-mysqldb 庫
mysql+mysqldb://<user>:<password>@<host>[:<port>]/<dbname>
  
# mysql-pymysql 庫
mysql+pymysql://<username>:<password>@<host>/<dbname>[?<options>]
  
# mysql-mysqlconnector 庫
mysql+mysqlconnector://<user>:<password>@<host>[:<port>]/<dbname>
  
# oracle-cx_Oracle 庫
oracle+cx_oracle://user:pass@hostname:port[/dbname][?service_name=<service>[&key=value&key=value...]]

# postgresql-pypostgresql 庫
postgresql+pypostgresql://user:password@host:port/dbname[?key=value&key=value...]

# SQL Server-PyODBC 庫
mssql+pyodbc://<username>:<password>@<dsnname>

 

建立一個資料庫引擎

engine = create_engine(
    url=SQLALCHEMY_DATABASE_URL,
    connect_args={"check_same_thread": False}
)
  •  {"check_same_thread": False} 僅適用於 SQlite,其他資料庫不需要用到
  • 預設情況下,SQLite 將只允許一個執行緒與其通訊,假設每個執行緒只處理一個獨立的請求
  • 這是為了防止被不同的事物(對於不同的請求)共享相同的連線
  • 但是在 FastAPI 中,使用普通函式 (def) 可以針對同一請求與資料庫的多個執行緒進行互動,因此需要讓 SQLite 知道它應該允許使用多執行緒
  • 需要確保每個請求在依賴項中都有自己的資料庫連線會話,因此不需要設定為同一個執行緒

 

建立一個資料庫會話

SessionLocal = sessionmaker(autocommit=False, autoflush=False, bind=engine)
  • SessionLocal 類的每個例項都是一個資料庫會話
  • 但 sessionmaker 本身還不是資料庫會話
  • 但是一旦建立了 SessionLocal 類的例項,這個例項就會成為實際的資料庫會話
  • 將其命名為 SessionLocal ,方便區分從 SQLAlchemy 匯入的 Session
  • 稍後將使用 Session(從 SQLAlchemy 匯入的那個)

 

建立一個 ORM 模型基類

Base = declarative_base()

後面會通過繼承這個 Base 類,來建立每個資料庫 Model,也稱為 ORM Model

 

models.py 程式碼

from sqlalchemy import Boolean, Column, ForeignKey, Integer, String
from sqlalchemy.orm import relationship

from .database import Base


class User(Base):
    # 1、表名
    __tablename__ = "users"

    # 2、類屬性,每一個都代表資料表中的一列
    # Column 就是列的意思
    # Integer、String、Boolean 就是資料表中,列的型別
    id = Column(Integer, primary_key=True, index=True, default=1, autoincrement=True)
    email = Column(String, unique=True, index=True)
    hashed_password = Column(String)
    is_active = Column(Boolean, default=True)

    items = relationship("Item", back_populates="owner")


class Item(Base):
    __tablename__ = "items"

    id = Column(Integer, primary_key=True, index=True, default=1, autoincrement=True)
    title = Column(String, index=True)
    description = Column(String, index=True)
    owner_id = Column(Integer, ForeignKey("users.id"))

    owner = relationship("User", back_populates="items")

 

Column

列,一個屬性代表資料表中的一列

 

常用引數

引數 作用
primary_key 如果設為 True ,這列就是表的主鍵
unique 如果設為 True ,這列不允許出現重複的值
index 如果設為 True ,為這列建立索引,提升查詢效率
nullable
  • 如果設為 True ,這列允許使用空值;
  • 如果設為 False ,這列不允許使用空值
default 為這列定義預設值

autoincrement

如果設為 True ,這列自增

 

String、Integer、Boolean

代表資料表中每一列的資料型別

 

schemas.py 程式碼

背景

為了避免混淆 SQLAlchemy 模型和 Pydantic 模型之間,將使用檔案 models.py 編寫 SQLAlchemy 模型和檔案 schemas.py 編寫 Pydantic 模型

 

實際程式碼

from typing import List, Optional

from pydantic import BaseModel


# Item 的基類,表示建立和查詢 Item 時共有的屬性
class ItemBase(BaseModel):
    title: str
    description: Optional[str] = None


# 建立 Item 時的 Model
class ItemCreate(ItemBase):
    pass


# 查詢 Item 時的 Model
class Item(ItemBase):
    id: int
    owner_id: int

    # 向 Pydantic 提供配置
    class Config:
        #  orm_mode 會告訴 Pydantic 模型讀取資料,即使它不是字典,而是 ORM 模型(或任何其他具有屬性的任意物件)
        orm_mode = True


class UserBase(BaseModel):
    email: str


class UserCreate(UserBase):
    password: str


class User(UserBase):
    id: int
    is_active: bool
    items: List[Item] = []

    class Config:
        orm_mode = True

 

ItemBase、UserBase

基類,宣告在建立或讀取資料時共有的屬性

 

ItemCreate、UserCreate

建立資料時使用的 Model

 

Item、User

讀取資料時使用的 Model

 

orm_mode

class Config:
   orm_mode = True
  • 這是一個 Pydantic 配置項
  • orm_mode 會告訴 Pydantic 模型讀取資料,即使它不是字典,而是 ORM 模型(或任何其他具有屬性的任意物件)
# 正常情況
id = data["id"]

# 還會嘗試從物件獲取屬性
id = data.id

設定了 orm_mode,Pydantic 模型與 ORM 就相容了,只需在路徑操作的 response_model 引數中宣告它即可

 

orm_mode 的技術細節

  • SQLAlchemy 預設情況下 lazy loading 懶載入,即需要獲取資料時,才會主動從資料庫中獲取對應的資料
  • 比如獲取屬性 current_user.items ,SQLAlchemy 會從 items 表中獲取該使用者的 item 資料,但在這之前不會主動獲取

 

如果沒有 orm_mode

  • 從路徑操作中返回一個 SQLAlchemy 模型,它將不會包括關係資料(比如 user 中有 item,則不會返回 item,後面再講實際的栗子)
  • 在 orm_mode 下,Pydantic 會嘗試從屬性訪問它要的資料,可以宣告要返回的特定資料,它甚至可以從 ORM 中獲取它

 

curd.py 程式碼

作用

  • 主要用來編寫與資料庫互動的函式,增刪改查,方便整個專案不同地方都能進行復用
  • 並且給這些函式新增專屬的單元測試

 

實際程式碼

程式碼只實現了查詢和建立

  1. 根據 id 查詢 user
  2. 根據 email 查詢 user
  3. 查詢所有 user
  4. 建立 user
  5. 查詢所有 item
  6. 建立 item
from sqlalchemy.orm import Session
from .models import User, Item
from .schemas import UserCreate, ItemCreate


# 根據 id 獲取 user
def get_user(db: Session, user_id: int):
    return db.query(User).filter(User.id == user_id).first()


# 根據 email 獲取 user
def get_user_by_email(db: Session, email: str):
    return db.query(User).filter(User.email == email).first()


# 獲取所有 user
def get_users(db: Session, size: int = 0, limit: int = 100):
    return db.query(User).offset(size).limit(limit).all()


# 建立 user,user 型別是 Pydantic Model
def create_user(db: Session, user: UserCreate):
    fake_hashed_password = user.password + "superpolo"
    # 1、使用傳進來的資料建立 SQLAlchemy Model 例項物件
    db_user = User(email=user.email, hashed_password=fake_hashed_password)
    # 2、將例項物件新增到資料庫會話 Session 中
    db.add(db_user)
    # 3、將更改提交到資料庫
    db.commit()
    # 4、重新整理例項,方便它包含來自資料庫的任何新資料,比如生成的 ID
    db.refresh(db_user)
    return db_user


# 獲取所有 item
def get_items(db: Session, size: int = 0, limit: int = 100):
    return db.query(Item).offset(size).limit(limit).all()


# 建立 item,item 型別是 Pydantic Model
def create_item(db: Session, item: ItemCreate, user_id: int):
    db_item = Item(**item.dict(), owner_id=user_id)
    db.add(db_item)
    db.commit()
    db.refresh(db_item)
    return db_item

 

create_user、create_item

函式內的操作步驟如下

# 1、使用傳進來的資料建立 SQLAlchemy Model 例項物件
db_user = User(email=user.email, hashed_password=fake_hashed_password)

# 2、將例項物件新增到資料庫會話 Session 中
db.add(db_user)

# 3、將更改提交到資料庫
db.commit()

# 4、重新整理例項,方便它包含來自資料庫的任何新資料,比如生成的 ID
db.refresh(db_user)

 

main.py 程式碼

from typing import List
import uvicorn
from fastapi import Depends, FastAPI, HTTPException, status, Path, Query, Body
from sqlalchemy.orm import Session
from models import Base
from schemas import User, UserCreate, ItemCreate, Item
from database import SessionLocal, engine
import curd

Base.metadata.create_all(bind=engine)

app = FastAPI()


# 依賴項,獲取資料庫會話物件
def get_db():
    db = SessionLocal()
    try:
        yield db
    finally:
        db.close()


# 建立使用者
@app.post("/users", response_model=User)
async def create_user(user: UserCreate, db: Session = Depends(get_db)):
    # 1、先查詢使用者是否有存在
    db_user = curd.get_user_by_email(db, user.email)
    if db_user:
        raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST, detail="user 已存在")
    res_user = curd.create_user(db, user)
    return res_user


# 根據 user_id 獲取使用者
@app.get("/user_id/{user_id}", response_model=User)
async def get_user(user_id: int = Path(...), db: Session = Depends(get_db)):
    return curd.get_user(db, user_id)


# 根據 email 獲取使用者
@app.get("/user_email/{email}", response_model=User)
async def get_user_by_email(email: str = Path(...), db: Session = Depends(get_db)):
    return curd.get_user_by_email(db, email)


# 獲取所有使用者
@app.get("/users_all/", response_model=List[User])
async def get_users(skip: int = Query(0),
                    limit: int = Query(100),
                    db: Session = Depends(get_db)):
    return curd.get_users(db, skip, limit)


# 建立 item
@app.post("/users/{user_id}/items", response_model=Item)
async def get_user_item(user_id: int = Path(...), item: ItemCreate = Body(...), db: Session = Depends(get_db)):
    return curd.create_user_item(db, item, user_id)


# 獲取所有 item
@app.get("/items/", response_model=List[Item])
async def get_items(skip: int = Query(0),
                    limit: int = Query(100),
                    db: Session = Depends(get_db)):
    return curd.get_items(db, skip, limit)


if __name__ == "__main__":
    uvicorn.run(app="main:app", host="127.0.0.1", port=8080, reload=True, debug=True)

 

依賴項

def get_db():
    db = SessionLocal()
    try:
        yield db
    finally:
        db.close()
  • 每個請求都有一個獨立的資料庫會話(SessionLocal)
  • 在請求完成後會自動關閉它
  • 然後下一個請求來的時候,會建立一個新會話

 

宣告依賴項

async def create_user(user: UserCreate, db: Session = Depends(get_db)) 
  • SessionLocal 是 sessionmaker() 建立的,是 SQLAlchemy Session 的代理
  • 通過宣告 db: Session ,IDE 就可以提供智慧程式碼提示啦

 

使用中介軟體 middleware 代替依賴項宣告資料庫會話

# 中介軟體
@app.middleware("http")
async def db_session_middleware(request: Request, call_next):
    # 預設響應
    response = Response("Internal server error", status_code=500)
    try:
        request.state.db = SessionLocal()
        response = await call_next(request)
    finally:
        # 關閉資料庫會話
        request.state.db.close()
    return response


# 依賴項,獲取資料庫會話物件
def get_db(request: Request):
    return request.state.db

 

request.state

  • request.state 是每個 Request 物件的一個屬性
  • 它用於儲存附加到請求本身的任意物件,例如本例中的資料庫會話 db
  • 也就是說,我不叫 db,叫 sqlite_db 也可以,只是一個屬性名

 

使用中介軟體 middleware 和使用 yield 的依賴項的區別

  • 中介軟體需要更多的程式碼,而且稍微有點複雜
  • 中介軟體必須是一個 async 函式,而且需要有 await 的程式碼,可能會阻塞程式並稍稍降低效能
  • 每個請求執行的時候都會先執行中介軟體,所以會為每個請求都建立一個資料庫連線,即使某個請求的路徑操作函式並不需要和資料庫互動

 

建議

  • 建立資料庫連線物件最好還是用帶有 yield 的依賴項來完成
  • 在其他使用場景也是,能滿足需求的前提下,最好用帶有 yield 的依賴項來完成

 

相關文章