Phoenix とExpoで作るスマホアプリ ④多対多のリレーション編

[Phoenix とExpoで作るスマホアプリ ①Phoenix セットアップ編 + phx_gen_auth](https://qiita.com/the_haigo/items/76dedef74ffe98230137)
[Phoenix とExpoで作るスマホアプリ ②JWT認証+CRUD編](https://qiita.com/the_haigo/items/e01e671ca117ed2b8648)
[Phoenix とExpoで作るスマホアプリ ③ファイルアップロード編](https://qiita.com/the_haigo/items/f9373d6021bb34c45676)
[Phoenix とExpoで作るスマホアプリ ④多対多のリレーション編]
(https://qiita.com/the_haigo/items/f3ecb242a8fbb9936c4b/) <- 本記事

[前回](https://qiita.com/the_haigo/items/f9373d6021bb34c45676)はファイルアップロード機能を実装しました

今回はタグのような多対多のリレーションを作成してAPIで操作をしていきたいと思います
tagモデルと中間テーブルを作成していきましょう

# Tagモデルの作成

```
mix phx.gen.json Tags Tag tags name:string user_id:references:users
mix ecto.gen.migration create_tags_posts
```

```[new]priv/repo/migrations/xxxx_create_tags_posts.ex
defmodule Sns.Repo.Migrations.CreateTagsPosts do
  use Ecto.Migration

  def change do
    create table(:tags_posts) do
      add :tag_id, references(:tags)
      add :post_id, references(:posts)
    end

    create unique_index(:tags_posts, [:tag_id, :post_id])
  end
end

```

json.genで作成したモデルにリレーションを設定していきます
join_throughには先程作成した中間テーブルを
on_replaceは postにelixir phoenixのタグがついている時に、post.tags = [nerves]で置き換えた時にelixir phoenix nervesと増やすのか、nervesだけにするのかの挙動を決めます
今回はon_replaceなので後者になります
on_deleteは元のレコードを消した時に一緒に消すのかの挙動を設定します

詳しい内容は[公式](https://hexdocs.pm/ecto/Ecto.Changeset.html#module-the-on_replace-option)をどうぞ

```[new]lib/sns/tags/tag.ex
defmodule Sns.Tags.Tag do
  use Ecto.Schema
  import Ecto.Changeset

  schema "tags" do
    field :name, :string

    belongs_to :user, Sns.Users.User 
    many_to_many :posts,
                 Sns.Posts.Post,
                 join_through: "tags_posts",
                 on_replace: :delete,
                 on_delete: :delete_all
    timestamps()
  end

  @doc false
  def changeset(tag, attrs) do
    tag
    |> cast(attrs, [:name])
    |> validate_required([:name])
  end
end
```


```[edit]lib/sns/posts/post.ex
defmodule Sns.Posts.Post do
....
  schema "posts" do
    field :body, :string

    belongs_to :user, Sns.Users.User
    has_many :images, Sns.Images.Image
    many_to_many :tags,
                 Sns.Tags.Tag,
                 join_through: "tags_posts",
                 on_replace: :delete,
                 on_delete: :delete_all
    timestamps()
  end
....
end

```

```
mix ecto.migrate
```

```[new]sns_web/controllers/api/v1/tag_controller.ex
defmodule SnsWeb.Api.V1.TagController do # <- Api.V1を追加
...
end
```

```[new]sns_web/views/api/v1/tag_view.ex
defmodule SnsWeb.Api.V1.TagView do # <- Api.V1を追加
  use SnsWeb, :view
  alias SnsWeb.Api.V1.TagView # <- Api.V1を追加
...
end

```

```[edit]sns_web/router.ex
defmodule SnsWeb.Router do
...
  scope "/api/v1", SnsWeb do
    pipe_through [:api, :jwt_authenticated]

    get "/mypage", Api.V1.UserController, :show
    resources "/posts", Api.V1.PostController, except: [:new, :edit]
    resources "/images", Api.V1.ImageController, except: [:new, :edit]
    resources "/tags", Api.V1.TagController, except: [:new, :edit] # <- ここ追加
  end
...
end
```

tag作成時にuser_idを入れるのも忘れずに

```[edit]lib/sns_web/controllers/api/v1/tag_controller.ex
defmodule SnsWeb.Api.V1.TagController do
...
  def create(conn, %{"tag" => tag_params}) do
    with {:ok, %Tag{} = tag} <- Tags.create_tag(
      Map.put(tag_params, "user_id", conn.user_id) # <- ここ変更
    ) do
      conn
      |> put_status(:created)
      |> put_resp_header("location", Routes.tag_path(conn, :show, tag))
      |> render("show.json", tag: tag)
    end
  end

...
end
```

tagを作成する準備が整ったので動作を確認してみましょう
![スクリーンショット 2020-10-14 21.33.48.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/64198/34cc9b23-8c1b-9bf8-36dc-7d520857ad22.png)



# タグ機能の実装
これで準備が整いましたのでpostにタグを付ける機能を実装していきましょう

```[edit]lib/sns_web/controllers/api/v1/post_controller.ex
defmodule SnsWeb.Api.V1.PostController do
...
  def add_tag(conn, %{"id" => id, "tags" => params}) do
    post = Posts.get_post_with_tags!(id)
    with {:ok, %Post{} = post} <- Posts.add_tag(post, params) do
      render(conn, "tags.json", post: post)
    end
  end
...
end
```

リレーション先を変更する際はpreloadする必要があるので
記事をタグ付きで取得する get_post_with_tag!と
タグを登録するadd_tagを実装します

```[edit]lib/sns/posts.ex
defmodule Sns.Posts do
...
  def get_post_with_tags!(id) do
    Post
    |> preload(:tags)
    |> Repo.get!(id)
  end

  def add_tag(%Post{} = post, ids) do
    post
    |> Ecto.Changeset.change
    |> Ecto.Changeset.put_assoc(:tags, Sns.Tags.get_tags!(ids))
    |> Repo.update()
  end
...
end
```

put_assocにに渡すidのリストからタグを取得する Tag.get_tags!を実装します

```[edit]lib/sns/tags.ex
defmodule Sns.Tags do
...
  def get_tags!(ids) do
    Tag
    |> where([t], t.id in ^ids)
    |> Repo.all
  end
...
end
```

結果を登録したタグの一覧で欲しいのでviewを追加します

```[edit]lib/sns_web/views/api/v1/post_view.ex
defmodule SnsWeb.Api.V1.PostView do
  use SnsWeb, :view
  alias SnsWeb.Api.V1.PostView
  alias SnsWeb.Api.V1.ImageView
  alias SnsWeb.Api.V1.TagView # <- tagのviewをTagViewで使うために追加
...
  def render("tags.json", %{post: post}) do
    %{data: render_many(post.tags, TagView, "tag.json")}
  end
...
  end
end

```

次にroutingを追加

```[edit]lib/sns_web/router.ex
defmodule SnsWeb.Router do
...
  scope "/api/v1", SnsWeb do
    pipe_through [:api, :jwt_authenticated]

    get "/mypage", Api.V1.UserController, :show
    post "/posts/add_tag", Api.V1.PostController, :add_tag # <- ここ追加
    resources "/posts", Api.V1.PostController, except: [:new, :edit]
    resources "/images", Api.V1.ImageController, except: [:new, :edit]
    resources "/tags", Api.V1.TagController, except: [:new, :edit]
  end
...
end
```

以上で実装が完了したので動作確認を行っていきましょう

# 動作確認
postmanで配列データを送信する際はtags[]と値のペアを複数セットする必要があるので注意してください


![スクリーンショット 2020-10-15 0.40.38.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/64198/0585bc3f-0c41-90b9-c715-0441cf4a2b77.png)
成功したので登録したタグの一覧が返ってきました

今度はid 1のタグを消してみましょう
![スクリーンショット 2020-10-15 0.40.58.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/64198/11de8d82-f7df-c0e1-ebe9-04c14157de5a.png)

id 2のタグだけが返ってきました
これで多対多のリレーションを作成しレコードを追加することができました
今回は以上になりますありがとうございました
次回はついにフロント部分に入っていきます

# 今回の差分
https://github.com/thehaigo/sns/commit/f03d25c1325b9fedeb2ab794eb9659647d77bf92
# 参考サイト
- https://www.koga1020.com/posts/ecto-assoc-functions
- https://qiita.com/himrock922/items/46a74bc8ff4b37668c31
- https://hexdocs.pm/ecto/Ecto.Changeset.html#module-the-on_replace-option
- https://qiita.com/yama-t/items/a4f940ecd7d79f27232a