今更ながらFastAPI + SQLAlchemy でテーブルリレーションの実装まとめ（多対多）

## 概要
前回の記事で、FastAPIとSqlalchemy を用いたバックエンドAPIの構築の際、
- １対１
- １対多

の構成についてまとめました。
次は、`多対多`の構成についてまとめてみたいと思います。

また、今回は、

https://www.gormanalysis.com/blog/many-to-many-relationships-in-fastapi/

ここの記事を大いに参考にして自分が実装を行ったので、その時の備忘録となります。

前回の記事にも書きましたが、FastAPI とSqlalchemy を用いたバックエンドサービス構成の実装自体は

https://zenn.dev/sh0nk/books/537bb028709ab9/viewer/f1b6fc

この記事が非常にわかりやすく、多大に参考にさせていただきました。
ただ、Async Session でリレーションが引っ張れない箇所があり、自分は従来のSessionを用いて構築しているところだけ少し異なっています。



## 多対多

ここで、多対多とは、
`Model1` が複数の`Model2`と紐付き、さらに`Model2`が複数の`Model1`とも結びついている時に実装する構成です。
多対多についての実装方法を調べるといろいろな記事が出てくると思いますが、いずれにしても`中間テーブル`を構成する必要があります。

`中間テーブル`とは、`Model1`と、`Model2`の関係性を保持するテーブルのことで、
例えば、`Model1Model2` のような名前のテーブルを追加で作成し、関係性をそこに追加することになります。

https://www.gormanalysis.com/blog/many-to-many-relationships-in-fastapi/

この記事を参考に私は実装を行いましたが、

1. 中間テーブルがリレーションだけを持つ場合
2. 中間テーブルがリレーション以外の情報も持つ場合

１に関しては、上のリンクの　`Many-to-many without extra data`に相当し、
２に関しては、上のリンクの `Many-to-many with extra data` に相当します。
私は、２についての実装を行いたかったのですが、その際に、まずは１を実装し、多対多の構成がうまく実装できていることを確かめた後、実装を変更し２の構成にできることを確かめるステップで実装を行いました。

それに従って、１、２と順を追ってまとめたいと思います。

### 中間テーブルがリレーションだけを持つ場合
私の実装では、`User` テーブルと、`Project` テーブルを多対多でリレーションするようにし、
中間テーブルは、`UserProject` という名前でテーブル定義しています。
実装は以下のようになります。


```api/models/user.py
class User(Base):
    __tablename__ = 'user'
    id = Column(Integer, primary_key=True, autoincrement=True)
    name = Column(String(length=30))
    password = Column(String(length=30))

    # relation
    projects = relationship("Project", secondary="user_project", back_populates="users")
    
```

```api/models/project.py
class Project(Base):
   __tablename__ = "project"
   id = Column(Integer, primary_key=True, autoincrement=True)
   name = Column(String(length=30))

   # relation
   users = relationship("User", secondary="user_project", back_populates="projects")

```

```api/models/user_project.py
class UserProject(Base):
    __tablename__ = 'user_project'
    # foreign key
    user_id = Column(Integer, ForeignKey("user.id"), primary_key=True)
    # foreign key
    project_id = Column(Integer, ForeignKey("project.id"), primary_key=True)

```


`User` と`Project` のrelation に、`secondary="user_project"`
として、中間テーブルを指定しているところがポイントです。
紐つけるところは、SQLAlchemy が実行してくれます。

### api/schemas

api/schemas  については、１対多で書いた時のように、
`User` に対しては `List[ProjectBase]` を保持させ、
`Project` に対しては　`List[UserBase]`を保持させるように、
双方に双方が紐つくように書きます。

```api/schemas/user.py

class UserBase(BaseModel):
    id: int = Field(alias="user_id")
    class Config:
        orm_mode = True

class User(UserBase):
    projects: Optional[List[ProjectBase]]

```

```api/schemas/project.py

class ProjectBase(BaseModel):
    id: int
    class Config:
        orm_mode = True

class Project(ProjectBase):
    users: List[UserBase]

```

### api/cruds

```api/cruds/user.py
async def get_users(db: Session) -> List[user_model.User]:

    users = db.query(user_model.User).options(joinedload(user_model.User.projects))

    return users.all()
```

例えば、上のように`User`を取得する時（つまりCRUDのR）は、
１対多の時と同じように、
`.options(joinedload(user_model.User.projects))`
を用いて、`User`に紐ついた`Project` も複数引っ張ってきます。
これは、`Project`を取得する時のロジックとしても同じロジックで実装します。

```api/cruds/project.py

async def create_project(
    db: Session, project_create: project_schema.ProjectCreateRequest
) -> project_model.Project:
    user = await user_cruds.get_user(db, project_create.user_id)
    project = project_model.Project(
        name=project_create.name
    )
    project.users.append(user)
    db.add(project)
    db.commit()

    return project
```

`Project` を `User` に紐つけて追加する時は、
紐つける先の`User`を取得し、
`project.users.append(user)`　もしくは
`user.projects.append(project)` によってデータを紐つけた後にコミットすることで、
中間テーブルに値をセットすることができます。



以上の実装で、多対多のテーブル関係を構築でき、多対多のテーブルに対して付加情報を挿入しなくて良いのであれば、ここで実装完了です。お疲れ様でした。


### 中間テーブルがリレーション以外の情報も持つ場合

この条件がどういうことかというのは、例を出すとわかりやすくなります。
私の実装の例だと、`User`は複数の`Project`を持っている可能性があり、`Project`には複数の`User`が所属しているため、`User` と`Project`は多対多の関係にあり、その関係は`UserProject`という中間テーブルの実装を行うことで構成できる、ということは前節でまとめました。

今回行いたいのは、各`User`が、`Project`に応じて役割（role)という付加情報を持つ時の実装です。

例えば、user1 はproject1 に所属しており、`owner`という役割を持っている。
しかし、user1 はproject2 にも所属しており、そこでは`developer`という役割を持っているとします。

そのとき、これらの役割は、`UserProject`中間テーブルに付加情報として保存されるべきです。
つまり、中間テーブルは、`User`、`Project`テーブルの関係をidで保持するだけでなく、`role`というカラムを保持します。
そのような構成が必要な時は、先ほどの多対多の実装を少し変更する必要があり、多少複雑になります。

### api/models

まず、`User`, `Project` 両方のモデルにおいて、
relationship を双方から`UserProject`の中間テーブルへと変更します。

```api/models/user.py
class User(Base):
    __tablename__ = 'user'
    id = Column(Integer, primary_key=True, autoincrement=True)
    name = Column(String(length=30))
    password = Column(String(length=30))

    # relation
    #projects = relationship("Project", secondary="user_project", back_populates="users")
    projects = relationship("UserProject", back_populates="user")
```

```api/models/project.py
class Project(Base):
   __tablename__ = "project"
   id = Column(Integer, primary_key=True, autoincrement=True)
   name = Column(String(length=30))

   # relation
   #users = relationship("User", secondary="user_project", back_populates="projects")
   users = relationship("UserProject", back_populates="project")
```

中間テーブルには、`role`というカラムを追加し、
relationship を追加します。
これは、`User`, `Project`が`UserProject`にリレーションを持つように変更されたので、その影響です。

まさに１対N対１を`User`対`UserProject`対`Project`で実装しているような格好になります。





```api/models/user_project.py
from sqlalchemy.ext.associationproxy import association_proxy

class UserProject(Base):
    __tablename__ = 'user_project'
    # foreign key
    user_id = Column(Integer, ForeignKey("user.id"), primary_key=True)

    # foreign key
    project_id = Column(Integer, ForeignKey("project.id"), primary_key=True)

    role = Column(String(30), nullable=False)

    user = relationship("User", back_populates="projects")
    project = relationship("Project", back_populates="users")

    # proxies
    user_name = association_proxy(target_collection="user", attr="name")
    project_name = association_proxy(target_collection="project", attr="name")
```

最後の `association_proxy`についてですが、
これは、`User`からみた時の`Project`.name は、

```api/schemas/user.py
class User(UserBase):
    projects: Optional[List[ProjectBase]]

    class Config:
        orm_mode = True
```
からschemaに定義されているように、`user.project.name` で取得できるはずですが、
今回は `User` は`UserProject`とリレーションを持っているため、
`user.project.name` ではアクセスできなくなっています。
それを解決するために、`user.projects.project_name` 
でアクセスできるようにしているというコードです。


### api/schemas

先述の変更に合わせて、schema も少しだけ変更する必要があります。

`UserBase`, `ProjectBase` に
`Field(alias)`が追加されています。
また、 `allow_population_by_field_name = True`
も追加されています。

これらは、`User` に紐ついている`Project`のidは、
pydantic schema の`User`からすると、`project.id` で取得できるはずですが、
実際は `project.project_name`で取得されます。理由は`User`が参照しているのは、`Project`ではなく`UserProject`だからですね。
したがって、そのことをpydantic が認識する必要があり、そのためにalias を用いる必要があります。

`project_id`として`UserProject`から参照されている（`user_id`, `user_name`, `project_name`も同じ）
ため、それらもalias で書き換えます。

```api/schemas/user.py
class UserBase(BaseModel):
    id: int = Field(alias="user_id")
    name: str = Field(alias="user_name")
    role: Optional[str]
    class Config:
        orm_mode = True
        allow_population_by_field_name = True
```

```api/schemas/project.py
class ProjectBase(BaseModel):
    id: int = Field(alias="project_id")
    name: str = Field(alias="project_name")
    role: Optional[str]
    class Config:
        orm_mode = True
        allow_population_by_field_name = True

```


以上を実行することで、無事今までと同じようなcruds を用いて、
実際にリレーションを加味してレコードの取得ができるようになりました。

これで問題なく取得できるのですが、出てくるスキーマに無駄なnull が入っていたりするので、そこの解消として、

```
Looking at this response, two annoying issues stand out:
```

から始まるセクションの変更を加えると、尚自然にレコードの取得ができるので、そこは元のリンクをご覧ください。

これでついに、多対多＋リレーション以外のレコードを含むタイプの中間テーブルの実装ができました。
お疲れ様でした。



### まとめ

前回と今回で、FastAPI + SQLAlchemy + RDS での

- １対１
- １対多
- 多対多

のそれぞれのリレーションを持つテーブルのバックエンドを構築することができました。

ここまでできれば、おそらくいろんな構成のサービスであってもバックエンドを構築することが可能なはずです。
もちろん、特殊なケースはこの分類に入らないものもあるかと思います。（例えば木構造を持たせたいとか）

しかしながら、一度このようなバックエンドAPI構築を経験しておくことは重要だと思い、今回記事を書いてみました。

FastAPIはSwaggerUIを自動生成し、非常に強力なバックエンドツールとして、
テストなどもしやすく、非常に魅力的だなと感じました。

次回は、ふと気になったので、FastAPI ＋　MongoDB(NoSQLタイプ）の実装というのもやってみたいと思います。

BackendAPIは今まではNestJSを使ったり、Flaskで簡易的に実装したり、などがありましたが、
慣れていればFastAPIいいなあと思いました。

ORMなど追いかけ始めるといろいろあるので経験するのは大変だとは思いますが、
いろいろ試してみて自分の好きなものを見つけるのも楽しいので、興味のある人がいればぜひ自分で構築してみてください。


みなさんもよいFastAPIライフを。



今回はこの辺で。


@kenmaro