プログラミング備忘録

プログラミングの学習状況をメモしています

17日目

今日の学習

Ruby on Rails

モデルの関連づけ

モデル間の関連づけ(アソシエーション)を学習。

参考

やんばるエキスパート教材 | モデルの関連付け その1(1対多)

テーブルを、データごとに分ける重要性をおさえる(User、Postなど)

user_idというカラムがあれば、postsテーブルの内容と組み合わせることで、どのuser_idの人がどんな投稿をしたかを判別できる。

idのような、テーブル内でレコードを重複せずに識別することができるカラムを、主キー(primary key)...PKと呼ぶ。

それに対して、user_idのような、別のテーブルのidに含まれているものしか入力できないカラムは、外部キー(foreign key)...FKと呼ぶ。

Userは投稿(Post)を複数回できる。一方で、Postは常にUser(投稿者)が一人と確定している。

このUserとPostの関係は、1対多の関係となる。

1対多の関連付け

モデルを作成する際に、user_id:integerとして登録するのではなく、user:referencesとすることで外部キーであることを明示できる。

親テーブル名(単数系):references

例えばPostモデルにuser:referencesを加えてモデルを作成すると、Postモデルにbelongs_to :userが自動で追加される。

belongs_to :親テーブル(単数系) (親は常に一つなので単数系)

belongs_to :userがあると、Postモデルのインスタンスにuserというインスタンスメソッドが作成され、メッセージを投稿したユーザーのデータが取得できるようになる。

また、user_idが必須になり、バリデーションも自動で入る。

Postは子、Userは親という関係性になる。今度は、親であるUserモデルに、has_many :postsを入れることで、userモデルのインスタンスにpostsというインスタンスメソッドが作成される。

これにより、ユーザーの投稿したメッセージの全データを取得することができて、以下の書き方が可能になる。

#has_manyを利用しない形
Post.create(content: "こんにちは", user_id: user1.id)

#has_manyを利用した形
user1.posts.create(content: "こんにちは")

また、外部キーの設定を入れている場合は、has_many :postsに引き続いてdependent: :destroyを入れておくことで、ユーザーを削除した際に、削除したユーザーの投稿も自動で削除されるようになる。

has_many :子の複数形, dependent: :destroy (親は複数の子を持てるので複数系)

*子のモデルにはdependent: : destroyを入れないようにする。メッセージを削除したときにユーザーが削除されてしまうため。

N+1問題

投稿数の数だけSQL(データベースへの命令)が発行されてしまうため、投稿数が多い場合はSQLが大量に発行されてしまい、動作が重くなってしまうという現象。

SQLはUserテーブル、Postテーブルをつなぐ作業をしてくれている。

N+1問題 - Qiita

【Ruby on Rails】N+1問題ってなんだ? - Qiita

関連テーブルの情報を事前に読み込めばOKなので、includesなどを利用する。

コントローラで、投稿一覧を表示する箇所に対して、

@posts = Post.includes(:user).all

このように記述し、問題を防ぐ。

テスト投稿が5件ある状態のターミナル。 f:id:hasegawa_note:20210518221143p:plain

includesで対応した後のターミナル。 f:id:hasegawa_note:20210518221208p:plain

短い!すっきり。

UserとPostを紐付ける

投稿

今まで作ってきた簡単なアプリでは、どのユーザーが投稿したか、などの設定はしてこなかったので、新規投稿を作成するcreateアクションには、

post = Post.create!(post_params)

このような記述をしてきたが、どのユーザーが投稿したのか?ということを決める必要があるため、

post = current_user.posts.create!(post_params)

#この書き方と同じ意味
post = Post.new(post_params)
post.user_id = current_user.id
post.save!

として、user_idを現在ログイン中のuser_idとpostを紐付ける必要性がある。

編集と削除

投稿物は、投稿した本人しか編集や削除できないように設定しておく必要がある。

そのために、まず「編集」「削除」といったものは、ログインしている人のuser_idが、投稿物のuser_idと合致する場合にのみ表示する。

ビューファイルに、

<% if current_user.id == post.user_id %>
    <%= link_to "編集", edit_post_path(post) %>
    <%= link_to "削除", post_path(post), method: :delete %>
<% end %>

このようなif文で、現在ログインしているユーザーのidが、その投稿のuser_idと合致するのであれば表示させるという仕組みに変えておく。

さらに、コントローラ側でも、ユーザーのidが合致していない場合は削除できないという記述をする必要がある。

class PostsController < ApplicationController

before_action :set_post, only: %i[edit update destroy]
...
...
private

  def set_post
    @post = current_user.posts.find(params[:id])
  end

before_actionを用いて、編集と削除に関連しているedit, update, destroyアクションに対して、他人の投稿したidを指定するとエラーが発生するようにしておく。

多対多の関連付け

Twitterなどで見られる「いいね」機能は、多対多と呼ばれる。

一人のユーザーは複数の投稿をいいねすることができる。(多)

また、一つの投稿は複数のユーザーからいいねを受けることができる。(多)

いいねをするユーザーのuser_id、いいねをされる投稿のpost_idの情報があれば、誰がどの投稿をいいねしたのかが分かるようになる。

そのデータを保存して取り扱うために、もう一つの「いいね用のテーブル」が必要になってくる。テーブル名は以後likesテーブルとする。

この他対他の関連付けを行うためのテーブルを、中間テーブルと呼ぶ。

中間テーブルを作成後は、いいねのデータが重複しないような制約を入れてからマイグレーションを行う必要がある。

class CreateLikes < ActiveRecord::Migration[6.0]
...
...
add_index :likes, [:user_id, :post_id], unique: true

user_id, post_idという組を重複して保存できないように設定。

さらに、特定のユーザーがいいねした投稿の一覧や、特定の投稿にいいねをしたユーザーの一覧を表示するためには、それぞれuser、postモデルに次のような記述をしなければならない

#postモデルの場合
has_many :liked_posts, through: :likes, source: :post

#userモデルの場合
has_many :liked_users, through: :likes, source: :user

has_many :メソッド名, through: :中間テーブル名(複数系), source: :相手のテーブル名(単数系) *相手のテーブル名は、中間テーブルとの関連付けに使用したメソッド名 これにより、中間テーブルを経由して、多対多の関連付けができる。

いいね機能を実装する

いいねをしているか、していないのかの条件分岐が必要となるため、モデルにいいねしているかどうかを判定するメソッドを作成する。

def liked_by?(user)

    likes.exists?(user_id: user.id)

end

いいね機能には、「いいねをする」と、「いいねを解除する」機能が必要なので、両方用意する。

まずルーティングを設定する必要があるが、likespostsネストさせることで、いいねの操作をする投稿のidを、params[:post_id]で受け取れるようになる。

RailsのRoutingネストについて - Qiita

【Rails基礎】プログラミング初学者がつまずきやすい「ルーティングのネスト」について簡単に解説|TechTechMedia

難しい...。

要素の紐付けのために必要。

あとは、likesコントローラにcreate(いいねをする)、destroy(いいねを解除)アクションを作成する。

def create

    current_user.likes.find_by(post_id: params[:post_id])

end

削除するdestroyアクションには、.destroy!をつけること。また、redirectでページを遷移させる。

また、likesテーブルが新たに加わったため、またN+1問題が発生してしまう。

そのため、includesの部分に、:likesを追加して対応すること。

いいね機能の非同期

create、destroyアクションの後にredirectをすることで、ページがリロードされるように設定していたが、ページのリロードなしでもいいねの切り替えができるようにする。

非同期処理というものを実装することで、可能になる。

js.erbファイルという、Railsに搭載されている機能を使う場合のやり方を学習。

js.erbを利用する場合は、リンクにremote: trueオプションを追加する。

このオプションがあると、HTMLのコードにdata-remote="true"が入るようになる。

わかりやすい記事があったので貼り付けておく。

Railsで remote: true と js.erbを使って簡単にAjax(非同期通信)を実装しよう!(いいね機能のデモ付) - Qiita

覚えることがたくさんあったが、いいね機能などは昨今のサイトではほぼ確実に実装されている要素であるし、他の使い道もあるはずなのでなんとか身につけたい。