←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
...
class Post < ApplicationRecord
+ belongs_to :user
+
...
...
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が大量に流れてパフォーマンスを落とす状態になるのでご注意ください。
...
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
# 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です。
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を確認していきます。