Upgrade to Pro — share decks privately, control downloads, hide ads and more …

SQLModel入門〜クエリと型〜

Sponsored · Ship Features Fearlessly Turn features on and off without deploys. Used by thousands of Ruby developers.
Avatar for mizzsugar mizzsugar
September 27, 2024

 SQLModel入門〜クエリと型〜

Avatar for mizzsugar

mizzsugar

September 27, 2024
Tweet

More Decks by mizzsugar

Other Decks in Programming

Transcript

  1. 目次 • SQLModelとは • 基本的なモデルの書き方 • スキーマ管理 • 基本的なクエリの書き方 •

    外部キーを伴うモデル、クエリの書き方 • Multiple Model • Multiple Modelおまけ 3
  2. 目次 • SQLModelとは • 基本的なモデルの書き方 • スキーマ管理           ←省略 •

    基本的なクエリの書き方 • 外部キーを伴うモデル、クエリの書き方 ←つまづきやすい • Multiple Model ←推したい • Multiple Modelおまけ 8
  3. SQLModelとは > 概要 • SQLModelは、FastAPIの作者によって開発されたPythonのORMライブ ラリ。 • SQLAlchemyとPydanticを基盤として構築されており、両者の強みを組 み合わせてデータベース操作と型安全を提供。 ◦

    発表者個人としての印象は、 モデルのテーブルへの紐づけとクエリは SQLAlchemy 静的型付けのお作法は Pydantic • FastAPIとの親和性が高く、データモデルの定義からWebAPIの構築ま で一貫して行えるため、高速な開発が可能。 ◦ SQLModelのクエリメソッドの返り値は Pydanticを継承しているので、 FastAPIのレスポンスにそ のまま使える。 ◦ FastAPI由来のOpenAPIの自動生成機能も使用できる。 https://sqlmodel.tiangolo.com 13
  4. SQLModelとは > SQLAlchemyでのモデル定義例 14 from sqlalchemy import Integer, String from

    sqlalchemy.orm import declarative_base, Mapped, mapped_column Base = declarative_base() class Hero(Base): id: Mapped[int] = mapped_column(Integer, primary_key=True) name: Mapped[str] = mapped_column(String, nullable=False) secret_name: Mapped[str] = mapped_column(String, nullable=False) age: Mapped[int | None] = mapped_column(Integer, nullable=True)
  5. SQLModelとは > SQLModelでのモデル定義例 15 from pydantic import BaseModel, Field class

    Hero(BaseModel): id: int | None = Field(default=None) name: str secret_name: str age: int | None = None
  6. SQLModelとは > SQLModelのモデルはPydanticを兼ねる 16 from sqlmodel import Field, SQLModel class

    Hero(SQLModel, table=True): id: int | None = Field(default=None, primary_key=True) name: str secret_name: str age: int | None = None FastAPIでレスポンス用に Pydanticのモデルを作る必 要なし
  7. SQLModelとは > セッション・クエリの比較 17 from sqlalchemy import create_engine from sqlalchemy.ext.declarative

    import declarative_base from sqlalchemy.orm import Session Base = declarative_base() engine = create_engine(sqlite_url, echo=True) Base.metadata.create_all(engine) hero_1 = Hero(name="Deadpond", secret_name="Dive Wilson") with Session(engine) as session: session.add(hero_1) session.commit()
  8. SQLModelとは > セッション・クエリの比較 18 from sqlmodel import SQLModel, create_engine from

    sqlmodel import Session engine = create_engine(sqlite_url, echo=True) SQLModel.metadata.create_all(engine) hero_1 = Hero(name="Deadpond", secret_name="Dive Wilson") with Session(engine) as session: session.add(hero_1) session.commit()
  9. SQLModelとは > まとめ 19 • モデル定義はPydanticのお作法。 ◦ モデル定義はPydantic+DBのために主キーなどの内容。 ◦ dataclassやPydanticのような簡潔な書き方。

    • クエリはSQLAlchemyと同じ。 • Pydanticと同じ簡潔なモデル定義とSQLAlchemyの豊富なクエリメソッド を使えるいいとこ取りがSQLModel。
  10. 基本的なモデルの書き方 > Heroモデル > table=True 21 from sqlmodel import Field,

    SQLModel class Hero(SQLModel, table=True): id: int | None = Field( default=None, primary_key=True) name: str secret_name: str age: int | None = None table=Trueを指定することに よってテーブルとの紐付けが 行われる。 table=Trueのモデルに対して のみ、session.add()などのク エリを実行できる。 デフォルトではtable=Falseで あり、単純なデータモデルとな る。
  11. 基本的なモデルの書き方 > Heroモデル > __tablename__ 22 from sqlmodel import Field,

    SQLModel class Hero(SQLModel, table=True): __tablename__ = "heroes" id: int | None = Field( default=None, primary_key=True) name: str secret_name: str age: int | None = None テーブル名を指定。 __tablename__なしだとモデル名 をスネークケースにした名前にな る。勝手に複数形にはならない。 Hero→hero TeamLink→team_link
  12. 基本的なモデルの書き方 > カラムの型ヒント 23 from sqlmodel import Field, SQLModel class

    Hero(SQLModel, table=True): __tablename__ = "heroes" id: int | None = Field(default=None, primary_key=True) name: str secret_name: str age: Optional[int] = None このテーブルは自動でidを生成され ることを期待する。 主キーがNullであるテーブルはあり えない。 なぜOptionalなのか?
  13. 基本的なモデルの書き方 > Optionalではなくintでは 24 from sqlmodel import Field, SQLModel class

    Hero(SQLModel, table=True): id: int = Field(primary_key=True) name: str secret_name: str age: Optional[int] = None
  14. 基本的なモデルの書き方 > Optionalにしないといけない理由 25 from sqlalchemy.orm import Session hero_1 =

    Hero(name="Deadpond", secret_name="Dive Wilson") with Session(engine) as session: session.add(hero_1) session.commit() idがないので型違反というエラーに なる。 しかし、自動でidを生成したいので 指定するわけにもいかない。
  15. 基本的なモデルの書き方 > やはりOptional 26 from sqlmodel import Field, SQLModel class

    Hero(SQLModel, table=True): id: int | None = Field(default=None, primary_key=True) name: str secret_name: str age: Optional[int] = None Createする時、通常主キー(特に 自動増分の場合)はデータベース によって自動的に割り当てられる。 この時点では、オブジェクトの主 キーの値はまだ存在しないため、 Noneとして扱えるようにする必要 がある。 読み取りのように必ずIDに値があること を保証したい時に困るじゃないか?とい う疑問の解決策はSQModelの機能に あるので後ほど紹介💡
  16. 基本的なモデルの書き方 > autoincrementを無効化なら非Optional 27 from sqlmodel import Field, SQLModel class

    Hero(SQLModel, table=True): id: int = Field(default=None, primary_key=True, autoincrement=False) name: str secret_name: str age: Optional[int] = None 主キーを自前で採番したい場合、 autoincrement=Falseを指定して型ヒントか ら | Noneを外す。 SQLModelでは、デフォルトで autoincrement=Trueである。
  17. スキーマ管理 > alembic 31 $ pip install alembic $ alembic

    --version alembic 1.13.2 $ alembic init migrations alembic init <フォルダ名> で環境を初期化。alembic.iniと migrations/ディレクトリが作成され る。 ※仮想環境に入っている前提です。
  18. スキーマ管理 > alembic 33 from sqlmodel import SQLModel from your_app

    import models # モデルを定義しているファイル … target_metadata = SQLModel.metadata SQLModelのメタデータを使用する ように設定 migrations/env.py
  19. スキーマ管理 > alembic 34 $ alembic revision -m "init" alembic

    revision -m <ファイル名> でマイグレーションファイルを生成。 migrations/versions/ 以下にマイグレー ションファイルが生成される。
  20. スキーマ管理 > alembic 35 """init Revision ID: e1addcb32a11 Revises: Create

    Date: 2024-09-07 13:43:24.530589 """ from typing import Sequence, Union from alembic import op import sqlalchemy as sa # revision identifiers, used by Alembic. revision: str = 'e1addcb32a11' down_revision: Union[str, None] = None branch_labels: Union[str, Sequence[str], None] = None depends_on: Union[str, Sequence[str], None] = None def upgrade() -> None: pass def downgrade() -> None: pass migrations/versions/e1addcb32a11_init.py upgrade()とdowngrade()にマイグ レーションの処理を追加。 downgrade()にはロールバック処理 を書く。
  21. スキーマ管理 > alembic 36 def upgrade(): op.create_table('hero', sa.Column('id', sa.Integer(), nullable=False),

    sa.Column('name', sa.String(), nullable=False), sa.Column('secret_name', sa.String(), nullable=False), sa.Column('age', sa.Integer(), nullable=True), sa.PrimaryKeyConstraint('id') ) migrations/versions/e1addcb32a11_init.py
  22. スキーマ管理 > alembic 38 $ alembic upgrade head alembic upgrade

    head でマイグレーションが実行され、 DBに反映される。
  23. スキーマ管理 > alembic 39 $ alembic revision -m "add_team" TeamモデルとHeroモデルに

    team_idを追加するマイグレーション ファイルを作成する。
  24. スキーマ管理 > alembic 40 """add_team Revision ID: 35dd08d1a0ea Revises: e1addcb32a11

    Create Date: 2024-09-07 14:13:47.315964 """ from typing import Sequence, Union from alembic import op import sqlalchemy as sa revision: str = '35dd08d1a0ea' down_revision: Union[str, None] = 'e1addcb32a11' branch_labels: Union[str, Sequence[str], None] = None depends_on: Union[str, Sequence[str], None] = None def upgrade() -> None: pass def downgrade() -> None: pass ロールバックした際にどのバージョン に戻るか。 migrations/versions/e1addcb32a1 1_init.pyのe1addcb32a11の部分。 自動で入力されている。
  25. スキーマ管理 > alembic 41 def upgrade(): # Create team table

    op.create_table('team', sa.Column('id', sa.Integer(), nullable=False), sa.Column('name', sa.String(), nullable=False), sa.Column('headquarters', sa.String(), nullable=True), sa.PrimaryKeyConstraint('id') ) op.add_column('hero', sa.Column('team_id', sa.Integer(), nullable=True)) op.create_foreign_key('fk_hero_team_id', 'hero', 'team', ['team_id'], ['id'])
  26. スキーマ管理 > alembic 42 def downgrade(): # Remove team_id from

    hero table op.drop_constraint('fk_hero_team_id', 'hero', type_='foreignkey') op.drop_column('hero', 'team_id') # Drop team table op.drop_table('team')
  27. スキーマ管理 > alembic 43 alembic upgrade head alembic downgrade -1

    # 直前のバージョンに戻る alembic downgrade e1addcb32a11 # バージョンを指定して戻る alembic downgrade base # 最初の状態(マイグレーション適用前)に戻 る
  28. 基本的なクエリの書き方 > 基本的なデータの作成方法 46 hero_1 = Hero(name="Deadpond", secret_name="Dive Wilson") with

    Session(engine) as session: session.add(hero_1) print("Hero1: ", hero_1) >> Hero1: id=None name='Deadpond' secret_name='Dive Wilson' age=None session.commit() print("Hero1: ", hero_1) >> Hero1:        内部的に期限切れとして認識されて おり、refreshされるまではNoneが 返される。 Noneだから何も表示され ていない。
  29. 基本的なクエリの書き方 > 基本的なデータの作成方法 47 … session.commit() print("Hero1: ", hero_1) >>

    Hero1: session.refresh(hero_1) print("Hero1: ", hero_1) >> age=None id=1 name='Deadpond' secret_name='Dive Wilson' エンジンがデータベースと通信してhero_1 の 最近のデータを取得し、セッションは最新デー タを hero_1に入れる。その後hero_1にアクセ スすると「期限切れではない」と認識されデータ が表示される。
  30. 基本的なクエリの書き方 > 基本的なデータの取得方法 48 from sqlmodel import Session, select def

    select_heroes_by_name(name: str) -> list[Hero]: with Session(engine) as session: statement = select(Hero).where(Hero.name == name) heroes: list[Hero] = session.exec(statement).all() return heroes ここでクエリを構築 SELECT * FROM heroes WHER heroes.name = {name}
  31. 基本的なクエリの書き方 > 基本的なデータの取得方法 49 from sqlmodel import Session, select def

    select_heroes_by_name(name: str) -> list[Hero]: with Session(engine) as session: statement = select(Hero).where(Hero.name == name) heroes: list[Hero] = session.exec(statement).all() return heroes session.exec()でクエリを実行 session.exec()までだとResultというイテ レータオブジェクトを返却
  32. 基本的なクエリの書き方 > 基本的なデータの取得方法 50 from sqlmodel import Session, select def

    select_heroes_by_name(name: str) -> list[Hero]: with Session(engine) as session: statement = select(Hero).where(Hero.name == name) heroes: list[Hero] = session.exec(statement).all() return heroes Resultクラスに組み込まれたall()メソッドを 実行すると クエリ実行結果のすべてをlistで返却
  33. 基本的なクエリの書き方 > 基本的なデータの取得方法 51 from sqlmodel import Session, select def

    select_hero_by_name(name: str) -> Hero | None: with Session(engine) as session: statement = select(Hero).where(Hero.name == name) hero: Hero | None = session.exec(statement).first() return hero first()にするとすべての結果のうち先頭のデータを返す。 結果がない場合はNoneを返す。
  34. 基本的なクエリの書き方 > 基本的なデータの取得方法 52 from sqlmodel import Session, select def

    select_hero_by_id(id: int) -> Hero: with Session(engine) as session: statement = select(Hero).where(Hero.id == id) hero: Hero= session.exec(statement).one() return hero one()の場合、結果が存在しないか複数存在す るとエラーになる。 主キーやユニークキーなど一意に特定できる 条件に使うと良い。 と、個人的には思うけど SQLModelチュートリアルだとユニークキーではない nameでのHero取得でone()を使っているのであくまで個人的な考え。
  35. 基本的なクエリの書き方 > 基本的なデータの取得方法 53 def get_hero_age_distribution(session: Session = Depends(get_session)) ->

    ??: with Session(engine) as session: query = ( select( Hero.age, func.count(Hero.id).label("count") ).group_by(Hero.age) ) return session.exec(query).all() SQLAlchemyのfunc.countとgroup_by を使う。 Heroモデルはtable=Trueで SQLAlchemyを継承するのでidとageは SQLAlchemyのColumnの機能を使うこ とができる。 SELECT age, COUNT(id) as count FROM hero GROUP BY age;
  36. 基本的なクエリの書き方 > 基本的なデータの取得方法 54 def get_hero_age_distribution(session: Session = Depends(get_session)) ->

    ??: with Session(engine) as session: query = ( select( Hero.age, func.count(Hero.id).label("count") ).group_by(Hero.age) ) return session.exec(query).all() ageとcountしか返さないので、Hero を返り値の定義に使えない。 ageとcountのみから成り立つ新しい モデルを定義する必要がある。
  37. 基本的なクエリの書き方 > 基本的なデータの取得方法 56 def get_hero_age_distribution(session: Session = Depends(get_session)) ->

    AgeDistribution: with Session(engine) as session: query = ( select( Hero.age, func.count(Hero.id).label("count") ).group_by(Hero.age) ) return session.exec(query).all()
  38. 基本的なクエリの書き方 > 基本的なデータの取得方法 57 def get_hero_age_distribution(session: Session = Depends(get_session)) ->

    AgeDistribution: with Session(engine) as session: query = ( select( Hero.age, func.count(Hero.id).label("count") ).group_by(Hero.age) ) return session.exec(query).all() mypyを実行するとこの2行で func.count()とgroup_by()にint型オブジェ クトは渡せないというエラーになる(実行は できる)
  39. 基本的なクエリの書き方 > 基本的なデータの取得方法 58 def get_hero_age_distribution(session: Session = Depends(get_session)) ->

    AgeDistribution: with Session(engine) as session: query = ( select( Hero.age, func.count(Hero.id).label("count") ).group_by(Hero.age) ) return session.exec(query).all() 実際はSQLAlchemyのColumnの動 きをしているが、Heroモデルの型定 義ではintと定義しているので乖離が 起きる。
  40. 基本的なクエリの書き方 > 基本的なデータの取得方法 59 def get_hero_age_distribution(session: Session = Depends(get_session)) ->

    AgeDistribution: hero_id_column: Column = Hero.id # type: ignore hero_age_column: Column = Hero.age # type: ignore with Session(engine) as session: query = ( select( Hero.age, func.count(hero_id_column).label("count") ).group_by(hero_age_column)) Hero.idとHero.ageはColumn型であると明示し てmypyにその2つはfunc.countとgroup_byに 渡せると認識させる。 モデルで定義しているint型と乖離しているので type: ignoreする。
  41. 基本的なクエリの書き方 > 基本的なデータの更新方法 60 def update_hero() -> Hero: with Session(engine)

    as session: statement = select(Hero).where(Hero.id == 1) hero = session.exec(statement).one() hero.age = 16 session.add(hero) session.commit() session.refresh(hero) hero commit()後にheroオブジェクトにア クセスしたい場合は必ずrefresh()を 実行すること。
  42. 基本的なクエリの書き方 > 基本的なデータの削除方法 61 with Session(engine) as session: statement =

    select(Hero).where(Hero.name == "Spider") hero = session.exec(statement).one() session.delete(hero) session.commit() print(hero) >> age=None id=1 name='Deadpond' secret_name='Dive Wilson' hero = session.exec(statement).first() print("Deleted hero:", hero) >>                クエリを再発行して再取得すると 表示されない。 heroの内容が表示される。 データがDBになくセッションに接続されていないので、「期 限切れ」と認識されずそのままでいるので、オブジェクト内 のデータにアクセスできる。
  43. 外部キーを伴うモデル、クエリの書き方 > 外部キーを伴うモデルの書き方 66 class Team(SQLModel, table=True): __tablename__ = "team"

    id: int | None = Field(default=None, primary_key=True) name: str = Field(index=True) headquarters: str class Hero(SQLModel, table=True): … team_id: int | None = Field(foreign_key="team.id") 「モデル名.アトリビュート名」ではなく 「テーブル名.カラム名」 を定義すること。
  44. 外部キーを伴うモデル、クエリの書き方 > 外部キーを伴うモデルの書き方 67 from sqlmodel import Field, Relationship, SQLModel

    class Team(SQLModel, table=True): … heroes: list["Hero"] = Relationship(back_populates="team") class Hero(SQLModel, table=True): … team_id: int | None = Field(foreign_key="team.id") team: Team | None = Relationship(back_populates="heroes")
  45. 外部キーを伴うモデル、クエリの書き方 > 外部キーを伴うモデルの書き方 68 from sqlmodel import Field, Relationship, SQLModel

    class Team(SQLModel, table=True): … heroes: list["Hero"] = Relationship(back_populates="team") class Hero(SQLModel, table=True): … team_id: int | None = Field(foreign_key="team.id") team: Team | None = Relationship(back_populates="heroes")
  46. 外部キーを伴うモデル、クエリの書き方 > back_populatesとは? back_populatesがないとどうなるか? 70 class Team(SQLModel, table=True): ... heroes:

    list["Hero"] = Relationship() class Hero(SQLModel, table=True): ... team_id: int | None = Field(foreign_key="team.id") team: Team | None = Relationship()
  47. 外部キーを伴うモデル、クエリの書き方 > back_populatesとは? > back_populatesがないとどうな るか? ここまでは問題なし。しかし… 71 with Session(engine)

    as session: preventers_team = session.exec( select(Team).where(Team.name == "Preventers") ).one() print("Preventers Team Heroes:", preventers_team.heroes) >> Preventers Team Heroes: [ Hero(name='Rusty, age=48, id=2, secret_name='Tom', team_id=2), Hero(name='Spider-Boy', age=None, id=3, secret_name='Pedro Parqueador', team_id=2), Hero(name='Tarantula', age=32, id=6, secret_name='Natalia', team_id=2),] ここに注目
  48. 外部キーを伴うモデル、クエリの書き方 > back_populatesとは? 72 with Session(engine) as session: … hero_spider_boy

    = session.exec( select(Hero).where(Hero.name == "Spider-Boy")).one() hero_spider_boy.team = None print("Spider-Boy:", hero_spider_boy) >> Spider-Boy: name='Spider-Boy' age=None id=3 secret_name='Pedro Parqueador' team_id=2 team=None print("Preventers Team Heroes:", preventers_team.heroes) >>
  49. 外部キーを伴うモデル、クエリの書き方 > back_populatesとは? 73 print("Preventers Team Heroes:", preventers_team.heroes) >> Preventers

    Team Heroes: [ … Hero(name='Spider-Boy', age=None, id=3, secret_name='Pedro Parqueador', team_id=2), … ] hero_spider_boy.team = None でhero_spider_boyはpreventers_teamからいなく なったはずなのに残っている。
  50. 外部キーを伴うモデル、クエリの書き方 > back_populatesとは? 74 session.add(hero_spider_boy) session.commit() session.refresh() print("Preventers Team Heroes:",

    preventers_team.heroes) >> Preventers Team Heroes: [ Hero(name='Rusty-Man', age=48, id=2, secret_name='Tommy Sharp', team_id=2), Hero(name='Tarantula', age=32, id=6, secret_name='Natalia Roman-on', team_id=2) ] commit,refreshした後に再度 preventers_team.heroesにアクセスすると hero_spider_boyはいなくなる。 commit,refresh前にRelationShipオブジェクト にアクセスしたい場合がある時に不具合につ ながる。
  51. 外部キーを伴うモデル、クエリの書き方 > back_populatesとは? > back_populatesがある場合 75 from sqlmodel import Field,

    Relationship, SQLModel class Team(SQLModel, table=True): … heroes: list["Hero"] = Relationship(back_populates="team") class Hero(SQLModel, table=True): … team_id: int | None = Field(foreign_key="team.id") team: Team | None = Relationship(back_populates="heroes") RelationShip内にback_populatesを指定す るとコミットする前に自動でチームのヒーロー 一覧から消してくれる。
  52. 外部キーを伴うモデル、クエリの書き方 > back_populatesとは? 76 with Session(engine) as session: … hero_spider_boy.team

    = None print("Preventers Team Heroes again:", preventers_team.heroes) >> Preventers Team Heroes: [ Hero(name='Rusty-Man', age=48, id=2, secret_name='Tommy Sharp', team_id=2), Hero(name='Tarantula', age=32, id=6, secret_name='Natalia Roman-on', team_id=2), ] hero_spider_boy.team = Noneにした時 点でhero_spider_boyはpreventers_team からいなくなっている!
  53. 外部キーを伴うモデル、クエリの書き方 > back_populatesとは? > back_populatesの考え方 77 class Team(SQLModel, table=True): ...

    heroes: list["Hero"] = Relationship(back_populates="team") class Hero(SQLModel, table=True): ... team_id: int | None = Field(foreign_key="team.id") team: Team | None = Relationship(back_populates="heroes") TeamモデルをHeroモデルではteamと呼んでいる。Teamモデルの heroesのback_populatesにはteamを指定する。 back_populatesには、このモ デルを他のモデルの属性とし て参照する時の名前 を書く。
  54. 外部キーを伴うモデル、クエリの書き方 > back_populatesとは? 78 class Team(SQLModel, table=True): ... heroes: list["Hero"]

    = Relationship(back_populates="team") class Hero(SQLModel, table=True): ... team_id: int | None = Field(foreign_key="team.id") team: Team | None = Relationship(back_populates="heroes") HeroモデルをTeamモデルでheroesと呼ん でいる。Heroモデルのteamの back_populatesにはheroesを指定する。
  55. 外部キーを伴うモデル、クエリの書き方 > Many to Manyでのモデルの書き方 HeroTeamLinkという中間テーブル用のモデルを作成する。 81 class HeroTeamLink(SQLModel, table=True):

    __tablename__ = "heroteamlink" team_id: int | None = Field(default=None, foreign_key="team.id", primary_key=True) hero_id: int | None = Field(default=None, foreign_key="hero.id", primary_key=True) 公式にはこう書いているけど、 team_idとhero_idはオプショナルじゃないと思う …
  56. 外部キーを伴うモデル、クエリの書き方 > Many to Manyでのモデルの書き方 82 class Team(SQLModel, table=True): …

    heroes: list["Hero"] = Relationship(back_populates="teams", link_model=HeroTeamLink) class Hero(SQLModel, table=True): … teams: list[Team] = Relationship(back_populates="heroes", link_model=HeroTeamLink) link_modelにHeroTeamLinkを指定 することで、HeroTeamLinkを中継し てTeamモデルにアクセスしているこ とを定義する。
  57. 外部キーを伴うモデル、クエリの書き方 > Many to Manyでのデータの作成方法 83 with Session(engine) as session:

    team_preventers = Team(name="Preventers", headquarters="Tower") team_z_force = Team(name="Z-Force", headquarters="Bar") hero_deadpond = Hero( name="Deadpond", secret_name="Dive Wilson", teams=[team_z_force, team_preventers]) session.add(hero_deadpond) session.commit()
  58. 外部キーを伴うモデル、クエリの書き方 > Many to Manyでのデータの作成方法 commitまでに実行されたSQL 84 INSERT INTO hero

    (name, secret_name, age) VALUES ('Deadpond', 'Dive Wilson', None) INSERT INTO team (name, headquarters) VALUES ('Z-Force', 'Sister Margaret's Bar') INSERT INTO team (name, headquarters) VALUES ('Preventers', 'Sharp Tower') INSERT INTO heroteamlink (team_id, hero_id) VALUES ((2, 3), (1, 1)) HeroとTeamのIDを使ってHeroTeamLinkを作成 Teamを作成 まずHeroを作成
  59. 外部キーを伴うモデル、クエリの書き方 > Many to Manyでのデータの作成方法 85 … session.refresh(hero_deadpond) print("Deadpond:", hero_deadpond)

    >> Deadpond: name="Deadpond" age=None id=1 secret_name="Dive Wilson" print("Deadpond teams:", hero_deadpond.teams) >> Deadpond teams: [Team(id=1, name="Z-Force", headquarters="Bar"), Team(id=2, name="Preventers", headquarters="Tower")]
  60. 外部キーを伴うモデル、クエリの書き方 > Many to Manyでのデータの取得方法 86 with Session(engine) as session:

    hero = session.get(Hero, 1) # SELECT hero.name AS hero_name, hero.secret_name AS hero_secret_name, hero.age AS hero_age, hero.id AS hero_id FROM hero WHERE hero.id = 1 print(hero.teams) ??
  61. 外部キーを伴うモデル、クエリの書き方 > Many to Manyでのデータの取得方法 87 … print(hero.teams) # SELECT

    team.name AS team_name, team.headquarters AS team_headquarters, team.id AS team_id FROM team, heroteamlink WHERE ? = heroteamlink.hero_id AND team.id = heroteamlink.team_id lazy_loadではなく1回のクエリでま とめて取得したい場合は?
  62. 外部キーを伴うモデル、クエリの書き方 > Many to Manyでのデータの取得方法 88 from sqlalchemy.orm import joinedload

    with Session(engine) as session: hero = session.query(Hero).options( joinedload(Hero.team)).get(hero_id) # SELECT hero.name, hero.secret_name, hero.age, hero.id, team_1.name, team_1.headquarters, team_1.id FROM hero LEFT OUTER JOIN (heroteamlink AS heroteamlink_1 JOIN team AS team_1 ON team_1.id = heroteamlink_1.team_id) ON hero.id = heroteamlink_1.hero_id WHERE hero.id = ? SQLModelはSQLAlchemyを継 承しているのでSQLAlchemyの 部品を使える
  63. Multiple Model > サンプルコードの前提 Hero Create API • リクエストでname, secret_name,

    age,team_idを渡す。 • idは自動採番され、HeroがテーブルにInsertされる。 • レスポンスは今InsertされたHeroのカラムをすべて返す。 93
  64. Multiple Model > サンプルコードの前提 Hero Get API • パスに取得したいHeroのIDを指定する。 •

    レスポンスでHeroのid, name, secret_name, ageだけでなく所属チームのid, name, headquartersも返す。 • 95
  65. Multiple Model > Multiple Modelを使わない場合 96 class Team(SQLModel, table=True): id:

    int | None = Field(default=None, primary_key=True) name: str = Field(index=True) headquarters: str heroes: list["Hero"] = Relationship(back_populates="team")
  66. Multiple Model > Multiple Modelを使わない場合 97 class Hero(SQLModel, table=True): id:

    int | None = Field(default=None, primary_key=True) name: str = Field(index=True) secret_name: str age: int | None = None team_id: int | None = Field(foreign_key="team.id") team: Team | None = Relationship(back_populates="heroes")
  67. Multiple Model > Multiple Modelを使わない場合 98 @app.post("/heroes/", response_model=Hero) def create_hero(hero:

    Hero, session: Session = Depends(get_session)): if hero.id is not None: raise HTTPException(status_code=400,detail="id should be None") session.add(hero) session.commit() session.refresh(hero) return hero Insertされたらidが自動採番 されるのでidは絶対に値があ るが、型定義上はNoneがあ りえるので、データ構造がわ かっていないと混乱を招く。 idは自動採番したいのでNoneであるべ き。だが、型定義上idに値を入れることが できてしまう。 わざわざNoneかどうかを確認するロジッ クを入れないといけない。
  68. Multiple Model > Multiple Modelを使わない場合    100 class HeroCreate(SQLModel): name:

    str secret_name: str age: int | None = None team_id: int | None table=Trueの指定がないので SQLAlchemy由来のクエリメソッドに使 えない。 指定がないとPydanticのデータモデル の働きのみ。 指定があると、SQLAlchemyモデルも 兼ねるので、session.add()や session.exec()などのクエリメソッドに 使える。
  69. Multiple Model > Multiple Modelを使わない場合 102 @app.post("/heroes/", response_model=HeroPublic) def create_hero(hero:

    HeroCreate, session: Session = Depends(get_session)): # HeroCreateをHeroに変換↓ db_hero = Hero.model_validate(hero) session.add(db_hero) session.commit() session.refresh(db_hero) return db_hero HeroPublicの項目がHeroモデルと一致して いるため、response_modelに指定するだけ で自動で変換される。
  70. Multiple Model > Multiple Modelを使わない場合 103 @app.get("/heroes/", response_model=list[HeroPublic]) def read_heroes(

    *, session: Session = Depends(get_session), offset: int = 0, limit: int = Query(default=100, le=100), ): heroes = session.exec(select(Hero).offset(offset).limit(limit)).all() return heroes
  71. Multiple Model > Multiple Modelを使わない場合 104 class TeamPublic(SQLModel): id: int

    name: str headquarters: str class HeroPublicWithTeam(SQLModel): … team: TeamPublic | None = None
  72. Multiple Model > Multiple Modelを使わない場合 105 @app.get("/heroes/{hero_id}", response_model=HeroPublicWithTeam) 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 このコードだとlazy_loadになるので 注意
  73. Multiple Model > Multiple Modelを使ったデータ作成 Base Modelの作り方 107 class Hero(SQLModel,

    table=True): __tablename__ = "heroes" id: int | None = Field(default=None, primary_key=True) name: str = Field(index=True) secret_name: str age: int | None = None team_id: int | None = Field(foreign_key="team.id") team: Team | None = Relationship(back_populates="heroes") この4属性は SelectとInsertで 共通
  74. Multiple Model > Multiple Modelを使ったデータ作成 Base Modelの作り方 108 class HeroBase(SQLModel):

    name: str = Field(index=True) secret_name: str age: int | None = None team_id: int | None = Field(foreign_key="teams.id") class Hero(HeroBase, table=True): id: int | None = Field(default=None, primary_key=True) team: Team | None = Relationship(back_populates="heroes") 共通属性を集めたBaseモデルクラスを作 成する。 テーブルと紐付かないただのデータモデル なのでクエリメソッドに使えないことに注 意。 テーブルと紐づくモデルを作成。 HeroBaseを継承するので 他の項目を定義する必要がない。
  75. Multiple Model > Multiple Modelを使ったデータ作成   109 class HeroCreate(HeroBase): pass

    class HeroPublic(HeroBase): id: int Insert用のモデルはHeroBaseとまった く同じなので 本質的には不要だがわかりやすさの ために定義。 一覧用のモデルはid以外はHeroBaseと 同じなので、その他項目を指定する必要 がない。 Heroモデルと違い、idがOptionalではない のでロジックを追わなくても必ずidに値が あることがわかる。
  76. Multiple Model > Multiple Modelを使ったデータ取得 111 @app.get("/heroes/{hero_id}", response_model=HeroPublicWithTeam) 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 HeroPublicWithTeamを Multiple Modelで定義する。
  77. Multiple Model > Multiple Modelを使ったデータ取得 112 class TeamBase(SQLModel): name: str

    = Field(index=True) headquarters: str class Team(TeamBase, table=True): id: int | None = Field(default=None, primary_key=True) heroes: list["Hero"] = Relationship(back_populates="team") class TeamPublic(TeamBase): id: int TeamBaseを起点に、Teamの テーブルに紐づくモデルと読み 取り用モデルを作成する
  78. Multiple Model > Multiple Modelを使ったデータ取得 113 class TeamPublic(SQLModel): id: int

    name: str = Field(index=True) headquarters: str class HeroPublic(HeroBase): id: int class HeroPublicWithTeam(HeroPublic): team: TeamPublic | None = None HeroPublicModelを継承し、 TeamPublicを追加する。 一覧用に作った HeroPublicModel。id, name, secret_name, age, team_idを持 つ。
  79. Multiple Model > Multiple Modelを使うメリット 1. 共通属性を基底クラスで定義し、継承で再利用することで、コードの重複を減ら し、保守性を向上させる ことができる。 2.

    入力と出力の型が正確に定義 できる。 3. 各モデルが明確に定義されることで、OpenAPIの定義も正確になり、クライア ント側に正確な型定義を提供 できる。 114
  80. Multiple Modelおまけ 今回作るシステム ヒーローのファン向けのヒーローファンサイト • ヒーローは事故の救助などの活動をする他、市民と触れ合うためのイベントを開 くこともある。 • ファンは募金やグッズ購入を通してヒーローを支援する。 •

    国や企業がヒーローの活動支援する場合もある。 • ヒーロー普及団体がヒーローの活動や資金状況の管理をする。 • ヒーローファンサイトは、ヒーローの魅力をファンに伝え、ヒーローの支援を促す ことを目的としている。 118
  81. Multiple Modelおまけ 管理者向け機能 • チーム: 作成・更新・詳細取得・一覧取得・削除 • ヒーロー: 作成・更新・詳細取得・一覧取得・削除 一般ユーザー向け機能

    • チーム: 詳細取得・一覧取得 • ヒーロー: 詳細取得・一覧取得 120 取得する項目に差異があ る (secret_name)
  82. Multiple Modelおまけ 管理者向け機能 • チーム: 作成・更新・詳細取得・一覧取得・削除 • ヒーロー: 作成・更新・詳細取得・一覧取得・削除 •

    活動予定: 作成・更新・詳細取得・一覧取得・削除 • グッズとグッズ売上: 作成・更新・詳細取得・一覧取得・削除 • 有料会員: 詳細取得・一覧取得・更新・削除 一般ユーザー向け機能 • チーム: 詳細取得・一覧取得 • ヒーロー: 詳細取得・一覧取得・いいね登録・取消 • 活動予定: 詳細取得・一覧取得 • グッズ: 詳細取得・一覧取得・購入 • 有料会員: 登録・解約・自身の取得・更新 122 いいね数など集計関数を使って取 得し、登録や更新時に使わない情 報の取得も増える。 チームやヒーローの情報を更新で きるのは変わらず管理者のみだ が、ユーザーの種類によって取得 できる項目が大きく変わるのがビ ジネス的に大事になる。
  83. Multiple Modelおまけ 124 class HeroBaseForAdmin(HeroBase): secret_name: str class Hero(HeroBaseForAdmin) id:

    int | None = Field(default=None, primary_key=True) team: Team | None = Relationship(back_populates="heroes") 管理者向け ※更新と詳細取得は省略
  84. Multiple Modelおまけ HeroBaseとHeroBaseForAdminどちらに入れるのが良さそう? →管理者のみだからHeroBaseForAdmin 128 class HeroBase(SQLModel): name: str =

    Field(index=True) age: int | None = None team_id: int | None = Field(default=None, foreign_key="team.id") class HeroBaseForAdmin(HeroBase): secret_name: str tel: str
  85. Multiple Modelおまけ 有料会員機能を作ることになった。 好きな食べ物という項目が有料会員と管理者にだけ見れるようになった。 129 class HeroBase(SQLModel): name: str =

    Field(index=True) … class HeroBaseForSubscriber(HeroBase): favorite_food: str class HeroBaseForAdmin(HeroBaseForSubscriber): secret_name: str
  86. Multiple Modelおまけ 132 アプローチ2 参照モデルと更新モデルでベースをわける。 class HeroBase(SQLModel): name: str =

    Field(index=True) age: int | None = None team_id: int | None = Field(default=None, foreign_key="team.id") class HeroReadBase(SQLModel): id: int name: str = Field(index=True) …
  87. Multiple Modelおまけ 133 class HeroBase(SQLModel): name: str = Field(index=True) age:

    int | None = None team_id: int | None = Field(foreign_key="team.id") secret_name: str class Hero(HeroBase, table=True): id: int | None = Field(default=None, primary_key=True) team: Team | None = Relationship(back_populates="heroes") class HeroCreate(HeroBase): pass 更新用モデル
  88. Multiple Modelおまけ 134 class HeroReadBase(SQLModel): id: int name: str =

    Field(index=True) age: int | None = None team_id: int | None = Field(foreign_key="team.id") class HeroReadForAdmin(HeroReadBase): secret_name: str class HeroReadPublic(HeroReadBase): pass 取得用モデル
  89. Multiple Modelおまけ 135 管理者にしか見えてはいけない、ヒーローの電話番号を登録することになった。→更 新はHeroBase、取得はHeroReadForAdminにtelを追加。 class HeroBase(SQLModel): name: str =

    Field(index=True) age: int | None = None team_id: int | None = Field(foreign_key="team.id") tel: str class HeroReadForAdmin(HeroReadBase): secret_name: str tel: str
  90. Multiple Modelおまけ Subscriberに関する変更を加える時にAdminのことを気にしないといけない? 141 HeroBase HeroCreate Hero … HeroRead Base

    HeroReadPublic HeroRead ForSubscriber HeroReadDetail Public HeroReadDetail ForSubscriber HeroRead ForAdmin HeroRead ForAdmin
  91. Multiple Modelおまけ 有料会員向けに人気投票に投票済かどうかのフラグを取得モデルに追加することに あった。管理者は投票しないのでフラグは不要。 142 class HeroReadBase(SQLModel): ... class HeroReadForSubscriber(HeroReadBase):

    ... voted: bool class HeroReadForAdmin(HeroReadForSubscriber): ... HeroReadForSubscriberを 継承しているのでvotedを含 んでしまう。 使わない属性を含めると、 取得方法(集計など)によっ ては 不要な属性によって取得時 のパフォーマンスが落ちるこ とにつながりかねない。
  92. Multiple Modelおまけ 144 class HeroReadBase(SQLModel): … class HeroReadForSubscriber(HeroReadBase): … voted:

    bool class HeroReadForAdmin(HeroReadBase): … class HeroReadPublic(HeroReadBase): … HeroReadForSubscriberに はvotedを含み、 HeroReadForAdminには含 めないのを実現。
  93. Multiple Modelおまけ 145 今回の場合の分け方。 • 取得用と更新用でBaseModelを分けた。 • Heroの更新は管理者しかできないのでそこまで分岐せず。 • 取得モデルはHeroReadBaseの下に3つのユーザータイプのモデルを作成。

    ◦ 3タイプ共通の項目はあるのでまずそれを HeroReadBaseで定義。 ◦ 有料会員特典情報や、管理者しか扱えない機密情報、一覧での集計表示など取得の要件が ユーザータイプごとに差分ので HeroReadBaseの下に3つモデルを定義。 ◦ 一覧にある情報は詳細でも取得するので、それぞれのユーザータイプで一覧用モデルを詳細 モデルに継承する。 • 登録・更新する項目を追加する時に取得モデルの追加も別途行わないといけな いのがデメリットだが、将来の機能追加を踏まえてユーザータイプごとのアクセ ス制御のしやすさを採った。
  94. Multiple Modelおまけ > まとめ • 取得・更新やユーザータイプ共通のモデルを作るアプローチと、取得・更新や ユーザータイプでモデルを分割するアプローチがある。 ◦ 前者はコードの重複が少なくなる反面、取得・更新やユーザータイプごとの柔軟な対応がしにく いデメリットがある。

    ◦ 後者はコードの重複が多くなる反面、柔軟な対応がしやすいメリットがある。 ◦ 共通化するの苦しいなって思ったら黄信号! • システムの将来の展望や、どちらのほうが変更しやすいかチームメンバーとすり 合わせながらどちらのアプローチを取るか決める必要がある。 146