[Rails]UserとArticleを紐つける6/20
はじめに
ログイン機能と投稿機能を作りましたが、ログインしているユーザだけ投稿を管理(編集、削除)できるようにさせたいのと、DB上にUserとArticleを紐つけていきたいです。
環境:
Rails 6.1.7.3
ruby 3.0.0
tl;dr
- ログインしていなければ投稿管理ができなくなるようにする
- Articleテーブルに
user_id
のForeign Key制約を追加する - モデルの関連付けを追加する
- Articleコントローラーを編集してユーザーの情報を追加する
- N+1問題について
- ビューに投稿者情報を追加する
- 投稿者だけに編集ボタンを表示する
ログインしていなければ投稿管理を利用できなくなる
パスワードリセット機能を実装した時に、before_action
メソッドを作って、ログインしてない場合にはログイン画面にリダイレクトさせるようにしましたので、articles_controller.rb
にも入れておけば完了です。
articles_controller.rb
にbefore_action
を追加する
before_action :user_login_required, only: %i[new create edit update destroy]
マイグレーションファイルを作成し、実行する
bin/rails generate migration AddUserIdToArticles user:references
Running via Spring preloader in process 13993
invoke active_record
create db/migrate/20230622073520_add_user_id_to_articles.rb
bin/rails db:migrate
== 20230622073520 AddUserIdToArticles: migrating ==============================
-- add_reference(:articles, :user, {:null=>false, :foreign_key=>true})
-> 0.0319s
== 20230622073520 AddUserIdToArticles: migrated (0.0320s) =====================
schema.rb
を確認する
schema.rb
のArticles
テーブルにuser_id
カラムを追加されたことを確認します。
create_table "articles", force: :cascade do |t|
t.string "title", null: false
t.text "body"
t.datetime "created_at", precision: 6, null: false
t.datetime "updated_at", precision: 6, null: false
t.integer "user_id", null: false
t.index ["user_id"], name: "index_articles_on_user_id"
end
モデルにアソシエーションを追加する
class User < ApplicationRecord
has_many :articles, dependent: :destroy
end
class User < ApplicationRecord
belongs_to :user
end
articles_controller.rb
を編集する
投稿を登録される時に、ログインしているユーザも代入するようにします。
class ArticlesController < ApplicationController
def index
# n+1問題回避
@articles = Article.all.includes(:user)
end
# ユーザも代入する
def create
@article = Current.user.articles.build(article_params)
if @article.save
flash[:success] = "記事が作成できました。"
redirect_to @article
else
flash[:danger] = "記事の作成が失敗しました。もう一度試してください。"
render :new
end
end
end
N+1問題
N+1 problem(N+1クエリ問題)は、データベースからデータを取得する際に余分なクエリが発生することによって発生します。
具体的には、次のような状況でN+1問題が発生します:
- 主要なクエリ(例:記事の一覧を取得するクエリ)を実行します。
- 主要なクエリの結果に基づいて、関連するレコードの詳細情報を取得するために追加のクエリを発行する必要があります。
- もし関連するレコードがN個ある場合、N+1回のクエリが発生します。1回目は主要なクエリ、N回は関連するレコードの詳細情報を取得するための追加のクエリです。
この問題が起こると、データベースへのクエリ数が増え、処理時間やリソースの消費が増加します。特に関連するデータが大量にある場合やループ内でクエリが発生する場合に問題が顕著になります。
例えば、ユーザーとその投稿(記事)の関連を考えると、N+1問題が発生する典型的なシナリオは次の通りです:
@users = User.all
@users.each do |user|
puts "User: #{user.name}"
user.posts.each do |post|
puts "Post: #{post.title}"
end
end
上記のコードでは、まずすべてのユーザーを取得しています。そして、各ユーザーごとに関連する投稿を取得して表示しています。この場合、ユーザーの数がNであれば、ユーザーの数だけクエリが発行されます。つまり、1回の主要なクエリとN回の追加のクエリが発生し、N+1回のクエリが実行されることになります。
N+1問題の解決策の1つは、Eager Loading(積極的な事前読み込み)を使用することです。これにより、必要なデータを効率的に1つのクエリで取得できます。
例えば、上記の例をEager Loadingを使用して修正すると、次のようになります:
@users = User.includes(:posts)
@users.each do |user|
puts "User: #{user.name}"
user.posts.each do |post|
puts "Post: #{post.title}"
end
end
投稿一覧に投稿者の情報を追加する
<h1>Articles</h1>
<table class="table">
<thead>
<tr>
<th scope="col">ユーザ名</th>
<th scope="col">タイトル</th>
<th scope="col">本文</th>
<th scope="col">投稿管理</th>
</tr>
</thead>
<tbody>
<% @articles.each do |article| %>
<tr>
<td><%= article.user.user_name %></td>
<td><%= link_to article.title, article %></td>
<td><%= article.body %></td>
<td><%= link_to "Show", article_path(article), class: 'btn btn-secondary' %>
# ユーザをタスクを紐つける
<% if Current.user && Current.user.id == article.user_id %>
<%= link_to "Edit", edit_article_path(article), class: 'btn btn-primary' %>
<%= link_to "Delete", article_path(article),
method: :delete, data: {confirm: "削除してよろしいでしょうか?"}, class: 'btn btn-danger' %></td>
<% end %>
</tr>
<% end %>
</tbody>
</table>
<%= link_to "New Article", new_article_path, class: "btn btn-primary" %>
別のアカウントでログインし、他のユーザの投稿に管理ボタンを表示されないことを確認します。
投稿者だけに管理ボタンを表示させる
このままだどビューだけ非表示はできていますが、URLからアクセスし編集、削除することができます。
コントローラーにも制御を追加していきます。
id
で投稿を特定することを共通化にしてbefore_action
に入れときます。
投稿者でない場合URLからアクセスするとエラーメッセージを表示させ、ページをリダイレクトさせます。
class ArticlesController < ApplicationController
before_action :set_article, only: %i[edit update destroy]
def edit
unless @article.user_id == Current.user.id
flash[:danger] = "投稿を管理する権限がありません。"
redirect_to @article
end
end
def destroy
unless @article.user_id == Current.user.id
flash[:danger] = "投稿を管理する権限がありません。"
redirect_to @article
end
@article.destroy
flash[:success] = "投稿が削除されました。"
redirect_to articles_path
end
private
def set_article
@article = Article.find_by(id: params[:id])
end
URLからarticles/1/edit
アクセスして動作を確認します。
終わりに
投稿をユーザと紐つけることができました。
関連付けは面白いですね、もっと機能を増やしていきたいと思います。
Discussion