Slide 1

Slide 1 text

SQLModel入門 〜クエリと型〜 2024-09-27 mizzsugar @PyConJP 1

Slide 2

Slide 2 text

お前、誰よ。 ● mizzsugar(みずきと呼ばれることが多いです) ○ Twitter: mizzsugar0425 ● 静岡県静岡市在住 ● 東京の開発会社でバックエンドエンジニア ○ BtoBWebサービスの開発 ○ 社内若手エンジニア教育プロジェクト 2

Slide 3

Slide 3 text

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

Slide 4

Slide 4 text

4 発表の前に1つ謝りたいことが あります😭

Slide 5

Slide 5 text

5 30分の発表なのに スライドの数が100枚を 超えてしまいました😱

Slide 6

Slide 6 text

6 SQLModel独自の内容が薄いので、 「スキーマ管理」は省略させてください󰢜 (スライドは作っています。みなさんもCfPへの盛り込みすぎには気をつ けましょう🥲)

Slide 7

Slide 7 text

7 特につまずきやすいところと SQLModelで推したいところ以外 駆け足になります󰢜

Slide 8

Slide 8 text

目次 ● SQLModelとは ● 基本的なモデルの書き方 ● スキーマ管理           ←省略 ● 基本的なクエリの書き方 ● 外部キーを伴うモデル、クエリの書き方 ←つまづきやすい ● Multiple Model ←推したい ● Multiple Modelおまけ 8

Slide 9

Slide 9 text

この発表のゴール ゴール ● SQLModelのモデル、クエリの考え方を理解する。 ● SQLModelの特徴的な機能であるMultiple Modelの考え方とメリットを 理解する。 ○ 特にFastAPIとの組み合わせ。 ○ 型安全なシステムをどう高速に開発できるか。 9

Slide 10

Slide 10 text

この発表のゴール サンプルコード ● SQLModel公式が提供するチュートリアルのサンプルコードを流用して います。 ● 詳細を知りたい方は、ぜひSQLModelチュートリアルを読んでください。 10

Slide 11

Slide 11 text

この発表のターゲットとなる人 この発表のターゲットとなる人 ● Djangoなど別のORMを使ったことがあるが、SQLModelには馴染みの ない方 話さないこと ● 非同期周り 前提知識 ● SQLの基本的な知識(CREATE TABLE文、CRUDの基本的な構文) ● Pythonの型ヒントに関する基本的な知識 11

Slide 12

Slide 12 text

SQLModelとは 12

Slide 13

Slide 13 text

SQLModelとは > 概要 ● SQLModelは、FastAPIの作者によって開発されたPythonのORMライブ ラリ。 ● SQLAlchemyとPydanticを基盤として構築されており、両者の強みを組 み合わせてデータベース操作と型安全を提供。 ○ 発表者個人としての印象は、 モデルのテーブルへの紐づけとクエリは SQLAlchemy 静的型付けのお作法は Pydantic ● FastAPIとの親和性が高く、データモデルの定義からWebAPIの構築ま で一貫して行えるため、高速な開発が可能。 ○ SQLModelのクエリメソッドの返り値は Pydanticを継承しているので、 FastAPIのレスポンスにそ のまま使える。 ○ FastAPI由来のOpenAPIの自動生成機能も使用できる。 https://sqlmodel.tiangolo.com 13

Slide 14

Slide 14 text

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)

Slide 15

Slide 15 text

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

Slide 16

Slide 16 text

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のモデルを作る必 要なし

Slide 17

Slide 17 text

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()

Slide 18

Slide 18 text

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()

Slide 19

Slide 19 text

SQLModelとは > まとめ 19 ● モデル定義はPydanticのお作法。 ○ モデル定義はPydantic+DBのために主キーなどの内容。 ○ dataclassやPydanticのような簡潔な書き方。 ● クエリはSQLAlchemyと同じ。 ● Pydanticと同じ簡潔なモデル定義とSQLAlchemyの豊富なクエリメソッド を使えるいいとこ取りがSQLModel。

Slide 20

Slide 20 text

基本的なモデルの書き方 20

Slide 21

Slide 21 text

基本的なモデルの書き方 > 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で あり、単純なデータモデルとな る。

Slide 22

Slide 22 text

基本的なモデルの書き方 > 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

Slide 23

Slide 23 text

基本的なモデルの書き方 > カラムの型ヒント 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なのか?

Slide 24

Slide 24 text

基本的なモデルの書き方 > 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

Slide 25

Slide 25 text

基本的なモデルの書き方 > 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を生成したいので 指定するわけにもいかない。

Slide 26

Slide 26 text

基本的なモデルの書き方 > やはり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の機能に あるので後ほど紹介💡

Slide 27

Slide 27 text

基本的なモデルの書き方 > 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である。

Slide 28

Slide 28 text

基本的なモデルの書き方 > まとめ 28 ● SQLModelのモデル定義ではdataclassやPydanticと簡潔な型定義を する。 ● 主キーを自動生成したいのと作成時に主キーを指定したくないのを両立 するために、SQLModelでは主キーをOptionalにする。

Slide 29

Slide 29 text

スキーマ管理 29

Slide 30

Slide 30 text

スキーマ管理 > alembic ● SQLModel自体にマイグレーション機能はないので、マイグレーション用 SQLとスクリプトを自身で作成して管理するか、alembicなどのマイグ レーションツールを使って管理する。 ● SQLModelはSQLAlchemyをベースとしているので、SQLAlchemy作者 が開発したalembicを使うのがおすすめ。 ● SQLModelでalembicを使う方法は、SQLAlchemyで使う方法とほとんど 同じ。 30 https://alembic.sqlalchemy.org/en/latest/

Slide 31

Slide 31 text

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

Slide 32

Slide 32 text

スキーマ管理 > alembic 32 sqlalchemy.url = os.getenv("DB_URL") alembilc.iniのsqlalchemy.urlを環 境に合った値に変更 alembilc.ini

Slide 33

Slide 33 text

スキーマ管理 > alembic 33 from sqlmodel import SQLModel from your_app import models # モデルを定義しているファイル … target_metadata = SQLModel.metadata SQLModelのメタデータを使用する ように設定 migrations/env.py

Slide 34

Slide 34 text

スキーマ管理 > alembic 34 $ alembic revision -m "init" alembic revision -m <ファイル名> でマイグレーションファイルを生成。 migrations/versions/ 以下にマイグレー ションファイルが生成される。

Slide 35

Slide 35 text

スキーマ管理 > 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()にはロールバック処理 を書く。

Slide 36

Slide 36 text

スキーマ管理 > 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

Slide 37

Slide 37 text

スキーマ管理 > alembic 37 def downgrade(): op.drop_table('hero') migrations/versions/e1addcb32a11_init.py

Slide 38

Slide 38 text

スキーマ管理 > alembic 38 $ alembic upgrade head alembic upgrade head でマイグレーションが実行され、 DBに反映される。

Slide 39

Slide 39 text

スキーマ管理 > alembic 39 $ alembic revision -m "add_team" TeamモデルとHeroモデルに team_idを追加するマイグレーション ファイルを作成する。

Slide 40

Slide 40 text

スキーマ管理 > 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の部分。 自動で入力されている。

Slide 41

Slide 41 text

スキーマ管理 > 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'])

Slide 42

Slide 42 text

スキーマ管理 > 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')

Slide 43

Slide 43 text

スキーマ管理 > alembic 43 alembic upgrade head alembic downgrade -1 # 直前のバージョンに戻る alembic downgrade e1addcb32a11 # バージョンを指定して戻る alembic downgrade base # 最初の状態(マイグレーション適用前)に戻 る

Slide 44

Slide 44 text

スキーマ管理 > まとめ 「alembicを使ったスキーマ管理」まとめ ● SQLModel自体にはマイグレーション機能がないので他のツールを使うか自前 でマイグレーションスクリプトを作るしかない ● SQLAlchemyとの親和性から、alembicがおすすめ ● SQLModel独自の手順はSQLModelのメタデータを使用するように設定するくら いで、他はSQLAlchemyで使うときと同じ 44

Slide 45

Slide 45 text

基本的なクエリの書き方 45

Slide 46

Slide 46 text

基本的なクエリの書き方 > 基本的なデータの作成方法 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だから何も表示され ていない。

Slide 47

Slide 47 text

基本的なクエリの書き方 > 基本的なデータの作成方法 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にアクセ スすると「期限切れではない」と認識されデータ が表示される。

Slide 48

Slide 48 text

基本的なクエリの書き方 > 基本的なデータの取得方法 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}

Slide 49

Slide 49 text

基本的なクエリの書き方 > 基本的なデータの取得方法 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というイテ レータオブジェクトを返却

Slide 50

Slide 50 text

基本的なクエリの書き方 > 基本的なデータの取得方法 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で返却

Slide 51

Slide 51 text

基本的なクエリの書き方 > 基本的なデータの取得方法 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を返す。

Slide 52

Slide 52 text

基本的なクエリの書き方 > 基本的なデータの取得方法 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()を使っているのであくまで個人的な考え。

Slide 53

Slide 53 text

基本的なクエリの書き方 > 基本的なデータの取得方法 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;

Slide 54

Slide 54 text

基本的なクエリの書き方 > 基本的なデータの取得方法 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のみから成り立つ新しい モデルを定義する必要がある。

Slide 55

Slide 55 text

基本的なクエリの書き方 > 基本的なデータの取得方法 55 class AgeDistribution(SQLModel): age: int | None count: int ageとcountから成り立つ モデルを作成

Slide 56

Slide 56 text

基本的なクエリの書き方 > 基本的なデータの取得方法 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()

Slide 57

Slide 57 text

基本的なクエリの書き方 > 基本的なデータの取得方法 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型オブジェ クトは渡せないというエラーになる(実行は できる)

Slide 58

Slide 58 text

基本的なクエリの書き方 > 基本的なデータの取得方法 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と定義しているので乖離が 起きる。

Slide 59

Slide 59 text

基本的なクエリの書き方 > 基本的なデータの取得方法 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する。

Slide 60

Slide 60 text

基本的なクエリの書き方 > 基本的なデータの更新方法 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()を 実行すること。

Slide 61

Slide 61 text

基本的なクエリの書き方 > 基本的なデータの削除方法 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になくセッションに接続されていないので、「期 限切れ」と認識されずそのままでいるので、オブジェクト内 のデータにアクセスできる。

Slide 62

Slide 62 text

基本的なクエリの書き方 > まとめ 62 ● 基本的にSQLAlchemyと同じ手順。 ● SQLAlchemyの部品をimportしてそのまま使える。 ● SQLModelのクラスのlistやOptionalなど返り値の指定ができ、型安全 なシステム開発に貢献。 ● 集計するときは返り値や集計関数で使う属性の指定に工夫が必要。

Slide 63

Slide 63 text

63 ここから少し難しくなるので ペース落とします!

Slide 64

Slide 64 text

外部キーを伴うモデル、クエリの 書き方 64

Slide 65

Slide 65 text

外部キーを伴うモデル、クエリの書き方 > サンプルコードの前提 これらのテーブルを例に外部キーを伴うモデルのサンプルコードを紹介。 65 https://sqlmodel.tiangolo.com/tutorial/connect/create-connected-tables/

Slide 66

Slide 66 text

外部キーを伴うモデル、クエリの書き方 > 外部キーを伴うモデルの書き方 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") 「モデル名.アトリビュート名」ではなく 「テーブル名.カラム名」 を定義すること。

Slide 67

Slide 67 text

外部キーを伴うモデル、クエリの書き方 > 外部キーを伴うモデルの書き方 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")

Slide 68

Slide 68 text

外部キーを伴うモデル、クエリの書き方 > 外部キーを伴うモデルの書き方 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")

Slide 69

Slide 69 text

外部キーを伴うモデル、クエリの書き方 > back_populatesとは? ● RelationShipのback_populatesを定義すると、自動的に同期させる。 (ただし、更新のたびに他モデルに同期するので大量のデータ一気に扱いたい時にはパフォーマンスに注 意) 69

Slide 70

Slide 70 text

外部キーを伴うモデル、クエリの書き方 > 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()

Slide 71

Slide 71 text

外部キーを伴うモデル、クエリの書き方 > 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),] ここに注目

Slide 72

Slide 72 text

外部キーを伴うモデル、クエリの書き方 > 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) >>

Slide 73

Slide 73 text

外部キーを伴うモデル、クエリの書き方 > 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からいなく なったはずなのに残っている。

Slide 74

Slide 74 text

外部キーを伴うモデル、クエリの書き方 > 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オブジェクト にアクセスしたい場合がある時に不具合につ ながる。

Slide 75

Slide 75 text

外部キーを伴うモデル、クエリの書き方 > 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を指定す るとコミットする前に自動でチームのヒーロー 一覧から消してくれる。

Slide 76

Slide 76 text

外部キーを伴うモデル、クエリの書き方 > 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 からいなくなっている!

Slide 77

Slide 77 text

外部キーを伴うモデル、クエリの書き方 > 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には、このモ デルを他のモデルの属性とし て参照する時の名前 を書く。

Slide 78

Slide 78 text

外部キーを伴うモデル、クエリの書き方 > 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を指定する。

Slide 79

Slide 79 text

外部キーを伴うモデル、クエリの書き方 > back_populatesのまとめ ● RelationShipのback_populatesには、モデルを他のモデルの属性として参照す る時の名前を書く。 ● back_populatesを定義すると自動的に同期させる。 79

Slide 80

Slide 80 text

外部キーを伴うモデル、クエリの書き方 > Many to Manyのモデルのサンプルコードの前提 Many to Manyの場合のモデル定義。 HeroとTeamが多対多とする。 80 https://sqlmodel.tiangolo.com/tutorial/many-to-many/create-models-with-link/

Slide 81

Slide 81 text

外部キーを伴うモデル、クエリの書き方 > 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はオプショナルじゃないと思う …

Slide 82

Slide 82 text

外部キーを伴うモデル、クエリの書き方 > 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モデルにアクセスしているこ とを定義する。

Slide 83

Slide 83 text

外部キーを伴うモデル、クエリの書き方 > 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()

Slide 84

Slide 84 text

外部キーを伴うモデル、クエリの書き方 > 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を作成

Slide 85

Slide 85 text

外部キーを伴うモデル、クエリの書き方 > 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")]

Slide 86

Slide 86 text

外部キーを伴うモデル、クエリの書き方 > 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) ??

Slide 87

Slide 87 text

外部キーを伴うモデル、クエリの書き方 > 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回のクエリでま とめて取得したい場合は?

Slide 88

Slide 88 text

外部キーを伴うモデル、クエリの書き方 > 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の 部品を使える

Slide 89

Slide 89 text

外部キーを伴うモデル、クエリの書き方 > まとめ 89 ● 外部キーに対応するモデルを属性として使いたい時はRelationShip。 ● RelationShipのback_populatesには、モデルを他のモデルの属性として参照す る時の名前を書く。 ● back_populatesを定義すると自動的に同期させる。 ● Many To Manyの場合は、中間テーブル用のモデルを作成し、RelationShipに link_modelを指定。

Slide 90

Slide 90 text

Multiple Model 90

Slide 91

Slide 91 text

Multiple Model > Multiple Modelとは ● SQLModelの特徴的な機能の一つ。 ● ORM用のHeroモデル、APIスキーマとしてのHeroモデルの両方を作ることに よって発生していた、類似したクラスやそれらクラス間の不整合の問題を解決。 ● FastAPIと組み合わせて使うと簡潔かつ高速に開発することに貢献。 91

Slide 92

Slide 92 text

Multiple Model > サンプルコードの前提 これらのテーブルを例にMultiple ModelとFastAPIの組み合わせ方を紹介。 92 https://sqlmodel.tiangolo.com/tutorial/connect/create-connected-tables/

Slide 93

Slide 93 text

Multiple Model > サンプルコードの前提 Hero Create API ● リクエストでname, secret_name, age,team_idを渡す。 ● idは自動採番され、HeroがテーブルにInsertされる。 ● レスポンスは今InsertされたHeroのカラムをすべて返す。 93

Slide 94

Slide 94 text

Multiple Model > サンプルコードの前提 Hero List API ● レスポンスでHeroのid, name, secret_name, age, team_idを返す。 94

Slide 95

Slide 95 text

Multiple Model > サンプルコードの前提 Hero Get API ● パスに取得したいHeroのIDを指定する。 ● レスポンスでHeroのid, name, secret_name, ageだけでなく所属チームのid, name, headquartersも返す。 ● 95

Slide 96

Slide 96 text

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")

Slide 97

Slide 97 text

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")

Slide 98

Slide 98 text

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かどうかを確認するロジッ クを入れないといけない。

Slide 99

Slide 99 text

Multiple Model > Multiple Modelを使わない場合 99

Slide 100

Slide 100 text

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()などのクエリメソッドに 使える。

Slide 101

Slide 101 text

Multiple Model > Multiple Modelを使わない場合 101 class HeroPublic(SQLModel): id: int name: str secret_name: str age: int | None = None

Slide 102

Slide 102 text

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に指定するだけ で自動で変換される。

Slide 103

Slide 103 text

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

Slide 104

Slide 104 text

Multiple Model > Multiple Modelを使わない場合 104 class TeamPublic(SQLModel): id: int name: str headquarters: str class HeroPublicWithTeam(SQLModel): … team: TeamPublic | None = None

Slide 105

Slide 105 text

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になるので 注意

Slide 106

Slide 106 text

Multiple Model > Multiple Modelを使わないで困った点 ● Hero作成のように型違反にはならないが、ロジックを知らないと意図しないデー タを入力または出力するかもしれないコードがある。 ● 理想の型定義を持ったモデルを作ろうとすると、同じ属性を何回も定義する必要 があり、属性への変更が発生した時に同じような修正を何回もするのが大変。 106

Slide 107

Slide 107 text

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で 共通

Slide 108

Slide 108 text

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を継承するので 他の項目を定義する必要がない。

Slide 109

Slide 109 text

Multiple Model > Multiple Modelを使ったデータ作成   109 class HeroCreate(HeroBase): pass class HeroPublic(HeroBase): id: int Insert用のモデルはHeroBaseとまった く同じなので 本質的には不要だがわかりやすさの ために定義。 一覧用のモデルはid以外はHeroBaseと 同じなので、その他項目を指定する必要 がない。 Heroモデルと違い、idがOptionalではない のでロジックを追わなくても必ずidに値が あることがわかる。

Slide 110

Slide 110 text

Multiple Model > Multiple Modelを使ったデータ取得 110 idがない。

Slide 111

Slide 111 text

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で定義する。

Slide 112

Slide 112 text

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の テーブルに紐づくモデルと読み 取り用モデルを作成する

Slide 113

Slide 113 text

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を持 つ。

Slide 114

Slide 114 text

Multiple Model > Multiple Modelを使うメリット 1. 共通属性を基底クラスで定義し、継承で再利用することで、コードの重複を減ら し、保守性を向上させる ことができる。 2. 入力と出力の型が正確に定義 できる。 3. 各モデルが明確に定義されることで、OpenAPIの定義も正確になり、クライア ント側に正確な型定義を提供 できる。 114

Slide 115

Slide 115 text

SQLModel入門 まとめ ● SQLAlchemyとPydanticを融合したSQLModelは、豊富なORM機能を使いなが ら簡潔そして高速なシステム開発に貢献する。 ● Pydanticを継承しているため、FastAPIとシームレスに統合できる。 ● Multiple Modelによって、関心の分離(データベース、入力、出力モデルの分 離)とコードの重複を避けることによる保守性に優れたシステム開発を実現でき る。 115

Slide 116

Slide 116 text

ありがとうございました! 116

Slide 117

Slide 117 text

Multiple Modelおまけ BaseModelをどこまで共通化するか、難しいですよね。 おまけでは、Heroモデルの取得APIが管理者用と公開用2種類あり どちらのAPIにするかによってレスポンスの項目が変わる場合の モデルの設計を考えます。 ケーススタディを通して、共通化の基準を考える一助になればと思います。 (ここから先はMultiple ModelとFastAPIを使う時に何を考えながらリクエスト・レスポンスの設計しているか の個人的な考え です。) 117

Slide 118

Slide 118 text

Multiple Modelおまけ 今回作るシステム ヒーローのファン向けのヒーローファンサイト ● ヒーローは事故の救助などの活動をする他、市民と触れ合うためのイベントを開 くこともある。 ● ファンは募金やグッズ購入を通してヒーローを支援する。 ● 国や企業がヒーローの活動支援する場合もある。 ● ヒーロー普及団体がヒーローの活動や資金状況の管理をする。 ● ヒーローファンサイトは、ヒーローの魅力をファンに伝え、ヒーローの支援を促す ことを目的としている。 118

Slide 119

Slide 119 text

Multiple Modelおまけ ヒーローファンサイトの要件 ● ヒーロー普及団体の人を管理者、ファンやヒーローの支援を考えているなどヒー ロー普及団体の外にいる人を一般ユーザーと呼ぶ。 ● 管理者は、ヒーローの情報を更新する。 ● 管理者が更新した情報を一般ユーザーは閲覧できる。 ● ヒーローの秘密の名前など、管理者は登録・閲覧できるけれども一般ユーザー は閲覧できない項目がある。 119

Slide 120

Slide 120 text

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

Slide 121

Slide 121 text

Multiple Modelおまけ ヒーローファンサイトの今後の展望 ● ヒーローの好きな食べ物や活動日・場所など情報をどんどん追加する。 ● 一般ユーザー向けに有料会員向け機能を作り、有料会員しか閲覧できな情報 ができる。 ● ヒーローの活動場所・日時を管理する予定。非公開の活動もある。 ● ヒーローのグッズ販売する。 ● ヒーロー一覧に、人気ランキング順位・いいねの数・直近の活動日・おすすめ グッズへのリンクなどを追加する。 121

Slide 122

Slide 122 text

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

Slide 123

Slide 123 text

Multiple Modelおまけ 登録、更新、取得モデルのアプローチ1 ヒーローのBaseModelはage・name・team_idだけ。ユーザーの種類・取得・更新す べてにおいて共通の項目をBaseModelに集める。 123 class HeroBase(SQLModel): name: str = Field(index=True) age: int | None = None team_id: int | None = Field(foreign_key="teams.id")

Slide 124

Slide 124 text

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") 管理者向け ※更新と詳細取得は省略

Slide 125

Slide 125 text

Multiple Modelおまけ 125 class HeroCreate(HeroBaseForAdmin): pass class HeroReadForAdmin(HeroBase): id: int 管理者向け ※更新と詳細取得は省略

Slide 126

Slide 126 text

Multiple Modelおまけ 126 class HeroReadPublic(HeroBase): id: int class HeroReadDetailPublic(HeroReadPublic): team: TeamReadPublic 一般ユーザー向け

Slide 127

Slide 127 text

Multiple Modelおまけ 管理者にしか見えてはいけない、ヒーローの電話番号を登録することになった。 HeroBaseとHeroBaseForAdminどちらに入れるのが良さそう? 127 class HeroBase(SQLModel): name: str = Field(index=True) age: int | None = None team_id: int | None = Field(foreign_key="team.id") class HeroBaseForAdmin(HeroBase): secret_name: str

Slide 128

Slide 128 text

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

Slide 129

Slide 129 text

Multiple Modelおまけ 有料会員機能を作ることになった。 好きな食べ物という項目が有料会員と管理者にだけ見れるようになった。 129 class HeroBase(SQLModel): name: str = Field(index=True) … class HeroBaseForSubscriber(HeroBase): favorite_food: str class HeroBaseForAdmin(HeroBaseForSubscriber): secret_name: str

Slide 130

Slide 130 text

Multiple Modelおまけ いいね数合計を追加することになった。いいね数合計取得時にしかいらない。どの ユーザータイプでも取得する。 130 class HeroReadPublic(HeroBase): id: int favorites_count: int class HeroReadForSubscriber(HeroReadPublic, HeroBaseForSubscriber): pass class HeroReadForAdmin(HeroReadPublic, HeroBaseForAdmin): pass

Slide 131

Slide 131 text

Multiple Modelおまけ HeroBaseが依存する先が多くなってきた。 HeroBaseに追加すると他のモデルでの重複が少なくなる分、 機密情報を入れていないかなど影響を気にするべきモデルの数が多い。 131 HeroBase HeroBaseFor Subscriber HeroReadPublic HeroRead DetailPublic HeroRead ForSubscriber HeroBase ForAdmin HeroRead ForAdmin HeroReadDetail ForSubscriber …

Slide 132

Slide 132 text

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) …

Slide 133

Slide 133 text

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 更新用モデル

Slide 134

Slide 134 text

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 取得用モデル

Slide 135

Slide 135 text

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

Slide 136

Slide 136 text

Multiple Modelおまけ 136 有料会員機能を作ることになった。 好きな食べ物という項目が有料会員と管理者にだけ見れるようになった。 更新用モデルはHeroBaseに追加。 class HeroBase(SQLModel): name: str = Field(index=True) age: int | None = None team_id: int | None = Field(foreign_key="team.id") tel: str favorite_food: str

Slide 137

Slide 137 text

Multiple Modelおまけ 137 取得用モデルはHeroReadForSubscriberを作成し、そこに項目追加。 HeroReadForAdminはHeroReadForSubscriberを継承して項目重複回避。 class HeroReadBase(SQLModel): id: int … class HeroReadForSubscriber(HeroReadBase): … favorite_food: str class HeroReadForAdmin(HeroReadForSubscriber): … secret_name: str

Slide 138

Slide 138 text

Multiple Modelおまけ いいね数合計を追加することになった。いいね数合計取得時にしかいらない。どの ユーザータイプでも取得する。→HeroReadBaseに追加。 138 class HeroReadBase(SQLModel): … favorites_count: int class HeroReadForSubscriber(HeroReadBase): … class HeroReadForAdmin(HeroReadForSubscriber): …

Slide 139

Slide 139 text

Multiple Modelおまけ 項目の重複は起こるが、継承の一番深いところが参照と更新で分かれるので取得で しか使わないいいね数などを追加する時に更新モデルを気にせずにすむ。 139 HeroBase HeroCreate Hero … HeroRead Base HeroReadPublic HeroRead ForSubscriber HeroReadDetail Public HeroReadDetail ForSubscriber HeroRead ForAdmin HeroRead ForAdmin

Slide 140

Slide 140 text

Multiple Modelおまけ 140 CQRSとは ● Command(更新操作)とQuery(読み取り操作)の分離 ● それぞれの操作に最適化されたモデルの使用 ● パフォーマンス、スケーラビリティ、セキュリティの向上

Slide 141

Slide 141 text

Multiple Modelおまけ Subscriberに関する変更を加える時にAdminのことを気にしないといけない? 141 HeroBase HeroCreate Hero … HeroRead Base HeroReadPublic HeroRead ForSubscriber HeroReadDetail Public HeroReadDetail ForSubscriber HeroRead ForAdmin HeroRead ForAdmin

Slide 142

Slide 142 text

Multiple Modelおまけ 有料会員向けに人気投票に投票済かどうかのフラグを取得モデルに追加することに あった。管理者は投票しないのでフラグは不要。 142 class HeroReadBase(SQLModel): ... class HeroReadForSubscriber(HeroReadBase): ... voted: bool class HeroReadForAdmin(HeroReadForSubscriber): ... HeroReadForSubscriberを 継承しているのでvotedを含 んでしまう。 使わない属性を含めると、 取得方法(集計など)によっ ては 不要な属性によって取得時 のパフォーマンスが落ちるこ とにつながりかねない。

Slide 143

Slide 143 text

Multiple Modelおまけ ユーザータイプごとに項目の変化がある場合は、無理に重複をなくそうとして違う ユーザータイプのモデルを継承せず分けると良い。 143 … HeroRead Base HeroReadPublic HeroRead ForSubscriber HeroReadDetail Public HeroReadDetail ForSubscriber HeroRead ForAdmin HeroRead ForAdmin

Slide 144

Slide 144 text

Multiple Modelおまけ 144 class HeroReadBase(SQLModel): … class HeroReadForSubscriber(HeroReadBase): … voted: bool class HeroReadForAdmin(HeroReadBase): … class HeroReadPublic(HeroReadBase): … HeroReadForSubscriberに はvotedを含み、 HeroReadForAdminには含 めないのを実現。

Slide 145

Slide 145 text

Multiple Modelおまけ 145 今回の場合の分け方。 ● 取得用と更新用でBaseModelを分けた。 ● Heroの更新は管理者しかできないのでそこまで分岐せず。 ● 取得モデルはHeroReadBaseの下に3つのユーザータイプのモデルを作成。 ○ 3タイプ共通の項目はあるのでまずそれを HeroReadBaseで定義。 ○ 有料会員特典情報や、管理者しか扱えない機密情報、一覧での集計表示など取得の要件が ユーザータイプごとに差分ので HeroReadBaseの下に3つモデルを定義。 ○ 一覧にある情報は詳細でも取得するので、それぞれのユーザータイプで一覧用モデルを詳細 モデルに継承する。 ● 登録・更新する項目を追加する時に取得モデルの追加も別途行わないといけな いのがデメリットだが、将来の機能追加を踏まえてユーザータイプごとのアクセ ス制御のしやすさを採った。

Slide 146

Slide 146 text

Multiple Modelおまけ > まとめ ● 取得・更新やユーザータイプ共通のモデルを作るアプローチと、取得・更新や ユーザータイプでモデルを分割するアプローチがある。 ○ 前者はコードの重複が少なくなる反面、取得・更新やユーザータイプごとの柔軟な対応がしにく いデメリットがある。 ○ 後者はコードの重複が多くなる反面、柔軟な対応がしやすいメリットがある。 ○ 共通化するの苦しいなって思ったら黄信号! ● システムの将来の展望や、どちらのほうが変更しやすいかチームメンバーとすり 合わせながらどちらのアプローチを取るか決める必要がある。 146