Pythonライブラリ(SQL):SQLModel(応用編-FastAPI)
1.概要
前回記事ではSQLModelの基礎編を紹介しました。本記事ではアプリ化する時のファイル構成やFastAPIとの連動を紹介します。
【公式Docs】
2.パッケージのファイル構成
本章では公式で紹介しているパッケージの作成方法を紹介します。
なおパッケージをターミナルから実行する場合は"-m"の引数が必要です。
2-1.CASE1:モデルを1モジュールで管理
DBテーブルのモデルを1つのモジュールのみで管理するパッケージのファイル構成および注意点は下記の通りです。
2-1-1.パッケージ構成
2-1-2.パッケージ作成時の注意点
パッケージ作成時の注意点は下記の通りです。
2-1-3.サンプルコード
[__init__.py] ※空ファイル
[app.py]
from sqlmodel import Session
from .database import create_db_and_tables, engine
from .models import Hero, Team
def create_heroes():
with Session(engine) as session:
team_z_force = Team(name="Z-Force", headquarters="Sister Margaret’s Bar")
hero_deadpond = Hero(name="Deadpond", secret_name="Dive Wilson", team=team_z_force)
session.add(hero_deadpond)
session.commit()
session.refresh(hero_deadpond)
print("Created hero:", hero_deadpond)
print("Hero's team:", hero_deadpond.team)
def main():
create_db_and_tables() #事前にmodelsからDBテーブルクラスのimportが必要
create_heroes()
if __name__ == "__main__":
main()
[database.py]
from sqlmodel import SQLModel, create_engine
sqlite_file_name = "database.db"
sqlite_url = f"sqlite:///{sqlite_file_name}"
engine = create_engine(sqlite_url) #DB接続情報
def create_db_and_tables():
SQLModel.metadata.create_all(engine) #DBファイル・テーブル作成
[models.py]
from typing import List, Optional
from sqlmodel import Field, Relationship, SQLModel
class Team(SQLModel, table=True):
id: Optional[int] = Field(default=None, primary_key=True)
name: str = Field(index=True)
headquarters: str
heroes: List["Hero"] = Relationship(back_populates="team") #HeroはString
class Hero(SQLModel, table=True):
id: Optional[int] = Field(default=None, primary_key=True)
name: str = Field(index=True)
secret_name: str
age: Optional[int] = Field(default=None, index=True)
team_id: Optional[int] = Field(default=None, foreign_key="team.id") #外部キー
team: Optional[Team] = Relationship(back_populates="heroes") #Teamはclass
ターミナルで下記を実行するとDBファイルが作成されます。
[Terminal]
python -m project.app
2-2.CASE2:モデルを複数モジュールで管理
次にDBテーブルのモデルを複数のモジュールで管理するパッケージのファイル構成および注意点は下記の通りです。
2-2-1.パッケージ構成
2-2-2.パッケージ作成時の注意点
パッケージ作成時の注意点は下記の通りです。
【Circular importsについて】
今回のモジュール(heroとteam)ではそれぞれ属性としてお互いのクラスを参照しているため循環参照のような形となりPythonではエラーが発生する現象です。よってSQLModelのコード内には工夫が必要です。
2-2-3.サンプルコード
モジュールで実行すると2回目以降にエラーがでるためテーブルクラスに"__table_args__ = {'extend_existing': True}"を追加しました。
[__init__.py] ※空ファイル
[app.py]
from sqlmodel import Session
from .database import create_db_and_tables, engine
from .models import Hero, Team
def create_heroes():
with Session(engine) as session:
team_z_force = Team(name="Z-Force", headquarters="Sister Margaret’s Bar")
hero_deadpond = Hero(name="Deadpond", secret_name="Dive Wilson", team=team_z_force)
session.add(hero_deadpond)
session.commit()
session.refresh(hero_deadpond)
print("Created hero:", hero_deadpond)
print("Hero's team:", hero_deadpond.team)
def main():
create_db_and_tables() #事前にmodelsからDBテーブルクラスのimportが必要
create_heroes()
if __name__ == "__main__":
main()
[database.py]
from sqlmodel import SQLModel, create_engine
sqlite_file_name = "database.db"
sqlite_url = f"sqlite:///{sqlite_file_name}"
engine = create_engine(sqlite_url) #DB接続情報
def create_db_and_tables():
SQLModel.metadata.create_all(engine) #DBファイル・テーブル作成
[team_model.py]
from typing import TYPE_CHECKING, List, Optional
from sqlmodel import Field, Relationship, SQLModel
if TYPE_CHECKING:
from .hero_model import Hero #Circular Importsによるエラー防止
class Team(SQLModel, table=True):
__table_args__ = {'extend_existing': True} #moduleで実行したあとに残るメタデータのエラー防止
id: Optional[int] = Field(default=None, primary_key=True)
name: str = Field(index=True)
headquarters: str
heroes: List["Hero"] = Relationship(back_populates="team") #型ヒントのclassは文字列で指定
[hero_model.py]
from typing import TYPE_CHECKING, Optional
from sqlmodel import Field, Relationship, SQLModel
if TYPE_CHECKING:
from .team_model import Team #Circular Importsによるエラー防止
class Hero(SQLModel, table=True):
__table_args__ = {'extend_existing': True} #moduleで実行したあとに残るメタデータのエラー防止
id: Optional[int] = Field(default=None, primary_key=True) #主キー制約
name: str = Field(index=True)
secret_name: str
age: Optional[int] = Field(default=None, index=True) #Nullable
team_id: Optional[int] = Field(default=None, foreign_key="team.id") #外部キー制約
team: Optional["Team"] = Relationship(back_populates="heroes") #型ヒントのclassは文字列で指定
出力は前節と同じとなります。
3.SQLModel×FastAPI:基礎知識編
3-1.FastAPIのおさらい
FastAPIを使用してSQLModelとの連動をしていきます。FastAPIの記事は作成済みですが備忘録として一部下記に抜粋しました。
3-2.テーブルモデルクラスの構成:Baseクラスの継承
構成は下記の通りとなります。
3-3.FastAPIのDependency機能:with構文の省略
今までsessionの起動/停止を実行するためにwith構文を使用したがFastAPIのDependency機能を使用することでシンプルな記載に変更できます(公式)。
通常とDependency機能のコード比較は下記の通りです。
[IN ※通常コード]k
from typing import List, Optional
from fastapi import FastAPI, HTTPException, Query
from sqlmodel import Field, Session, SQLModel, create_engine, select
class HeroBase(SQLModel):
name: str = Field(index=True)
secret_name: str
age: Optional[int] = Field(default=None, index=True)
class Hero(HeroBase, table=True):
id: Optional[int] = Field(default=None, primary_key=True)
class HeroCreate(HeroBase):
pass
engine = create_engine('sqlite:///:memory:', echo=True, connect_args={"check_same_thread": False})
def create_db_and_tables():
SQLModel.metadata.create_all(engine)
app = FastAPI()
@app.on_event("startup")
def on_startup():
create_db_and_tables()
@app.post("/heroes/", response_model=HeroRead)
def create_hero(hero: HeroCreate):
with Session(engine) as session:
db_hero = Hero.from_orm(hero)
session.add(db_hero)
session.commit()
session.refresh(db_hero)
return db_hero
[IN ※Dependency]
from typing import List, Optional
from fastapi import Depends, FastAPI, HTTPException, Query #Dependency使用=Depends
from sqlmodel import Field, Session, SQLModel, create_engine, select
class HeroBase(SQLModel):
name: str = Field(index=True)
secret_name: str
age: Optional[int] = Field(default=None, index=True)
class Hero(HeroBase, table=True):
id: Optional[int] = Field(default=None, primary_key=True)
class HeroCreate(HeroBase):
pass
def create_db_and_tables():
SQLModel.metadata.create_all(engine)
def get_session(): #Dependency機能追加
with Session(engine) as session:
yield session
app = FastAPI()
@app.on_event("startup")
def on_startup():
create_db_and_tables()
@app.post("/heroes/", response_model=HeroRead)
def create_hero(*, session: Session = Depends(get_session), hero: HeroCreate):
db_hero = Hero.from_orm(hero)
session.add(db_hero)
session.commit()
session.refresh(db_hero)
return db_hero
4.SQLModel×FastAPI:実践編-1テーブル
本章から実際のコードを作成していきます。
4-1.DB構成
DB構成は下記の通りです。
4-2.全コード
全コードおよび要点は下記の通りです。
[main1.py]
from typing import List, Optional
from fastapi import Depends, FastAPI, HTTPException, Query
from sqlmodel import Field, Session, SQLModel, create_engine, select
#DBテーブルモデルの定義
#継承用ベースモデル
class HeroBase(SQLModel):
name: str = Field(index=True)
secret_name: str
age: Optional[int] = Field(default=None, index=True)
#テーブルモデル用クラス
class Hero(HeroBase, table=True):
id: Optional[int] = Field(default=None, primary_key=True)
class HeroCreate(HeroBase): #idはDB側で自動生成するため不要(HeroBaseをそのまま継承)
pass
class HeroRead(HeroBase):
id: int #idに型ヒントをつけることでAPIドキュメントが明確になる
class HeroUpdate(SQLModel): #継承はHeroBaseではなくSQLModel
name: Optional[str] = None #1つの属性作成時に他属性がNoneとなるためOptionalをつける
secret_name: Optional[str] = None
age: Optional[int] = None
#engine作成
sqlite_file_name = "220504_noteSQLModel.db"
sqlite_url = f"sqlite:///{sqlite_file_name}"
connect_args = {"check_same_thread": False}
engine = create_engine(sqlite_url, echo=True, connect_args=connect_args)
def create_db_and_tables():
SQLModel.metadata.create_all(engine)
#with構文省略のためのFastAPI Dependency機能
def get_session():
with Session(engine) as session:
yield session
#FastAPIのアプリ作成
app = FastAPI()
#FastAPI起動時の動作設定
@app.on_event("startup")
def on_startup():
create_db_and_tables()
#POSTメソッド
@app.post("/heroes/", response_model=HeroRead)
def create_hero(*, session: Session = Depends(get_session), hero: HeroCreate):
db_hero = Hero.from_orm(hero) #JSONデータ(hero)からオブジェクト作成するため.from_orm()メソッドを使用
session.add(db_hero)
session.commit()
session.refresh(db_hero) #DBからオブジェクトデータを再取得
return db_hero
#GETメソッド
@app.get("/heroes/", response_model=List[HeroRead])
def read_heroes(*,session: Session = Depends(get_session),
offset: int = 0, limit: int = Query(default=100, lte=100),): #limitは初期値=100かつ、制限:100以下
heroes = session.exec(select(Hero).offset(offset).limit(limit)).all() #出力はList(response_modelに記載の通り)
return heroes
@app.get("/heroes/{hero_id}", response_model=HeroRead)
def read_hero(*, session: Session = Depends(get_session), hero_id: int):
hero = session.get(Hero, hero_id) #session.get()でprimary_keyを直接指定して取得
if not hero: #heroが空の場合
raise HTTPException(status_code=404, detail="Hero not found")
return hero
#UPDATE(PUT)メソッド
@app.patch("/heroes/{hero_id}", response_model=HeroRead)
def update_hero(*, session: Session = Depends(get_session), hero_id: int, hero: HeroUpdate):
#更新したいデータを取得
db_hero = session.get(Hero, hero_id)
if not db_hero: #db_heroが空の場合
raise HTTPException(status_code=404, detail="Hero not found")
hero_data = hero.dict(exclude_unset=True) #Pydanticの機能で{"name": None,"secret_name": None,"age": None,} から値があるkeyだけ残してNoneは削除
for key, value in hero_data.items():
setattr(db_hero, key, value) #db_heroのkey属性にvalueをセット(db_hero.key = valueと同義->keyは既に存在するため更新になる)
session.add(db_hero)
session.commit()
session.refresh(db_hero)
return db_hero
#DELETEメソッド
@app.delete("/heroes/{hero_id}")
def delete_hero(*, session: Session = Depends(get_session), hero_id: int):
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の起動(+APIドキュメント移動)は下記で実行しました。
[Terminal ※開発中のため--reload記載]
uvicorn main1:app --reload
4-3.HTTPクライアントでAPIを叩く:requests
FastAPIを起動したらHTTPクライアントでCRUD操作を実施してみます。
[IN ※新しくJUpyter Notebookなどで処理]
import requests
import json
root_api = "http://127.0.0.1:8000" #開発中のためlocalhostのIPアドレス指定
#登録用データ※for文で回したいためListにまとめて記載
heroes =[{'name':"Deadpond",
'secret_name':"Dive Wilson"}, #id=1, age=None
{'name':"Spider-Boy",
'secret_name':"Pedro Parqueador"}, #id=2, age=None
{'name':"Rusty-Man",
'secret_name':"Tommy Sharp",
'age':48}] #id=3
#CREATE操作
for hero in heroes:
res = requests.post(root_api + "/heroes/", data=json.dumps(hero))
if res.status_code==200:
print('データ登録:', res.json())
#READ操作
res = requests.get(root_api + "/heroes/")
if res.status_code == 200:
print(res.json())
[OUT] ※CREATE側のprintは省略
[{'name': 'Deadpond', 'secret_name': 'Dive Wilson', 'age': None, 'id': 1},
{'name': 'Spider-Boy', 'secret_name': 'Pedro Parqueador', 'age': None, 'id': 2},
{'name': 'Rusty-Man', 'secret_name': 'Tommy Sharp', 'age': 48, 'id': 3}]
後述のAPIドキュメント(GET)で確認すると下記の通りとなります。
4-4.APIドキュメント確認
FastAPI起動後にブラウザで「http://127.0.0.1:8000/docs」に接続するとAPIドキュメントを確認することができます。
4-3-1.API-Docs:HTTPメソッド
APIドキュメントのdefaultより下記が確認できます。
4-3-2.API-Docs:Schemas
Schemas(データモデル)より下記が確認できます。
5.SQLModel×FastAPI:実践編-複数テーブル
5-1.DB構成
5-2.全コード
全コード、前章コードからの変更点および要点は下記の通りです。
[main2.py]
from typing import List, Optional
from fastapi import Depends, FastAPI, HTTPException, Query
from sqlmodel import Field, Relationship, Session, SQLModel, create_engine, select
#DBテーブルモデルの定義
#継承用ベースモデル
class TeamBase(SQLModel):
name: str = Field(index=True)
headquarters: str
class Team(TeamBase, table=True):
id: Optional[int] = Field(default=None, primary_key=True)
heroes: List["Hero"] = Relationship(back_populates="team") #先に記載するためList[String]
class TeamCreate(TeamBase):
pass
class TeamRead(TeamBase):
id: int
class TeamUpdate(SQLModel):
id: Optional[int] = None
name: Optional[str] = None
headquarters: Optional[str] = None
#継承用ベースモデル
class HeroBase(SQLModel):
name: str = Field(index=True)
secret_name: str
age: Optional[int] = Field(default=None, index=True)
team_id: Optional[int] = Field(default=None, foreign_key="team.id") #外部キー
class Hero(HeroBase, table=True):
id: Optional[int] = Field(default=None, primary_key=True)
team: Optional[Team] = Relationship(back_populates="heroes") #Teamの後にくるためOptional[class]
class HeroCreate(HeroBase): #idはDB側で自動生成するため不要(HeroBaseをそのまま継承)
pass
class HeroRead(HeroBase):
id: int #idに型ヒントをつけることでAPIドキュメントが明確になる
class HeroUpdate(SQLModel): #継承はHeroBaseではなくSQLModel
name: Optional[str] = None #1つの属性作成時に他属性がNoneとなるためOptionalをつける
secret_name: Optional[str] = None
age: Optional[int] = None
team_id: Optional[int] = None
#属性(Relationship())込みのAPI
class HeroReadWithTeam(HeroRead):
team: Optional[TeamRead] = None #属性が無限ループしないため型ヒント設置
class TeamReadWithHeroes(TeamRead):
heroes: List[HeroRead] = [] #属性が無限ループしないため型ヒント設置
#engine作成
sqlite_file_name = "220504_noteSQLModel2Tbl.db"
sqlite_url = f"sqlite:///{sqlite_file_name}"
connect_args = {"check_same_thread": False}
engine = create_engine(sqlite_url, echo=True, connect_args=connect_args)
def create_db_and_tables():
SQLModel.metadata.create_all(engine)
#with構文省略のためのFastAPI Dependency機能
def get_session():
with Session(engine) as session:
yield session
#FastAPIのアプリ作成
app = FastAPI()
@app.on_event("startup")
def on_startup():
create_db_and_tables()
#HeroテーブルのCRUD操作
#POSTメソッド
@app.post("/heroes/", response_model=HeroRead)
def create_hero(*, session: Session = Depends(get_session), hero: HeroCreate):
db_hero = Hero.from_orm(hero)
session.add(db_hero)
session.commit()
session.refresh(db_hero)
return db_hero
#GETメソッド
@app.get("/heroes/", response_model=List[HeroRead])
def read_heroes(*, session: Session = Depends(get_session),
offset: int = 0, limit: int = Query(default=100, lte=100),):
heroes = session.exec(select(Hero).offset(offset).limit(limit)).all()
return heroes
@app.get("/heroes/{hero_id}", response_model=HeroReadWithTeam)
def read_hero(*, session: Session = Depends(get_session), hero_id: int):
hero = session.get(Hero, hero_id)
if not hero:
raise HTTPException(status_code=404, detail="Hero not found")
return hero
#UPDATE(PUT)メソッド
@app.patch("/heroes/{hero_id}", response_model=HeroRead)
def update_hero(*, session: Session = Depends(get_session), hero_id: int, hero: HeroUpdate):
db_hero = session.get(Hero, hero_id) #hero_idを元にDBから取得
if not db_hero:
raise HTTPException(status_code=404, detail="Hero not found")
hero_data = hero.dict(exclude_unset=True) #不要なNoneデータを削除
for key, value in hero_data.items():
setattr(db_hero, key, value) #オブジェクト内のデータ置換
session.add(db_hero)
session.commit()
session.refresh(db_hero)
return db_hero
#DELETEメソッド
@app.delete("/heroes/{hero_id}")
def delete_hero(*, session: Session = Depends(get_session), hero_id: int):
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}
#TeamテーブルのCRUD操作
@app.post("/teams/", response_model=TeamRead)
def create_team(*, session: Session = Depends(get_session), team: TeamCreate):
db_team = Team.from_orm(team)
session.add(db_team)
session.commit()
session.refresh(db_team)
return db_team
#GETメソッド
@app.get("/teams/", response_model=List[TeamRead])
def read_teams(*, session: Session = Depends(get_session),
offset: int = 0, limit: int = Query(default=100, lte=100),):
teams = session.exec(select(Team).offset(offset).limit(limit)).all()
return teams
@app.get("/teams/{team_id}", response_model=TeamReadWithHeroes)
def read_team(*, team_id: int, session: Session = Depends(get_session)):
team = session.get(Team, team_id)
if not team:
raise HTTPException(status_code=404, detail="Team not found")
return team
#UPDATE(PUT)メソッド
@app.patch("/teams/{team_id}", response_model=TeamRead)
def update_team(*, session: Session = Depends(get_session),
team_id: int, team: TeamUpdate,):
db_team = session.get(Team, team_id)
if not db_team:
raise HTTPException(status_code=404, detail="Team not found")
team_data = team.dict(exclude_unset=True)
for key, value in team_data.items():
setattr(db_team, key, value)
session.add(db_team)
session.commit()
session.refresh(db_team)
return db_team
#DELETEメソッド
@app.delete("/teams/{team_id}")
def delete_team(*, session: Session = Depends(get_session), team_id: int):
team = session.get(Team, team_id)
if not team:
raise HTTPException(status_code=404, detail="Team not found")
session.delete(team)
session.commit()
return {"ok": True}
5-3.HTTPクライアントでAPIを叩く:requests
FastAPIを起動したらHTTPクライアントでCRUD操作を実施してみます。
[IN]
import requests
import json
root_api = "http://127.0.0.1:8000" #開発中のためlocalhostのIPアドレス指定
#登録用データ※for文で回したいためListにまとめて記載
heroes =[{'name':"Deadpond",
'secret_name':"Dive Wilson"}, #id=1, age=None
{'name':"Spider-Boy",
'secret_name':"Pedro Parqueador"}, #id=2, age=None
{'name':"Rusty-Man",
'secret_name':"Tommy Sharp",
'age':48}] #id=3
teams= [{'name':"Preventers",
'headquarters':"Sharp Tower"}, #id=1
{'name':"Z-Force",
'headquarters':"Sister Margaret’s Bar"}] #id=2
#CREATE操作
for hero in heroes:
res = requests.post(root_api + "/heroes/", data=json.dumps(hero))
if res.status_code==200:
print('データ登録(Hero):', res.json())
for teams in teams:
res = requests.post(root_api + "/teams/", data=json.dumps(teams))
if res.status_code==200:
print('データ登録(Team):', res.json())
#READ操作
res_hero = requests.get(root_api + "/heroes/")
res_team = requests.get(root_api + "/teams/")
if res_hero.status_code == 200:
print(res_hero.json())
if res_team.status_code == 200:
print(res_team.json())
[OUT]
データ登録(Hero): {'name': 'Deadpond', 'secret_name': 'Dive Wilson', 'age': None, 'team_id': None, 'id': 1}
データ登録(Hero): {'name': 'Spider-Boy', 'secret_name': 'Pedro Parqueador', 'age': None, 'team_id': None, 'id': 2}
データ登録(Hero): {'name': 'Rusty-Man', 'secret_name': 'Tommy Sharp', 'age': 48, 'team_id': None, 'id': 3}
データ登録(Team): {'name': 'Preventers', 'headquarters': 'Sharp Tower', 'id': 1}
データ登録(Team): {'name': 'Z-Force', 'headquarters': 'Sister Margaret’s Bar', 'id': 2}
[{'name': 'Deadpond', 'secret_name': 'Dive Wilson', 'age': None, 'team_id': None, 'id': 1}, {'name': 'Spider-Boy', 'secret_name': 'Pedro Parqueador', 'age': None, 'team_id': None, 'id': 2}, {'name': 'Rusty-Man', 'secret_name': 'Tommy Sharp', 'age': 48, 'team_id': None, 'id': 3}]
[{'name': 'Preventers', 'headquarters': 'Sharp Tower', 'id': 1}, {'name': 'Z-Force', 'headquarters': 'Sister Margaret’s Bar', 'id': 2}]
5-4.APIドキュメント確認
5-4-1.API-Docs:HTTPメソッド
APIドキュメントのdefaultより下記が確認できます。
5-4-2.Schemas
Schemas(データモデル)より下記が確認できます。
参考記事
あとがき
SQLModelを実際に使っていきたいけどどのタイミングで取り入れるかは要検討かな。
この記事が気に入ったらサポートをしてみませんか?