Rails ユーザーフォロー機能を追加する
この記事について
- この記事は「フィヨルドブートキャンプ Part 1 Advent Calendar 2020」の15日目の記事です。parts2はこちらです。
- 昨日の記事はMashio Sanoさんの「日報を書こう! (日報自動投稿プログラムもあるヨ!)」でした。
- フィヨルドブートキャンプで[Railsでユーザーフォローを作る]課題を終えてのブログです。何度もレビューをもらい修正したため、理解を深めるために今回ブログにまとめることにしました。スクールに在学中の方には一部回答になる部分が含まれているのでお気をつけください。
- 今回の記事では、Railsで作成したアプリ(ログイン機能作成はdeviseを利用)にユーザーのフォロー機能を追加します。
まず最初に
フォロー/フォロワーの関係は常に一対一ではなく、一対多の関係です。つまり1人のユーザーは複数人をフォローできるし、複数人にフォローされます。一対多の関係性の実現には、フォロー・フォロワーの情報を保持する中間テーブルを作る必要があります。
中間テーブルの作成
中間テーブルとして relationships
テーブルと そのモデルにあたる Relationship
クラスを追加します。follower_id
と followed_id
カラムには常にinteger(usersテーブルのidカラムと同じ型)が入るよう設定しました。
rails generate model Relationship follower_id:integer followed_id:integer
上記コマンドを実行すると自動で migrate
ファイルが出来上がります。follower_id
とfollowed_id
が null
になることはないためnull制約を追加しておきます。
# db/migrate/20201106221457_create_relationships.rb
class CreateRelationships < ActiveRecord::Migration[6.0]
def change
create_table :relationships do |t|
t.integer :follower_id, null: false
t.integer :followed_id, null: false
t.timestamps
end
end
end
更に add_index
を使ってテーブルにインデックスを追加していきます。 インデックスを使うと特定のカラムからのデータ検索を楽にしてくれます。ここでは relatiionships
テーブルの、 followed_id
に対しindexを追加しました。なぜ follower_id
にはインデックスを追加しないのかというと、それは次の行にある follower_id, followed_id
への複合インデックスに含まれているからです。 この複合インデックスはfollower_id
と、 followed_id
の組み合わせを常にユニークにするために追加しています(同じ人を何度もフォローするような状況を防ぎます)。
# db/migrate/20201106221457_create_relationships.rb
class CreateRelationships < ActiveRecord::Migration[6.0]
# ...
add_index :relationships, :followed_id
add_index :relationships, %i[follower_id followed_id], unique: true
end
end
マイグレーションを実行します。
$ rake db:migrate
中間テーブルの作成が完了しました。$ rails dbconsole
で relationships
テーブルが存在することを確認しておきます。
$ rails dbconsole
sqlite> .headers ON
sqlite> SELECT * FROM relationships;
id|follower_id|followed_id|created_at|updated_at
テーブルが存在するこを確認し、DBは完了です!
モデルの関連付け
モデルの関連付けをしていきます。 Railsのモデルが関連したモデルのインスタンスを取得する際、関連付けの名前から自動的にモデルのクラス名を推測します。しかし実際には belongs_to :follower
に対するFollower
や belongs_to :followed
に対する Followed
というモデルは存在しません。follower
と followed
は両方ともUser
モデルです。そこでbelongs_to
のclass_name
オプションをつけることで実際に参照するモデル(User
)を指定することができます。
モデルの関連付けの際に利用する primary key を保存するカラム名も、関連付けの名前から推測します。relationship.follower
の関連の取得には、 follower_id
を使って users
テーブルからレコードを取得します。 relashionship.followed
も同様に followed_id
を使って users
テーブルからレコードを取得します。
詳細(https://railsguides.jp/association_basics.html#belongs-to関連付け)
# app/models/relationship.rb
class Relationship < ApplicationRecord
belongs_to :follower, class_name: 'User'
belongs_to :followed, class_name: 'User'
end
user
モデルに has_many
を追加しuser
とfollower
、user
とfollowing
を一対多の関係にします。 複数あることを示すためここでは followers
と followings
のように複数形の名前をつけます。
belongs_to
と同様に、Railsが推測する外部キーのカラム名は Relashionships
テーブルに存在しないuser_id
になります。そのため foreign_key
オプションを利用し実際に使うカラム名を指定しています。 dependent: :destroy,
は、ユーザーが削除された場合、フォロー・フォロワーの関係性も削除されるようにするものです。
# app/models/user.rb
has_many :followers, class_name: 'Relationship',
foreign_key: 'follower_id',
dependent: :destroy,
inverse_of: :follower
has_many :followings, class_name: 'Relationship',
foreign_key: 'followed_id',
dependent: :destroy,
inverse_of: :followed
Userモデルから中間テーブルを介し、following/followerのUserモデルのインスタンスを取得するためにthroughオプションを設定します。
https://railsguides.jp/association_basics.html#has-many-through関連付け
# app/models/user.rb
# ..
# :following_users(フォローする人)は、中間テーブルのfollowersを通り、followedにたどり着きます
# :follower_users(フォローされる人)は、中間テーブルのfollowingsを通り、followerにたどり着きます
has_many :following_users, through: :followers, source: :followed
has_many :follower_users, through: :followings, source: :follower
これで中間モデルの作成と紐付けが完了しました。
フォロー機能・フォロワー機能の作成
Userモデルにフォロー機能、そしてフォローしているかどうかを確認する機能を追加します。
# model/user.rb
# ..
# フォロー
def follow(user_id)
followers.create(followed_id: user_id)
end
# 今フォローしているか確認する
def following?(user)
following_users.include?(user)
end
フォロー・フォロー解除
relationships
コントローラーを作成します。
$ rails g controller relationships
relationships
コントローラーにフォロー・アンフォローアクションを追加します。元々 destory
メソッドで削除するrelationship
をDBから取得する際に find_by
メソッドを使っていたのですが、その場合該当のid
が見つからなかったときにnil
が返るためnil.destory
でエラーとなってしまうというアドバイスをいただきました。そのため該当レコードがない場合に例外を返す find
メソッドへ変更しました。またコントローラーのメソッド名を当初は follow
unfollow
としていましたが、Railsのレールに則り create
destory
を使用するよう変更しました。
# app/controllers/relationships_controller.rb
class RelationshipsController < ApplicationController
def create
user = User.find_by_id(params[:follow_id])
if user
current_user.follow(params[:follow_id]) unless current_user.following?(user)
end
redirect_to root_path
end
def destroy
Relationship.find(params[:id]).destroy
redirect_to root_path
end
end
フォロー・フォロワー一覧
フォローフォロワー一覧の機能をUsers
コントローラーにfollowings
、followers
として追加します。@user
は全てのメソッド内で利用するため、 before_action
で生成するようにしました。
# app/controllers/users_controller.rb
class UsersController < ApplicationController
before_action :set_user
def show
@relationship = @user.followings.find_by(follower_id: current_user.id)
end
def followings
@followings = @user.following_users
end
def followers
@followers = @user.follower_users
end
private
def set_user
@user = User.find(params[:id])
end
end
これでコントローラーの実装は完了です。
ルーティングとビューの実装
まずroutesを設定します。 今回はフォローしているユーザー一覧、フォローされているユーザー一覧ページも作成します。
# config/routes.rb
..
resources :users do
member do
get :followings, :followers # 今回追加したルーティング
end
end
resources :relationships, only: %i[create destroy] # 今回追加したルーティング
resources :users, only: :show
resources :books
end
ユーザーのプロフィールページに、フォロー人数、フォロワー人数を表示します。また、現在開いているページが自分自身のページでない場合、フォロー・アンフォローボタンを表示します。
# app/viwes/users/show.html.erb
<p><%= link_to 'show.follow'+": #{@user.followers.count}", followings_user_path(@user.id) %></p>
<p><%= link_to 'show.follower'+": #{@user.followings.count}", followers_user_path(@user.id) %></p>
<% unless @user == current_user %>
<% if current_user.following?(@user) %>
<%= button_to t('show.unfollow'), relationship_path(@relationship), method: :delete %>
<% else %>
<%= form_for(@user.followers.build) do |f| %>
<%= hidden_field_tag :follow_id, @user.id %>
<%= f.submit t('show.follow'), class: 'btn btn-primary btn-block' %>
<% end %>
<% end %>
<% end %>
フォロー一覧ページ、フォロワー一覧ページを作成します。
# app/viwes/users/followings.html.erb
<% @followings.each do |user| %>
<table>
<tr>
<td> <%= user.name %> </td>
<td><%= link_to 'follow.profile', user_path(user) %></td>
</tr>
</table>
<% end %>
# app/viwes/users/followers.html.erb
<% @followers.each do |user| %>
<table>
<tr>
<td> <%= user.name %> </td>
<td><%= link_to 'profile', user_path(user) %></td>
</tr>
</table>
<% end %>
以上で終了です!
感想
この課題では、Railsがリレーションの際に自動的につける名前のルールを理解するのに苦労しました。また提出後にメンターの方から「Railsのルールに則るとこうした方が良い」というレビューをいくつかいただき、自分がRailsのルールではなく自己流でやってしまっていた部分を修正しました。Railsのルールに則るとシンプルで読みやすいコードになったため、今後はRailsルールを意識してコードを書いていこうと思います。丁寧なレビューをありがとうございました。また、アドベントカレンダーに参加しブログを書くことで良い復習になりました。今後も学習を続けていきます!
treeコマンドをインストールした
Macにtreeコマンドをインストールしました。 treeコマンドとはディレクトリ、フォルダ内のサブフォルダやファイルをツリー表示で出力してくれるコマンドです。
Homebrewからインストールします。
$ brew install tree ------ ==> Pouring tree-1.8.0.catalina.bottle.tar.gz 🍺 /usr/local/Cellar/tree/1.8.0: 8 files, 121.1KB
インストール完了。
lsコマンドと比較する!
$ ls ------ codewars
treeコマンド
$ tree . └── codewars ├── Multiply │ └── 20200827.js └── Reversed\ Strings └── 20200827.js
おお〜 見やすいしファイル名を打つ手間が省けて便利になった!
LinuxにPostgreSQLをインストール/testユーザーの作成
Linux(Debian)にpostgreSQLをインストールする
$ sudo apt install postgresql
$ psql --version
psql (PostgreSQL) 11.7 (Debian 11.7-0+deb10u1)
postgreSQLの中にtestユーザーを追加する
参考 ロール(ユーザー)の作成
postgres=# CREATE ROLE test with LOGIN PASSWORD 'testtest';
CREATE ROLE
postgres=# \du;
List of roles
Role name | Attributes | Member of
-----------+------------------------------------------------------------+-----------
postgres | Superuser, Create role, Create DB, Replication, Bypass RLS | {}
test | | {}
postgres=# exit
testユーザを使ってpostgreSQLへ接続する
$ psql -U test -h localhost
Password for user test:
psql (11.7 (Debian 11.7-0+deb10u1))
SSL connection (protocol: TLSv1.3, cipher: TLS_AES_256_GCM_SHA384, bits: 256, compression: off)
Mac - nkfコマンドを使って文字コードを変換する
nkfコマンドを使って文字コードを変換する
『ゼロからはじめるデータベース操作』のサンプルコードをダウンロードして手を動かしながら進めよう〜と思っていたら、本書がWIn向けに書かれているからだと思うのですが、Macでサンプルコードを開くと日本語が文字化けしていました。
ReadMe.txtも文字化けしていて読めなかったので、とりあえず文字化けを直してみることに。(参考 Macでファイルの文字コードの確認 / 変更の仕方)
こんなふうに文字化けしていた。
まず現在の文字コードの種類を調べるために下記を実行する。
$ file --mine ReadMe.txt
-------
ReadMe.txt: text/plain; charset=unknown-8bit
どうやらunknown-8bitという文字コードになっている。文字コードcharset=unknown-8bitは、Shift-JIS コードを表しているとのこと。とりあえず、これをMacで日本語に対応しているUTF-8に変更したい!
文字コードを変換するnkfコマンド(Network Kanji Filte command)に、-w
(UTF-8)へ変換するオプションをつけて実行する。
$ nkf -w --overwrite ReadMe.txt
-------
-bash: nkf: command not found
nkfコマンドが入っていないとのこと。Homebrewでnkfコマンドをインストールする🍺
$ brew install nkf
無事インストールされたようなので文字コード変換をもう一回行う。
$ nkf -w --overwrite ReadMe.txt
正しく変換されてるか確認する。
$ file --mime ReadMe.txt
-------
ReadMe.txt: text/plain; charset=utf-8
charset=utf-8になっている。できた!🍺文字化けが直って読めるようになった。
サンプルコードも日本語が含まれていて文字化けがあったりので、ワイルドカードを使って文字コードを変更して進めました。
追記(2020/08/27)
上記内容をフィヨルドブートキャンプ内の日報に書いたところ、メンターのJunichi Itoさんから簡単な文字コード変換の方法を教えてもらいました。文字コードの切り替えができるテキストエディタがあるということで、おすすめしてもらったCotEditorを入れてみました。
ファイルを開くだけで上部メニューから文字コードを選択できる。覚える必要もないくらい簡単でした。自分で編集する必要のないファイルだったらこの方法が良いと思いました🙌