🦓

[Rails]UserとArticleを紐つける6/20

2023/06/22に公開

はじめに

ログイン機能と投稿機能を作りましたが、ログインしているユーザだけ投稿を管理(編集、削除)できるようにさせたいのと、DB上にUserとArticleを紐つけていきたいです。

環境:

Rails 6.1.7.3
ruby 3.0.0

tl;dr

  1. ログインしていなければ投稿管理ができなくなるようにする
  2. Articleテーブルにuser_idのForeign Key制約を追加する
  3. モデルの関連付けを追加する
  4. Articleコントローラーを編集してユーザーの情報を追加する
  5. N+1問題について
  6. ビューに投稿者情報を追加する
  7. 投稿者だけに編集ボタンを表示する

ログインしていなければ投稿管理を利用できなくなる

パスワードリセット機能を実装した時に、before_actionメソッドを作って、ログインしてない場合にはログイン画面にリダイレクトさせるようにしましたので、articles_controller.rbにも入れておけば完了です。

articles_controller.rbbefore_actionを追加する

app/controllers/articles_controller.rb
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.rbArticlesテーブルにuser_idカラムを追加されたことを確認します。

db/schema.rb
  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

モデルにアソシエーションを追加する

app/models/user.rb
class User < ApplicationRecord
  has_many :articles, dependent: :destroy
end
app/models/user.rb
class User < ApplicationRecord
  belongs_to :user
end

articles_controller.rbを編集する

投稿を登録される時に、ログインしているユーザも代入するようにします。

app/controllers/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問題が発生します:

  1. 主要なクエリ(例:記事の一覧を取得するクエリ)を実行します。
  2. 主要なクエリの結果に基づいて、関連するレコードの詳細情報を取得するために追加のクエリを発行する必要があります。
  3. もし関連するレコードが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

投稿一覧に投稿者の情報を追加する

app/views/articles/index.html.erb
<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からアクセスするとエラーメッセージを表示させ、ページをリダイレクトさせます。

app/controllers/articles_controller.rb
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アクセスして動作を確認します。

終わりに

投稿をユーザと紐つけることができました。
関連付けは面白いですね、もっと機能を増やしていきたいと思います。

https://railsguides.jp/association_basics.html

Discussion