FastAPI可以和任何数据库和任意样式的库配合使用,这里看一下使用SQLAlchemy的示例。下面的示例很容易的调整为PostgreSQLMySQLSQLiteOracle等。当前示例中我们使用SQLite

ORM对象关系映射

FastAPI可以与任何数据库在任何样式的库中一起与数据库进行通信。一种常见的模式就是“ORM”:对象关系映射。所谓对象关系映射就是,ORM具有在代码和数据库表(“关系型”)中的对象中间转换(“映射”)的工具。即使用ORM,在SQL数据库创建一个代表映射的类,该类的每个数据代表数据表中的一个列,具有名称和类型。约定俗成:类使用大写如Pet,代表SQL为表的pets,该累的每个实例对象标识数据库中的一行数据。比如一个对象orion_cat(Pet的一个实例)可以有一个属性orion_cat.type, 对标数据库中的type列。并且该属性的值可以是其它,例如"cat"

文件结构

假设有个fastapi_sqlalchemy的项目,其下有一个包为sql_app,结构如下所示:

fastapi_sqlalchemy
├── sql_app
│   ├── __init__.py
│   ├── crud.py
│   ├── database.py
│   ├── main.py
│   ├── models.py
│   ├── requirements.txt
│   └── schemas.py
└── sql_app.db

既然是sql_app包,肯定少不了__init__.py,这里的初始化函数为空。其中依赖requirement.txt中为

fastapi==0.111.0
pydantic==2.7.4
SQLAlchemy==2.0.30

📢📢:这里使用的SQLAlchemy 为2.0以上版本,如果使用v1,则有些语法不同。

创建SQLAlchemy部件

首先看一下sql_app/database.py

创建SQLAlchemy引擎和初始化Base类

# @File : database.py
from sqlalchemy import create_engine
from sqlalchemy.ext.declarative import declarative_base
from sqlalchemy.orm import sessionmaker

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

# 来允许SQLite这样
engine = create_engine(
    SQLALCHEMY_DATABASE_URL, connect_args={"check_same_thread": False}
)

SessionLocal = sessionmaker(autocommit=False, autoflush=False, bind=engine)

# 用于建立数据库SQLAlchemy模型models
Base = declarative_base()


创建一个SQLAlchemy 引擎,其中connect_args={“check_same_thread”: False} 仅用于SQLite,其他数据库不需要 。

默认情况下,SQLite 只允许一个线程与其通信,假设有多个线程的话,也只将处理一个独立的请求。这是为了防止意外地为不同的事物(不同的请求)共享相同的连接。但是在 FastAPI 中,使用普通函数(def)时,多个线程可以为同一个请求与数据库交互,所以我们需要使用connect_args={“check_same_thread”: False}来让SQLite允许这样。

此外,我们将确保每个请求都在依赖项中获得自己的数据库连接会话,因此不需要该默认机制。

连接到一个SQLite数据库,该文件在当前目录下的sql_app.db中

创建SessionLocal的时候,每个 SessionLocal 的类的实例都是一个数据库会话,当然该类本身还不是数据库会话。但是一旦我们创建了一个SessionLocal类的实例,这个实例将是实际的数据库会话。我们将它命名为SessionLocal是为了将它与我们从 SQLAlchemy 导入的Session区别开来。稍后我们将使用Session(从 SQLAlchemy 导入的那个)。要创建SessionLocal类,请使用函数sessionmaker

Base = declarative_base(),在后面的会继承这个Base类,来创建每个数据库模型或者类(ORM模型)

创建数据库模型models

在文件sql_app/models.py中,使用上一步创建的Base类派生子类来创建SQLAlchemy模型。
📢📢:

  1. SQLAlchemy中的“模型”指的的是和数据库交互的类和实例
  2. Pydantic中的“模型”指的是不同的东西,即数据验证、转换以及文档类和实例
# @File : models.py


from sql_app.database import Base

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


class User(Base):
    __tablename__ = "users"

    id = Column(Integer, primary_key=True)  # primary_key 为True标识主键
    email = Column(String, unique=True, index=True)  # unique 如果为True表示这列不允许出现重复的值,index=True为这列创建索引,提升查询效率
    hashed_password = Column(String)
    is_active = Column(Boolean, default=True)  # default=True 为这列设置默认值

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


class Item(Base):
    __tablename__ = "items"

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

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

  1. 这个__tablename__属性是用来告诉 SQLAlchemy 要在数据库中为每个模型使用的数据库表的名称
  2. 使用Column来表示 SQLAlchemy 中的默认值。传递一个 SQLAlchemy “类型”,如Integer、String和Boolean,它定义了数据库中的类型,作为参数,以及其他限定。这些属性中的每一个都代表其相应数据库表中的一列
  3. 关系或者说内联外联的关系,使用relationship,当访问 user 中的属性items时,如 中my_user.items,它将有一个ItemSQLAlchemy 模型列表(来自items表),这些模型具有指向users表中此记录的外键。当您访问my_user.items时,SQLAlchemy 实际上会从items表中的获取一批记录并在此处填充进去。同样,当访问 Item中的属性owner时,它将包含表中的UserSQLAlchemy 模型users。使用owner_id属性/列及其外键来了解要从users表中获取哪条记录

创建Pydantic模型

接下来看看sql_app/shchemas.py
📢📢:为了避免 SQLAlchemy模型和 Pydantic模型之间的混淆,我们将有models.py(SQLAlchemy 模型的文件)和schemas.py( Pydantic 模型的文件)。这些 Pydantic 模型或多或少地定义了一个“schema”(一个有效的数据形状)。因此,这将帮助我们在使用两者时避免混淆。

创建初始化Pydantic模式/模型

创建一个ItemBaseUserBase Pydantic模型(或者我们说“schema”),他们拥有创建或读取数据时具有的共同属性。然后创建一个继承自他们的ItemCreateUserCreate,并添加创建时所需的其他数据(或属性)。因此在创建时也应当有一个password属性。但是为了安全起见,password不会出现在其他同类 Pydantic模型中,例如通过API读取一个用户数据时,它不应当包含在内。

# @File : schemas.py
from pydantic import BaseModel


class ItemBase(BaseModel):
    title: str
    description: str | None = None


class UserBase(BaseModel):
    email: str


class ItemCreate(ItemBase):
    pass


class Item(ItemBase):
    id: int
    owner_id: int

    class Config:
        # orm_mode = True
        from_attributes = True


class UserCreate(UserBase):
    password: str


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

    class Config:
        # orm_mode = True
        # 解决pydantic/_internal/_config.py:334: UserWarning: Valid config keys have changed in V2:
        # * 'orm_mode' has been renamed to 'from_attributes'
        from_attributes = True

先介绍下Config类的作用

    class Config:
        # orm_mode = True
        from_attributes = True

如果是Pydantic 是v1版本,则使用orm=True, 本机环境安装的是v2+版本。这个类是一个Pydantic配置项。orm_mode或者from_attributes 会告诉Pydantic模型读取数据,而不仅仅是字典,而是一个ORM模型(或者说一个具有属性的对象)。这样,而不是仅仅试图从dict上 id 中获取值,如下所示:

id = data["id"]

它还会尝试从属性中获取它,如:

id = data.id

有了这个,Pydantic模型与 ORM 兼容,您只需在路径操作response_model的参数中声明它即可。将能够返回一个数据库模型,它将从中读取数据

📢📢:SQLAlchemy 风格和 Pydantic 风格¶
请注意,SQLAlchemy模型使用 =来定义属性,并将类型作为参数传递给Column,例如:

name = Column(String)

虽然 Pydantic模型使用: 声明类型,但新的类型注释语法/类型提示是:

name: str

请牢记这一点,这样您在使用:还是=时就不会感到困惑

CRUD工具

现在让我们看看文件sql_app/crud.py。在这个文件中,我们将编写可重用的函数用来与数据库中的数据进行交互。

CRUD分别为:增加(Create)、查询(Read)、更改(Update)、删除(Delete),即增删改查。

…虽然在这个例子中我们只是新增和查询。

读取数据¶
sqlalchemy.orm中导入Session,这将允许您声明db参数的类型,并在您的函数中进行更好的类型检查和完成。导入之前的models(SQLAlchemy 模型)和schemas(Pydantic模型/模式)。

创建一些工具函数来完成:

  • 通过 ID 和电子邮件查询单个用户。
  • 查询多个用户。
  • 查询多个项目
# @File : crud.py
from sqlalchemy.orm import Session

from sql_app import models, schemas


def get_user(db: Session, user_id: int):
    return db.query(models.User).filter(models.User.id == user_id).first()


def get_user_by_email(db: Session, email: str):
    return db.query(models.User).filter(models.User.email == email).first()


def get_users(db: Session, skip: int = 0, limit: int = 100):
    return db.query(models.User).offset(skip).limit(limit).all()


def create_user(db: Session, user: schemas.UserCreate):
    fake_hashed_password = user.password + "notreallyhashed"
    db_user = models.User(email=user.email, hashed_password=fake_hashed_password)
    db.add(db_user)
    db.commit()
    db.refresh(db_user)
    return db_user


def get_items(db: Session, skip: int = 0, limit: int = 100):
    return db.query(models.Item).offset(skip).limit(limit).all()


def create_user_item(db: Session, item: schemas.ItemCreate, user_id: int):
    db_item = models.Item(**item.dict(), owner_id=user_id)
    db.add(db_item)
    db.commit()
    db.refresh(db_item)
    return db_item

📢📢:Tip

这里不是将每个关键字参数传递给Item并从Pydantic模型中读取每个参数,而是先生成一个字典,其中包含Pydantic模型的数据:

item.dict()

然后我们将dict的键值对 作为关键字参数传递给 SQLAlchemy Item:

Item(**item.dict())

然后我们传递 Pydantic模型未提供的额外关键字参数owner_id:

Item(**item.dict(), owner_id=user_id)

因此创建数据的步骤即为:
现在创建工具函数来创建数据。

  1. 使用您的数据创建一个 SQLAlchemy 模型实例。
  2. 使用add来将该实例对象添加到数据库会话。
  3. 使用commit来将更改提交到数据库(以便保存它们)。
  4. 使用refresh来刷新您的实例对象(以便它包含来自数据库的任何新数据,例如生成的 ID)

FastAPI应用程序

现在在sql_app/main.py文件中 让我们集成和使用我们之前创建的所有其他部分。

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

from sql_app import schemas, crud, models
from sql_app.database import SessionLocal, engine

models.Base.metadata.create_all(bind=engine)

app = FastAPI()


# Dependency
def get_db():
    db = SessionLocal()
    try:
        yield db
    finally:
        db.close()


@app.post("/users/", response_model=schemas.User)
def create_user(user: schemas.UserCreate, db: Session = Depends(get_db)):
    db_user = crud.get_user_by_email(db, email=user.email)
    if db_user:
        raise HTTPException(status_code=400, detail="Email already registered")
    return crud.create_user(db=db, user=user)


@app.get("/users/", response_model=list[schemas.User])
def read_users(skip: int = 0, limit: int = 100, db: Session = Depends(get_db)):
    users = crud.get_users(db, skip=skip, limit=limit)
    return users


@app.get("/users/{user_id}", response_model=schemas.User)
def read_user(user_id: int, db: Session = Depends(get_db)):
    db_user = crud.get_user(db, user_id=user_id)
    if db_user is None:
        raise HTTPException(status_code=404, detail="User not found")
    return db_user


@app.post("/users/{user_id}/items/", response_model=schemas.Item)
def create_item_for_user(
    user_id: int, item: schemas.ItemCreate, db: Session = Depends(get_db)
):
    return crud.create_user_item(db=db, item=item, user_id=user_id)


@app.get("/items/", response_model=list[schemas.Item])
def read_items(skip: int = 0, limit: int = 100, db: Session = Depends(get_db)):
    items = crud.get_items(db, skip=skip, limit=limit)
    return items

现在使用我们在sql_app/database.py文件中创建的SessionLocal来创建依赖项。我们需要每个请求有一个独立的数据库会话/连接(SessionLocal),在整个请求中使用相同的会话,然后在请求完成后关闭它。然后将为下一个请求创建一个新会话。为此,我们将创建一个包含yield的依赖项,正如前面关于Dependencies with yield的部分中所解释的那样。我们的依赖项将创建一个新的 SQLAlchemy SessionLocal,它将在单个请求中使用,然后在请求完成后关闭它。

📢📢:参数db实际上是 type SessionLocal,但是这个类(用 创建sessionmaker())是 SQLAlchemy 的“代理” Session,所以,编辑器并不真正知道提供了哪些方法。

但是通过将类型声明为Session,编辑器现在可以知道可用的方法(.add()、.query()、.commit()等)并且可以提供更好的支持(比如完成)。类型声明不影响实际对象。

运行

# 执行命令
% uvicorn sql_app.main:app --reload

在这里插入图片描述
打开浏览器:http://127.0.0.1:8000/docs
在这里插入图片描述

Logo

腾讯云面向开发者汇聚海量精品云计算使用和开发经验,营造开放的云计算技术生态圈。

更多推荐