学習記録

アウトプット用に作りました

ログイン後にログインページとサインアップのページに遷移できないようにする

ログインしているユーザーがログイン画面、またユーザー新規登録画面にアクセスできるようになっていたので修正していこうと思います。

どうやって直していくのか少し考えます、、、

  • ビューファイルでも、モデルでもなくコントローラをいじらないといけない。
  • user_sessions#newとusers#newが該当のアクションになる
  • 現在のユーザーが判別できたらリダイレクトする仕様にする

この感じの流れで解決できそうだと思ったので実装していきます。

まず最初にログイン画面(user_sessions#new)のアクションを修正していきます。current_userがログインしているならリダイレクトするというコードを記入しました。このコードはlogged_in?メソッドがないというメソッドエラーになります。Sorceryのメソッドですが、ビューでのみ使えるメソッドのようです。

class UserSessionsController < ApplicationController

省略

  def new
    if current_user.logged_in?
      redirect_to user_diaries_path(current_user.name), danger: t('.fail')
    end
  end

省略

end

もしcurrent_userならリダイレクトするというコードを書いたらどうだろうと思ったので実装していきます。このコードで思っていた実装ができました!!

class UserSessionsController < ApplicationController

省略

  def new
    if current_user
      redirect_to user_diaries_path(current_user.name), danger: t('.fail')
    end
  end

省略

end

ログイン画面へのアクセス制限ができたので、次はユーザー登録画面のアクセス制限を行います。コードは同じように書いていきたいと思います。こちらも実装できました!

class UsersController < ApplicationController
  
省略

  def new
    @user = User.new
    if current_user
      redirect_to user_diaries_path(current_user.name), danger: t('.fail')
    end
  end

省略

end

あとはi18nの設定を追加して実装は終わりです。

パスにuser_idを含まない仕様にする

参考にしたサイト https://blog.takady.net/blog/2015/11/29/rails-routing-with-username-instead-of-id/

現在のパスにuser_idが含まれているままだとアプリのユーザー数がわかってしまったり、もしデータベースからユーザー情報が消えてしまった場合、同じユーザーIDでアプリに入った時に新規登録したユーザーのアカウントでログインできてしまう可能性ああります。なのでパスにユーザーIDは含めたくないなあと思っていました。

なので本リリースする前にユーザーが全然いない時にやっておいた方がいいかな〜と思ったので今回実装していきます。

デバックや検証ツールなどを使用して進めていけたのでとても良い練習になりました。

まず最初にユーザーモデルにto_paramというactive recordのメソッドを使ってURLの:idの部分に id以外を指定できるようにします。今回はnameカラムを使用したいので下記のように記載しました。

user.rb

def to_param
  name
end

そして次にルーティングを作成していきました。param: :nameとすることで:nameをURLに含めることができるようになります。

resources :users, param: :name, only: [:new, :create, :index, :destroy] do

    resources :followers, only: [:index]

    get 'following', to: 'users#following'
    get 'follower', to: 'users#follower'

    resources :diaries do
      member do
        resources :bookmarks, only: [:index]
      end
    end

end

たくさんネストしているので重点的に上記のパスが正常に動くか確認しながら進めていきたいと思います。

まず最初にサイトにアクセスしてみてエラーが出た箇所のコントローラを修正していきます。

日記一覧ページにアクセスするとidがnilというエラーが出ました。なのでdiaries_controller.rbのindexアクションを見てみます。

def index
    @user = User.find(params[:user_id])
    @diaries = @user.diaries
end

パラメータで送られてきたデータのuser_idを使用しているのでここをuser_nameにすればいけそうと思ったので修正します。

def index
    @user = User.find_by(name: params[:user_name])
    @diaries = @user.diaries
end

これでエラーは解消しました。

次は細かいですがdiaries_controller.rbの色々なリダイレクト先でcurrent_user@userでパスに送っている部分をcurrent_user.name@user.nameと記入して指定します。

ビューファイルも同様にパスに追記しておきます。

次はuser_controller.rbです。ここはdestroyアクションでuser_nameを指定しておかないと削除したいユーザーがわからずにエラーになります。最初params[:user_name]としていたのですがエラーが出てしまったのでコンソールで確認しながら修正しました。

def destroy
    @user = User.find_by(name: params[:name])
    @user.destroy
    redirect_to root_path, success: 'thank you'
end

最後にフォローするボタンです。フォローを外すボタンは作動していたのですがフォローをするボタンで422エラーが出ていました。

@other_user = User.find_by(name: params[:follower])

このように変更することで、解決しました。検証ツールのコンソールを見ることの大切さを感じました。

一応全てのページを確認してエラーが出るページ、思ってもいないページへの遷移はしていないようでした。

お問い合わせ機能の実装

参考にしたサイト https://www.web-knowledge-info.com/wp/ruby-on-rails26/

お問い合わせのページはフッターにリンクを用意してどのページからもアクセスできるようにします。

<%= link_to "お問い合わせ", "#", class: "copylight-link" %>

お問い合わせのコントローラを作成します。

$ bundle exec rails g controller Contacts
Running via Spring preloader in process 10657
      create  app/controllers/contacts_controller.rb
      invoke  erb
      create    app/views/contacts

問い合わせするフォームが必要なので作成していきます。今作成したコントローラのアクションは新規作成の分だけで良いのでnewとcreateアクションだけ追加します。またログインしていなくてもお問い合わせフォームにはアクセスできるようにしたいのでrequire_loginをスキップします。

class ContactsController < ApplicationController
  skip_before_action :require_login

  def new
  end

  def create
  end
end

ルーティングを追加します。

resources :contacts, only: [:new, :create]

次のお問い合わせフォームで送られたemailとname、お問い合わせ内容を扱うためのモデルを作成します。

$ bundle exec rails g model Contact name:string email:string content:text
Running via Spring preloader in process 11324
      invoke  active_record
      create    db/migrate/20210717131106_create_contacts.rb
      create    app/models/contact.rb

作成されたマイグレーションファイルに制約をつけていきます。

class CreateContacts < ActiveRecord::Migration[6.1]
  def change
    create_table :contacts do |t|
      t.string :name, null: false
      t.string :email, null: false
      t.text :content, null: false

      t.timestamps
    end
  end
end

どの項目も入力必須にしたいのでこのように記入しました。

記入が終わったらデータベースを作成しました。

$ bundle exec rails db:migrate
== 20210717131106 CreateContacts: migrating ===================================
-- create_table(:contacts)
   -> 0.2038s
== 20210717131106 CreateContacts: migrated (0.2039s) ==========================

cnotact.rbにもnull制約をつけます。

class Contact < ApplicationRecord
  validates :name, presence: true
  validates :email, presence: true
  validates :content, presence: true
end

先ほど作成したcontacts_controller.rbに記述していきます。

class ContactsController < ApplicationController
  skip_before_action :require_login

  def new
    @contact = Contact.new
  end

  def create
    @contact = Contact.new(contact_params)
    if @contact.save
      ContactMailer.contact_us_email(@contact).deliver
      redirect_to root_path
    else
      render :new
    end
  end

    private

  def contact_params
    params.require(:contact).permit(:name, :email, :content)
  end
end

お問い合わせフォームを作成します。まず最初にビューファイルを作成します。

$ touch app/views/contacts/new.html.erb

次に作成したビューファイルにフォームを記述します。

<% content_for(:title, 'contact us') %>

<div class="container">
  <div class="row justify-content-center">
    <div class="card resulting col-10 col-md-6">
      
      <div class="card-body">
        <h2 class="result card-title text-center">✉️ contact us</h2>
        <div class="register">
        <%= simple_form_for @contact, local: true do |f| %>
            <div class="form-group">
              <%= f.input :name, class: 'form-control', placeholder: 'たろう' %>
            </div>
            <div class="form-group">
              <%= f.input :email, class: 'form-control', placeholder: 'emory@example.com' %>
            </div>
            <div class="form-group">
            <%= f.input :content, as: :text, class: 'form-control', input_html: { rows: 5, cols: 5 }, placeholder: 'お問い合わせ内容をご記入ください' %>
            </div>

            <div class="text-center">
              <%= f.submit t('defaults.submit'), class: "btn btn-normal" %>
            </div>
          <% end %>
        </div>
      </div>
    </div>
  </div>
</div>

次にMailerを作成しました。

$ bundle exec rails g mailer ContactMailer contact_us_email
Running via Spring preloader in process 10808
      create  app/mailers/contact_mailer.rb
      invoke  erb
      create    app/views/contact_mailer
      create    app/views/contact_mailer/contact_us_email.text.erb
      create    app/views/contact_mailer/contact_us_email.html.erb

作成されたMailerに送信するための設定を行います。

class ContactMailer < ApplicationMailer

  # Subject can be set in your I18n file at config/locales/en.yml
  # with the following lookup:
  #
  #   en.contact_mailer.contact_us_email.subject
  #
  def contact_us_email(contact)
    @contact = contact
    mail(:to => ENV["YOUR_GMAIL_ADDRESS"],
        :subject => "emoryへのお問い合わせ")
  end
end

メールの送信先には.envファイルで管理している私のメールアドレスを登録しています。

次にメールのテンプレートを作成します。

contact_mailer/contact_us_email.html.erb

<p>emoryへのお問い合わせ内容</p>
<p>====================================</p>
<p>お名前 : <%= @contact.name %></p>
<p>メールアドレス : <%= @contact.email %></p>
<p>内容 : <%= @contact.content %></p>
contact_mailer/contact_us_email.text.erb

emoryへのお問い合わせ内容

お名前 : <%= @contact.name %>
メールアドレス : <%= @contact.email %>
内容 : <%= @contact.content %>

フッターのお問い合わせのリンクにパスを記述しておきます。

<%= link_to "お問い合わせ", new_contact_path, class: "copylight-link" %>

お問い合わせフォームの内容を記入するtextareaの大きさを変えることができてしまっているのでcssで固定します。またフォームのレイアウトを綺麗にします。

/* お問い合わせフォームのtextareaを固定する */
textarea {
  resize: none !important;
  border: 1px solid var(--color5) !important;
  padding: 5px 8px !important;
  border-radius: 20px !important;
  overflow: hidden !important;
  background: var(--color5) !important;
  color: gray !important;
}

/* お問い合わせフォームのtextarea選択中 */
textarea:focus {
  outline: none !important;
  background: var(--color5) !important;
  box-shadow: none !important;
  text-shadow: none !important;
  outline: 0 !important;
  border-color: var(--color2) !important;
}

/* お問い合わせフォームのプレースホルダー */
textarea::placeholder { 
  color: lightgray !important;
}

何故か!importantをつけないとcssがつかなかったので全てに記入しました。

参考にしたサイト https://www-creators.com/archives/2496

PFのテーマカラーを選択できるようにする

PFのテーマカラーがデフォルトのピンクとブルー系の2つからユーザーが選択して設定できるようにしたいです。

進めていく手順

  1. Userテーブルにcolorカラムを追加する
  2. ユーザー新規登録ページとプロフィール編集ページにフォームを追加
  3. コントローラのストロングパラメータにcolor追加
  4. cssファイルを編集

まず最初にUsersテーブルにcolorカラムを追加しました。

$ bundle exec rails g migration AddColorToUsers color:integer
Running via Spring preloader in process 70954
      invoke  active_record
      create    db/migrate/20210714133754_add_color_to_users.rb

作成されたマイグレーションファイルにデフォルト値を追加します。

class AddColorToUsers < ActiveRecord::Migration[6.1]
  def change
    add_column :users, :color, :integer, default: 0
  end
end

マイグレートしてデータベースに追加しました。

$ bundle exec rails db:migrate
== 20210714133754 AddColorToUsers: migrating ==================================
-- add_column(:users, :color, :integer, {:default=>0})
   -> 0.7259s
== 20210714133754 AddColorToUsers: migrated (0.7260s) =========================

そしてuserモデルにcolorカラムについての記述を行います。

class User < ApplicationRecord
# サイトのテーマカラー
  enum color: { pink: 0, blue: 1}
end

pinkをデフォルトにします。

次にフォームをビューに追加していこうと思います。simple_formを使用しています。最初にユーザー新規作成画面

selectedでフォームの初期値を入れています。

users/new.html.erb

<div class="form-group">
    <%= f.input :color, as: :select, selected: 'pink', collection: User.colors.keys, class: "form-control", hint: "マイページのテーマカラーを選択してください" %>
</div>

collection:でセレクトフォームで選べる項目を入れています。Userのcolor項目のハッシュからキーを配列で取得しています。実際にコンソールで確認しながら作成しました。

Sayo-MacBook-Pro:emoji_diary SAYO$ rails c
Running via Spring preloader in process 74168
Loading development environment (Rails 6.1.3.2)
irb: warn: can't alias context from irb_context.
irb(main):001:0> User.colors
=> {"pink"=>0, "blue"=>1}
irb(main):002:0> User.create(nickname: '👓', name: "megane", password: 'password', password_confirmation: 'password', email: 'megane@example.com', color: 'blue')
  TRANSACTION (0.2ms)  BEGIN
  User Exists? (0.4ms)  SELECT 1 AS one FROM `users` WHERE `users`.`email` = 'megane@example.com' LIMIT 1
  User Exists? (39.1ms)  SELECT 1 AS one FROM `users` WHERE `users`.`name` = 'megane' LIMIT 1
  User Create (88.9ms)  INSERT INTO `users` (`nickname`, `name`, `crypted_password`, `salt`, `created_at`, `updated_at`, `email`, `color`) VALUES ('👓', 'megane', '$2a$10$AhqCyQAETZxQEjUZtl7qb.alfkxg.KS3zSgj5rW3s1A8ulbTS.y.e', 'o4tKY5Vtr8RWcxNxt8Fw', '2021-07-14 15:34:14.588242', '2021-07-14 15:34:14.588242', 'megane@example.com', 1)
  TRANSACTION (7.1ms)  COMMIT
=> #<User id: 25, nickname: "👓", name: "megane", crypted_password: "$2a$10$AhqCyQAETZxQEjUZtl7qb.alfkxg.KS3zSgj5rW3s1A...", salt: "o4tKY5Vtr8RWcxNxt8Fw", created_at: "2021-07-15 00:34:14.588242000 +0900", updated_at: "2021-07-15 00:34:14.588242000 +0900", email: "megane@example.com", role: "general", color: "blue">
irb(main):005:0> user = User.last
  User Load (92.5ms)  SELECT `users`.* FROM `users` ORDER BY `users`.`id` DESC LIMIT 1
=> #<User id: 25, nickname: "👓", name: "megane", crypted_password: "$2a$10$AhqCyQAETZxQEjUZtl7qb.alfkxg.KS3zSgj5rW3s1A...", salt: "o4tKY5Vtr8RWcxNxt8Fw", created_at: "2021-07-...
irb(main):006:0> user.color
=> "blue"
irb(main):007:0> user = User.first
  User Load (29.2ms)  SELECT `users`.* FROM `users` ORDER BY `users`.`id` ASC LIMIT 1
=> #<User id: 1, nickname: "🐒", name: "sayo", crypted_password: "$2a$10$idRbazbxLnZamo9L0JOzuOf7Dy40FD7lK/GCUMb6O7/...", salt: nil, created_at: "2021-06-30 15:30:20.862197000 ...
irb(main):008:0> user.color
=> "pink"

次にプロフィール編集ページのフォームに追加します。

<div class="form-group">
    <%= f.input :color, as: :select, include_blank: false, collection: User.colors.keys, class: "form-control", hint: "マイページのテーマカラーを選択してください" %>
</div>

include_blank: falseで空白で投稿できないようにします。

そしてフォームを追加したのでストロングパラメーターにcolorを追加して、データを受け取れるようにします。

class UsersController < ApplicationController
    def user_params
    params.require(:user).permit(:nickname, :name, :password, :password_confirmation, :email, :color)
  end
end
class ProfilesController < ApplicationController
    def user_params
    params.require(:user).permit(:nickname, :name, :password, :password_confirmation, :email, :color)
  end
end

cssファイルにblueのテーマカラーを追加していきます。私が考えた実装の仕方なのでとても冗長的で汚いコードになっていると思うし、こんなこと絶対にしない!って思われるかもしれないです。でも一応思ったように動いたので、これで良しとしました。

やったこと

  • cssファイルで色を変えたい記述にcss変数を使う
  • application.html.erbでif文を使って切り替える

まず最初にapplication.scssファイルにデフォルトのpinkの色をcss変数を使って定義していきます。

:root {
  --color1: #fbe6de;
  --color2: #c76e54;
  --color3: #f5cdbd;
  --color4: #bf9288;
  --color5: #f5ece4;
}

色をつける部分で下記のように指定します。全て変更してサイトの色が変わらず出力されているか確認しました。

#header {
  background-color: var(--color1);
}

次に青色のテーマを管理するapplication_blue.scssファイルを作成しました。これをapplication.scssファイルの代わりに使いたいので、必要なcssファイルや、bootstrapの記述も書き写しました。

@import 'home';
@import 'simple_calendar';
@import 'diaries.scss';
@import 'users.scss';
@import 'profiles.scss';
@import 'header_footer.scss';
@import '~bootstrap/scss/bootstrap';
@import '~@fortawesome/fontawesome-free/scss/fontawesome';

.blue {
  --color1: #ABBDDA;
  --color2: #399ECC;
  --color3: #7097C3;
  --color4: #444f7c;
  --color5: #E0ECFF;
}

そしてapplication.html.erbファイルで条件分岐を使って、colorがpinkとログインしていない時のcssファイルでapplication.scssファイルを使うこと、そしてそれ以外ではapplicaction_blue.scssファイルを使用することを定義していきます。

<!DOCTYPE html>
<% if !logged_in? || (current_user.color == "pink") %>
<html>
  <head>
    <title><%= page_title(yield(:title)) %></title>
    <meta name="viewport" content="width=device-width,initial-scale=1">
    <%= csrf_meta_tags %>
    <%= csp_meta_tag %>

    <%= stylesheet_pack_tag 'application', media: 'all' %>
    <%= javascript_pack_tag 'application' %>

    <script src="//ajax.googleapis.com/ajax/libs/jquery/2.1.0/jquery.min.js"></script>
  </head>

  <body>
    <% if logged_in? %>
      <%= render 'shared/header' %>
    <% else %>
      <%= render 'shared/before_login_header' %>
    <% end %>
    <%= render 'shared/flash_message' %>
    <%= yield %>
    <%= render 'shared/footer' %>
  </body>
</html>

<% else %>

<html class="blue">
  <head>
    <title><%= page_title(yield(:title)) %></title>
    <meta name="viewport" content="width=device-width,initial-scale=1">
    <%= csrf_meta_tags %>
    <%= csp_meta_tag %>

    <%= stylesheet_pack_tag 'application_blue', media: 'all' %>

    <%= javascript_pack_tag 'application' %>

    <script src="//ajax.googleapis.com/ajax/libs/jquery/2.1.0/jquery.min.js"></script>
  </head>

  <body>
    <% if logged_in? %>
      <%= render 'shared/header' %>
    <% else %>
      <%= render 'shared/before_login_header' %>
    <% end %>
    <%= render 'shared/flash_message' %>
    <%= yield %>
    <%= render 'shared/footer' %>
  </body>
</html>
<% end %>

タグから切り替えるようにしているのでとても長いコードになってしまいました。最初はstylesheet_pack_tagのコードを下記のように条件分岐した感じで書いていたのですがcssファイルの:rootと書いている部分の記述でうまくいかなかったのでこのようになりました。

blueに変更するときは<html class="blue">と書いてクラスを当てています。

ローカル環境ではこの記述で問題なくテーマカラーを変更することができました。しかしherokuにpushして本番環境で動作するか確認すると500エラーが出てしましました。エラー文を残していなくてとても後悔しています。。唯一検索履歴に残っていたWebpacker can't find application_blue.cssこれがエラーの部分です。webpackerがapplication_blue.cssファイルを見つけられない?といっています。調べていくと同じようなエラーが出ている人を見つけました。

https://github.com/rails/webpacker/issues/2071

config/webpacker.ymlの中でextract_css: trueの場合にはlinkを出力し、extract_css: falseの場合にはJavaScriptを使って動的にCSSを読み込むという動作になります。development環境ではfalseになっていたのでproduction環境でもfalseに設定しました。

production:
  <<: *default

  # Production depends on precompilation of packs prior to booting for performance.
  compile: false

  # Extract and emit a css file
  extract_css: false

  # Cache manifest.json for performance
  cache_manifest: true

詳しい説明が下記に載っていました。

https://zenn.dev/ryouzi/articles/da8a77accc221e#stylesheet_pack_tagがproduction環境でエラーが出る

ransackを使ったユーザー検索機能の実装

ransackについては以前ブログで何度かまとめました。日付検索や、プルダウン表示などをまとめていたと思います。

今回はユーザー検索のページと検索結果のページを分けて表示していこうと思います。

まず最初にGemfileransackを追加して、bundle installします。

gem 'ransack'

ユーザーコントローラのindexアクションに検索できるような設定を行います。

def index
  @q = User.ransack(params[:q])
  @users = @q.result(distinct: true)
end

ユーザー検索機能をつけるusers/index.html.erbファイルに検索機能をつけました。

<% content_for(:title, 'User search') %>

<p id="notice"><%= notice %></p>

<h1>search</h1>

# ここを追記
<%= search_form_for @q do |f| %>
  <%= f.search_field :name_cont, placeholder: 'user name' %>
  <%= f.submit class: 'btn btn-primary' %>
<% end %>

<table class="table table-sm col-12">
  <thead class="thead-light">
    <tr>
      <th class="text-center">nickname</th>
      <th class="text-center">name</th>
      <th colspan="5"></th>
    </tr>
  </thead>

  <tbody>
    <% @users.each do |user| %>
      <tr>
        <td class="text-center"><%= link_to user.nickname, user_diaries_path(user) %></td>
        <td class="text-center"><%= user.name %></td>
        <td class="text-center"><% if logged_in? && current_user != user %>
          <% if current_user.following?(user) %>
            <%= render 'relationships/unfollow_button', user: user %>
          <% else %>
            <%= render 'relationships/follow_button', user: user %>
          <% end %>
        <% end %></td>
      </tr>
    <% end %>
  </tbody>
</table>

ユーザー一覧の上に検索フォームが作成されました。しかし私は検索フォームで入力した後に、その検索条件に一致するユーザー情報だけ表示させたかったので、少しだけ変更していこうと思います。

まず最初に検索するページと検索結果が表示されるページの二つのページが必要になるので、usersアクションにもう一つアクションを追加します。

class UsersController < ApplicationController
    def index
    @q = User.ransack(params[:q])
  end

    def search
    @q = User.search(params[:q])
    @users = @q.result(distinct: true)
  end
end

ルーティングも追加します。

get 'search', to: 'users#search'

検索結果を表示するビューファイルを作成します。

Sayo-MacBook-Pro:emoji_diary SAYO$ touch app/views/users/search.html.erb
<% content_for(:title, 'search result') %>

<table class="table table-sm col-12">
  <thead class="thead-light">
    <tr>
      <th class="text-center">nickname</th>
      <th class="text-center">name</th>
      <th colspan="5"></th>
    </tr>
  </thead>

  <tbody>
    <% @users.each do |user| %>
      <tr>
        <td class="text-center"><%= link_to user.nickname, user_diaries_path(user) %></td>
        <td class="text-center"><%= user.name %></td>
        <td class="text-center"><% if logged_in? && current_user != user %>
          <% if current_user.following?(user) %>
            <%= render 'relationships/unfollow_button', user: user %>
          <% else %>
            <%= render 'relationships/follow_button', user: user %>
          <% end %>
        <% end %></td>
      </tr>
    <% end %>
  </tbody>
</table>

users/index.html.erbの記述も少し変更します。

<% content_for(:title, 'User search') %>

<p id="notice"><%= notice %></p>

<h1>search</h1>

<%= search_form_for @q, url: search_path do |f| %>
  <%= f.search_field :name_cont, placeholder: 'user name' %>
  <%= f.submit class: 'btn btn-primary' %>
<% end %>

url: search_pathを追記することでフォームを送信するurlを指定しています。

このままだと検索フォームに何も入力せずに送信するとユーザー情報を全件表示してしますので直していこうと思います。

調べてみるとフォームにrequired: trueをつけることで空白では送信できなくなるようだったので下記のようにしてみました。

<%= f.search_field :name_cont, placeholder: 'user name', required: true %>

あとは/searchに直接アクセスした時にユーザー一覧が表示されてしまったのでそれを回避しようと思います。

class UsersController < ApplicationController
    def search
    @q = User.search(search_params)
    @users = @q.result(distinct: true)
  end

  private

  def search_params
    params.require(:q).permit(:name_cont)
  end
end

search_paramsで送信する情報をname_contのみに限定しています。アクセスしてみるとparam is missing or the value is empty: qというエラー出るようになったので成功です。

seed_fuを使って初期データを作成

以前seed_fuを使ってデータを作成したことがあったのですが、そのときには出会わなかったエラーに遭遇したので、記録として残しておきます。

以前書いたブログ seed_fuを使って初期データを作成 - 学習記録



ユーザーテーブルと日記テーブルの初期データを作成していきます。

まず最初にGemfileseed_fugemを追加してbundle installします。

gem 'seed-fu'

seedファイルを置くためのディレクトリを作成します。

$ mkdir db/fixtures
$ mkdir db/fixtures/development
$ mkdir db/fixtures/production
$ mkdir db/fixtures/test

開発環境のUserテーブルとDiaryテーブルにデータを作成していきたいと思います。まずdevelopment配下に下記2つのファイルを用意します。

$ touch db/fixtures/development/users.rb
$ touch db/fixtures/development/diaries.rb

日記のデータを作成していきます。このようなデータをidを変えて20個作成しました。

Diary.seed do |s|
  s.id = 1
  s.feeling = '😆'
  s.body = '⛱🐠'
  s.start_time  = Date.yesterday
  s.user_id = 1
end

ユーザーのデータを作成します。id、nickname、nameを変えて10個作成しました。

User.seed do |s|
  s.id = 1
  s.nickname = 'sayo'
  s.name = 'kimsayo'
  s.password  = "password"
end

そして開発環境のデータを作成しようとコマンドを叩くと、エラーに。。InvalidForeignKeyとなっているのですが、外部キーの設定が無効?

$ rails db:seed_fu RAILS_ENV=development

== Seed from /Users/SAYO/workspace/portfolio/emoji_diary/emoji_diary/db/fixtures/development/diaries.rb
 - Diary {:id=>1, :feeling=>"😆", :body=>"⛱🐠", :start_time=>Tue, 01 Jun 2021, :user_id=>1}
 - Diary {:id=>2, :feeling=>"😆", :body=>"⛱🐠", :start_time=>Tue, 01 Jun 2021, :user_id=>2}
 - Diary {:id=>3, :feeling=>"😆", :body=>"⛱🐠", :start_time=>Tue, 01 Jun 2021, :user_id=>3}
rails aborted!
ActiveRecord::InvalidForeignKey: Mysql2::Error: Cannot add or update a child row: a foreign key constraint fails (`emoji_diary_development`.`diaries`, CONSTRAINT `fk_rails_f03fd03c63` FOREIGN KEY (`user_id`) REFERENCES `users` (`id`))
/Users/SAYO/workspace/portfolio/emoji_diary/emoji_diary/bin/rails:5:in `<top (required)>'
/Users/SAYO/workspace/portfolio/emoji_diary/emoji_diary/bin/spring:10:in `block in <top (required)>'
/Users/SAYO/workspace/portfolio/emoji_diary/emoji_diary/bin/spring:7:in `tap'
/Users/SAYO/workspace/portfolio/emoji_diary/emoji_diary/bin/spring:7:in `<top (required)>'

Caused by:
Mysql2::Error: Cannot add or update a child row: a foreign key constraint fails (`emoji_diary_development`.`diaries`, CONSTRAINT `fk_rails_f03fd03c63` FOREIGN KEY (`user_id`) REFERENCES `users` (`id`))
/Users/SAYO/workspace/portfolio/emoji_diary/emoji_diary/bin/rails:5:in `<top (required)>'
/Users/SAYO/workspace/portfolio/emoji_diary/emoji_diary/bin/spring:10:in `block in <top (required)>'
/Users/SAYO/workspace/portfolio/emoji_diary/emoji_diary/bin/spring:7:in `tap'
/Users/SAYO/workspace/portfolio/emoji_diary/emoji_diary/bin/spring:7:in `<top (required)>'
Tasks: TOP => db:seed_fu
(See full trace by running task with --trace)

日記のデータを作るときに失敗しているのかな?と思ったので、user_idの指定の仕方を少し変えて再度試してみました。

Diary.seed do |s|
  s.id = 20
  s.feeling = '😆'
  s.body = '⛱🐠'
  s.start_time  = Date.today
  s.user_id = User.find(10)
end

またエラーです。NotNullViolationuser_idがnullになってしまっているらしい。。よくみてみるとuserのデータが全て入ってしまっています。これがエラーになる原因ですね。

$ rails db:seed_fu RAILS_ENV=development

== Seed from /Users/SAYO/workspace/portfolio/emoji_diary/emoji_diary/db/fixtures/development/diaries.rb
 - Diary {:id=>1, :feeling=>"😆", :body=>"⛱🐠", :start_time=>Tue, 01 Jun 2021, :user_id=>#<User id: 1, nickname: "sayo", name: "kimura34", crypted_password: "$2a$10$uIOXd191ACl0YkrVBwDvweTWj7T8XFkm4IpvMkkzGvb...", salt: "xkBkgsz_i9KxjRjyG2-X", created_at: "2021-05-22 17:06:12.899598000 +0900", updated_at: "2021-05-22 17:06:12.899598000 +0900">}
rails aborted!
ActiveRecord::NotNullViolation: Mysql2::Error: Field 'user_id' doesn't have a default value
/Users/SAYO/workspace/portfolio/emoji_diary/emoji_diary/bin/rails:5:in `<top (required)>'
/Users/SAYO/workspace/portfolio/emoji_diary/emoji_diary/bin/spring:10:in `block in <top (required)>'
/Users/SAYO/workspace/portfolio/emoji_diary/emoji_diary/bin/spring:7:in `tap'
/Users/SAYO/workspace/portfolio/emoji_diary/emoji_diary/bin/spring:7:in `<top (required)>'

Caused by:
Mysql2::Error: Field 'user_id' doesn't have a default value
/Users/SAYO/workspace/portfolio/emoji_diary/emoji_diary/bin/rails:5:in `<top (required)>'
/Users/SAYO/workspace/portfolio/emoji_diary/emoji_diary/bin/spring:10:in `block in <top (required)>'
/Users/SAYO/workspace/portfolio/emoji_diary/emoji_diary/bin/spring:7:in `tap'
/Users/SAYO/workspace/portfolio/emoji_diary/emoji_diary/bin/spring:7:in `<top (required)>'
Tasks: TOP => db:seed_fu
(See full trace by running task with --trace)

調べてみると、Userのデータが入る前に、DiaryがUserを参照しようとしてしまっているからみたいです。なのでシードデータの順番を制御することで解決するようです。seedのファイル名を下記のようにしました。

db/fixtures/development/01_users.rb
db/fixtures/development/02_diaries.rb

データを作成することができました。

$ rails db:seed_fu RAILS_ENV=development

== Seed from /Users/SAYO/workspace/portfolio/emoji_diary/emoji_diary/db/fixtures/development/01_users.rb
 - User {:id=>1, :nickname=>"sayo", :name=>"kimsayo", :password=>"password"}
 - User {:id=>2, :nickname=>"eri", :name=>"eri1112", :password=>"password"}
 - User {:id=>3, :nickname=>"yuka", :name=>"yuka911", :password=>"password"}
 - User {:id=>4, :nickname=>"ayaka", :name=>"110aya", :password=>"password"}
 - User {:id=>5, :nickname=>"asa", :name=>"asako", :password=>"password"}
 - User {:id=>6, :nickname=>"yuki", :name=>"bu8yuki", :password=>"password"}
 - User {:id=>7, :nickname=>"rina", :name=>"rina93", :password=>"password"}
 - User {:id=>8, :nickname=>"yuna", :name=>"yunana", :password=>"password"}
 - User {:id=>9, :nickname=>"nami", :name=>"namihe", :password=>"password"}
 - User {:id=>10, :nickname=>"mari", :name=>"maririn", :password=>"password"}

== Seed from /Users/SAYO/workspace/portfolio/emoji_diary/emoji_diary/db/fixtures/development/02_diaries.rb
 - Diary {:id=>1, :feeling=>"😆", :body=>"⛱🐠", :start_time=>Tue, 01 Jun 2021, :user_id=>1}
 - Diary {:id=>2, :feeling=>"😆", :body=>"⛱🐠", :start_time=>Tue, 01 Jun 2021, :user_id=>2}
 - Diary {:id=>3, :feeling=>"😆", :body=>"⛱🐠", :start_time=>Tue, 01 Jun 2021, :user_id=>3}
 - Diary {:id=>4, :feeling=>"😆", :body=>"⛱🐠", :start_time=>Tue, 01 Jun 2021, :user_id=>4}
 - Diary {:id=>5, :feeling=>"😆", :body=>"⛱🐠", :start_time=>Tue, 01 Jun 2021, :user_id=>5}
 - Diary {:id=>6, :feeling=>"😆", :body=>"⛱🐠", :start_time=>Tue, 01 Jun 2021, :user_id=>6}
 - Diary {:id=>7, :feeling=>"😆", :body=>"⛱🐠", :start_time=>Tue, 01 Jun 2021, :user_id=>7}
 - Diary {:id=>8, :feeling=>"😆", :body=>"⛱🐠", :start_time=>Tue, 01 Jun 2021, :user_id=>8}
 - Diary {:id=>9, :feeling=>"😆", :body=>"⛱🐠", :start_time=>Tue, 01 Jun 2021, :user_id=>9}
 - Diary {:id=>10, :feeling=>"😆", :body=>"⛱🐠", :start_time=>Tue, 01 Jun 2021, :user_id=>10}
 - Diary {:id=>11, :feeling=>"😆", :body=>"⛱🐠", :start_time=>Wed, 02 Jun 2021, :user_id=>1}
 - Diary {:id=>12, :feeling=>"😆", :body=>"⛱🐠", :start_time=>Wed, 02 Jun 2021, :user_id=>2}
 - Diary {:id=>13, :feeling=>"😆", :body=>"⛱🐠", :start_time=>Wed, 02 Jun 2021, :user_id=>3}
 - Diary {:id=>14, :feeling=>"😆", :body=>"⛱🐠", :start_time=>Wed, 02 Jun 2021, :user_id=>4}
 - Diary {:id=>15, :feeling=>"😆", :body=>"⛱🐠", :start_time=>Wed, 02 Jun 2021, :user_id=>5}
 - Diary {:id=>16, :feeling=>"😆", :body=>"⛱🐠", :start_time=>Wed, 02 Jun 2021, :user_id=>6}
 - Diary {:id=>17, :feeling=>"😆", :body=>"⛱🐠", :start_time=>Wed, 02 Jun 2021, :user_id=>7}
 - Diary {:id=>18, :feeling=>"😆", :body=>"⛱🐠", :start_time=>Wed, 02 Jun 2021, :user_id=>8}
 - Diary {:id=>19, :feeling=>"😆", :body=>"⛱🐠", :start_time=>Wed, 02 Jun 2021, :user_id=>9}
 - Diary {:id=>20, :feeling=>"😆", :body=>"⛱🐠", :start_time=>Wed, 02 Jun 2021, :user_id=>10}

作成されたユーザーの情報に問題がありました。user_idが1と2のユーザーのpasswordのデータはちゃんと入っているのですが、user_idが3以降のユーザーのパスワードがnilになっていました。

irb(main):001:0> User.all
  User Load (51.0ms)  SELECT `users`.* FROM `users` /* loading for inspect */ LIMIT 11
=> #<ActiveRecord::Relation [#<User id: 1, nickname: "sayo", name: "kimsayo", crypted_password: "$2a$10$uIOXd191ACl0YkrVBwDvweTWj7T8XFkm4IpvMkkzGvb...", salt: "xkBkgsz_i9KxjRjyG2-X", created_at: "2021-05-22 17:06:12.899598000 +0900", updated_at: "2021-06-02 17:56:01.179817000 +0900">, 
#<User id: 2, nickname: "eri", name: "eri1112", crypted_password: "$2a$10$ebAXC5bqM0y8ZA9F/PIuF.2C6n5dSCbG6WUaMduqGQc...", salt: "SbmP96di6H6L9u8n7hyD", created_at: "2021-05-22 17:15:10.416046000 +0900", updated_at: "2021-05-22 17:15:10.416046000 +0900">, 
#<User id: 3, nickname: "yuka", name: "yuka911", crypted_password: nil, salt: nil, created_at: "2021-06-02 17:56:01.234058000 +0900", updated_at: "2021-06-02 17:56:01.234058000 +0900">, 
#<User id: 4, nickname: "ayaka", name: "110aya", crypted_password: nil, salt: nil, created_at: "2021-06-02 17:56:01.241600000 +0900", updated_at: "2021-06-02 17:56:01.241600000 +0900">, 
#<User id: 5, nickname: "asa", name: "asako", crypted_password: nil, salt: nil, created_at: "2021-06-02 17:56:01.245346000 +0900", updated_at: "2021-06-02 17:56:01.245346000 +0900">, 
#<User id: 6, nickname: "yuki", name: "bu8yuki", crypted_password: nil, salt: nil, created_at: "2021-06-02 17:56:01.249030000 +0900", updated_at: "2021-06-02 17:56:01.249030000 +0900">, 
#<User id: 7, nickname: "rina", name: "rina93", crypted_password: nil, salt: nil, created_at: "2021-06-02 17:56:01.255090000 +0900", updated_at: "2021-06-02 17:56:01.255090000 +0900">, 
#<User id: 8, nickname: "yuna", name: "yunana", crypted_password: nil, salt: nil, created_at: "2021-06-02 17:56:01.261832000 +0900", updated_at: "2021-06-02 17:56:01.261832000 +0900">, 
#<User id: 9, nickname: "nami", name: "namihe", crypted_password: nil, salt: nil, created_at: "2021-06-02 17:56:01.264915000 +0900", updated_at: "2021-06-02 17:56:01.264915000 +0900">, 
#<User id: 10, nickname: "mari", name: "maririn", crypted_password: nil, salt: nil, created_at: "2021-06-02 17:56:01.270509000 +0900", updated_at: "2021-06-02 17:56:01.270509000 +0900">]>

seedファイルの書き方を今一度確認してみます。

User.seed do |s|
  s.id = 1
  s.nickname = 'sayo'
  s.name = 'kimsayo'
  s.password  = "password"
end
User.seed do |s|
  s.id = 2
  s.nickname = 'eri'
  s.name = 'eri1112'
  s.password  = "password"
end
User.seed do |s|
  s.id = 3
  s.nickname = 'yuka'
  s.name = 'yuka911'
  s.password  = "password"
end
User.seed do |s|
  s.id = 4
  s.nickname = 'ayaka'
  s.name = '110aya'
  s.password  = "password"
end
User.seed do |s|
  s.id = 5
  s.nickname = 'asa'
  s.name = 'asako'
  s.password  = "password"
end

passwordとプラスでpassword_cofirmaitonを追加してみようと思います。

User.seed do |s|
  s.id = 1
  s.nickname = 'sayo'
  s.name = 'kimsayo'
  s.password  = "password"
  s.password_confirmation = "password"
end
User.seed do |s|
  s.id = 2
  s.nickname = 'eri'
  s.name = 'eri1112'
  s.password  = "password"
  s.password_confirmation = "password"
end
User.seed do |s|
  s.id = 3
  s.nickname = 'yuka'
  s.name = 'yuka911'
  s.password  = "password"
  s.password_confirmation = "password"
end
User.seed do |s|
  s.id = 4
  s.nickname = 'ayaka'
  s.name = '110aya'
  s.password  = "password"
  s.password_confirmation = "password"
end
User.seed do |s|
  s.id = 5
  s.nickname = 'asa'
  s.name = 'asako'
  s.password  = "password"
  s.password_confirmation = "password"
end

rails consoleで調べてみます。

irb(main):001:0> User.all
  User Load (6.9ms)  SELECT `users`.* FROM `users` /* loading for inspect */ LIMIT 11
=> #<ActiveRecord::Relation [#<User id: 1, nickname: "sayo", name: "kimsayo", crypted_password: "password", salt: "password", created_at: "2021-05-22 17:06:12.899598000 +0900", updated_at: "2021-06-05 11:00:21.640283000 +0900">, 
#<User id: 2, nickname: "eri", name: "eri1112", crypted_password: "password", salt: "password", created_at: "2021-05-22 17:15:10.416046000 +0900", updated_at: "2021-06-05 11:00:21.654480000 +0900">, 
#<User id: 3, nickname: "yuka", name: "yuka911", crypted_password: "password", salt: "password", created_at: "2021-06-02 17:56:01.234058000 +0900", updated_at: "2021-06-05 11:00:21.659504000 +0900">, 
#<User id: 4, nickname: "ayaka", name: "110aya", crypted_password: "password", salt: "password", created_at: "2021-06-02 17:56:01.241600000 +0900", updated_at: "2021-06-05 11:00:21.662597000 +0900">, 
#<User id: 5, nickname: "asa", name: "asako", crypted_password: "password", salt: "password", created_at: "2021-06-02 17:56:01.245346000 +0900", updated_at: "2021-06-05 11:00:21.665946000 +0900">, 
#<User id: 6, nickname: "yuki", name: "bu8yuki", crypted_password: "password", salt: "password", created_at: "2021-06-02 17:56:01.249030000 +0900", updated_at: "2021-06-05 11:00:21.670000000 +0900">, 
#<User id: 7, nickname: "rina", name: "rina93", crypted_password: "password", salt: "password", created_at: "2021-06-02 17:56:01.255090000 +0900", updated_at: "2021-06-05 11:00:21.673932000 +0900">, 
#<User id: 8, nickname: "yuna", name: "yunana", crypted_password: "password", salt: "password", created_at: "2021-06-02 17:56:01.261832000 +0900", updated_at: "2021-06-05 11:00:21.678020000 +0900">, 
#<User id: 9, nickname: "nami", name: "namihe", crypted_password: "password", salt: "password", created_at: "2021-06-02 17:56:01.264915000 +0900", updated_at: "2021-06-05 11:00:21.687091000 +0900">, 
#<User id: 10, nickname: "mari", name: "maririn", crypted_password: "password", salt: "password", created_at: "2021-06-02 17:56:01.270509000 +0900", updated_at: "2021-06-05 11:00:21.692096000 +0900">, ...]>

passwordは入っているように見えるのですが、全然ログインができません。。暗号化されていないからかな?と思ったので、パスワードの書き方を少し変えてみました。

参考 : https://qiita.com/reflet/items/eeced34f9c5c2a9fbaf6#暗号化そのものを行う関数

User.seed(
  :id,
  { id: 1, nickname: 'sayo', name: 'kimsayo', crypted_password: User.encrypt('password') },
  { id: 2, nickname: 'eri', name: 'eri1112', crypted_password: User.encrypt('password') },
  { id: 3, nickname: 'yuka', name: 'yuka911', crypted_password: User.encrypt('password') },
  { id: 4, nickname: 'miki', name: 'mickey', crypted_password: User.encrypt('password') },
  { id: 5, nickname: 'yuki', name: 'yuki8', crypted_password: User.encrypt('password') },
  { id: 6, nickname: 'ayaka', name: 'ayaka110', crypted_password: User.encrypt('password') },
  { id: 7, nickname: 'asa', name: 'asako', crypted_password: User.encrypt('password') },
  { id: 8, nickname: 'mari', name: 'mari7', crypted_password: User.encrypt('password') },
  { id: 9, nickname: 'yuna', name: 'yuna61', crypted_password: User.encrypt('password') },
  { id: 10, nickname: 'hina', name: 'hina4', crypted_password: User.encrypt('password') },
)

rails consoleで確かめてみるとsaltは未だ入っていなかったのですが、ログインできる情報は入っているのでOKです。

irb(main):001:0> User.all
  User Load (2.4ms)  SELECT `users`.* FROM `users` /* loading for inspect */ LIMIT 11
=> #<ActiveRecord::Relation [#<User id: 1, nickname: "sayo", name: "kimsayo", crypted_password: "$2a$10$xIWJ0vGGQ3TKMrFo52XNj.4an2CEobjzH6nA4crF/Sf...", salt: nil, created_at: "2021-06-05 12:16:43.581614000 +0900", updated_at: "2021-06-05 12:16:43.581614000 +0900">, 
#<User id: 2, nickname: "eri", name: "eri1112", crypted_password: "$2a$10$1Ojfr/S1IddywFPlkk9oNueWv81/LUA7z8Kf0ZWscu9...", salt: nil, created_at: "2021-06-05 12:16:43.587105000 +0900", updated_at: "2021-06-05 12:16:43.587105000 +0900">, 
#<User id: 3, nickname: "yuka", name: "yuka911", crypted_password: "$2a$10$uJtj9DG4283XqTOUObhMVOy3ee2t0dR.Cab/mUoRQLv...", salt: nil, created_at: "2021-06-05 12:16:43.592558000 +0900", updated_at: "2021-06-05 12:16:43.592558000 +0900">, 
#<User id: 4, nickname: "miki", name: "mickey", crypted_password: "$2a$10$p.aV4X8F6Xmn/RG/0wL5hO1cw.6rgs2qEbcU7v6EQSk...", salt: nil, created_at: "2021-06-05 12:16:43.595620000 +0900", updated_at: "2021-06-05 12:16:43.595620000 +0900">, 
#<User id: 5, nickname: "yuki", name: "yuki8", crypted_password: "$2a$10$MijtD1ZqwjdCGTyLuTuB7efHQFTBndffBhKUHsnKDPu...", salt: nil, created_at: "2021-06-05 12:16:43.598516000 +0900", updated_at: "2021-06-05 12:16:43.598516000 +0900">, 
#<User id: 6, nickname: "ayaka", name: "ayaka110", crypted_password: "$2a$10$1Pe2Uj.7DURX4HS11tL9.uUK3UZ7IGxWn.pSO47A5OZ...", salt: nil, created_at: "2021-06-05 12:16:43.601421000 +0900", updated_at: "2021-06-05 12:16:43.601421000 +0900">, 
#<User id: 7, nickname: "asa", name: "asako", crypted_password: "$2a$10$M/nYjlWnvo6ur7uhLVV4g.9uFJgqtII0GLK5l.UzS2a...", salt: nil, created_at: "2021-06-05 12:16:43.604262000 +0900", updated_at: "2021-06-05 12:16:43.604262000 +0900">, 
#<User id: 8, nickname: "mari", name: "mari7", crypted_password: "$2a$10$qIkdzXxvvAXtbsxPoRQgUO346iEwLX.Ga3s5pRB/Lyz...", salt: nil, created_at: "2021-06-05 12:16:43.606981000 +0900", updated_at: "2021-06-05 12:16:43.606981000 +0900">, 
#<User id: 9, nickname: "yuna", name: "yuna61", crypted_password: "$2a$10$5KTdmiWhHQaoFGh4nCkF1OW5ctaj5meqV1osK9UONPk...", salt: nil, created_at: "2021-06-05 12:16:43.610756000 +0900", updated_at: "2021-06-05 12:16:43.610756000 +0900">, 
#<User id: 10, nickname: "hina", name: "hina4", crypted_password: "$2a$10$.QoTsvb0tJSH7dQ5ZNBSOeqnEqCUclTt0qd3OviLNXq...", salt: nil, created_at: "2021-06-05 12:16:43.614056000 +0900", updated_at: "2021-06-05 12:16:43.614056000 +0900">, ...]>

フォロー機能を実装する

ポートフォリオで使用するために一度試しに実装してみたいと思います。事前にユーザー登録、ログインはsorceryで実装済みです。

参考にしたサイト Railsでユーザーフォロー機能を実装する(Ajax使うよ)① - Qiita

テーブル設計

ユーザーフォロー機能の実装のために用意したテーブル設計は下記のようになります。本当に分かりづらいのですが、RelationshipsテーブルがUsersテーブル(フォローするユーザー)とUsersテーブル(フォローされるユーザー)の中間テーブルとしての役割を担っています。

f:id:kimura34:20210601225801p:plain

Relationshipモデルを作成する

まずはRealtionshipモデルを作成します。

$ bundle exec rails g model Relationship
Running via Spring preloader in process 1766
      invoke  active_record
      create    db/migrate/20210531014628_create_relationships.rb
      create    app/models/relationship.rb

外部キーのuser_idカラムとfollower_idカラムを作成します。ここでuser_idはフォローする人、follower_idはフォローされる人を指します。

follower_idの参照先のテーブルはUsersテーブルにしてあげたいので、foreign_key: {to_table: :users}としてあげてます。この設定をしないと存在しないfollowerテーブルを参照しようとします。この設定をすることで、followerを探すときはUsersテーブルのfollower_idをみるようになります。

class CreateRelationships < ActiveRecord::Migration[6.1]
  def change
    create_table :relationships do |t|
      t.references :user
      t.references :follower, foreign_key: { to_table: :users }

      t.timestamps

      t.index [:user_id, :follower_id], unique: true
    end
  end
end

外部キー制約をつけたので、インデックスは自動で付与されるので、add_index :relationships, :follower_idという記載は不要です。

t.index [:user_id, :follower_id], unique: trueこの記載は、unique制約で同じ人を2回フォローできないようにするために必要です。

マイグレーションファイルの修正が終わったので、テーブルを作成します。

$ rails db:migrate
== 20210531014628 CreateRelationships: migrating ==============================
-- create_table(:relationships)
   -> 0.1209s
== 20210531014628 CreateRelationships: migrated (0.1210s) =====================

次からはフォローする、フォローされるの2つの状況を1つずつ分けて理解していきたいと思います。まず最初はユーザーが他の人をフォローするという状況を説明していきます。

フォローする

Userモデルのアソシエーションの記述は下記のようになります。

class User < ApplicationRecord
  has_many :relationships, dependent: :destroy
  has_many :followings, through: :relationships, source: :follower
end

一行目は、UserはたくさんのRelationshipsを持っています。userが消去されたら結びつくrelationshipのデータを消去するという意味です。

二行目のfollowingsは「フォローしている人を取得するため」のアソシエーションの別名としてつけました。followingモデルは存在しないので、参照するモデルとしてRelationshipを指定しています。この設定をすることで、user.followingsと打つだけで、userが中間テーブルrelationshipsを取得し、 relationshipfollower_idからフォローしている人を取得しています。source: :followerというオプションをつけて関連先のモデル名としてfollowerを参照するようにします。しかしfollowerテーブルは作っていないので、どうなるのかというと、、次のRelationshipモデルのアソシエーションで定義しています。

Relationshipモデルのアソシエーションの記載は下記のようになります。

class Relationship < ApplicationRecord
  belongs_to :follower, class_name: 'User'
end

class_name: 'User'と補足設定することで、Followerクラスという存在しないクラスを参照することを防ぎ、Userクラスであることを明示しています。

実際にユーザーを作成してフォローすることができているか確認します。まず最初にユーザーデータを3つ作成しました。

irb(main):002:0> user = User.new(email: 'sayo@example.com', password: 'password', password_confirmation: 'password')
=> #<User id: nil, email: "sayo@example.com", crypted_password: nil, salt: nil, created_at: nil, updated_at: nil>
irb(main):003:0> user.save
  TRANSACTION (0.2ms)  BEGIN
  User Exists? (0.3ms)  SELECT 1 AS one FROM `users` WHERE `users`.`email` = 'sayo@example.com' LIMIT 1
  User Create (87.0ms)  INSERT INTO `users` (`email`, `crypted_password`, `salt`, `created_at`, `updated_at`) VALUES ('sayo@example.com', '$2a$10$tXrj8J450XST3ON.PlaGdetehohD1BQj92d0SctEZ6wfO5BfLOJ2u', 'hrxhPytqWsU4N__UzSZQ', '2021-05-31 02:00:17.356071', '2021-05-31 02:00:17.356071')
  TRANSACTION (2.1ms)  COMMIT
=> true
irb(main):006:0>  user = User.new(email: 'eri@example.com', password: 'password', password_confirmation: 'password')
=> #<User id: nil, email: "eri@example.com", crypted_password: nil, salt: nil, created_at: nil, updated_at: nil>
irb(main):007:0> user.save
  TRANSACTION (2.4ms)  BEGIN
  User Exists? (0.8ms)  SELECT 1 AS one FROM `users` WHERE `users`.`email` = 'eri@example.com' LIMIT 1
  User Create (41.4ms)  INSERT INTO `users` (`email`, `crypted_password`, `salt`, `created_at`, `updated_at`) VALUES ('eri@example.com', '$2a$10$tZCz10GG/8h.8TJKdaOyjO2P2XxsQwGh9jtrh4yj6RApLHO1J1Exa', 'MdzTjVF715m3aVxN39tN', '2021-05-31 02:01:46.998133', '2021-05-31 02:01:46.998133')
  TRANSACTION (0.8ms)  COMMIT
=> true
irb(main):008:0>  user = User.new(email: 'yuka@example.com', password: 'password', password_confirmation: 'password')
=> #<User id: nil, email: "yuka@example.com", crypted_password: nil, salt: nil, created_at: nil, updated_at: nil>
irb(main):009:0> user.save
  TRANSACTION (0.2ms)  BEGIN
  User Exists? (0.5ms)  SELECT 1 AS one FROM `users` WHERE `users`.`email` = 'yuka@example.com' LIMIT 1
  User Create (40.3ms)  INSERT INTO `users` (`email`, `crypted_password`, `salt`, `created_at`, `updated_at`) VALUES ('yuka@example.com', '$2a$10$dJsYPcoFqBl3Q95JUxJNR.cQu20uwxiBrjkUsdIqBhr2EV85PhDTy', 'tbwU-CfEuFXmxJXKNyc5', '2021-05-31 02:02:11.111278', '2021-05-31 02:02:11.111278')
  TRANSACTION (0.4ms)  COMMIT
=> true

次に作成したユーザー(id: 3)が(id: 2)のユーザーをフォローします。

irb(main):010:0> user.relationships.create!(follower_id: 2)
  TRANSACTION (0.2ms)  BEGIN
  User Load (0.4ms)  SELECT `users`.* FROM `users` WHERE `users`.`id` = 2 LIMIT 1
  Relationship Create (65.1ms)  INSERT INTO `relationships` (`user_id`, `follower_id`, `created_at`, `updated_at`) VALUES (3, 2, '2021-05-31 02:02:21.616116', '2021-05-31 02:02:21.616116')
  TRANSACTION (41.8ms)  COMMIT
=> #<Relationship id: 1, user_id: 3, follower_id: 2, created_at: "2021-05-31 02:02:21.616116000 +0000", updated_at: "2021-05-31 02:02:21.616116000 +0000">
irb(main):011:0> user.followings
  User Load (40.6ms)  SELECT `users`.* FROM `users` INNER JOIN `relationships` ON `users`.`id` = `relationships`.`follower_id` WHERE `relationships`.`user_id` = 3 /* loading for inspect */ LIMIT 11
=> #<ActiveRecord::Associations::CollectionProxy [#<User id: 2, email: "eri@example.com", crypted_password: "$2a$10$tZCz10GG/8h.8TJKdaOyjO2P2XxsQwGh9jtrh4yj6RA...", salt: "MdzTjVF715m3aVxN39tN", created_at: "2021-05-31 02:01:46.998133000 +0000", updated_at: "2021-05-31 02:01:46.998133000 +0000">]>

これでフォローするというアソシエーションが機能していることがわかりました。

フォローされている

次は、あるユーザーがたくさんの人にフォローされているという関係を作成します。

Userモデルのアソシエーションの記述は下記のようになります。

class User < ApplicationRecord
  has_many :passive_relationships, class_name: 'Relationship', foreign_key: 'follower_id', dependent: :destroy
  has_many :followers, through: :passive_relationships, source: :user
end

一行目は、has_many :relationshipsでいいかな?と思ったのですが、ユーザーをフォローするという関係性でrelationshipモデルを参照してしまっていてuser.relationshipsのように使うことができなくなってしまうので、別名としてpassive_relationshipsと名付けました。class_name: 'Relationship'Relationshipモデルを参照するように指定しています。foreign_key: 'follower_id'relaitonshipsテーブルにアクセスする時、follower_idを元にアクセスしてくださいということを意味しています。

二行目は、Userはたくさんのfollowerspassive_relationshipsを通じて持っています。passive_relationshipsは、直前の定義によりRelationshipクラスを参照するようになっています。

Relationshipモデルのアソシエーションの記載は下記のようになります。

class Relationship < ApplicationRecord
    belongs_to :user
end

コンソールでid:3のユーザーがid:2のユーザーをフォローするというデータは作ったので、id:2のユーザーは、id:3のユーザーにフォローされているはずです。

irb(main):001:0> user2 = User.second
  User Load (1.9ms)  SELECT `users`.* FROM `users` ORDER BY `users`.`id` ASC LIMIT 1 OFFSET 1
=> #<User id: 2, email: "eri@example.com", crypted_password: "$2a$10$tZCz10GG/8h.8TJKdaOyjO2P2XxsQwGh9jtrh4yj6RA...", salt: "MdzTjVF715m3aVxN39tN", created_at: "2021-05-31 02:01:46.998133000 +0000", updated_at: "2021-05-31 02:01:46.998133000 +0000">
irb(main):002:0> user2.followers
  User Load (42.8ms)  SELECT `users`.* FROM `users` INNER JOIN `relationships` ON `users`.`id` = `relationships`.`user_id` WHERE `relationships`.`follower_id` = 2 /* loading for inspect */ LIMIT 11
=> #<ActiveRecord::Associations::CollectionProxy [#<User id: 3, email: "yuka@example.com", crypted_password: "$2a$10$dJsYPcoFqBl3Q95JUxJNR.cQu20uwxiBrjkUsdIqBhr...", salt: "tbwU-CfEuFXmxJXKNyc5", created_at: "2021-05-31 02:02:11.111278000 +0000", updated_at: "2021-05-31 02:02:11.111278000 +0000">]>

最終的なUserモデルとRelationshipモデルのアソシエーションを下記にまとめておきます。

class User < ApplicationRecord
  has_many :relationships, dependent: :destroy
  has_many :followings, through: :relationships, source: :follower
  has_many :passive_relationships, class_name: 'Relationship', foreign_key: 'follower_id', dependent: :destroy
  has_many :followers, through: :passive_relationships, source: :user
end
class Relationship < ApplicationRecord
  belongs_to :user
  belongs_to :follower, class_name: 'User'
end

relationshipsコントローラを作成する

次にフォロー機能を定義するrelationshipsコントローラを作成します。

$ rails g controller relationships
Running via Spring preloader in process 2378
      create  app/controllers/relationships_controller.rb
      invoke  erb
      create    app/views/relationships
      invoke  helper
      create    app/helpers/relationships_helper.rb
      invoke  assets
      invoke    scss
      create      app/assets/stylesheets/relationships.scss

作成したコントローラを実装していきます。

class RelationshipsController < ApplicationController
  def create
    @other_user = User.find(params[:follower])
    current_user.follow(@other_user)
    redirect_back fallback_location: root_path
  end

  def destroy
    @user = current_user.relationships.find(params[:id]).follower
    current_user.unfollow(params[:id])
    redirect_back fallback_location: root_path
  end
end

createアクションはフォローするという処理、destroyアクションはフォローを解除するという処理です。ここで出てきたfollowunfollowメソッドはモデルに定義します。

Userモデルにフォローする、フォローしているか確認する、フォロー解除するというメソッドをUserモデルに作成しました。

def follow(other_user)
  return if self == other_user
  relationships.find_or_create_by!(follower: other_user)
end

def following?(user)
  followings.include?(user)
end

def unfollow(relathinoship_id)
  relationships.find(relathinoship_id).destroy!
end

return if self == other_userで、フォローしようとしているother_userが自分自身ではないかを検証しています。

ルーティングとビュー

フォロー機能のルーティングとユーザー一覧機能のルーティングの追加もしておきます。

Rails.application.routes.draw do
    resources :users, only: [:new, :create, :index]
    resources :relationships, only: [:create, :destroy]
end

ルーティングを追加したのでusersコントローラにindexアクションも追加しました。

class UsersController < ApplicationController
  # ユーザー一覧
  def index
    @users = User.all
  end
end

ユーザー一覧のビュー(index.html.erb)も準備します。

$ touch app/views/users/index.html.erb

フォローボタン、解除ボタンをそれぞれパーシャルに切り出して、管理しやすいようにしておきたいので、それぞれのファイルを作成します。

$ touch app/views/relationships/_follow_button.html.erb
$ touch app/views/relationships/_unfollow_button.html.erb
relationships/_follow_button.html.erb

<%= button_to 'フォロー', relationships_path(follower: user), method: :post %>
relationships/_unfollow_button.html.erb

<%= button_to 'フォロー解除', relationship_path(current_user.relationships.find_by(follower: user)), method: :delete %>

上記で作ったテンプレートを埋め込んでユーザー一覧画面を作成します。

<p id="notice"><%= notice %></p>

<h1>Users</h1>

<table>
  <thead>
    <tr>
      <th>Email</th>
      <th colspan="3"></th>
    </tr>
  </thead>

  <tbody>
    <% @users.each do |user| %>
      <tr>
        <td><%= user.email %></td>
        <td>
        <% if logged_in? && current_user != user %>
          <div class="form-group">
            <% if current_user.following?(user) %>
              <%= render 'relationships/unfollow_button', user: user %>
            <% else %>
              <%= render 'relationships/follow_button', user: user %>
            <% end %>
          </div>
        <% end %>
        </td>
      </tr>
    <% end %>
  </tbody>
</table>

<br>

<%= link_to 'New User', new_user_path %>

ここまでの設定でログイン中のユーザーにはボタンが出ず、そのほかのユーザーにはボタンが出るようになって、また「フォロー」ボタンを押して画面をリロード後「フォロー解除」ボタンが現れるようになりました。

f:id:kimura34:20210601230139p:plain

この時の処理をメモとして残しておきます。フォローした時の状況です。

Started POST "/relationships?follower=2" for ::1 at 2021-06-01 11:32:08 +0900
Processing by RelationshipsController#create as HTML
  Parameters: {"authenticity_token"=>"[FILTERED]", "follower"=>"2"}
  User Load (12.4ms)  SELECT `users`.* FROM `users` WHERE `users`.`id` = 2 LIMIT 1
  ↳ app/controllers/relationships_controller.rb:3:in `create'
  User Load (43.7ms)  SELECT `users`.* FROM `users` WHERE `users`.`id` = 1 LIMIT 1
  ↳ app/controllers/relationships_controller.rb:4:in `create'
  Relationship Load (93.0ms)  SELECT `relationships`.* FROM `relationships` WHERE `relationships`.`user_id` = 1 AND `relationships`.`follower_id` = 2 LIMIT 1
  ↳ app/models/user.rb:22:in `follow'
  TRANSACTION (13.2ms)  BEGIN
  ↳ app/models/user.rb:22:in `follow'
  Relationship Create (90.3ms)  INSERT INTO `relationships` (`user_id`, `follower_id`, `created_at`, `updated_at`) VALUES (1, 2, '2021-06-01 02:32:09.536051', '2021-06-01 02:32:09.536051')
  ↳ app/models/user.rb:22:in `follow'
  TRANSACTION (30.1ms)  COMMIT
  ↳ app/models/user.rb:22:in `follow'
Redirected to http://localhost:3000/users
Completed 302 Found in 743ms (ActiveRecord: 325.5ms | Allocations: 5074)

Started GET "/users" for ::1 at 2021-06-01 11:32:09 +0900
Processing by UsersController#index as HTML
  Rendering layout layouts/application.html.erb
  Rendering users/index.html.erb within layouts/application
  User Load (100.0ms)  SELECT `users`.* FROM `users`
  ↳ app/views/users/index.html.erb:14
  User Load (2.7ms)  SELECT `users`.* FROM `users` WHERE `users`.`id` = 1 LIMIT 1
  ↳ app/views/users/index.html.erb:18
  User Exists? (64.6ms)  SELECT 1 AS one FROM `users` INNER JOIN `relationships` ON `users`.`id` = `relationships`.`follower_id` WHERE `relationships`.`user_id` = 1 AND `users`.`id` = 2 LIMIT 1
  ↳ app/models/user.rb:26:in `following?'
  Relationship Load (1.6ms)  SELECT `relationships`.* FROM `relationships` WHERE `relationships`.`user_id` = 1 AND `relationships`.`follower_id` = 2 LIMIT 1
  ↳ app/views/users/index.html.erb:23
  User Exists? (1.3ms)  SELECT 1 AS one FROM `users` INNER JOIN `relationships` ON `users`.`id` = `relationships`.`follower_id` WHERE `relationships`.`user_id` = 1 AND `users`.`id` = 3 LIMIT 1
  ↳ app/models/user.rb:26:in `following?'
  Relationship Load (2.0ms)  SELECT `relationships`.* FROM `relationships` WHERE `relationships`.`user_id` = 1 AND `relationships`.`follower_id` = 3 LIMIT 1
  ↳ app/views/users/index.html.erb:23
  Rendered users/index.html.erb within layouts/application (Duration: 211.0ms | Allocations: 5505)
[Webpacker] Everything's up-to-date. Nothing to do
  Rendered layout layouts/application.html.erb (Duration: 268.0ms | Allocations: 10045)
Completed 200 OK in 274ms (Views: 99.5ms | ActiveRecord: 172.1ms | Allocations: 10499)