1698
1682

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

【初心者向け】丁寧すぎるRails『アソシエーション』チュートリアル【幾ら何でも】【完璧にわかる】

Last updated at Posted at 2018-12-03

RubyやRailsを学び始めてひと月とちょっとが経ち、色々見よう見まねで作ってはみたものの、未だに いいね機能フォロー、フォロワー機能 などの仕組みがわからず。
というか、よくよく考えてみるとどういう仕組みでSNSを使っているユーザーが投稿していて、その 投稿とユーザーが結びついているのがわかっていない 、という状況にありました。

こういったモデル同士、つまりユーザーやその投稿、そして投稿につけられる"いいね"やユーザーに対するフォローなどの
関連付け の事を アソシエーション と言うのですが、そのような概念がプログラミング初心者の方にはとても難しく感じるのではないか?という動機からこのような記事を執筆するに至りました。

正直、アソシエーションやリレーションの記事などは、SQLから学ぶのなどもう既にたくさん出ています。
ですが、意外とテキストベースの解説が多く個人的には初心者がとっつきにくいような気がしており、なるべく図などを使ってわかりやすく説明できたらなと思っております(画像はほとんど @mopiemon が作ってくれました🙇‍♂️感謝)。

目次

目次
アソシエーションとは
データベース設計とは
UserとTweetの実装(1対多)
Favoriteの実装(多対多)
Follow機能の実装(自己結合多対多)
完成版のソースコード

この記事を読むにあたっての前提

・CRUDを理解している
・MVCをざっくり理解している
・特に、Model, DataBase, Table周りの意味も簡単に知っている。

もちろん、それ以外の方も是非是非挑戦してみてください。理解なんて後から帳尻合わせていきましょう。

できるようになること

丁寧すぎるherokuへのデプロイの方法はこちら - 【初心者向け】railsアプリをherokuを使って確実にデプロイする方法【決定版】

  • 簡単な設計ができるようになる。
  • ユーザー、投稿、イイネ、コメント、フォロー機能のアソシエーションが理解でき、実装できるようになる。

注意してほしいこと

  • validationは説明しません(ごめんなさい)
  • dependent系のオプションも説明しません(この記事読んだあと調べればすぐわかります。)
  • 例外処理もしません
  • indexとかも貼りません

本当にアソシエーション周りだけの理解をするための記事です。ご了承ください。
validation, dependent, viewのパーシャル化だけ実装したコードは最後に上げておきます。

アソシエーションとは

目次
アソシエーションとは ←今ココ
データベース設計とは
UserとTweetの実装(1対多)
Favoriteの実装(多対多)
Follow機能の実装(自己結合多対多)
完成版のソースコード

https://railsguides.jp/association_basics.html
モデル間の関連付け に使われる概念です。
モデル間の関連付けと言われてもピンとこない人が多いと思うので(自分がそうでした)、図を駆使しながら具体例で考えてみましょう。

プログラミング初心者のAさんがあるブログアプリを作ったとします。そのブログは最初はAさん一人で使うものだったため、記事投稿機能のみがつけられていて、モデルも Articleモデル 一つのみ実装されていました。

qiita1.png

ですが、そのサイトをたまたま見つけてしまったBさんが、勝手に自分の投稿をし始めてしまいました。

qiita2.png

これでは どれが誰の投稿かわからなくなってしまう 、とAさんはブログに ユーザーログイン機能 を付け足しました。これでアプリには ArticleモデルUserモデル の二つモデルが実装されていることになります。

qiita3.png

そして、この時にまさしく どれが誰の投稿なのか を関連付けるものが アソシエーション なのです。

qiita4.png

そして、アソシエーションを行う際は、その モデル同士の親子関係 がどのようになっているかがとても大事になってきます。

モデル同士の親子関係とは

図で考えてみましょう。
Aさん(User)は、自分が作ったブログサイトでたくさんの記事(Article)を投稿します。

qiita5.png

Bさんも少しは投稿します。

qiita6.png

つまり、 User一人一人は沢山のArticleを持っている(User has many articles.) 、と考えることができます。(この突然出てきた英文は伏線ですよ!!!!)

qiita6.5.png

逆の立場(Article)も考えてみましょう。
ある日投稿された記事(Article)は、Aさん(User)によって書かれました。

qiita7.png

次の日投稿された記事はBさんによって書かれました。
他の日に投稿された記事も、それぞれAさん、Bさんどちらかによって書かれた記事です。 Aさん、Bさんが共作で書いた記事というのはありえません

つまり、 Articleは誰か一人のUserに所属している(Article belongs to a user.) と考えることができます。

qiita7.5.png

UserがいなくてはArticleは生まれないし、Articleは必ず誰か一人のUserから生まれます。そう、 Userが親でArticleが子 となっているわけなのです。これが親子関係です。

このような関係をUserとArticleは 一対多の関係 または 1:Nの関係 といいます。もちろんUserが1でArticleが多です。(これがめちゃめちゃ大事です!!!!!!!!!!)

他にも 一対一(1:1) の関係や 多対多(M:N) の関係などもあります。
一対一は簡単なので割愛します。多対多は少し難しいですが、これからやっていくチュートリアルで登場&解説するので、ご安心ください。

さあ、それではいざアプリ制作に取り掛かっていきたいのですが、その前に データベース設計 を行わなくてはなりません。
そう、 アソシエーションはデータベース設計と密接に関わっております

データベース設計とは?(少し難しい話です)

目次
アソシエーションとは
データベース設計とは ←今ココ
UserとTweetの実装(1対多)
Favoriteの実装(多対多)
Follow機能の実装(自己結合多対多)
完成版のソースコード

情報システムにおいて、どのような情報をデータベースに格納すべきかを検討し、その「格納すべき情報」を「どのような形で保持するか?」を設計すること です。

一般化した定義のためわかりづらいかもしれませんが、

  • 情報システムTwitterアプリそのもの
  • どのような情報
    • ユーザー (のemailやpassword)
    • tweet (本文やどのユーザーが投稿したのか?など)
    • お気に入り など
  • 「格納すべき情報」を「どのような形で保持するか?」 というのは
    • 情報システムをより細かく考えたデータ
      • つまりユーザーでいうと「emailは 文字列型 、その識別idは 整数型 」など

といったアプリの動的な部分の設計のことを言います。データのことをちゃんと詳しく決めよう!!ということです。

そして、データベース設計において大事な概念が4つあります。

  • エンティティ
  • 属性
  • 関連(リレーション)
  • 関連の多重度

順番に説明して行きますが、完全に理解しなくて大丈夫ですので軽〜く読んでみてください。

エンティティ

エンティティとは、モデリングの対象となるアプリの中で管理する必要がある情報です。ツイッターでいうとUser, Tweet, Favorite, Followなどがこれにあたります。RailsのModelに近いですね。(実際は違います!)
そして項目を見るとわかると思いますが、 エンティティはアプリの要件定義と表裏一体 なのです。ここら辺は調べるといくらでも深い記事が出てくるので割愛します。

qiitaER1.png

関連(リレーション)

関連(リレーション)とは、結び付きのあるエンティティ同士をリンクさせるものです。先ほどの例だとUserとArticle(ツイッターでいうとUserとTweet)が簡単な関連となります。
ここで、「あれ、関連付けって、アソシエーションっていうじゃないの?」と思う方もいらっしゃると思いますが、アソシエーションとリレーションは同じものと思って大丈夫です。

qiitaER2.png

属性(プロパティ)

属性とは、あるエンティティ(Model)に従属する項目のことで、 エンティティを1つに定めたときに、一緒に分かる情報 だと思ってください。例えば、あるユーザーを指定したら、そのユーザーのemailやアカウント名などがこれにあたります。 テーブルのカラム と同じですね。

qiitaER3.png

関連の多重度

関連のあるエンティティAとBについて、片方から他方を見たときに 相手が1つなのか、複数なのか ということを明らかにすることです。
先ほどの例にすると、 Userから見るとArticleは複数Articleから見るとUserは1つ ということになります。
すなわち、 1つのAからBを見たとき および 1つのBからAを見たとき の相手の数を明らかにすることが、 多重度を決定する ということになります。

qiitaER4.png

・・・・はい、ここまでがデータベースの設計についての基本の概念であり、その中の 関連(リレーション)関連の多重度 がまさにアソシエーションにあたります。
アソシエーションとデータベース設計が密接に関わっていること、お分りいただけたでしょうか。

早速難しい話からスタートしてしまいましたが、最初はわからないのは当たり前です。
実際の開発では、設計を完璧に仕上げることがほとんどの場合においてマストですが、とりあえず今回は小さく設計して実装 -> また小さく設計&実装という形で一つ一つ確認していきましょう。
では、チュートリアルに入ります!

(ありがちだけど)twitterクローンを作ってみよう

Rails tutorialやその他シラバスなどでおなじみの、twitterクローンでアソシエーションを学んでいきます。
今回学びたいのはアソシエーションの部分のみ なので、見栄えやその他細かい機能は必要最低限で行こうと思います。ご了承ください。

それではまずは設計です。設計の時によく使われるのが、 ER図 と いうものです。

ER図とは

データベース設計(データモデリング)で使う設計手法です。 お絵かきの方法 です。
ER図は、データベースが必要なWEBサイトやシステムの設計では必ずと言ってよいほど作成されます。逆に言うと、ER図なしにはデータベースを構築できません。データベース設計の基本中の基本と言える設計手法です。
様々な記法があったりするのですがここでは IE記法 というものを採用します。(とても簡単です)

IE記法

データベース設計のうち、 関連(リレーション)、関連の多重度 の二つを決めるものです。要するに アソシエーションを決める記号 となります。
上記のデータベース設計で出て来た画像も、ER図であり、IE記法でかかれています。
以下三つの記号から成り立っています。

qiitaIE.png

今回はこれのうち 鳥の足(多) を使ってツイッタークローンをシンプルに設計します。

ログイン機能とツイート機能をER図を使って設計しよう

目次
アソシエーションとは
データベース設計とは
UserとTweetの実装(1対多) ←今ココ
Favoriteの実装(多対多)
Follow機能の実装(自己結合多対多)
完成版のソースコード

(設計図の作成には Cacooというのを使用していますが、皆さんは手書きで問題ありません。)

最初に言った通り、まずは小さく設計しましょう。UserとTweetについて決めて行きます。

上述の通りエンティティ(Model)はUserとTweetの二つになります。
次にプロパティです。
Userはdeviseというgemを使う予定なので、デフォルトのカラムであるemailとpassword(deviseではencrypted_passwordという名前になっています。)を使って行きましょう。
Tweetは本文(body)のみで大丈夫でしょう。

とりあえずここまでを図に落とし込んでみましょう。

qiita8.png

次にアソシエーション部分である、 関連関連の多重度 についてです。

UserとTweetは最初に出した例であるUserとArticleの関係と同じであり、 一つのUserはTweetを複数持っています。

つまり、UserとTweetは一対多(1:N)の関係というわけです。

なので、ER図はこのようになります。

qiita9.png

ただ、ここで注意するべきことがあります。このままだと TweetがどのUserに所属しているのか という情報がありません。

それを設定するために、foreign_keyを設定する必要があります。

foreign_keyとは

まず、データには一つ一つ識別するためのidがあります。これを Primary Key(主キー) と言い、Railsでは id というカラム名でテーブル作成時に標準搭載されています。

そして、 foreign key というのはその 親のid(Primary key)を保存するカラム というわけです。
これにより どの親に所属しているのか? というのを識別することができます。

qiita10.png

(赤丸のuser_idforeign_key)

なのでプロパティにforeign_keyを追加しましょう。 親のモデル名_id という名前とするとRailsの命名規則により色々便利になるので、 user_id としましょう。
ER図はこのようになります。

qiita11.png

これでUser,Tweet部分の設計は完了しました!!では実装して行きましょう。

ようやくプログラミング!

設計の通りに下準備

それではアプリを作成し、ER図の通りにUserとTweetについて準備してきましょう。

※1 $ マークはターミナルのコマンドという意味です。実際には打たないでください。
※2 #~~ はコメントアウトです。打たなくて大丈夫です。

ターミナル
$rails new association_tutorial
$cd association_tutorial/

Userはログイン機能をつけるため、deviseというgemを使用します。(deviseの詳しい説明は割愛します。こちらの記事がおすすめです。)

Gemfileに以下の記述をしましょう。(色々書いてあると思いますが消さずに追加してください。)

Gemfile
gem 'devise'  # 一番下に追加

ターミナルに戻り、以下のコマンドを打ってください。deviseの機能をインストールし、 その機能を取り込んだ Userモデルを作成します。

ターミナル
$bundle install  # gemのダウンロード
$rails g devise:install  # deviseの機能のインストール
$rails g devise User  # deviseの機能を取り込んだUserモデルの作成
$rails g controller users index show #(deviseの機能ではない)usersコントローラの作成。今回はindexとshowアクションを用意

カラム(プロパティ)は、deviseのデフォルトのカラムがemailpasswordとなっており、設計と同じなのでこのままで問題ありません。

次にTweetについてです。(foreign_keyであるuser_idもこのタイミングで追加しています。)

ターミナル
$rails g model Tweet body:text user_id:integer # 要件通りにカラムとその型も一緒に定義。
$rails g controller tweets new index show create

忘れずにテーブルの確定をしましょう。

ターミナル
$rails db:migrate

以下のように表示されればOKです。
image.png

そしてルーティングの設定です。以下のように記述してください。

config/routes.rb
Rails.application.routes.draw do

  root 'tweets#index'  # 追加

# ===========ここはいらないので削除orコメントアウト==========
  #get 'tweets/new'
  #get 'tweets/index'
  #get 'tweets/show'
  #get 'tweets/create'
  #get 'users/index'
  #get 'users/show'
# ==================================================

  devise_for :users
  resources :tweets  # 追加
  resources :users  # 追加
  
  # For details on the DSL available within this file, see http://guides.rubyonrails.org/routing.html
end

tweets, usersコントローラもサクッと表示部分を書いておきます。

app/controllers/tweets_controller.rb
class TweetsController < ApplicationController
  before_action :authenticate_user!, except: [:index]  # deviseのメソッドで「ログインしていないユーザーをログイン画面に送る」メソッド
  def new
    @tweet = Tweet.new # 新規投稿用の空のインスタンス
  end

  def create
    # 後で書きます。
  end

  def index
    @tweets = Tweet.all
  end

  def show
    @tweet = Tweet.find(params[:id])
  end
end
app/controllers/users_controller.rb
class UsersController < ApplicationController
  before_action :authenticate_user!
  def index
    @users = User.all
  end

  def show
    @user = User.find(params[:id])
  end
end

最後に、レイアウトなどの各ページも作っておきましょう。(フロントがぐちゃぐちゃなのは本当にごめんなさい!パーシャル化はあえてしておりません。)

app/views/layouts/application.html.erb
<!DOCTYPE html>
<html>
  <head>
    <title>AssociationTutorial</title>
    <%= csrf_meta_tags %>
    <%= csp_meta_tag %>

    <%= stylesheet_link_tag    'application', media: 'all', 'data-turbolinks-track': 'reload' %>
    <%= javascript_include_tag 'application', 'data-turbolinks-track': 'reload' %>
  </head>

  <body>
  <!-- 追加 -->
    <% if user_signed_in? %>  <!-- deviseのメソッドで、「ログインしているかしていないかでtrue or falseを返す」メソッド -->
      <%= link_to "新規投稿", new_tweet_path %>
      <%= link_to "ログアウト", destroy_user_session_path, method: :delete %>
      <%= link_to "マイページ", user_path(current_user.id) %>
    <% else %>
      <%= link_to "新規登録", new_user_registration_path %>
      <%= link_to "ログイン", new_user_session_path %>
    <% end %>
      <%= link_to "ツイート一覧", tweets_path %>
      <%= link_to "ユーザー一覧", users_path %>
  <!-- ここまで -->
    <%= yield %>
  </body>
</html>
app/views/tweets/new.html.erb
<h1>Tweets#new</h1>
<p>Find me in app/views/tweets/new.html.erb</p>

<%= form_for @tweet do |f| %>
  <p>
    <%= f.label :body,"ツイート" %>
    <%= f.text_field :body %>
  </p>
  <%= f.submit %>
<% end %>
app/views/tweets/index.html.erb
<h1>Tweets#index</h1>
<p>Find me in app/views/tweets/index.html.erb</p>

<% @tweets.each do |tweet| %>
  <hr>
  <p><span>ツイート内容: </span><%=link_to tweet.body, tweet_path(tweet.id) %></p>
<% end %>
app/views/tweets/show.html.erb
<h1>Tweets#show</h1>
<p>Find me in app/views/tweets/show.html.erb</p>

<p><span>ツイート内容: </span><%= @tweet.body %></p>
app/views/users/index.html.erb
<h1>Users#index</h1>
<p>Find me in app/views/users/index.html.erb</p>

<% @users.each do |user| %>
  <hr>
  <p><span>email: </span><%=link_to user.email, user_path(user.id) %></p>
<% end %>
app/views/users/show.html.erb
<h1>Users#show</h1>
<p>Find me in app/views/users/show.html.erb</p>
<hr>
<p><span>email: </span><%= @user.email %></p>

いよいよアソシエーションを記述しよう

さあ、いよいよアソシエーションの記述になります。ER図で言うと棒線の部分がこれにあたります。

・・・と、いってもRailsは命名規則によってものすごくよしなにやってくれるので、記述することはとても少ないです。

has_many

UserはたくさんのTweetを持っています(User has many tweets.) 。なので、 Userモデル に以下の記述をしてください。

app/models/user.rb
class User < ApplicationRecord
  # Include default devise modules. Others available are:
  # :confirmable, :lockable, :timeoutable, :trackable and :omniauthable
  devise :database_authenticatable, :registerable,
         :recoverable, :rememberable, :validatable

  has_many :tweets  # これを追加
end

このように 子の方のモデル名の複数形 を書きます。has_manyの方が 複数形 なことは Railsの命名規則でマスト ですのでお気をつけください。(英文と同じですね!)

belongs_to

Tweetは一つのUserに所属しています(Tweet belongs to a User.)
もうおわかりですね。tweetモデルに以下のように記述します。

app/models/tweet.rb
class Tweet < ApplicationRecord
    belongs_to :user  # これを追加
end

はい、こちらは 一つ なので 単数形 です。

そして、これだけの記述だけでアソシエーションをすることができています。tweetsテーブルに追加してある foreign_keyuser_id も、 モデル名_id としたおかげで勝手に認識してくれています。railsはこういったところがとてもすごいですね。

コントローラの編集

さて、userとtweetをアソシエーションさせた状態で保存させたいです。なので、tweetを保存するタイミング、要するにtweets#createとストロングパラメータ(tweets_params)を以下のように記述しましょう。

app/controllers/tweets_controller.rb
class TweetsController < ApplicationController
  before_action :authenticate_user!, except: [:index]  # deviseのメソッドで「ログインしていないユーザーをログイン画面に送る」メソッド

  def new
    @tweet = Tweet.new  # 新規投稿用の空のインスタンス
  end

  def create
# ============追加================
    @tweet = Tweet.new(tweet_params)  # フォームから送られてきたデータ(body)をストロングパラメータを経由して@tweetに代入
    @tweet.user_id = current_user.id # user_idの情報はフォームからはきていないので、deviseのメソッドを使って「ログインしている自分のid」を代入
    @tweet.save
    redirect_to tweets_path
# =================================
  end

  def index
    @tweets = Tweet.all
  end

  def show
    @tweet = Tweet.find(params[:id])
  end
# ===============追加=============
  private
    def tweet_params
      params.require(:tweet).permit(:body) # tweetモデルのカラムのみを許可
    end
# =================================
end

ここで注意したいポイントは、 user_idの情報はフォームから送られていないので、追加で代入してあげないといけない と言うことです。

この場合はdeviseのメソッドであるcurrent_userログイン中のユーザーの情報 が取得できるため(つまりツイートした本人)、current_user.id@tweet.user_idに代入しています。

これで、アソシエーションをした状態でデータの保存をすることができました!
とりあえず何人か新規登録して、投稿してみましょう。
deviseの新規登録画面 - http://localhost:3000/users/sign_up
新規投稿画面 - http://localhost:3000/tweets/new

ログをみても、きちんとuser_idに代入されているのがわかると思います。
image.png

アソシエーションしているデータの受け取り方

ここまででアソシエーションした状態でデータ保存をすることができました。
次は「 ユーザーがツイートしたデータ 」や、「 このツイートをしたユーザー 」などといった 表示の方法 です。

Userがtweetしたデータ

結論から言うと、

@user.tweets

(@userは一つのユーザーのデータです。)
これで「ユーザーに関連したツイート」を取得することができます。
tweetsと複数形になっていることに注意してください。この記述はUserモデルに記述したhas_many :tweetsによって決まっています。
そして、複数形になっていることからもわかりますが、 @user.tweetsは複数のツイートが入った配列 となっております。

これらを用いて、Usersのコントローラとビューを以下のように変更しましょう。

app/controllers/users_controller.rb
class UsersController < ApplicationController
  before_action :authenticate_user!
  def index
    @users = User.all
  end

  def show
    @user = User.find(params[:id])
# ===============追加==============
    @tweets = @user.tweets
# ================================
  end
end
app/views/users/show.html.erb
<h1>Users#show</h1>
<p>Find me in app/views/users/show.html.erb</p>
<hr>
<p><span>email: </span><%= @user.email %></p>

<!-- 追加 -->
<% @tweets.each do |tweet| %>
  <hr>
  <p><span>ツイート内容: </span><%=link_to tweet.body, tweet_path(tweet.id) %></p>
<% end %>

http://localhost:3000/users/1
このようになっていればうまくいっています。

image.png

このtweetをしたUser

こちらも同じように

@tweet.user

このようにすれば「このツイートをしたユーザー」を取得することができます。
こちらもTweetモデルに記述したbelongs_to :userより、 単数 にすることで取得します。

そして、こちらは 単数 なので、 取得したデータも一つ のみです。

なので、Tweetsコントローラとビューは以下のようになります。

app/controllers/tweets_controller.rb
class TweetsController < ApplicationController
  before_action :authenticate_user!, except: [:index]  # deviseのメソッドで「ログインしていないユーザーをログイン画面に送る」メソッド

  def new
    @tweet = Tweet.new  # 新規投稿用の空のインスタンス
  end

  def create
    @tweet = Tweet.new(tweet_params)  # フォームから送られてきたデータ(body)をストロングパラメータを経由して@tweetに代入
    @tweet.user_id = current_user.id # user_idの情報はフォームからはきていないので、deviseのメソッドを使って「ログインしている自分のid」を代入
    @tweet.save
    redirect_to tweets_path
  end

  def index
    @tweets = Tweet.all
  end

  def show
    @tweet = Tweet.find(params[:id])
# ===============追加==============
    @user = @tweet.user
# ================================
  end

  private
    def tweet_params
      params.require(:tweet).permit(:body) # tweetモデルのカラムのみを許可
    end
end
app/views/tweets/show.html.erb
<h1>Tweets#show</h1>
<p>Find me in app/views/tweets/show.html.erb</p>

<p><span>email: </span><%= @user.email %></p> <!-- 追加 -->
<p><span>ツイート内容: </span><%= @tweet.body %></p>

indexもemailを表示させちゃいましょう。

app/views/tweets/index.html.erb
<h1>Tweets#index</h1>
<p>Find me in app/views/tweets/index.html.erb</p>
<% @tweets.each do |tweet| %>
  <hr>
  <p><span>email: </span><%=link_to tweet.user.email, user_path(tweet.user.id) %></p> <!-- 追加 -->
  <p><span>ツイート内容: </span><%=link_to tweet.body, tweet_path(tweet.id) %></p>
<% end %>

ちょっと見慣れないかもしれないですが、このようにtweet.user.emailなど、メソッドチェーンで関連先を一気に取得することも可能です。(あまり長くなりすぎるとプログラムの可読性が悪くなるので注意!)

これで一対多の実装ができました!!!ひとまずお疲れ様です。

一対多まとめ

  • 親子関係を考え、 子供に親のid(foreign_key)が入るカラム を追加
  • 親側モデルに has_many :子供のモデル名の複数形 、子供側モデルに belongs_to :親のモデル名(単数形) と記述
  • 関連したモデルの情報は、 関連元のモデルのインスタンス.関連先のモデル名 で取得可能。関連先のモデル名は、 関連元のモデルに記述した関連先の名前 が入る。(has_manyなら複数、belongs_toなら単数。)

お気に入り機能をER図を使って設計しよう

目次
アソシエーションとは
データベース設計とは
UserとTweetの実装(1対多)
Favoriteの実装(多対多) ←今ココ
Follow機能の実装(自己結合多対多)
完成版のソースコード

さて、次は お気に入り機能 の実装です。ここまででめちゃめちゃ長くなってしまいましたが、気合いで頑張っていきましょう!

お気に入り機能とはどういう機能なのか?

お気に入りとは、 ユーザーがツイートに対してつける印 だとみなすことができます。

また片方ずつ考えていきましょう。
ユーザーは「たくさんのツイートをお気に入り」することができます。

qiita12.png

逆も考えてみます。
「ツイートはたくさんのユーザーにお気に入り」されます。

qiita13.png

このように 「ユーザーもツイートもたくさん持っている」関係を多対多(M:N)の関係 といいます。

多対多(M:N)を設計しよう

それではER図に落とし込んでいきたいのですが、Railsでは多対多の関係をUserモデル、Tweetモデルのみでは実装することができません。

実装するためには、中間テーブルというものが必要です。

中間テーブルとは

多対多をプログラムで実装するためには、お互いがお互いのforeign_keyを知らなくてはなりません。

qiita15.png

ですが「複数のuserによってどんどんファボが増えていった」場合、画像のように配列にどんどん追加していくか、カラムをtweetsテーブルにどんどん追加しなくてはならず、非常に面倒です。

そのような状態を避けるために、 お互いのidだけを保存する テーブルが必要となります。

それを、 中間テーブル といいます。

これで「どうやってお気に入り数や、誰のお気に入りなのかを判断するの?」と思うかもしれませんが、それは 中間テーブルに保存されるtweet_iduser_idで判断 しています。

qiita16.png

例えば、「あるツイートについたお気に入り数」をみたいときは、 そのtweet_idと同じレコードに保存されているuser_idの数 を見ればよく、

「あるユーザーのお気に入りしたツイート」がみたいときは、 そのuser_idと同じレコードに保存されているtweet_idから、ツイートを検索 すれば良いことになります。

中間テーブルを使ってER図の作成

それでは中間テーブル込みのER図を作っていきましょう。

通常、中間テーブルはテーブル1_テーブル2の複数系(user_tweets)などとつけることが多いですが、今回中間テーブルの意味が「お気に入り登録」とはっきりわかっているため、favoritesテーブルとしてしまいましょう。

qiita18.png

中間テーブルは たくさん持たれる側(どちらにもbelongs_to) なのに注意してください。
それでは、これを実装していきましょう!

設計の通りに下準備

設計の通り、Favorite関連のコントローラ、モデル、テーブルを作成します。

ターミナル
$rails g model Favorite user_id:integer tweet_id:integer
$rails g controller favorites create destroy
$rails db:migrate

image.png

あとはルーティングですが、後々のことを考えて、tweetsにネストしたルーティングを作成しましょう。
下記のように do ~ endの中に入れる形で書く ことを、 ネスト(入れ子)する と言います。

config/routes.rb
Rails.application.routes.draw do
  #==================削除orコメントアウト================
  # get 'favorites/create'
  # get 'favorites/destroy' 
  #=====================================
  root 'tweets#index'
  devise_for :users

  # ================ここをネスト(入れ子)した形に変更
  resources :tweets do
    resource :favorites, only: [:create, :destroy]
  end
  #======================================

  resources :users 
  # For details on the DSL available within this file, see http://guides.rubyonrails.org/routing.html
end

理由としては、 お気に入り時に、foreign_keyの一つであるtweet_idを取得するのが楽になるから です。(一対多の時もそうでしたが、foreign_keyを取得しないとリレーションができません。)

詳しくはこちら - Railsのルーティングを極める(後編)

また、favoriteの詳細ページは作らない、つまりfavoriteのidは要らず省略したいため、resourceと、単数形のメソッドを利用しています。

rails routesでルーティングを確認すると、以下のようにfavoritesのcreateとdestroyに:tweet_idが含まれ、かつ:id(:favorite_id)が省略された形で生成されているのがわかると思います。

スクリーンショット_2018-12-02_20_17_13.png

これで下準備は完了です。

###アソシエーションの記述

中間テーブルfavoritesたくさん持たれる側 なので、このようになります。

app/models/favorite.rb
class Favorite < ApplicationRecord
    belongs_to :user
    belongs_to :tweet
end

したがって、User, Tweetモデルも以下のようになります。

app/models/user.rb
class User < ApplicationRecord
  # Include default devise modules. Others available are:
  # :confirmable, :lockable, :timeoutable, :trackable and :omniauthable
  devise :database_authenticatable, :registerable,
         :recoverable, :rememberable, :validatable

  has_many :tweets
  has_many :favorites  # 追加
end
app/models/tweet.rb
class Tweet < ApplicationRecord
    belongs_to :user
    has_many :favorites  # 追加
end

これでアソシエーションが組めたので、
@user.favorites@tweet.favoritesなどで それぞれに関連したお気に入り(実際にはuser,tweetのidの2つ組の情報) が取得できるようになりました。
・・・が、viewを作る前に、一つ注意しなくてはならないことがあります。

お気に入り機能は 登録だけでなく解除 もできなくてはならないことです。
そのため、Tweetモデルに「このツイートをユーザーがファボしているかどうか」を判定するメソッドを用意しましょう。

ユーザーがツイートをお気に入りしたかどうかの判定メソッド

「ツイートがファボしてあるかどうか」を判定したいので、 Tweetモデル に以下のように記述しましょう。

app/models/tweet.rb
class Tweet < ApplicationRecord
    belongs_to :user
    has_many :favorites

# 追加
    def favorited_by?(user)
        favorites.where(user_id: user.id).exists?
    end
end

これは、Tweetのインスタンスメソッドなので、ビューなどで

if @tweet.favorited_by?(current_user)

という風に使うことができます。これはつまり

if @tweet.favorites.where(user_id: current_user.id).exists?

ということと同じなので、 アソシエーションの記述である@tweet.favoritesの中に、引数で送られたuserのidがあるかどうか? ということを判定していることになります。

インスタンスメソッドについてはこちら - Rubyのクラスメソッドとインスタンスメソッドの例

それではお気に入りボタンを作っていきましょう。

お気に入りボタンの実装

tweetsのindexに実装します。先ほど定義したfavorited_by?を早速使います。
countメソッドで、 配列の中の要素数 を取得しており、そしてそれがそのまま お気に入りの数 となります。

app/views/tweets/index.html
<h1>Tweets#index</h1>
<p>Find me in app/views/tweets/index.html.erb</p>
<% @tweets.each do |tweet| %>
  <hr>
  <p><span>email: </span><%=link_to tweet.user.email, user_path(tweet.user.id) %></p>
  <p><span>ツイート内容: </span><%=link_to tweet.body, tweet_path(tweet.id) %></p>
  <!-- 追加 -->
  <% if user_signed_in? %>
    <% if tweet.favorited_by?(current_user) %> <!-- ログインしているユーザーがファボしたかどうかで分岐 -->
        <p><span>お気に入り解除: </span><%=link_to tweet.favorites.count, tweet_favorites_path(tweet.id), method: :delete %></p>
    <% else %>
        <p><span>お気に入り登録: </span><%=link_to tweet.favorites.count, tweet_favorites_path(tweet.id), method: :post %></p>
    <% end %>
  <% else %>
    <p><span>お気に入り数: </span><%= tweet.favorites.count %></p>
  <% end %>
  <!-- ここまで -->
<% end %>

お気に入り登録の時はmethod: :post
お気に入り解除の時はmethod: :deleteにすることに注意してください。rails routesVerbのところをきちんと確認しましょう。

スクリーンショット_2018-12-02_20_17_13.png

あとはコントローラです。

FavoritesControllerの実装

繰り返しますが URIに:tweet_id が含まれており、tweet_favorites_path(tweet.id)と引数でtweetのidを送ってあるので、 params[:tweet_id] とすれば お気に入り登録しようとしているツイートのidが取得できます

app/controllers/favorites_controller.rb
class FavoritesController < ApplicationController
  def create
    # こう記述することで、「current_userに関連したFavoriteクラスの新しいインスタンス」が作成可能。
    # つまり、favorite.user_id = current_user.idが済んだ状態で生成されている。
    # buildはnewと同じ意味で、アソシエーションしながらインスタンスをnewする時に形式的に使われる。
    favorite = current_user.favorites.build(tweet_id: params[:tweet_id])
    favorite.save
    redirect_to tweets_path
  end

  def destroy
    favorite = Favorite.find_by(tweet_id: params[:tweet_id], user_id: current_user.id)
    favorite.destroy
    redirect_to tweets_path
  end
end

ポイントは

current_user.favorites.build(tweet_id: params[:tweet_id])

です。これは、 current_userに関連したFavoriteインスタンスを生成 しています。なので、Favoriteのプロパティはすでに user_id: current_user.id が代入されております。
あとは残りのtweet_idにviewから送られてきたparams[:tweet_id]を代入するだけです。(params[:id]ではないことに注意!ルーティングで生成されるURIを見ればparams[:tweet_id]であることを確認できます。)

これにより、お気に入り登録&解除を行うことができるようになりました!
http://localhost:3000/tweets

has many through

中間テーブルに保存されている情報は それぞれの親id でしかないので、直接取得しviewに表示しようと思うと、少しコツがいります。

# コントローラ
  def show
    @user = User.find(params[:id])
    @tweets = @user.tweets
    # mapメソッドを使いfavoriteをtweetの情報に変換
    @favorite_tweets = @user.favorites.map{|favorite| favorite.tweet}
  end
end
<!-- ビュー -->
<% @favorite_tweets.each do |tweet| %>
  <hr>
  <p><span>ファボツイート内容: </span><%=link_to tweet.body, tweet_path(tweet.id) %></p>
<% end %>

このように、mapメソッドでfavoritesの情報をtweetの集まりに変換してあげなくてはいけません。

ですが、has many throughを使えば、 ユーザーがファボしたツイート を直接アソシエーションで取得することができます。

このように実装します。

app/models/user.rb
class User < ApplicationRecord
  # Include default devise modules. Others available are:
  # :confirmable, :lockable, :timeoutable, :trackable and :omniauthable
  devise :database_authenticatable, :registerable,
         :recoverable, :rememberable, :validatable

  has_many :tweets
  has_many :favorites
  has_many :favorite_tweets, through: :favorites, source: :tweet  # 追加
end

source

source「参照元のモデル」 をさすオプションです。
これを指定することで アソシエーションでメソッドチェーンする時の名称を変更 することができます。

本当はhas_many :tweets, through: :favorites
と記述したいのですが、上のhas_many :tweetsと重複してしまうため、favorite_tweetsと名称を変更しています。

has many throughを使った「ユーザーがファボしたツイートの表示」

これで、 @user.favorite_tweets とやることで、「ユーザーがファボしたツイート」を取得することができるようになりました。

それでは、Usersコントローラ、ビューも以下のように変更してください。

app/controllers/users_controller.rb
class UsersController < ApplicationController
  def index
    @users = User.all
  end

  def show
    @user = User.find(params[:id])
    @tweets = @user.tweets
    @favorite_tweets = @user.favorite_tweets # 追加
  end
end
app/views/users/show.html.erb
<h1>Users#show</h1>
<p>Find me in app/views/users/show.html.erb</p>
<hr>
<p><span>email: </span><%= @user.email %></p>

<% @tweets.each do |tweet| %>
  <hr>
  <p><span>ツイート内容: </span><%=link_to tweet.body, tweet_path(tweet.id) %></p>
<% end %>
<!-- 追加 -->
<% @favorite_tweets.each do |tweet| %>
  <hr>
  <p><span>ファボツイート内容: </span><%=link_to tweet.body, tweet_path(tweet.id) %></p>
<% end %>

これでファボ機能が実装できました、確認してみましょう!!
http://localhost:3000

コメント機能も作ってみよう

コメント機能も多対多となります。

qiita19.png

ここまでの知識があればきっと実装することができると思うので、是非ともやってみてください!
最後にソースコードを置いておきます。

ヒント
・中間テーブルに文章を入れれるカラムを追加
・「一回コメントした人も何度もコメントできるようにするか」はあなたの自由(favorited_by?の話です)
・form_for の引数に渡すインスタンスに注意

ネストしてあるコントローラへのルーティングは、form_forの場合

<%= form_for([@tweet, @comment]) do |f| %>

のように、 配列で二つ([関連元のインスタンス, 関連先のインスタンス]) 渡す必要があります。

多対多まとめ

  • Railsでは多対多を実装するために 中間テーブル が必要。
  • 中間テーブル側が子供(たくさん持たれる方)となるので、 belongs_to :親1 belongs_to :親2 と書こう.
  • 親側はもちろん has_many :中間テーブルの名前の複数形
  • has_many_throughを使うと記述が省略できるので積極的に使おう
  • has_many_throughを使うときに指定するsource参照元 のモデル名なのに注意
  • 「ファボしてるかしていないか」、という判定のメソッド(今回はfavorited_by?)を用意することに注意(コメントなど、用意しなくてもいい時もある)

フォロー、フォロワー機能をER図を使って設計しよう

目次
アソシエーションとは
データベース設計とは
UserとTweetの実装(1対多)
Favoriteの実装(多対多)
Follow機能の実装(自己結合多対多) ←今ココ
完成版のソースコード

いよいよラストです!(本当に長かった)最後に、ユーザーフォロー機能を追加していきます。

フォロー、フォロワー機能とはどういう機能なのか?

結論から言うと フォローする側のユーザー、フォローされる側のユーザーというユーザー同士の多対多 となります。

qiita20.png

とりあえず多対多なので中間テーブルは必要そうです。
しかし、ご存知の通りUserモデルは一つしかなく、今のままだとフォローする側のユーザー、フォローされる側のユーザーがどれだか全くわかりません。

qiita21.png

なので、 ユーザーモデルをフォローする側、される側にうまく切り分ける ことがこの実装の鍵となります。

そしてこのような自分自身との結合のことを 自己結合 と言います。自己結合は多対多だけでなく、一対多でもあり得ます。(雇用者モデルにおいて、管理者:従業員の1対多など)

UserとUserの多対多(M:N)を設計しよう(自己結合)

ユーザーモデルは一旦忘れて考えましょう。
FollowingモデルFollowerモデルがあったとします。(フォローする方とされる方)

qiita 21.5.png

これらはもちろん多対多で関連しているため、中間テーブルが存在します。関連を表現しているため、Relationshipモデルと名付けます。
中間テーブルなので各親テーブルのprimary_keyforeign_keyとして保存したいです。
なので、

following_idにはFollowingモデルのidを
follower_idにはFollowerモデルのidを
Relationshipforeign_keyに設定すれば良いと言うことがわかります。

qiita22-3.png

こうすればやることは普通の多対多と同じです。

  • Followingから見て、Followerを(Relationshipを介して)集める
  • Followerから見て、Followingを(Relationshipを介して)集める

と言うことになります。

qiita23.png ここでふた通りのRelationshipを考えなくてはならないのは、プログラムで記述するときにわかります。

これをUserモデルに直して考えると、

  • フォローする側のUserから見て、フォローされる側のUserを(中間テーブルを介して)集める
  • フォローされる側のUserから見て、フォローしてくる側のUserを(中間テーブルを介して)集める

と言うことになります。

qiita24.png

ヒジョーーにややこしくなってきましたが、ここまで落とし込めば十分に Railsで実装可能 です。やっていきましょう!

設計の通りに下準備

ここは今までと同じく、設計通りにコマンドを打ち込むだけです。(中間テーブルの名前はRelationshipsとしました。)

ターミナル
$rails g model relationship following_id:integer follower_id:integer
$rails g controller relationships create destroy
$rails db:migrate

favoriteの時と同じく、親であるuserのidが欲しいため、usersの中にネストしましょう。
また、フォロー一覧、フォロワー一覧の画面も用意したいため、follows, followersと言うアクションへのルーティングを作ります。on memberメソッドを使います。
詳細 - Railsのルーティングを極める(後編)

config/routes.rb
Rails.application.routes.draw do
  
  #==================削除orコメントアウト================
  # get 'relationships/create'
  # get 'relationships/destroy'
  #=====================================
  root 'tweets#index' 

  devise_for :users

  resources :tweets do
    resource :favorites, only: [:create, :destroy]
  end

  # ================ここをネスト(入れ子)した形に変更
  resources :users do
    resource :relationships, only: [:create, :destroy]
    get :follows, on: :member # 追加
    get :followers, on: :member # 追加
  end
  #======================================
  # For details on the DSL available within this file, see http://guides.rubyonrails.org/routing.html
end

まだviewやコントローラも触れていませんが、先にアソシエーションの記述を完成させます。

アソシエーションの記述

いよいよアソシエーションの記述です。

中間テーブルRelationshipモデルの設定

とりあえず、Userモデルと言う呼び方はやめてFollowing, Followerと言うモデル名に変更したいです。
そしてそれはclass_nameと言うオプションを使えば実現できます。

class_name

:class_name - 関連するモデルクラス名を指定。関連名と参照先のクラス名を分けたい場合に使う
ものとなります。

これを使うことにより

belongs_to 変更したい親モデル名, class_name: "元々の親モデル名"

とすることができます( "元々の親モデル名"文字列 なのに注意!)。これでFollowモデルFollowerモデルを擬似的に作り出すことができそうです。

実際、Relationshipsテーブルのforeign_keyとしてfollowing_idfollower_idと名前をつけており、モデル名もFollowing, Followerとすれば帳尻が合います。
Relationshipモデルにそれぞれ所属先を定義しちゃいましょう。

relationship.rb
class Relationship < ApplicationRecord
    belongs_to :following, class_name: "User"
    belongs_to :follower, class_name: "User"
end

Userモデルの設定

Relationshipモデル側にFollowing, Followerモデルに所属している、と言うことを定義してありますので、それをうまく使えば良さそうです。

ここで、設計の時に決めた自己結合の文句を確認してみましょう。

  • フォローする側のUserから見て、フォローされる側のUserを(中間テーブルを介して)集める。
  • フォローされる側のUserから見て、フォローしてくる側のUserを(中間テーブルを介して)集める。

です。
ですが、とりあえずこの通りに記述しようとしても、has_many :relationshipsをふた通り書かなくてはならないため、名前被りが起きてしまいます。なので、__フォローする側、される側ふた通り__の中間テーブルの名前を再定義しなくては行けなさそうです。

今回は、

  • フォローする側のUserからみたRelationshipをactive_relationship
  • フォローされる側のUserからみたRelationshipをpassive_relationship

としてみます。

参照元のモデルは先ほどもやったclass_name: "Relationship"と指定すれば良いだけなので、

has_many :active_relationships, class_name: "Relationship"
has_many :passive_relationships, class_name: "Relationship"

となります。

ですがこれだとまだ、 親モデルの外部キーがなんなのか という情報が足りません。
active_relationshipで言うと「 フォローする側のUserからみた 」と言う情報が足りていない、と言うことになります。

そこで親モデルの外部キー指定するオプションとして、foreign_keyと言うのがあります。

foreign_key

:foreign_key - 参照先のテーブルの外部キーのカラム名を指定できる
これにそれぞれfollowing_id, follower_id、つまり 親のprimary_key を指定してあげれば、「 フォローする側のUserからみた 」と言う情報も取得することができます。

よって、最終的なUserモデルの最終的なアソシエーションの記述は以下のようになります。

app/models/user.rb
class User < ApplicationRecord
  # Include default devise modules. Others available are:
  # :confirmable, :lockable, :timeoutable, :trackable and :omniauthable
  devise :database_authenticatable, :registerable,
         :recoverable, :rememberable, :validatable

  has_many :tweets
  has_many :favorites
  has_many :favorite_tweets, through: :favorites, source: :tweet

  # ====================自分がフォローしているユーザーとの関連 ===================================
  #フォローする側のUserから見て、フォローされる側のUserを(中間テーブルを介して)集める。なので親はfollowing_id(フォローする側)
  has_many :active_relationships, class_name: "Relationship", foreign_key: :following_id
  # 中間テーブルを介して「follower」モデルのUser(フォローされた側)を集めることを「followings」と定義
  has_many :followings, through: :active_relationships, source: :follower
  # ========================================================================================

  # ====================自分がフォローされるユーザーとの関連 ===================================
  #フォローされる側のUserから見て、フォローしてくる側のUserを(中間テーブルを介して)集める。なので親はfollower_id(フォローされる側)
  has_many :passive_relationships, class_name: "Relationship", foreign_key: :follower_id
  # 中間テーブルを介して「following」モデルのUser(フォローする側)を集めることを「followers」と定義
  has_many :followers, through: :passive_relationships, source: :following
  # =======================================================================================

  def followed_by?(user)
    # 今自分(引数のuser)がフォローしようとしているユーザー(レシーバー)がフォローされているユーザー(つまりpassive)の中から、引数に渡されたユーザー(自分)がいるかどうかを調べる
    passive_relationships.find_by(following_id: user.id).present?
  end
end

先ほどまでの話にプラスして、has many throughで(中間テーブルを介して)followingsの時はfollowerを、followersの時はfollowingを集める記述をしています。
もちろんsourceでモデルの参照元を指定しています。
followingsで集めるUserはフォローされる側(follower)。逆も然りです。

以下の画像は、これまでの自己結合アソシエーションの記述をまとめたものです。

qiita26.png

favoriteと同じく、 Userがfollow済みかどうか判定 したいため、 followed_by? と言うメソッドも追加しています。

これでUserモデルのアソシエーションの記述も完了です。

残るはcontroller、viewを実装です!

コントローラ、ビューの実装

これで本当にチュートリアルは最後です。頑張っていきましょう!!!

ビューページの実装

(最後にある完成版のソースコードに、もう少し綺麗にしたviewファイルの奴も置いてあるので、もしよろしければそちらも参考ください。)

フォローボタンの実装

app/views/tweets/index.html.erb
<h1>Tweets#index</h1>
<p>Find me in app/views/tweets/index.html.erb</p>
<% @tweets.each do |tweet| %>
  <hr>
  <p><span>email: </span><%=link_to tweet.user.email, user_path(tweet.user.id) %></p>
  <p><span>ツイート内容: </span><%=link_to tweet.body, tweet_path(tweet.id) %></p>
  <% if user_signed_in? %>
    <% if tweet.favorited_by?(current_user) %> <!-- ログインしているユーザーがファボしたかどうかで分岐 -->
        <p><span>お気に入り解除: </span><%=link_to tweet.favorites.count, tweet_favorites_path(tweet.id), method: :delete %></p>
    <% else %>
        <p><span>お気に入り登録: </span><%=link_to tweet.favorites.count, tweet_favorites_path(tweet.id), method: :post %></p>
    <% end %>

    <!-- ここを追加 -->
    <% if current_user != tweet.user %>
        <% if tweet.user.followed_by?(current_user) %>
            <p><%=link_to "フォロー済み", user_relationships_path(tweet.user.id), method: :delete %></p>
        <% else %>
            <p><%=link_to "フォローする", user_relationships_path(tweet.user.id), method: :post %></p>
        <% end %>
    <% end %>
    <!-- ここまで -->

  <% else %>
    <p><span>お気に入り数: </span><%= tweet.favorites.count %></p>
  <% end %>
<% end %>
app/views/users/index.html.erb
<h1>Users#index</h1>
<p>Find me in app/views/users/index.html.erb</p>

<% @users.each do |user| %>
  <hr>
  <p><span>email: </span><%= link_to user.email, user_path(user.id) %></p>
    <!-- ここを追加 -->
    <% if current_user != user %>
        <% if user.followed_by?(current_user) %>
            <p><%=link_to "フォロー済み", user_relationships_path(user.id), method: :delete %></p>
        <% else %>
            <p><%=link_to "フォローする", user_relationships_path(user.id), method: :post %></p>
        <% end %>
    <% end %>
    <!-- ここまで -->
<% end %>

自分をフォローする自体は避けたいので、if current_user != userで自分をのぞいた人たちのみボタンを表示するような実装にしています。あとはほとんどfavoriteボタンと同じですね。

tweetsのshowページは省略します。理屈がわかっていれば絶対にできるはずです、やってみましょう!
次にコントローラです。

relationshipsコントローラの実装

app/controllers/relationships_controller.rb
class RelationshipsController < ApplicationController
  def create
    follow = current_user.active_relationships.build(follower_id: params[:user_id])
    follow.save
    redirect_to users_path
  end

  def destroy
    follow = current_user.active_relationships.find_by(follower_id: params[:user_id])
    follow.destroy
    redirect_to root_path
  end
end

favoriteの時と同じくcurrent_user.active_relationships.buildとすることで「following_id: current_user.id」を代入しながらインスタンスを作成することができます。

最後に、フォロー、フォロワー一覧を実装しましょう。

usersコントローラの実装

app/controllers/users_controller.rb
class UsersController < ApplicationController
  before_action :authenticate_user!
  def index
    @users = User.all
  end

  def show
    @user = User.find(params[:id])
    @tweets = @user.tweets
    @favorite_tweets = @user.favorite_tweets
  end

# ==============追加================
  def follows
    user = User.find(params[:id])
    @users = user.followings
  end

  def followers
    user = User.find(params[:id])
    @users = user.followers
  end
# ==============追加================
end

ビューの実装

follows, followersのビューは作っていなかったので、このタイミングで新規作成してください。
image.png

app/views/users/follows.html.erb
<h1>Users#follows</h1>
<p>Find me in app/views/users/follows.html.erb</p>

<% @users.each do |user| %>
  <hr>
  <p><span>email: </span><%= link_to user.email, user_path(user.id) %></p>
    <% if current_user != user %>
        <% if user.followed_by?(current_user) %>
            <p><%=link_to "フォロー済み", user_relationships_path(user.id), method: :delete %></p>
        <% else %>
            <p><%=link_to "フォローする", user_relationships_path(user.id), method: :post %></p>
        <% end %>
    <% end %>
<% end %>
app/views/users/followers.html.erb
<h1>Users#followers</h1>
<p>Find me in app/views/users/followers.html.erb</p>

<% @users.each do |user| %>
  <hr>
  <p><span>email: </span><%= link_to user.email, user_path(user.id) %></p>
    <% if current_user != user %>
        <% if user.followed_by?(current_user) %>
            <p><%=link_to "フォロー済み", user_relationships_path(user.id), method: :delete %></p>
        <% else %>
            <p><%=link_to "フォローする", user_relationships_path(user.id), method: :post %></p>
        <% end %>
    <% end %>
<% end %>
app/views/users/show.html.erb
<h1>Users#show</h1>
<p>Find me in app/views/users/show.html.erb</p>
<hr>
<p><span>email: </span><%= @user.email %></p>

<!-- 追加 -->
<p><%=link_to "フォロー", follows_user_path(@user.id) %></p>
<p><%=link_to "フォロワー", followers_user_path(@user.id) %></p>
<% if current_user != @user %>
    <% if @user.followed_by?(current_user) %>
        <p><%=link_to "フォロー済み", user_relationships_path(@user.id), method: :delete %></p>
    <% else %>
        <p><%=link_to "フォローする", user_relationships_path(@user.id), method: :post %></p>
    <% end %>
<% end %>
<!--ここまで -->

<% @tweets.each do |tweet| %>
  <hr>
  <p><span>ツイート内容: </span><%=link_to tweet.body, tweet_path(tweet.id) %></p>
<% end %>

<% @favorite_tweets.each do |tweet| %>
  <hr>
  <p><span>ファボツイート内容: </span><%=link_to tweet.body, tweet_path(tweet.id) %></p>
<% end %>

以上でフォロー機能の実装は完了です!!本当におつかれさまでした!!!!!!

自己結合多対多まとめ

  • User同士の関連付け なので、名前の重複が色々起きてしまう。そこはclass_nameオプションを使って名称変更しよう。その時のforeign_keyの指定も忘れずに。
  • followする側が持つのはfollower達 。つまり foreign_keyはfollowing_id なのに注意(逆も然り)
  • あとは普通の多対多とだいたい同じ

完成版のソースコード

目次
アソシエーションとは
データベース設計とは
UserとTweetの実装(1対多)
Favoriteの実装(多対多)
Follow機能の実装(自己結合多対多)
完成版のソースコード ←今ココ

そのまま実装 - https://github.com/mormorbump/association_tutorial
リファクタリング済み(ヴァリデーション、パーシャル化済み) - https://github.com/mormorbump/association_tutorial_refactor
heroku - https://rails-association-tutorial.herokuapp.com
丁寧すぎるherokuへのデプロイの方法はこちら - 【初心者向け】railsアプリをherokuを使って確実にデプロイする方法【決定版】

まとめ

・・・・・・・・・・・・・・・・・・・・・・・はい、本当に長くなってしまいましたが、これでアソシエーションの根幹の部分はほぼ全て抑えられたと思います。
アソシエーションだけに絞ったため細かい実装部分は色々足りていません(validationとか)。そこはご了承ください。
dependentオプションやscopeなども本当はやりたかったのですが、割愛しました。興味があれば調べてみるのをオススメします。

ポリモーフィック関連(おまけ)

※コメント機能を実装してある前提で進めます。

いつか書きます。

1698
1682
3

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
1698
1682

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?