LoginSignup
8
6

More than 3 years have passed since last update.

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

Posted at

Phoenix とExpoで作るスマホアプリ ①Phoenix セットアップ編 + phx_gen_auth
Phoenix とExpoで作るスマホアプリ ②JWT認証+CRUD編
Phoenix とExpoで作るスマホアプリ ③ファイルアップロード編
Phoenix とExpoで作るスマホアプリ ④多対多のリレーション編 <- 本記事

前回はファイルアップロード機能を実装しました

今回はタグのような多対多のリレーションを作成して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は元のレコードを消した時に一緒に消すのかの挙動を設定します

詳しい内容は公式をどうぞ

[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

タグ機能の実装

これで準備が整いましたので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
成功したので登録したタグの一覧が返ってきました

今度はid 1のタグを消してみましょう
スクリーンショット 2020-10-15 0.40.58.png

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

今回の差分

参考サイト

8
6
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
8
6