6月22日からFjordBootCampでプログラミングを勉強してます!

Rails ユーザーフォロー機能を追加する

この記事について

まず最初に

フォロー/フォロワーの関係は常に一対一ではなく、一対多の関係です。つまり1人のユーザーは複数人をフォローできるし、複数人にフォローされます。一対多の関係性の実現には、フォロー・フォロワーの情報を保持する中間テーブルを作る必要があります。

中間テーブルの作成

中間テーブルとして relationships テーブルと そのモデルにあたる Relationship クラスを追加します。follower_idfollowed_id カラムには常にinteger(usersテーブルのidカラムと同じ型)が入るよう設定しました。

rails generate model Relationship follower_id:integer followed_id:integer

上記コマンドを実行すると自動で migrate ファイルが出来上がります。follower_idfollowed_idnull になることはないため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 dbconsolerelationships テーブルが存在することを確認しておきます。

$ rails dbconsole
sqlite> .headers ON
sqlite> SELECT * FROM relationships;
id|follower_id|followed_id|created_at|updated_at

テーブルが存在するこを確認し、DBは完了です!

モデルの関連付け

モデルの関連付けをしていきます。 Railsのモデルが関連したモデルのインスタンスを取得する際、関連付けの名前から自動的にモデルのクラス名を推測します。しかし実際には belongs_to :followerに対するFollowerbelongs_to :followedに対する Followed というモデルは存在しません。followerfollowed は両方ともUserモデルです。そこでbelongs_toclass_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を追加しuserfolloweruserfollowingを一対多の関係にします。 複数あることを示すためここでは followersfollowings のように複数形の名前をつけます。

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コントローラーにfollowingsfollowersとして追加します。@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)

参考 PostgreSQLのPeer認証と他の認証方法への変更

Mac - nkfコマンドを使って文字コードを変換する

nkfコマンドを使って文字コードを変換する

『ゼロからはじめるデータベース操作』のサンプルコードをダウンロードして手を動かしながら進めよう〜と思っていたら、本書がWIn向けに書かれているからだと思うのですが、Macでサンプルコードを開くと日本語が文字化けしていました。

ReadMe.txtも文字化けしていて読めなかったので、とりあえず文字化けを直してみることに。(参考 Macでファイルの文字コードの確認 / 変更の仕方)

こんなふうに文字化けしていた。

f:id:yuna627:20200827052652p:plain

まず現在の文字コードの種類を調べるために下記を実行する。

$ 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になっている。できた!🍺文字化けが直って読めるようになった。

f:id:yuna627:20200827062706p:plain

サンプルコードも日本語が含まれていて文字化けがあったりので、ワイルドカードを使って文字コードを変更して進めました。

追記(2020/08/27)

上記内容をフィヨルドブートキャンプ内の日報に書いたところ、メンターのJunichi Itoさんから簡単な文字コード変換の方法を教えてもらいました。文字コードの切り替えができるテキストエディタがあるということで、おすすめしてもらったCotEditorを入れてみました。

f:id:yuna627:20200827100405p:plain

ファイルを開くだけで上部メニューから文字コードを選択できる。覚える必要もないくらい簡単でした。自分で編集する必要のないファイルだったらこの方法が良いと思いました🙌