官网文档:https://fastapi.tiangolo.com/zh/tutorial/sql-databases/
SQL (关系型) 数据库¶
FastAPI不需要你使用SQL(关系型)数据库。
但是您可以使用任何您想要的关系型数据库。
这里我们将看到一个使用SQLModel的示例。
SQLModel是在SQLAlchemy和Pydantic的基础上构建的。它是由FastAPI的同一作者制作的,与需要使用SQL数据库的FastAPI应用程序完美匹配。
小贴士
你可以使用任何其他你想要的SQL或NoSQL数据库库(在某些情况下称为“ORM”),FastAPI不会强迫你使用任何东西。
由于SQLModel基于SQLAlchemy,您可以轻松使用SQLAlchemi支持的任何数据库(这使得它们也受SQLModel支持)您可以很容易地将其调整为任何SQLAlchemy支持的数据库,如:
- PostgreSQL
- MySQL
- SQLite
- Oracle
- Microsoft SQL Server,等等其它数据库
在此示例中,我们将使用SQLite,因为它使用单个文件并且 在Python中具有集成支持。因此,您可以复制此示例并按原样来运行它。
稍后,对于您的产品级别的应用程序,您可能会要使用像PostgreSQL这样的数据库服务器。
Tip
这儿有一个FastAPI和PostgreSQL的官方项目生成器,全部基于Docker,包括前端和更多工具:https://github.com/tiangolo/full-stack-fastapi-postgresql
这是一个非常简单而简短的教程,如果你想了解数据库、SQL或更高级的功能,请参阅SQLModel文档。
安装SQLModel
首先,确保创建虚拟环境,激活它,然后安装sqlmodel:
pip install sqlmodel
Successfully installed SQLAlchemy-2.0.36 sqlmodel-0.0.22
使用单个模型创建应用程序
我们将首先使用单个SQLModel模型创建该应用程序最简单的第一个版本。
稍后,我们将通过以下多种型号来提高它的安全性和多功能性。🤓
from typing import Annotatedfrom fastapi import Depends, FastAPI, HTTPException, Query
from sqlmodel import Field, Session, SQLModel, create_engine, selectclass Hero(SQLModel, table=True):id: int | None = Field(default=None, primary_key=True)name: str = Field(index=True)age: int | None = Field(default=None, index=True)secret_name: strsqlite_file_name = "database.db"
sqlite_url = f"sqlite:///{sqlite_file_name}"connect_args = {"check_same_thread": False}
engine = create_engine(sqlite_url, connect_args=connect_args)def create_db_and_tables():SQLModel.metadata.create_all(engine)def get_session():with Session(engine) as session:yield sessionSessionDep = Annotated[Session, Depends(get_session)]app = FastAPI()@app.on_event("startup")
def on_startup():create_db_and_tables()@app.post("/heroes/")
def create_hero(hero: Hero, session: SessionDep) -> Hero:session.add(hero)session.commit()session.refresh(hero)return hero@app.get("/heroes/")
def read_heroes(session: SessionDep,offset: int = 0,limit: Annotated[int, Query(le=100)] = 100,
) -> list[Hero]:heroes = session.exec(select(Hero).offset(offset).limit(limit)).all()return heroes@app.get("/heroes/{hero_id}")
def read_hero(hero_id: int, session: SessionDep) -> Hero:hero = session.get(Hero, hero_id)if not hero:raise HTTPException(status_code=404, detail="Hero not found")return hero@app.delete("/heroes/{hero_id}")
def delete_hero(hero_id: int, session: SessionDep):hero = session.get(Hero, hero_id)if not hero:raise HTTPException(status_code=404, detail="Hero not found")session.delete(hero)session.commit()return {"ok": True}
创建模型
导入SQLModel并创建数据库模型:
from typing import Annotatedfrom fastapi import Depends, FastAPI, HTTPException, Query
from sqlmodel import Field, Session, SQLModel, create_engine, selectclass Hero(SQLModel, table=True):id: int | None = Field(default=None, primary_key=True)name: str = Field(index=True)age: int | None = Field(default=None, index=True)secret_name: str
🤓 其他版本和变体
Hero类与Pydantic模型非常相似(事实上,在下面,它实际上是一个Pydantic模式)。
存在一些差异:
- table=True告诉SQLModel这是一个表模型,它应该表示SQL数据库中的一个表,它不仅仅是一个数据模型(就像任何其他常规Pydantic类一样)。
- 字段(primary_key=True)告诉SQLModel id是SQL数据库中的主键(您可以在SQLModel文档中了解有关SQL主键的更多信息)。
- 通过将类型设置为int|None,SQLModel将知道该列在SQL数据库中应该是INTEGER,并且应该是NULLABLE。
- 字段(index=True)告诉SQLModel,它应该为该列创建SQL索引,这样在读取由该列筛选的数据时可以更快地在数据库中查找。
- SQLModel将知道声明为str的内容将是TEXT类型的SQL列(或VARCHAR,具体取决于数据库)。
创建引擎
SQLModel引擎(其下实际上是SQLAlchemy引擎)负责保存与数据库的连接。
您将有一个单一的引擎对象,用于所有代码连接到同一个数据库。
sqlite_file_name = "database.db"
sqlite_url = f"sqlite:///{sqlite_file_name}"connect_args = {"check_same_thread": False}
engine = create_engine(sqlite_url, connect_args=connect_args)
使用check_same_thread=False允许FastAPI在不同线程中使用相同的SQLite数据库。这是必要的,因为一个请求可能会使用多个线程(例如在依赖关系中)。
别担心,根据代码的结构方式,我们将确保稍后每个请求使用一个SQLModel会话,这实际上是check_same_thread试图实现的。
创建表格
然后,我们添加一个函数,该函数使用SQLModel.media.create_all(engine)为所有表模型创建表。
def create_db_and_tables():SQLModel.metadata.create_all(engine)
创建会话依赖关系
会话是将对象存储在内存中并跟踪数据中所需的任何更改,然后使用引擎与数据库通信。
我们将使用yield创建一个FastAPI依赖关系,为每个请求提供一个新的Session。这就是确保我们每个请求使用单个会话的原因。🤓
然后,我们创建一个带注释的依赖项SessionDep,以简化将使用此依赖项的其余代码。
def get_session():with Session(engine) as session:yield sessionSessionDep = Annotated[Session, Depends(get_session)]
启动时创建数据库表
我们将在应用程序启动时创建数据库表。
app = FastAPI()@app.on_event("startup")
def on_startup():create_db_and_tables()
在这里,我们在应用程序启动事件上创建表。
对于生产环境,您可能会使用在启动应用程序之前运行的迁移脚本。🤓
小贴士
SQLModel将有封装Alembic的迁移实用程序,但现在,您可以直接使用Alembic。
创建英雄库
因为每个SQLModel模型也是一个Pydantic模型,所以您可以在使用Pydantics模型的相同类型注释中使用它。
例如,如果你声明一个Hero类型的参数,它将从JSON正文中读取。
同样,您可以将其声明为函数的返回类型,然后数据的形状将显示在自动API文档UI中。
@app.post("/heroes/")
def create_hero(hero: Hero, session: SessionDep) -> Hero:session.add(hero)session.commit()session.refresh(hero)return hero
在这里,我们使用SessionDep依赖项(Session)将新的Hero添加到Session实例中,将更改提交到数据库中,刷新Hero中的数据,然后返回它。
读英雄库
我们可以使用select()从数据库中读取Heros。我们可以包含一个限制和偏移量来对结果进行分页。
@app.get("/heroes/")
def read_heroes(session: SessionDep,offset: int = 0,limit: Annotated[int, Query(le=100)] = 100,
) -> list[Hero]:heroes = session.exec(select(Hero).offset(offset).limit(limit)).all()return heroes
读一个英雄条目
我们可以读一个英雄。
@app.get("/heroes/{hero_id}")
def read_hero(hero_id: int, session: SessionDep) -> Hero:hero = session.get(Hero, hero_id)if not hero:raise HTTPException(status_code=404, detail="Hero not found")return hero
如果不是英雄:
引发HTTPException(状态码=404,详细信息=“未找到英雄”)
删除英雄
我们也可以删除英雄。
@app.delete("/heroes/{hero_id}")
def delete_hero(hero_id: int, session: SessionDep):hero = session.get(Hero, hero_id)if not hero:raise HTTPException(status_code=404, detail="Hero not found")session.delete(hero)session.commit()return {"ok": True}
运行应用程序
您可以运行该应用程序:
fastapi-dev-main.py
然后转到/docs UI,您将看到FastAPI正在使用这些模型来记录API,它也将使用它们来序列化和验证数据。
使用多个模型更新应用程序
现在,让我们稍微重构一下这个应用程序,以提高安全性和多功能性。
如果你查看之前的应用程序,在UI中你可以看到,到目前为止,它让客户端决定要创建的英雄的id。😱
我们不应该让这种情况发生,他们可能会覆盖我们已经在数据库中分配的id。决定id应该由后端或数据库完成,而不是由客户端完成。
此外,我们为英雄创建了一个secret_name,但到目前为止,我们到处都在返回它,这不是什么秘密。。。😅
我们将通过添加一些额外的模型来解决这些问题。SQLModel将在这里大放异彩。✨
源代码:
from typing import Annotatedfrom fastapi import Depends, FastAPI, HTTPException, Query
from sqlmodel import Field, Session, SQLModel, create_engine, selectclass HeroBase(SQLModel):name: str = Field(index=True)age: int | None = Field(default=None, index=True)class Hero(HeroBase, table=True):id: int | None = Field(default=None, primary_key=True)secret_name: strclass HeroPublic(HeroBase):id: intclass HeroCreate(HeroBase):secret_name: strclass HeroUpdate(HeroBase):name: str | None = Noneage: int | None = Nonesecret_name: str | None = Nonesqlite_file_name = "database.db"
sqlite_url = f"sqlite:///{sqlite_file_name}"connect_args = {"check_same_thread": False}
engine = create_engine(sqlite_url, connect_args=connect_args)def create_db_and_tables():SQLModel.metadata.create_all(engine)def get_session():with Session(engine) as session:yield sessionSessionDep = Annotated[Session, Depends(get_session)]
app = FastAPI()@app.on_event("startup")
def on_startup():create_db_and_tables()@app.post("/heroes/", response_model=HeroPublic)
def create_hero(hero: HeroCreate, session: SessionDep):db_hero = Hero.model_validate(hero)session.add(db_hero)session.commit()session.refresh(db_hero)return db_hero@app.get("/heroes/", response_model=list[HeroPublic])
def read_heroes(session: SessionDep,offset: int = 0,limit: Annotated[int, Query(le=100)] = 100,
):heroes = session.exec(select(Hero).offset(offset).limit(limit)).all()return heroes@app.get("/heroes/{hero_id}", response_model=HeroPublic)
def read_hero(hero_id: int, session: SessionDep):hero = session.get(Hero, hero_id)if not hero:raise HTTPException(status_code=404, detail="Hero not found")return hero@app.patch("/heroes/{hero_id}", response_model=HeroPublic)
def update_hero(hero_id: int, hero: HeroUpdate, session: SessionDep):hero_db = session.get(Hero, hero_id)if not hero_db:raise HTTPException(status_code=404, detail="Hero not found")hero_data = hero.model_dump(exclude_unset=True)hero_db.sqlmodel_update(hero_data)session.add(hero_db)session.commit()session.refresh(hero_db)return hero_db@app.delete("/heroes/{hero_id}")
def delete_hero(hero_id: int, session: SessionDep):hero = session.get(Hero, hero_id)if not hero:raise HTTPException(status_code=404, detail="Hero not found")session.delete(hero)session.commit()return {"ok": True}
创建多个模型
在SQLModel中,任何具有table=True的模型类都是表模型。
任何没有table=True的模型类都是数据模型,这些模型实际上只是Pydantic模型(带有一些小的额外功能)。🤓
使用SQLModel,我们可以使用继承来避免在所有情况下复制所有字段。
HeroBase-基类
让我们从一个HeroBase模型开始,该模型包含所有模型共享的所有字段:
名称
年龄
name
age
class HeroBase(SQLModel):name: str = Field(index=True)age: int | None = Field(default=None, index=True)
英雄-桌子模型
然后,让我们创建Hero,即实际的表模型,其中包含其他模型中并不总是包含的额外字段:
身份证件
秘密名称
id
secret_name
因为Hero继承自HeroBase,所以它也有在HeroBase中声明的字段,所以Hero的所有字段都是:
身份证件
名称
年龄
秘密名称
id
name
age
secret_name
class HeroBase(SQLModel):name: str = Field(index=True)age: int | None = Field(default=None, index=True)class Hero(HeroBase, table=True):id: int | None = Field(default=None, primary_key=True)secret_name: str
HeroPublic-公共数据模型
接下来,我们创建一个HeroPublic模型,该模型将返回给API的客户端。
它具有与HeroBase相同的字段,因此不包括secret_name。
最后,我们英雄的身份得到了保护!🥷
它还重新声明id:int。通过这样做,我们与API客户端签订了合同,这样他们就可以总是期望id在那里并且是int(永远不会是None)。
小贴士
让返回模型确保一个值总是可用的,并且总是int(而不是None)对API客户端非常有用,他们可以编写更简单的具有这种确定性的代码。
此外,自动生成的客户端将具有更简单的接口,因此与您的API通信的开发人员可以更好地使用您的API。😎
HeroPublic中的所有字段都与HeroBase中的相同,id声明为int(不是None):
身份证件
名称
年龄
秘密名称
id
name
age
secret_name
class HeroBase(SQLModel):name: str = Field(index=True)age: int | None = Field(default=None, index=True)class Hero(HeroBase, table=True):id: int | None = Field(default=None, primary_key=True)secret_name: strclass HeroPublic(HeroBase):id: int
HeroCreate-创建英雄的数据模型
现在我们创建一个HeroCreate模型,这个模型将验证来自客户端的数据。
它具有与HeroBase相同的字段,还具有secret_name。
现在,当客户端创建一个新英雄时,他们将发送secret_name,它将存储在数据库中,但这些秘密名称不会在API中返回给客户端。
小贴士
这就是你处理密码的方式。接收它们,但不要在API中返回它们。
您还可以在存储密码之前对其值进行哈希运算,切勿以纯文本形式存储。
HeroCreate的字段包括:
名称
年龄
秘密名称
name
age
secret_name
class HeroBase(SQLModel):name: str = Field(index=True)age: int | None = Field(default=None, index=True)class Hero(HeroBase, table=True):id: int | None = Field(default=None, primary_key=True)secret_name: strclass HeroPublic(HeroBase):id: intclass HeroCreate(HeroBase):secret_name: str
HeroUpdate-更新英雄的数据模型
在之前的应用程序版本中,我们没有更新英雄的方法,但现在有了多个模型,我们可以做到。🎉
HeroUpdate数据模型有点特殊,它具有创建新英雄所需的所有相同字段,但所有字段都是可选的(它们都有一个默认值)。这样,当你更新英雄时,你可以只发送你想要更新的字段。
因为所有字段实际上都发生了变化(类型现在包括None,它们现在的默认值为None),我们需要重新声明它们。
我们真的不需要从HeroBase继承,因为我们正在重新声明所有字段。为了保持一致性,我会让它继承,但这不是必需的。这更多的是个人品味的问题。🤷
HeroUpdate的字段包括:
名称
年龄
秘密名称
name
age
secret_name
class HeroUpdate(HeroBase):name: str | None = Noneage: int | None = Nonesecret_name: str | None = None
使用HeroCreate创建并返回一个HeroPublic
现在我们有了多个模型,我们可以更新应用程序中使用它们的部分。
我们在请求中接收HeroCreate数据模型,并从中创建Hero表模型。
这个新的表模型Hero将具有客户端发送的字段,并且还将具有数据库生成的id。
然后,我们返回与函数中相同的表模型Hero。但是,当我们使用HeroPublic数据模型声明response_model时,FastAPI将使用HeroPublic来验证和序列化数据。
@app.post("/heroes/", response_model=HeroPublic)
def create_hero(hero: HeroCreate, session: SessionDep):db_hero = Hero.model_validate(hero)session.add(db_hero)session.commit()session.refresh(db_hero)return db_hero
小贴士
现在我们使用response_model=HeroPublic而不是返回类型注释->HeroPublic,因为我们返回的值实际上不是HeroPublic。
如果我们声明了->HeroPublic,你的编辑和linter会抱怨(这是理所当然的)你返回的是Hero而不是HeroPublic。
通过在response_model中声明它,我们告诉FastAPI去做它的事情,而不会干扰类型注释以及编辑器和其他工具的帮助。
用HeroPublic阅读英雄
我们可以像以前一样读取Heros,同样,我们使用response_model=list[HeroPublic]来确保数据被正确验证和序列化。
@app.get("/heroes/", response_model=list[HeroPublic])
def read_heroes(session: SessionDep,offset: int = 0,limit: Annotated[int, Query(le=100)] = 100,
):heroes = session.exec(select(Hero).offset(offset).limit(limit)).all()return heroes
与HeroPublic一起阅读《一个英雄》
我们可以读一个英雄:
@app.get("/heroes/{hero_id}", response_model=HeroPublic)
def read_hero(hero_id: int, session: SessionDep):hero = session.get(Hero, hero_id)if not hero:raise HTTPException(status_code=404, detail="Hero not found")return hero
使用HeroUpdate更新英雄
我们可以更新英雄。为此,我们使用HTTP PATCH操作。
在代码中,我们得到一个包含客户端发送的所有数据的字典,只有客户端发送的数据,不包括任何仅作为默认值的值。为此,我们使用exclude_unset=True。这是主要的伎俩。🪄
然后,我们使用hero_db.sqlmodel_update(hero_data)用hero_da中的数据更新hero_db。
@app.patch("/heroes/{hero_id}", response_model=HeroPublic)
def update_hero(hero_id: int, hero: HeroUpdate, session: SessionDep):hero_db = session.get(Hero, hero_id)if not hero_db:raise HTTPException(status_code=404, detail="Hero not found")hero_data = hero.model_dump(exclude_unset=True)hero_db.sqlmodel_update(hero_data)session.add(hero_db)session.commit()session.refresh(hero_db)return hero_db
再次删除英雄
删除英雄几乎是一样的。
我们不会满足在这个项目中重构所有内容的愿望。😅
@app.delete("/heroes/{hero_id}")
def delete_hero(hero_id: int, session: SessionDep):hero = session.get(Hero, hero_id)if not hero:raise HTTPException(status_code=404, detail="Hero not found")session.delete(hero)session.commit()return {"ok": True}
再次运行应用程序
您可以再次运行该应用程序:
fastapi dev main.py
输出信息:Uvicorn正在运行http://127.0.0.1:8000(按CTRL+C退出)
如果你转到/docs API UI,你会看到它现在已经更新,并且它不会期望在创建英雄时从客户端接收id,等等。
回顾
您可以使用SQLModel与SQL数据库交互,并使用数据模型和表模型简化代码。
您可以在SQLModel文档中了解更多信息,其中有一个关于使用SQLModel和FastAPI的较长迷你教程。🚀
实践
安装SQLModel
首先,确保创建虚拟环境,激活它,然后安装sqlmodel:
pip install sqlmodel
源代码
存储文件到sql.py
from typing import Annotatedfrom fastapi import Depends, FastAPI, HTTPException, Query
from sqlmodel import Field, Session, SQLModel, create_engine, selectclass HeroBase(SQLModel):name: str = Field(index=True)age: int | None = Field(default=None, index=True)class Hero(HeroBase, table=True):id: int | None = Field(default=None, primary_key=True)secret_name: strclass HeroPublic(HeroBase):id: intclass HeroCreate(HeroBase):secret_name: strclass HeroUpdate(HeroBase):name: str | None = Noneage: int | None = Nonesecret_name: str | None = Nonesqlite_file_name = "database.db"
sqlite_url = f"sqlite:///{sqlite_file_name}"connect_args = {"check_same_thread": False}
engine = create_engine(sqlite_url, connect_args=connect_args)def create_db_and_tables():SQLModel.metadata.create_all(engine)def get_session():with Session(engine) as session:yield sessionSessionDep = Annotated[Session, Depends(get_session)]
app = FastAPI()@app.on_event("startup")
def on_startup():create_db_and_tables()@app.post("/heroes/", response_model=HeroPublic)
def create_hero(hero: HeroCreate, session: SessionDep):db_hero = Hero.model_validate(hero)session.add(db_hero)session.commit()session.refresh(db_hero)return db_hero@app.get("/heroes/", response_model=list[HeroPublic])
def read_heroes(session: SessionDep,offset: int = 0,limit: Annotated[int, Query(le=100)] = 100,
):heroes = session.exec(select(Hero).offset(offset).limit(limit)).all()return heroes@app.get("/heroes/{hero_id}", response_model=HeroPublic)
def read_hero(hero_id: int, session: SessionDep):hero = session.get(Hero, hero_id)if not hero:raise HTTPException(status_code=404, detail="Hero not found")return hero@app.patch("/heroes/{hero_id}", response_model=HeroPublic)
def update_hero(hero_id: int, hero: HeroUpdate, session: SessionDep):hero_db = session.get(Hero, hero_id)if not hero_db:raise HTTPException(status_code=404, detail="Hero not found")hero_data = hero.model_dump(exclude_unset=True)hero_db.sqlmodel_update(hero_data)session.add(hero_db)session.commit()session.refresh(hero_db)return hero_db@app.delete("/heroes/{hero_id}")
def delete_hero(hero_id: int, session: SessionDep):hero = session.get(Hero, hero_id)if not hero:raise HTTPException(status_code=404, detail="Hero not found")session.delete(hero)session.commit()return {"ok": True}
启动服务
执行命令:
fastapi dev sql.py
执行后显示:
INFO Importing from /Users/skywalk/work/fastapi ╭─ Python module file ─╮ │ │ │ 🐍 sql.py │ │ │ ╰──────────────────────╯ INFO Importing module sql
INFO Found importable FastAPI app ╭─ Importable FastAPI app ─╮ │ │ │ from sql import app │ │ │ ╰──────────────────────────╯ INFO Using import string sql:app ╭────────── FastAPI CLI - Development mode ───────────╮ │ │ │ Serving at: http://127.0.0.1:8000 │ │ │ │ API docs: http://127.0.0.1:8000/docs │ │ │ │ Running in development mode, for production use: │ │ │ │ fastapi run │ │ │ ╰─────────────────────────────────────────────────────╯ INFO: Will watch for changes in these directories: ['/Users/xxx/work/fastapi']
INFO: Uvicorn running on http://127.0.0.1:8000 (Press CTRL+C to quit)
INFO: Started reloader process [37935] using WatchFiles
INFO: Started server process [37941]
INFO: Waiting for application startup.
INFO: Application startup complete.
测试
浏览docs页面:
执行curl添加指令
curl -X 'POST' \'http://127.0.0.1:8000/heroes/' \-H 'accept: application/json' \-H 'Content-Type: application/json' \-d '{"name": "string","age": 0,"secret_name": "string"
}'
输出:
{"name":"string","age":0,"id":2}
证明一条信息被添加
查看一下:
curl http://127.0.0.1:8000/heroes/
[{"name":"string","age":0,"id":1},{"name":"string","age":0,"id":2}]
果然多了一条信息。