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 |
|
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 程式碼
作用
- 主要用來編寫與資料庫互動的函式,增刪改查,方便整個專案不同地方都能進行復用
- 並且給這些函式新增專屬的單元測試
實際程式碼
程式碼只實現了查詢和建立
- 根據 id 查詢 user
- 根據 email 查詢 user
- 查詢所有 user
- 建立 user
- 查詢所有 item
- 建立 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 的依賴項來完成