Railsで実装する「いいね」機能の方法 — 多対多の活用術

はじめに

Railsではユーザーが投稿やコメントなどに対して「いいね」を付けられる仕組みを実装することがよくあります。
「いいね」機能はSNSやブログ、商品レビューのような分野で頻繁に活用されており、ユーザー同士の交流や評価を可視化するうえでとても便利です。

しかし、多くの初心者の方は「どうやってデータベース設計を行えばいいのか」「コントローラやモデルをどのように連携させればいいのか」といった疑問を持つことがあるかもしれません。
そこで本記事では、Railsにおける「いいね」機能の実装を多対多の関係を使って整理します。

多対多の関係を用いることで、ユーザーと投稿(あるいは他のオブジェクト)の間に柔軟な「いいね」情報を保存しやすくなります。
さらに実務で役立つように、パフォーマンス、セキュリティ、データの整合性といった観点も含めて解説します。

最後まで読むことで、記事やSNSなどさまざまな場面で応用できる「いいね」機能の知識をしっかり身につけられるでしょう。
初心者の方にもわかりやすい形で整理していますので、どうぞ気軽に読み進めてみてください。

この記事を読むとわかること

  • 「いいね」機能の基本的な仕組みと、多対多テーブルを用いる理由
  • 実務で想定される具体的な利用シーンとメリット
  • Railsでのモデル設計・コントローラ設計・ビュー実装の流れ
  • パフォーマンスやセキュリティに関する考え方
  • トラブルシュートやテストのポイント

ここから順を追って解説します。
実際にコードを組んだり、アプリケーションに組み込んだりする際のイメージがつかめるように、なるべく具体的にお話ししていきます。

Railsで「いいね」機能の基本を理解する

「いいね」機能とは

「いいね」機能は、ユーザーが投稿・コメント・写真などに対して気軽に評価や好意を示す仕組みを指します。
SNSはもちろんのこと、商品の口コミサイトやブログにも取り入れられる場合があります。

この機能があると、ユーザー同士でのコミュニケーションが活性化しやすいです。
単純に「面白い」「役立った」という感情を素早く共有できるため、投稿を作る側にもモチベーションが生まれるというメリットがあります。

Railsで実装するにあたっては、ユーザーと「いいね」される対象(記事、写真、コメントなど)のモデルをどうやって管理するか、という視点が重要です。
初心者の方でも、モデル間のリレーションをきちんと把握すれば難しいものではありません。

多対多の関係を使う理由

「いいね」機能を実装する場合、ユーザーと「いいね」対象は多対多の関係になりやすいです。
理由は、一人のユーザーが複数の投稿に「いいね」する可能性があるし、一つの投稿が複数のユーザーから「いいね」される可能性もあるからです。

Railsでは、多対多のリレーションを扱うために「中間テーブル」(joinテーブル)を使います。
このテーブルにユーザーIDと投稿IDを格納することで、どのユーザーがどの投稿を「いいね」したかが明確になります。

多対多リレーションを使うメリットとしては、データの読み込みや書き込みがわかりやすく管理できる点があります。
例えばユーザーがすでに「いいね」をしているかどうかを確認したり、「いいね」の数をカウントしたりといった操作は、ジョインテーブルを通してスムーズに行えます。

実務での活用シーン

SNSの投稿システム

代表的な活用シーンとしては、SNSにおける投稿の「いいね」機能が挙げられます。
多くのSNSは、ユーザーが投稿を見て「いいね」ボタンを押すと、その投稿に対する評価がリアルタイムに反映されます。

この仕組みを支えているのが、ユーザーと投稿を結びつける中間テーブルです。
ユーザーは投稿に自由に「いいね」できるため、関係性としては多対多となります。
中間テーブルにデータを追加・削除するだけで、「いいね」や「いいねの取り消し」を簡単に管理できます。

特にSNSは同時アクセス数が多いことから、パフォーマンスの観点でも多対多テーブルの活用は重要です。
余計なデータ構造を使わず、シンプルにJOINを行う形で「いいね」の一覧や件数が扱いやすくなるのが強みです。

Eコマースサイトの商品レビュー

「いいね」機能は投稿に限りません。
例えばECサイトで商品レビューに「役に立った」ボタンを設けたり、ユーザー同士のコメントに「参考になった」と評価する仕組みを入れたりするケースもよく見られます。

このときも多対多の関係は同じです。
商品レビューのテーブルとユーザーのテーブルを中間テーブルで結びつければ、どのユーザーがどのレビューを支持しているのかがわかります。

こうしたシーンでは、レビュー自体の信頼性を高めたり、ユーザーにフィードバックする場面で「いいね」の数が利用されることが多いです。
実装方法は投稿の場合とほとんど変わりませんが、対象がレビューなのか投稿なのかによってモデル名などが変わる程度です。

多対多テーブルで「いいね」を実装する手順

モデル設計

Railsでは、ユーザーモデルを仮にUser、投稿モデルをPostという名前で定義するとします。
このとき「いいね」情報を管理する中間テーブルをLikesなどと呼ぶことが多いです。

  • User
  • Post
  • Like

ポイントとしては、LikeモデルにユーザーIDと投稿IDの両方を持たせることです。
UserとPostの間を多対多で結ぶ場合、Userモデルでは has_many :likeshas_many :posts, through: :likes のように書くことができます。
同様にPostモデルでも has_many :likeshas_many :users, through: :likes と書きます。

なお、ユーザーが特定の投稿に対して2回「いいね」できないようにするために、User IDとPost IDの組み合わせに対してユニーク制約を設けることが一般的です。
これはマイグレーションファイルで行います。

コントローラでの処理の流れ

「いいね」機能をコントローラで扱う場合、シンプルな形としては以下のような流れになります。

  1. ユーザーが投稿に対して「いいね」ボタンをクリックする
  2. LikesController(またはPostsController内のアクションなど)で、Like.create(user_id: current_user.id, post_id: params[:post_id]) のようにデータを追加
  3. リダイレクトまたは非同期通信によって、画面を更新して「いいね」の数を反映

取り消し(「いいね」解除)を行う場合は、同様にコントローラで対象の Like レコードを探して削除する流れです。
たとえば Like.find_by(user_id: current_user.id, post_id: params[:post_id]).destroy といった形になります。

ここではRailsのセキュリティ機能を活用し、before_actionでログイン済みかどうかをチェックすると良いでしょう。
また、ユーザーがログインしていない状態で「いいね」ボタンを押せないようにするなどの実装も必要です。

ビューでの表示と「いいね」ボタンの設置

ビューでは「いいね」の数を投稿のそばに表示しておきます。
たとえば <%= post.users.count %> のように書くと、その投稿に「いいね」したユーザーの数を得られます。

ただし、単純にcountを毎回呼ぶと、投稿が大量にあるときに速度面で負荷がかかる場合があります。
後述するパフォーマンス対策として、キャッシュやカウンターカラムを使う方法も検討しましょう。

「いいね」ボタンは単純なリンクでもよいですが、見た目を分かりやすくするためにアイコンを使うことも多いです。
たとえばハートマークなどをFont Awesomeなどのアイコンライブラリで表示すると、ユーザーに直感的に伝わりやすくなります。

パフォーマンスとデータベース設計の考え方

インデックスの貼り方

中間テーブルを使う場合、「いいね」を探すクエリが頻繁に走るシステムでは、データベースのインデックス設計がカギになります。
特にユーザーIDと投稿IDの列には複合インデックスを貼ることが多いです。

複合インデックスを設定すると、(user_id, post_id)のセットで検索する際に高速化が期待できます。
Railsのマイグレーションで add_index :likes, [:user_id, :post_id], unique: true のようなコードを書くことで同時にユニーク制約を設けることもできます。

もし「いいね」のデータ量が膨大になったときに、ユーザーIDや投稿IDごとにデータを探すクエリが増えがちなので、あらかじめインデックスを設けておくと後々のパフォーマンス劣化を緩和できます。

テーブルの分割やキャッシュ戦略

大量の「いいね」データを扱うアプリケーションになると、一つのテーブルにレコードが増えすぎてパフォーマンスが下がる可能性があります。
この場合、テーブルを分割(Sharding)したり、集計を別テーブルに持たせたりする方法も考えられます。

また、「いいね」数の表示自体は頻繁に更新されるわけではないので、数秒間だけキャッシュする戦略が有効なケースもあります。
たとえばRailsのフラグメントキャッシュ機能で「いいね」数を一時的にキャッシュし、ビュー描画時のクエリ回数を抑えることができます。

とはいえ、初心者の方が最初に学ぶ段階では、まずは基本的な中間テーブルの設計とインデックス設計で十分です。
上記のような高度な手法は、アクセス数が多くなったときに検討するイメージで問題ありません。

バリデーションとバグ回避

連打を防ぐ工夫

「いいね」ボタンをユーザーが連打すると、同じユーザーと投稿の組み合わせで重複したレコードが入ることがあります。
その場合、ユニーク制約を設定しておけばエラーが発生し、重複が避けられます。

加えて、フロント側でボタンを押した直後に一時的に無効化するなどのUI上の工夫を行うことで、余計な通信を抑えることもできます。
これらのシンプルな対策だけでも、意図しないトラブルをある程度回避できます。

もし万が一エラーが起きた場合は、Railsが返す例外をキャッチしてユーザーに「すでに『いいね』済みです」というメッセージを表示するなどの細かい配慮も考えられます。
これによりユーザー体験を損なわずに、バグを防ぐことができるでしょう。

ユニーク制約の設定

中間テーブルにユニーク制約を入れるメリットは大きいです。
重複が発生してしまうと、最終的に「いいね」数が正しくカウントされず、表示や集計結果が狂ってしまうリスクがあります。

データベース側で制約をかければ、アプリケーションのミスやタイミング依存のバグなどからも守られます。
Railsのマイグレーションでは、先ほども触れたように add_index :likes, [:user_id, :post_id], unique: true のように書いておくだけでセットできます。

こうしたテーブル設計上の工夫を一度身につければ、「いいね」以外にも「フォロー・フォロワー」機能や「お気に入り」機能などさまざまな機能を同じ仕組みで作りやすくなります。
中間テーブルとユニーク制約はRailsの定番設計なので、しっかり理解しておくと良いでしょう。

コード例

ここでは簡単な例として、投稿に対する「いいね」を実現する場合のモデルやコントローラ、ルーティングなどを示します。
細部はプロジェクトに合わせて調整しながら進めると理解が進みやすくなります。

モデル例

# app/models/user.rb
class User < ApplicationRecord
  has_many :likes, dependent: :destroy
  has_many :posts, through: :likes
end

# app/models/post.rb
class Post < ApplicationRecord
  has_many :likes, dependent: :destroy
  has_many :users, through: :likes
end

# app/models/like.rb
class Like < ApplicationRecord
  belongs_to :user
  belongs_to :post
end

上記のように書くことで、UserとPostの間をLikeモデルが仲介する形になります。

コントローラ例

# app/controllers/likes_controller.rb
class LikesController < ApplicationController
  before_action :authenticate_user!

  def create
    post = Post.find(params[:post_id])
    Like.create(user: current_user, post: post)
    redirect_to post_path(post)
  end

  def destroy
    post = Post.find(params[:post_id])
    like = Like.find_by(user: current_user, post: post)
    like.destroy if like
    redirect_to post_path(post)
  end
end

ポイントは、createアクションで新規に「いいね」を作り、destroyアクションで既存の「いいね」を削除することです。
実装が簡潔なので、初めてでも処理の流れを追いやすいでしょう。

ルーティング例

# config/routes.rb
Rails.application.routes.draw do
  resources :posts do
    resource :like, only: [:create, :destroy]
  end

  # ...
end

このように書くと、/posts/:post_id/like に対して createdestroy のルートが作られます。
resources :likes と書かずに単数リソース resource :like としているのは、ユーザーごとに1回しか「いいね」できない関係を想定しているためです。

「いいね」数の表示と集計

計算ロジック

「いいね」数の計算は、先ほど少し触れたように post.likes.countpost.users.count を使う形が基本的です。
ユーザー側から見ると user.posts.count で、自分が「いいね」した投稿の数を取得できます。

ただし、この方法では都度クエリが発行される可能性があります。
アクセスが多い場合には、ビューごとに大量のクエリが飛ぶ懸念があるので、キャッシュやカウンターキャッシュを活用することが考えられます。

カウンターキャッシュを設定すれば、テーブルに専用のカラムを持たせて、増減があるたびに「いいね」数を更新する仕組みが作れます。
これにより、一覧ページなどで多くの投稿情報をまとめて表示するときにもクエリを最小限に抑えられます。

集計結果の活用

「いいね」数がどのくらいあるかを可視化することは、ユーザー同士のコミュニケーションを促進するうえで役立ちます。
たとえば、トップページに「人気投稿」として「いいね」の多い投稿をランキング表示する機能を作るケースがあります。

その場合、Post.joins(:likes).group(:id).order('COUNT(likes.id) DESC') などで並び替えをすることも検討できます。
ただし多くのSQLが走ってしまう可能性があるため、頻繁に更新しないのであれば、一時的に集計テーブルを作ったりメモリキャッシュで保持しておいたりといった方法も考えられます。

こうした「いいね」数を使った機能拡張は、ユーザビリティ向上にもつながるため、実務で重宝されます。
開発時はクエリ効率やキャッシュ戦略なども含めて設計すると、のちのスケールにも対応しやすくなります。

フロントエンドとの連携とUI設計

非同期通信での「いいね」

Railsでは、remote: true オプションを使ったり、JavaScriptのライブラリを活用したりすることで、「いいね」ボタンをクリックしたときにページ全体をリロードせずに部分更新できます。
ユーザーにとってはストレスなく反映されるため、快適に「いいね」を押せるでしょう。

非同期通信の場合は、JSONでレスポンスを返してJavaScript側でDOMを更新する仕組みにするか、Vue.jsやReactなどを使ってフロントエンドを構築するなど、さまざまな方法があります。
いずれにしても、中間テーブルにレコードを追加・削除する点は変わりません。

もしリアルタイム性を高めたいなら、Action CableなどのWebSocketを使う方法も考えられます。
ただし高度な実装になるため、まずは基本の非同期通信の仕組みから押さえていくのが良いです。

リアルタイム更新の例

ユーザーが多くアクセスするようなSNSでは、「いいね」が押された瞬間にほかの人の画面にも即座に反映されると、より活発なコミュニケーションが生まれます。
これを実現する方法として、Railsが提供するAction Cableを利用すると、サーバーとクライアントが常時接続した状態でイベントを受け取れる仕組みを作れます。

Action Cableでは、クライアントがチャンネルに購読し、サーバー側で「いいね」が新たに作成されたタイミングでブロードキャストします。
これにより、同じ投稿を見ている他のユーザーの画面にも「いいね」数の変化が自動で更新されます。

こうしたリアルタイム更新は高負荷になる可能性があるので、使用する場面を限定するのがよいでしょう。
SNSのタイムラインなどに適用すると、ユーザー体験が向上しますが、一方でシステム面での負荷も上がるため、導入には慎重な検討が求められます。

実際の現場でのトラブルシューティング

N+1問題とその対処

Railsのアプリケーションで頻繁に起こる問題の一つがN+1問題です。
投稿を一覧表示するときに、それぞれの投稿に対してユーザー数をカウントするクエリが個別に発行されると、クエリの回数が膨大になります。

これを避けるには、includespreload を使って関連テーブルの情報をまとめて先に読み込む方法があります。
たとえば Post.includes(:users) のように書くと、投稿とユーザーの関連を一度にロードできるので、後から個別にクエリを発行する必要が減ります。

多対多のリレーションを含む画面ではN+1が起きやすいので、開発中にクエリの数をチェックしておくといいでしょう。
ログを確認したり、専用のgemを導入したりして定期的にパフォーマンスを把握すると安心です。

トランザクション管理

ユーザーが集中して「いいね」ボタンを押すような場面では、トランザクションが絡む問題が発生することもあります。
例えば同じ投稿に対して同時に「いいね」操作が走ったとき、ユニーク制約に抵触する競合が起きたりする可能性があります。

RailsのActiveRecord::Transactionsを使うことで、一連の操作をトランザクションとして扱い、整合性が保たれるようにすることができます。
Like.create! の処理をトランザクション内で実行すると、失敗時にはロールバックされるため、データの不整合が起きにくくなります。

実務では「いいね」程度の操作で深刻なデータ不整合が起きるケースは少ないかもしれませんが、開発者としては念のための対処法を知っておくと役に立ちます。

テストの考え方

モデルテスト

Likeモデルについては、以下のような点をテストすることが多いです。

  • belongs_to :userbelongs_to :post の関連付けが正しく機能しているか
  • バリデーション(ユニーク制約など)が正しく働いているか
  • user_idpost_id がない場合に無効となるか

こうしたテストを自動化しておけば、本番運用中に新しい機能を追加した際も安心してリリースできます。
特に多対多リレーションのテストは最初戸惑いやすいので、コンソールでレコードを追加する練習などをやってみるとイメージがつきやすいです。

システムテスト

システムテストでは、実際のブラウザ操作を模したテストを行います。
具体的には、ユーザーがログインして投稿詳細ページを開き、「いいね」ボタンをクリックすると「いいね」数が増えているか、再度クリックすると解除されるか、といった一連の操作を確認します。

これにより、「いいね」機能が期待どおり動作するかを通しでチェックできます。
画面上の表示やレイアウトの乱れまで確認したい場合、スクリーンショットを撮る機能なども検討すると便利です。

実務では、テストコードを整備することで改修時に不具合を生み出すリスクを下げられます。
「いいね」ボタンのように高頻度で利用される機能ほど、テストをしっかり組んでおくと信頼性が高まります。

トラブルシューティング

Migrationの注意点

中間テーブルを作る際に、マイグレーションファイルでの記述を間違えてしまうと、テーブル構造が思わぬ形になってしまうことがあります。
特に references 型を使うときに、foreign_key: true の指定や null: false の指定を意識しておかないと、外部キー制約がつかないまま運用してしまう恐れがあります。

もしマイグレーションの時点でミスに気づかず運用してしまうと、あとでデータ移行が必要になる場合があります。
必ずローカル開発環境で中間テーブルのスキーマや制約を確認し、意図どおりになっているかをチェックしましょう。

また、テーブル名やカラム名はできるだけわかりやすい名称にすることが大切です。

create_table :likes do |t|
  t.references :user, null: false
  t.references :post, null: false
  t.timestamps
end

のように書いてあれば、最初の段階では問題ありません。
あとで名前を変えると混乱を招くので、最初にしっかり決めておくのがおすすめです。

データ不整合の修正

もし運用中に何らかのバグで重複した「いいね」レコードが入ってしまった場合は、データベース内でユニーク制約エラーが発生したり、ユーザーから「二重にいいねされてる」という報告が上がったりすることがあります。
その際はレコードを手動で削除したり、スクリプトを組んで削除したりして整理しましょう。

ただし、手作業の削除ではさらなる不整合を招く可能性があるので、十分に注意して行います。
まずはバックアップを取り、どのユーザーの「いいね」を削除するのかを正確に把握してから操作するのが無難です。

このようなトラブルは、ユニーク制約を正しく設定している場合はほとんど起こりません。
もし事後対処が必要になったら、同じミスを繰り返さないように、テストやバリデーションを見直す良い機会だと考えることも大切です。

セキュリティ対策

CSRF対策

Railsでは標準でCSRF(Cross-Site Request Forgery)対策が施されていますが、念のためトークンの仕組みを確認しておくと安心です。
例えばフォームやAjaxリクエストに対して、CSRFトークンを埋め込む設定になっているかをチェックします。

「いいね」機能もPOSTやDELETEリクエストを扱う場合がありますので、CSRFトークンが正しく含まれていることを意識しましょう。
悪意のあるサイトからリクエストが飛んだ場合に備え、Railsのデフォルト設定を大きく変更しないようにするのが安全です。

アクセス制限

「いいね」は通常、ログインしたユーザーだけが行える機能です。
そのためコントローラのアクションにアクセス制限を設ける必要があります。
たとえば before_action :authenticate_user! を設定することで、未ログインユーザーがアクセスできないようにするのが一般的です。

また、ユーザーが自分の管理外の投稿に対して操作してよいのかどうか、という観点も大切です。
普通の「いいね」機能であれば問題ないかもしれませんが、もし権限のあるアカウントだけが「いいね」できるようにする場合などは、さらに厳密なチェックが必要となります。

このように、Railsの機能を使うだけである程度のセキュリティは担保できますが、開発者が気を抜くと不正操作や不正アクセスが生じる可能性もあります。
テストやコードレビューを通じて、安全を確保しましょう。

多くのシステムでは「いいね」が何度押されてもシステムに大きな問題は起きにくいですが、SNSなど多数のユーザーが利用する場面では細心の注意が必要です。
負荷テストやセキュリティテストも、余裕があれば計画してみると良いでしょう。

まとめ

Railsで「いいね」機能を実装するにあたっては、多対多の関係を活用し、中間テーブル(Likeモデル)を使う方法が効果的です。
この方法なら、ユーザーと投稿のリレーションがシンプルに保たれるうえ、拡張もしやすい構造となります。

実務で特に注意したいポイントは、インデックス設定やユニーク制約、N+1問題の対処などです。
テストやデバッグを通じてデータの整合性とパフォーマンスを確保しながら、UIを整えていくことでユーザーにとって使いやすい「いいね」機能を提供できるでしょう。

さらに、非同期通信やリアルタイム更新を取り入れれば、ユーザーが直感的に触れるインタラクティブなサービスを作ることができます。
「いいね」機能を入り口として、フォロー機能やお気に入り機能などにも応用できるため、Railsの多対多リレーションをしっかり習得しておくといろいろな場面に活かせるはずです。

この記事をきっかけに、ぜひ「いいね」機能の実装に挑戦してみてください。
みなさんが作りたいサービスの中で、きっとユーザー同士の交流を促進してくれる頼もしい機能になるでしょう。

Rubyをマスターしよう

この記事で学んだRubyの知識をさらに伸ばしませんか?
Udemyには、現場ですぐ使えるスキルを身につけられる実践的な講座が揃っています。