2
4

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 3 years have passed since last update.

Rails 6で認証認可入り掲示板APIを構築する #12 userとpostの関連付け

Last updated at Posted at 2020-09-16

Rails 6で認証認可入り掲示板APIを構築する #11 userモデルのテストとバリデーション追加

postからuserへの関連付けをする

postとuserの関連付けを行います。
想定読者がRailsチュートリアル完了済み前提のため意味の説明は割愛しますが、postにbelongs_to :userを、userにhas_many :postsを追加しましょう。

$ rails g migration AddUserIdToPosts user:references

レコードがある状態だとnotnull制約に引っかかってmigrationがエラーになるので、db:resetしてしまいます(乱暴)

$ rails db:reset
$ rails db:migrate
app/models/post.rb
...
 class Post < ApplicationRecord
+  belongs_to :user
+
...
app/models/user.rb
...
   include DeviseTokenAuth::Concerns::User

 
+  has_many :posts, dependent: :destroy
+
...

2つのテーブルの関連付けを行ったら、ちゃんと動作するかrails cで実験してみます。

$ rails  c
[1] pry(main)> user = User.create!(name: "hoge", email: "test@example.com",  password: "password")
[2] pry(main)> post = Post.create!(subject: "test", body: "testtest", user: user)
[3] pry(main)> user.posts
  Post Load (0.3ms)  SELECT "posts".* FROM "posts" WHERE "posts"."user_id" = $1  [["user_id", 1]]
=> [#<Post:0x000000000488dbb0
  id: 1,
  subject: "test",
  body: "testtest",
  created_at: Tue, 08 Sep 2020 08:36:20 UTC +00:00,
  updated_at: Tue, 08 Sep 2020 08:36:20 UTC +00:00,
  user_id: 1>]
[4] pry(main)> post.user
=> #<User id: 1, provider: "email", uid: "test@example.com", name: "hoge", email: "test@example.com", created_at: "2020-09-08 08:36:11", updated_at: "2020-09-08 08:36:11">

どうやら、無事userからpostsを呼んだり、postからuserを呼んだりできていますね。

postのserializerを直す

postsのAPIから、ユーザーのIDと名前、メールアドレスを取得したいと思います。
その際に直すべきはserializerとcontroller。
最低限動くにはserializerだけで良いのですが、controllerも手を入れないとN+1問題という無駄なSQLが大量に流れてパフォーマンスを落とす状態になるのでご注意ください。

app/serializers/post_serializer.rb
...
 class PostSerializer < ActiveModel::Serializer
   attributes :id, :subject, :body
+  belongs_to :user

こうするとuserがくっついてきます。

$ curl localhost:8080/v1/posts/1
{"post":{"id":1,"subject":"test","body":"testtest","user":{"id":1,"provider":"email","uid":"test@example.com","name":"hoge","email":"test@example.com","created_at":"2020-09-08T08:36:11.972Z","updated_at":"2020-09-08T08:36:11.972Z"}}}

くっついてきたは良いけど、なんかuserの不要な情報までいっぱい取れてきてしまいましたね。
userのserializerがないので追加しましょう。

userのserializerを作る

modelを作った際にserializerは自動生成されるのですが、devise_token_authでmodel生成したため手動でコマンドを叩きます。
なお、devise_token_authによって自動生成されたcontrollerのレスポンスjsonはactiveModelSerializerが効きません。もし有効化したい場合はdevise系のcontrollerをオーバーライドする必要があるのですが、今回は割愛します。

今後postモデルからuser

$ rails g serializer user
app/serializers/user_serializer.rb
# frozen_string_literal: true

class UserSerializer < ActiveModel::Serializer
  attributes :id, :name, :email
end
$ curl localhost:8080/v1/posts/1
{"post":{"id":1,"subject":"test","body":"testtest","user":{"id":1,"name":"hoge","email":"test@example.com"}}}

これでひとまずOK。

N+1問題への対応

それでは、複数ユーザー・複数投稿データをrails cで作ってみて、curl localhost:8080/v1/postsを叩いてみます。
無事にデータは取ってこれますが、rails sで立ち上げているターミナルに移動してみると…

Started GET "/v1/posts" for 127.0.0.1 at 2020-09-08 08:48:08 +0000
Processing by V1::PostsController#index as */*
  Post Load (0.3ms)  SELECT "posts".* FROM "posts" ORDER BY "posts"."created_at" DESC LIMIT $1  [["LIMIT", 20]]
   app/controllers/v1/posts_controller.rb:12:in `index'
[active_model_serializers]   User Load (0.2ms)  SELECT "users".* FROM "users" WHERE "users"."id" = $1 LIMIT $2  [["id", 3], ["LIMIT", 1]]
[active_model_serializers]   ↳ app/controllers/v1/posts_controller.rb:12:in `index'
[active_model_serializers]   CACHE User Load (0.0ms)  SELECT "users".* FROM "users" WHERE "users"."id" = $1 LIMIT $2  [["id", 3], ["LIMIT", 1]]
[active_model_serializers]    app/controllers/v1/posts_controller.rb:12:in `index'
[active_model_serializers]   CACHE User Load (0.0ms)  SELECT "users".* FROM "users" WHERE "users"."id" = $1 LIMIT $2  [["id", 3], ["LIMIT", 1]]
[active_model_serializers]   ↳ app/controllers/v1/posts_controller.rb:12:in `index'
[active_model_serializers]   User Load (0.2ms)  SELECT "users".* FROM "users" WHERE "users"."id" = $1 LIMIT $2  [["id", 2], ["LIMIT", 1]]
...
[active_model_serializers]    app/controllers/v1/posts_controller.rb:12:in `index'
[active_model_serializers]   CACHE User Load (0.0ms)  SELECT "users".* FROM "users" WHERE "users"."id" = $1 LIMIT $2  [["id", 1], ["LIMIT", 1]]
[active_model_serializers]   ↳ app/controllers/v1/posts_controller.rb:12:in `index'
[active_model_serializers] Rendered ActiveModel::Serializer::CollectionSerializer with ActiveModelSerializers::Adapter::Json (30.42ms)
Completed 200 OK in 34ms (Views: 32.5ms | ActiveRecord: 0.8ms | Allocations: 21448)

省略していますが、こんな感じに大量のSQLが流れています。
これがN+1問題です。

postに紐づくuserを、1レコードずつfindして取ってきているので無駄なSQLが大量に流れます。

これはPost.allしてくるタイミングでincludesしておけばOKです。

app/controllers/v1/posts_controller.rb
     def index
-      posts = Post.order(created_at: :desc).limit(20)
+      posts = Post.includes(:user).order(created_at: :desc).limit(20)
       render json: posts
     end
Started GET "/v1/posts" for 127.0.0.1 at 2020-09-08 08:51:50 +0000
Processing by V1::PostsController#index as */*
  Post Load (0.2ms)  SELECT "posts".* FROM "posts" ORDER BY "posts"."created_at" DESC LIMIT $1  [["LIMIT", 20]]
   app/controllers/v1/posts_controller.rb:12:in `index'
  User Load (0.4ms)  SELECT "users".* FROM "users" WHERE "users"."id" IN ($1, $2, $3)  [["id", 3], ["id", 2], ["id", 1]]
  ↳ app/controllers/v1/posts_controller.rb:12:in `index'
[active_model_serializers] Rendered ActiveModel::Serializer::CollectionSerializer with ActiveModelSerializers::Adapter::Json (5.32ms)
Completed 200 OK in 41ms (Views: 32.7ms | ActiveRecord: 5.1ms | Allocations: 17394)

usersとposts、それぞれのテーブル1回ずつだけの計2本になりました。

とりあえずアプリケーションの動きとしては直ったように見えますが、実はこの状態でrspecを動かすと盛大にコケまくります。
次回はrspecとseedを確認していきます。

続き

Rails 6で認証認可入り掲示板APIを構築する #13 認証ヘッダの付与
連載目次へ

2
4
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
2
4

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?