Associate post and user.
Assuming that the intended reader has completed the Rails tutorial, I will omit the explanation of the meaning, but let's add belongs_to: user
to post and has_many: posts
to user.
$ rails g migration AddUserIdToPosts user:references
If there is a record, it will be caught in the not null constraint and migration will result in an error, so db: reset will occur (rough)
$ 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
+
...
After associating the two tables, try experimenting with rails c
to see if it works.
$ rails c
[1] pry(main)> user = User.create!(name: "hoge", email: "[email protected]", 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: "[email protected]", name: "hoge", email: "[email protected]", created_at: "2020-09-08 08:36:11", updated_at: "2020-09-08 08:36:11">
Apparently, you can safely call posts from user and user from post.
I would like to get the user's ID, name and email address from the posts API. At that time, the serializer and controller should be fixed. At the very least, you only need serializer to work, but please be aware that if you do not modify the controller, a large amount of useless SQL called N + 1 problem will flow and performance will drop.
app/serializers/post_serializer.rb
...
class PostSerializer < ActiveModel::Serializer
attributes :id, :subject, :body
+ belongs_to :user
This will make the user stick together.
$ curl localhost:8080/v1/posts/1
{"post":{"id":1,"subject":"test","body":"testtest","user":{"id":1,"provider":"email","uid":"[email protected]","name":"hoge","email":"[email protected]","created_at":"2020-09-08T08:36:11.972Z","updated_at":"2020-09-08T08:36:11.972Z"}}}
It's good that they're stuck together, but I've got a lot of unnecessary information from the user. There is no serializer for user, so let's add it.
The serializer is automatically generated when the model is created, but since the model was created with devise_token_auth, the command is manually hit. In addition, activeModelSerializer does not work for the response json of the controller automatically generated by devise_token_auth. If you want to enable it, you need to override the devise controller, but I will omit it this time.
From now on user from post model
$ 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":"[email protected]"}}}
This is OK for the time being.
Now, let's create multiple user / multiple post data with rails c and hit curl localhost: 8080 / v1 / posts
.
I can get the data safely, but when I move to the terminal launched with 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)
Although omitted, a large amount of SQL is flowing like this. This is the N + 1 problem.
Since the user associated with post is found and fetched one record at a time, a large amount of useless SQL flows.
This is OK if you include it at the timing when Post.all
comes.
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)
There are now two tables, one for users and one for posts.
For the time being, the behavior of the application seems to have been fixed, but in fact, if you move rspec in this state, it will moss grandly. Next time, I will check rspec and seed.
→ Building a bulletin board API with authentication authorization in Rails 6 # 13 Adding authentication header [To the serial table of contents]