ログイン後にログインページとサインアップのページに遷移できないようにする
ログインしているユーザーがログイン画面、またユーザー新規登録画面にアクセスできるようになっていたので修正していこうと思います。
どうやって直していくのか少し考えます、、、
- ビューファイルでも、モデルでもなくコントローラをいじらないといけない。
- 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がつかなかったので全てに記入しました。
PFのテーマカラーを選択できるようにする
PFのテーマカラーがデフォルトのピンクとブルー系の2つからユーザーが選択して設定できるようにしたいです。
進めていく手順
- Userテーブルにcolorカラムを追加する
- ユーザー新規登録ページとプロフィール編集ページにフォームを追加
- コントローラのストロングパラメータにcolor追加
- 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のテーマカラーを追加していきます。私が考えた実装の仕方なのでとても冗長的で汚いコードになっていると思うし、こんなこと絶対にしない!って思われるかもしれないです。でも一応思ったように動いたので、これで良しとしました。
やったこと
まず最初に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については以前ブログで何度かまとめました。日付検索や、プルダウン表示などをまとめていたと思います。
今回はユーザー検索のページと検索結果のページを分けて表示していこうと思います。
まず最初にGemfile
にransack
を追加して、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を使って初期データを作成 - 学習記録
ユーザーテーブルと日記テーブルの初期データを作成していきます。
まず最初にGemfile
にseed_fu
のgem
を追加して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
またエラーです。NotNullViolation
、user_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
テーブル(フォローされるユーザー)の中間テーブルとしての役割を担っています。
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
を取得し、 relationship
のfollower_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
はたくさんのfollowers
をpassive_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
アクションはフォローを解除するという処理です。ここで出てきたfollow
とunfollow
メソッドはモデルに定義します。
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 %>
ここまでの設定でログイン中のユーザーにはボタンが出ず、そのほかのユーザーにはボタンが出るようになって、また「フォロー」ボタンを押して画面をリロード後「フォロー解除」ボタンが現れるようになりました。
この時の処理をメモとして残しておきます。フォローした時の状況です。
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)