- 投稿日:2019-02-09T21:55:02+09:00
Railsでページ遷移時にPay.jpのフォームが表示されない問題の解決方法
Turbolinksを切らないと、Pay.jpのフォームは表示されない
https://pay.jp/docs/checkout の注意事項にも書かれているように、
RailsでPay.jpのチェックアウトを利用しようとすると、Turbolinksに邪魔されてフォームがうまく表示されません。
具体的にいうと、ページ遷移時にチェックアウトのフォームが表示されません。
ページ遷移時にjQueryが発火しない問題自体は、
https://qiita.com/kumagi/items/289ccadf344f32613304
https://hackbaka.hatenablog.com/entry/2018/02/15/175259
あたりを参考にして、
$(document).on('turbolinks:load', function() { });
で解決できるわけですが、
これを使ってページ遷移時にチェックアウトのScriptをappendしても、チェックアウトのフォームは表示されませんでした。
Script自体は追加されているようでしたが、srcがきちんと機能しないせいで表示されないみたいです。つまり、 どうあがいてもTurbolinksが邪魔してページ遷移時にフォームを表示することができなかったので、ページ遷移時にフォームを表示するためには、やはりTrubolinksを切る必要があるようです。
とはいえ、自分の場合はTurbolinksを完全に切るのは嫌だったので、フォームを設置したページのturbolinksだけ切ることで問題を解決しました。
具体的には、画面遷移前のリンクに以下の属性を追加して、リンク先のturbolinksを一時的に切ることで、画面遷移後にもチェックアウトのフォームを表示できました。
リンク先のturbolinksを切る.html.erb<!-- aタグでturbolinksを切る --> <a href="<%= book_url(book) %>" class="link" data-turbolinks="false"></a> <!-- link_toでturbolinksを切る --> <%= link_to @book, data: { turbolinks: false} do %>参考: https://qiita.com/morrr/items/54f4be21032a45fd4fe9
多分、turbolinksを切らずに、htmlにjsを埋め込みたい場合に全般的に役に立つ方法だと思います。
- 投稿日:2019-02-09T21:37:42+09:00
RailsのDBモデルの命名規則をまとめてみた
初めに
初めて複数単語のモデルを作る機会があって悩んだので、モデルの命名規則と生成されるもの一覧をまとめてみました
http://railsdoc.com/model
こちらの情報です。単語一つの場合の命名規則
例としてmodelという名前のモデル
モデル名 mod_el モデルクラス名 Model ファイル名 model.rb テーブル名 models 単語複数の場合の命名規則
例としてmodel schedule
モデル名 model schedule モデルクラス名 ModelSchedule ファイル名 model_schedule.rb テーブル名 model_chedules 単語複数の場合のモデル作成コマンド
モデル作成時のコマンドは下記のどちらでも大丈夫です
rails g model ModelSchedule user_id:integer schedule:text rails g model model_schedule user_id:integer schedule:textただし単語複数の場合も、単語一つの場合もモデル作成時sを付けて複数形にするのはだめです!
rails g model Models user_id:integer schedule:text rails g model ModelSchedules user_id:integer schedule:textmodelは必ず単数形で作成しましょう
- 投稿日:2019-02-09T19:11:31+09:00
Rails における rake タスクの :environment について
rails generate task [namespace] [task_name]
で rake タスクを生成するとき、デフォルトで以下のようなファイルがされます。sample.rakenamespace :sample do desc "TODO" task foo: :environment do end endこの
:environment
の部分なんなんでしょう?なにか環境変数的なのを読み込んでるんでしょうか?rake における Prerequisites
いろいろ調べてみると、そもそも rake には Prerequisites といういわば事前タスクを設定する機能があって、
:environment
はそれに相当するもののようです。
http://docs.seattlerb.org/rake/doc/rakefile_rdoc.html#label-Tasks+with+Prerequisites以下のような rake タスクの場合、まず
bar
が実行されて、その後にfoo
が実行されます。これが Prerequisites の役割です。foo.rake# frozen_string_literal: true namespace :sample do desc 'TODO' task foo: :bar do puts 'This is a main task.' end task :bar do puts 'This is a prerequisite task.' end end$ rake sample:foo This is a prerequisite task. This is a main task.Rails のソースコード内にある
environment
タスクの中身このことから、何かしら Rails 側で Prerequisites を用意しているだろうと考え、ソースコードを追ってみることにしました。結果、
railties/lib/rails/application.rb
の中で:environment
タスクを実行していたことが分かりました。中身を少し詳しくみてみます。railties/lib/rails/application.rb#L509def run_tasks_blocks(app) #:nodoc: railties.each { |r| r.run_tasks_blocks(app) } super require "rails/tasks" task :environment do ActiveSupport.on_load(:before_initialize) { config.eager_load = false } require_environment! end endhttps://github.com/rails/rails/blob/master/railties/lib/rails/application.rb#L509
task :environment
のブロック内で呼び出しているrequire_environment!
をみてみると、以下のようにconfig/environment.rb
を require していることが分かると思います。railties/lib/rails/application.rb#L335def require_environment! #:nodoc: environment = paths["config/environment"].existent.first require environment if environment endhttps://github.com/rails/rails/blob/master/railties/lib/rails/application.rb#L335
config/environment.rb
の中身を見ると分かりますが、ここでまさに Rails のアプリケーションコードを読み込んでいます。同じ階層のapplication.rb
を require して、Rails.application.initialize!
で初期化しています。config/environment.rb# frozen_string_literal: true # Load the Rails application. require_relative 'application' # Initialize the Rails application. Rails.application.initialize!
environment
は Rails のアプリケーションコードを読み込むタスク冒頭の rake タスクをあらためて見てみると、
foo
の前にenvironment
で Rails のアプリケーションコードを読み込んでくれていることが分かります。実際 Rails 側のクラスやモジュールを呼び出すタスクを:environment
抜きで実行するとエラーが起きます。sample.rakenamespace :sample do desc "TODO" task foo: :environment do end end↓↓ User モデルを一つ取り出してその name を出力しようとするも、「そんなモデル知らんがな」と言われてエラーになってしまいます。
sample2.rake# frozen_string_literal: true namespace :sample do desc 'TODO' task :foo do user = User.first puts "user name: #{user.name}" end end$ rake sample:foo rake aborted! NameError: uninitialized constant User /usr/local/bundle/gems/bootsnap-1.3.2/lib/bootsnap/load_path_cache/core_ext/active_support.rb:74:in `block in load_missing_constant' /usr/local/bundle/gems/bootsnap-1.3.2/lib/bootsnap/load_path_cache/core_ext/active_support.rb:8:in `without_bootsnap_cache' /usr/local/bundle/gems/bootsnap-1.3.2/lib/bootsnap/load_path_cache/core_ext/active_support.rb:74:in `rescue in load_missing_constant' /usr/local/bundle/gems/bootsnap-1.3.2/lib/bootsnap/load_path_cache/core_ext/active_support.rb:56:in `load_missing_constant' /sample/lib/tasks/sample.rake:6:in `block (2 levels) in <main>'このことを考えると、手動で rake タスクを作るより
rails generate
コマンドで行儀よく作った方が思わぬミスは少なくなりそうです。
- 投稿日:2019-02-09T16:37:58+09:00
RailsのCoffeeScriptで共通の処理を別のファイルへ切り出したい
タイトルの通りのことをやろうとしたときの、解決方法を見つけるまでに
調べたことを自分用のまとめも兼ねて書いておきます。Rails初学者向けです。
解決策は1番下にあります。CoffeeScriptとは?
前提知識として。
CoffeeScriptとはすごく簡単に言うと、JavaScriptをより書きやすくするためのものです。
Rubyっぽくかけます。Railsで採用されています。CoffeeScriptはCoffeeScriptのままでは
使えません。CoffeeScriptで書かれたものを普通のJavaScriptに変換して使用します。
そのあたりの変換はRailsが勝手にやってくれます。
なお、RailsはCoffeeScriptもJavaScriptも併用できます
ぐぐるとすぐどんなものかわかると思いますが、書きやすさの片鱗を1ミリだけ見せると以下です。javascriptalert("Hello");coffeescript# 引数をとる関数を呼び出すときの括弧いらない。 # 行末のセミコロンいらない。 alert "Hello"CoffeeScriptで書くと、他にも色々書きやすくなる点があります。
調べてみてください。以下本題です。
やりたいこと
やりたいことは単純です。
test1.coffeesayHello = -> alert "Hello"test2.coffeesayHello = -> alert "Hello"(なお、上記をJavaScriptで書くとこう)function sayHello() { alert("Hello"); }test1.coffeeとtest2.coffeeには同じ関数が定義されています。
同じことを2回書いているので、下記のように共通処理をまとめた
common.coffeeを作り、そこから呼び出せるようにしたい。common.coffeesayHello = -> alert "Hello"test1.coffee# common.coffeeを先に読み込んでいる前提 sayHello() # common.coffeeのsayHello関数を呼び出したい(できない)test2.coffee# common.coffeeを先に読み込んでいる前提 sayHello() # common.coffeeのsayHello関数を呼び出したい(できない)しかし、コメントにも書きましたが、実際にはcommon.coffeeからsayHello関数を
test1.coffeeやtest2.cofeeから呼び出すことはできません。
しかしJavaScriptならこれができます。common.jsfunction sayHello() { alert("Hello"); }test1.js// common.jsを先に読み込んでいる前提 sayHello(); // common.coffeeのsayHello関数を呼び出せるtest2.js// common.jsを先に読み込んでいる前提 sayHello(); // common.coffeeのsayHello関数を呼び出せるCoffeeScriptだとなぜできないのか
これはCoffeeScriptの仕様です。
CoffeeScriptファイルは、そのファイルで定義したものを即時・無名関数で丸ごと包むことにより、
ローカルスコープに閉じ込めるため、他のファイルからは使用できません。何言ってるかわかりませんね。あとで説明します。
JavaScriptだとなぜできるのか
その前に、なぜJavaScriptだとできるのかを抑えておきます。
これを理解するには、以下のキーワードを知る必要があります。
- スコープ(一般的な概念)
- グローバルスコープ(プログラミングの一般的な概念)
- ローカルスコープ(プログラミングの一般的な概念)
スコープとは
スコープとは「範囲」です。イメージしやすいように、ここでは「エリア」と言い換えます。
グローバルスコープとは
グローバルスコープとは、「そこに置いたものは、どこからでも呼び出せるようになる」エリアです。
関数の外側で定義された変数や関数が、このエリアに所属することになります。
このエリアで定義された変数や関数は、どこからでも上書きしたり、呼び出すことができます。test1.js(関数funcの外側がグローバルスコープのエリア)var hoge = "global" function func() { alert(hoge); // → "global"と表示される。変数hogeはグローバルなのでfunc関数内で使える。 } alert(hoge); // → "global"と表示される。変数hogeはグローバルなのでfunc関数外でも使える。test2.js// 事前にtest1.jsが読み込まれている前提 alert(hoge); // → "global"と表示される。変数hogeはグローバルなので、ここでも使える。 // test1.jsの変数hogeは書き換えることができる。 func(); // → "global"と表示される。 var hoge = "replace!"; func(); // → "replace!"と表示される。(test1.jsの変数hogeを書き換えできたことがわかる)ローカルスコープとは
ローカルスコープとは、「そこに置いたものは、その場所でしか呼び出せない」エリアです。
関数の内側で定義された変数や関数が、このエリアに所属することになります。
このエリアで定義された変数や関数は、関数の内側以外では、上書きしたり、呼び出すことはできません。test1.js(関数funcの内側がローカルスコープのエリア)function func() { var hoge = "local" alert(hoge); // → "local"と表示される。変数hogeはローカルなのでfunc関数内で使える。 } alert(hoge); // → エラーになる。変数hogeはローカルなのでfunc関数外では使えない。test2.js// 事前にtest1.jsが読み込まれている前提 alert(hoge); // → エラーになる。変数hogeはローカルなのでここでも使えない。 // test1.jsの変数hogeは書き換えることができない。 func(); // → "local"と表示される。 var hoge = "replace!"; func(); // → "local"と表示される。(test1.jsの変数hogeを書き換えできなかったことがわかる)※但し、
var hoge
のvar
を取ると、グローバルな変数とすることもできます。
ただ、var
の有る無しでローカススコープのエリアで定義しているのに、
グローバルとローカルが切り替わってしまうため、基本的にはvarは付けたほうがいい。
なお、CoffeeScriptにはvarはもともと存在せず、グローバル変数にはできません。JavaScriptだとなぜできるのか(再掲)
ではもう一度、JavaScriptだとなぜできるのか。
再度、JavaScriptのファイルを見てみます。common.jsfunction sayHello() { alert("Hello"); }test1.js// common.jsを先に読み込んでいる前提 sayHello(); // common.coffeeのsayHello関数を呼び出せるtest2.js// common.jsを先に読み込んでいる前提 sayHello(); // common.coffeeのsayHello関数を呼び出せるcommon.jsのsayHello()はグローバルスコープのエリアで定義されたものです。
よって、test1.jsからもtest2.jsからも呼び出せます。これがJavaScriptだとできる理由です。
CoffeeScriptだとなぜできないのか(再掲)
CoffeeScriptでできない理由も再度見てみましょう。
CoffeeScriptファイルは、そのファイルで定義したものを即時・無名関数で丸ごと包むことにより、
ローカルスコープに閉じ込めるため、他のファイルからは使用できません。以下のキーワードを知る必要があります。
- 即時関数(JavaScriptの機能)
- 無名関数(JavaScriptの機能)
即時関数とは
即時関数とは、定義した関数が即実行される関数です。よくわかりませんね。
普通の関数はfunctionで定義して、その後、明示的にfunctionの名前で呼び出して実行します。
普通の関数function sayHello() { alert("Hello"); } sayHello(); // → ここで初めて実行される。即時関数は下記のように書きます。
即時関数(function sayHello() { // → ここで定義された時点で即実行される。 alert("Hello"); }());何かの初期化処理など1回だけ実行されればいい時など、その関数を再利用する
必要がない時に使います。これが即時関数です。無名関数とは
一方、無名関数とは名前の無い関数です。よくわかりませんね。
普通の関数はfunctionのあとに関数名を書きますが、無名関数はその関数名を省略した関数です。
変数に関数を入れる例で書くと以下のようになります。
(JavaScript初学者の方には、え?って思うかもしれませんが、変数は値だけでなく関数も入れられます。)普通の関数var func = function sayHello() { alert("Hello") }; func()無名関数var func = function () { alert("Hello") }; // → sayHelloという関数名を省略できます。 func()無名関数は、関数名が必要ないときに使います。
即時関数と無名関数は組み合わせることができます。
ここではこれを即時・無名関数と呼んでおきます。即時・無名関数(function () { alert("Hello"); }());CoffeeScriptからJavaScriptへの変換
CoffeeScriptからJavaScriptへの変換処理は、
変換するときに、中身を丸ごと即時・無名関数として包みます。変換前CoffeeScriptsayHello = -> alert "Hello"変換後JavaScript(function () { sayHello = function() { return alert("Hello"); }; }());関数の内側に自分が書いたコードが包まれるので、変換後は全ての変数や関数は
ローカルスコープに閉じ込められるということです。なぜこんなことをするのか。
それは、前述のローカルスコープのところで説明した効果を得たいためです。ローカルスコープとは、「そこに置いたものは、その場所でしか呼び出せない」エリアです。
関数の内側で定義された変数や関数が、このエリアに所属することになります。
このエリアで定義された変数や関数は、関数の内側以外では、上書きしたり、呼び出すことはできません。もしたくさんのCoffeeScriptファイルを取り扱うようになった時、あるファイルでせっかく
定義した関数や変数を、別のファイルの中で同じ名前の関数や変数を使って全く違う定義を
誤ってしてしまうかもしれません。それを防ぐ効果があります。この効果のために、共通処理を切り出せなくなっています。
解決策
いくつか方法があるようです。
方法1
JavaScriptにはwindowオブジェクトという特殊なオブジェクトがあります。
先程から良く使っているalert関数も実はwindowオブジェクトが持つ関数の一つです。
つまりこう書けます。このwindowは省略できるため、alertだけで普段使えています。window.alert("Hello");このwindowオブジェクトはグローバルスコープに属しています。
グローバルスコープに属しているものはどこからでも上書き・呼び出しができます。
上書きしてしまうとalertもなにも使えなくなってしまうので、このオブジェクトに
関数を追加します。common.coffeewindow.sayHello = -> alert "Hello"test1.coffeesayHello()test2.coffeesayHello()方法2
@を使います。@はCoffeeScriptに用意されたJavaScriptのthisの別名です。
JavaScriptのthisは、グローバルスコープのエリアでは、windowオブジェクトを指します。よって方法1の下記は・・・
common.coffeewindow.sayHello = -> alert "Hello"下記のように書けます。
common.coffee@sayHello = -> alert "Hello"test1.coffeesayHello()test2.coffeesayHello()終わり
以上で終わりです。
新しいバージョンのJavaScriptだとちゃんと関数の外に定義した変数や関数も
グローバルにならないように宣言する方法があるみたいです。
ただ、どこまで今存在するWebブラウザが、そのバージョンに対応しているかは
調べられていません。「ECMAScript 6」とかでぐぐると出てきます。なにかこう・・・JavaScriptって、昔から、こう、、、アレなんですよね。。。
- 投稿日:2019-02-09T16:19:26+09:00
ドットインストール Ruby on Rails 5入門 #02 動作確認をしてみようでActiveRecord::ConnectionNotEstablishedエラー
ドットインストール Ruby on Rails 5入門 #02 動作確認をしてみよう
のWebサーバ立ち上げの箇所でエラーが発生しハマったので解決法を覚書として残します。前提条件
以下の講座にてmacの開発環境構築とRuby on railsをインストール済であること
ローカル開発環境の構築 macOS編 (全14回)
Ruby on Rails 5入門 #01 Ruby on Railsを使ってみよう事象
Ruby on Rails 5入門 #02 動作確認をしてみよう
では以下の流れでWebサーバを立ち上げます。$ rails new myapp # myappディレクトリを作成 $ cd myapp # myappディレクトリに移動 $ ip a # IPアドレスの確認 $ rails server -b 192.168.33.10 -d # Webサーバをバックグラウンドで立ち上げURLに以下IPアドレスを入力、Rails初期画面が出たらOK
http://192.168.33.10:3000しかしチュートリアル通り進めていくと、Rails初期画面は表示されず以下のエラー画面が出力されます。
※エラー文
ActiveRecord::ConnectionNotEstablished
No connection pool with 'primary' found.DBへのコネクションプールの接続が出来てないようであるため
rakeコマンドを用いてマイグレーションファイルを実行しDBのbuildを試みます。
すると以下のエラーが表示されました。
GemFileのsqlliteの設定部分に1.3.6以上のversionを指定しなければいけないようです。$ rake db:migrate rake aborted! LoadError: Error loading the 'sqlite3' Active Record adapter. Missing a gem it depends on? can't activate sqlite3 (~> 1.3.6), already activated sqlite3-1.4.0. Make sure all dependencies are added to Gemfile. Caused by: Gem::LoadError: can't activate sqlite3 (~> 1.3.6), already activated sqlite3-1.4.0. Make sure all dependencies are added to Gemfile.解決法
myappディレクトリ直下にあるGemFileを以下のように変更しました。
$ vi /home/vagrant/rails_lessons/myapp/Gemfile # Gemfileを編集
- 変更前
source 'https://rubygems.org' git_source(:github) { |repo| "https://github.com/#{repo}.git" } ruby '2.3.1' # Bundle edge Rails instead: gem 'rails', github: 'rails/rails' gem 'rails', '~> 5.2.2' # Use sqlite3 as the database for Active Record gem 'sqlite3' # Use Puma as the app server gem 'puma', '~> 3.11' 以下略
- 変更後
source 'https://rubygems.org' git_source(:github) { |repo| "https://github.com/#{repo}.git" } ruby '2.3.1' # Bundle edge Rails instead: gem 'rails', github: 'rails/rails' gem 'rails', '~> 5.2.2' # Use sqlite3 as the database for Active Record gem 'sqlite3', '~> 1.3.6' # 変更箇所(バージョンを指定) # Use Puma as the app server gem 'puma', '~> 3.11' 以下略変更後、bundleコマンドを使用してsqlliteの再インストールを行います。
$ bundle install先程立ち上げたRailsのWebサーバを停止させて再度立ち上げ直します。
バックグラウンドで立ち上げているのでプロセスIDを調べてからkillします。$ cat tmp/pids/server.pid # プロセスIDの確認 プロセスID[vagrant@localhost myapp]$ $ kill -9 プロセスID # Webサーバの停止 $ rails server -b 192.168.33.10 -d # Webサーバの立ち上げURLに以下IPアドレスを入力
http://192.168.33.10:3000あとがき
新人のプログラム初学者からの質問により今回のエラーが発生/解決しました。
今後の新人プログラマーたちに少しでも役立ててもらえれば幸いです。参考URL
Error loading the 'sqlite3' Active Record adapter. Missing a gem it depends on?エラーで困ってます
- 投稿日:2019-02-09T16:08:08+09:00
【Rails】RailsAPIテスト - ベストプラクティス
概要
Railsで作成されたAPIのテストについてこちらを参考に必要最低限の情報をまとめました。
実際にこちらの記事で作成したAPIに対してテストを書いていきます。何をテストする?
適切なAPIはHTTPのレスポンスの
ステータスコード
と実際のデータを含んだレスポンスボディ
を返します。
よって主にその2つをテストしていきます。ステータスコード
通常APIによって返されるステータスコードは以下の4つに分類されます。
APIの動きによってこちらのコードと照らし合わせる事でテストします。
- 200: OK - リクエストは成功し、レスポンスとともに要求に応じた情報が返される。
- 401: Unauthorized - 認証失敗。認証が必要である。
- 403: Forbidden - 禁止されている。リソースにアクセスすることを拒否された。
- 404: Not Found - 未検出。リソースが見つからなかった。
レスポンスボディ
GETリクエスト
の場合は単にリクエストボディに要求したデータが入っているかをテストします。
POST
、PUT
、DELETE
リクエストの際にはそれぞれ要求したデータ通りの動きをするかテストします。APIのテストはインテグレーションテスト
APIのテストは特定のURLに対してデータの要求等をした際の動きをテストするので、インテグレーションテスト(結合テスト)として扱います。
※通常Railsで結合テストを行う際には
capybara
というgemがよく使われますが、こちらはAPIのテストをする際には適していません。詳しくはこちらから。RspecのRequest Specsを使用する。
APIだからといって特別なテストツール等は使用する必要はなく通常のrailsアプリで頻繁に使用される
rspec
を使用してテストします。
APIに対するリクエストに関してテストですのでrspec/requests/
ディレクトリ下にテストを作成していきます。実装
上記で挙げたポイントを以下にまとめましたので、こちらを意識して実際に簡易的なテストを書いていきます。
(※1番上にも挙げましたがこちらのAPIにテストを書いていきます。)
(※あくまでも簡易的なテストですので、とてもシンプルな構造で書いていますが、実際には状況に応じてここからテストを充実させる必要が考えられます。)ここまでのポイント
- ステータスコードとレスポンスボディをテストする。
- APIのテストはインテグレーションテスト
- rspecを使用し
rspec/requests/
ディレクトリ下にテストを作成する。下準備
factory_botを使用してデータを作成しますので以下の手順でセットアップします。
$ gem install factory_bot_railsGemfilegroup :development, :test do gem 'factory_bot_rails' endspec/factories/posts.rbFactoryBot.define do factory :post do title { 'title' } end end
GET
リクエスト
GET
リクエストでは正しくデータを取得した際にステータスコード200
が返るか、正しく要求したデータが取得できたかをテストします。
/api/v1/posts
に対するテスト。spec/requests/api/v1/posts_spec.rbrequire 'rails_helper' describe 'PostAPI' do it '全てのポストを取得する' do FactoryBot.create_list(:post, 10) get '/api/v1/posts' json = JSON.parse(response.body) # リクエスト成功を表す200が返ってきたか確認する。 expect(response.status).to eq(200) # 正しい数のデータが返されたか確認する。 expect(json['data'].length).to eq(10) end end
/api/v1/posts/:id
に対するテスト。spec/requests/api/v1/posts_spec.rbrequire 'rails_helper' describe 'PostAPI' do it '特定のpostを取得する' do post = create(:post, title: 'test-title') get "/api/v1/posts/#{post.id}" json = JSON.parse(response.body) # リクエスト成功を表す200が返ってきたか確認する。 expect(response.status).to eq(200) # 要求した特定のポストのみ取得した事を確認する expect(json['data']['title']).to eq(post.title) end end
POST
リクエスト
POST
リクエストでは正しくデータが作成できたか、正しくデータを作成した際にステータスコード200
が返るかをテストします。require 'rails_helper' describe 'PostAPI' do it '新しいpostを作成する' do valid_params = { title: 'title' } #データが作成されている事を確認 expect { post '/api/v1/posts', params: { post: valid_params } }.to change(Post, :count).by(+1) # リクエスト成功を表す200が返ってきたか確認する。 expect(response.status).to eq(200) end end
PUT
リクエスト
PUT
リクエストでは正しくデータを編集した際にステータスコード200
が返るか、正しくしたデータが編集できたかをテストします。require 'rails_helper' describe 'PostAPI' do it 'postの編集を行う' do post = create(:post, title: 'old-title') put "/api/v1/posts/#{post.id}", params: { post: {title: 'new-title'} } json = JSON.parse(response.body) # リクエスト成功を表す200が返ってきたか確認する。 expect(response.status).to eq(200) #データが更新されている事を確認 expect(json['data']['title']).to eq('new-title') end end
DELETE
リクエスト
DELETE
リクエストでは正しくしたデータが削除できたか、正しくデータを削除した際にステータスコード200
が返るかをテストします。require 'rails_helper' describe 'PostAPI' do it 'postを削除する' do post = create(:post) #データが削除されている事を確認 expect { delete "/api/v1/posts/#{post.id}" }.to change(Post, :count).by(-1) # リクエスト成功を表す200が返ってきたか確認する。 expect(response.status).to eq(200) end end参考
- 投稿日:2019-02-09T14:21:09+09:00
英語と日本語で考える、整理された RSpec
RSpec やテストを苦手とする人はかなり多いだろうと思う。
実装は瞬殺だったのに、RSpec の書き方に悩んでしまって何日もプルリクエストを出せていない。
不運にもそんな状況に陥ってしまった人に贈る。私は理論的な背景を研究したわけでもないので、実務家としてこうだろうと理解したことをアウトプットする。
spec は仕様という意味
wikipedia のスペックの説明を見て欲しい。
スペックとは、英語で spec (specification の省略形。読みは「スペシフィケーション」)は、いわゆる仕様書のことであるが、和製英語の範疇では一般に工業製品に期待される性能のことである。俗に「カタログスペック」や「基本スペック」、「諸元」などのように表現される。自動車のカタログなどでよくみられる。
なかでも、
いわゆる仕様書のことであるが、和製英語の範疇では一般に工業製品に期待される性能のことである。
という部分。
この和製英語の範疇で理解していた人も多いのではないだろうか。是非とも「仕様」という意味で頭の中の定義を上書きしておいて欲しい。
仕様という言葉の意味は広くて曖昧だが、それでも我々は平気で「仕様」と口にする。そのときに想起するような意味だと思えば良い。構造: 主語・主題と述語と条件節 (describe, it, context)
it ってなんだろう
ネストの一番深くには、
it
や、その別名とされるexample
,specify
がいる。これらに渡すブロックの中で実際の期待を書くのだ。it '文字列を返す' do expect(some_object.some_method(arg)).to be_a String end # 別の書き方 example 'それが文字列を返す' do expect(some_object.some_method(arg)).to be_a String end specify 'それが文字列を返すこと' do expect(some_object.some_method(arg)).to be_a String endそれぞれ響き方が違う。自然な響き方をする書き方が違う。
it は代名詞で、主語の位置にいる
辞書を引くまでもないだろう。
振る舞いにフォーカスする
英語話者にとって、
it
による例は次のように響いているはずだ。それは '文字列を返す' do expect(some_object.some_method(arg)).to be_a String end「それ」が主語であり「文字列を返す」は「それ」の振る舞いである。
主語を自由に選べないという制約によって、「文字列を返す」という振る舞いの記述に焦点が当たる。
英語話者はこういう感覚でプログラミングしているのだ。describe: つまり「それ」ってなんなんだ
it
が代名詞であるというのなら、その代名詞が何を指示しているのかを特定しなければならない。
それはネストのより浅いところで宣言されているはずだ。describe 'SomeClass' do describe '#some_method' do it '文字列を返す' do expect(some_object.some_method(arg)).to be_a String end end enddescribe は説明する・描写する・記述するという意味だが、説明する対象を目的語にとる。
「describe an apple (あるリンゴを説明する)」とあれば、
あるリンゴがどのような形で、どのような色味を帯びているかを説明するのだろう。「そのリンゴは赤い」「そのリンゴはゴツゴツしている」
つまり、
「それは赤い」「それはゴツゴツしている」
何か繋がった感じがしないだろうか。
まとめて読んでみよう。あるリンゴを説明する。 それは赤い。 それはごつごつしている。describe は主題を特定する
describe は記述対象を目的語に取る。
rubyとしての語順は壊れてしまうけど、例を次のように訳してみよう。'とあるクラス' を説明する do 'とあるインスタンスメソッド(#some_method)' を説明する do それは '文字列を返す' do expect(some_object.some_method(arg)).to be_a String end end end「それ」が何を示しているのか、一目瞭然ではないだろうか。
「とあるクラスのとあるインスタンスメソッドは文字列を返す」
describe の度に主題の範囲が狭まっていくのがわかるだろう。繰り返すけれど、英語話者はこんな感覚でプログラミングしてるはずだ。
context には主題でもなく振る舞いでもないものを
主題でも振る舞いでもないものって何があるだろう。
オブジェクトの状態、メソッドがとる引数、その他依存先。
そういうものを context に書くのが良いのではないかと思う。
context は文脈や前後関係と訳せる。
前提、背景、状況。「〇〇のとき」「〇〇であるならば」
振る舞い方がそういった外部の状況に依存しているのであれば、それはコンテクストだ。
まとめ
- it に主語を固定すれば振る舞いに焦点が当たる
- describe は主題を特定し、主語を明確にする
- context は依存関係
私はそんなことを意識して書いてる。もちろんいつでも適用できるわけではないけれど。
ちなみになるべく「こと」は書かない主義。
- 投稿日:2019-02-09T11:59:39+09:00
1つの親レコードに対して複数の子レコードである画像をアップロードしたい。
5日間くらい詰まって進まなかったので、同じところで詰まる人がいないようメモ
やりたいこと
フリマアプリのように、出品するアイテムに複数の画像をアップロードする機能を作りたい。
環境
Rails 5.0.7.1
使用するgem
- carrierwave
- mini_magick
手順
- gemをGemfileに記述し、bundle install
- モデル作成
- アップロードのためのviewを作成
- コントローラー作成
以上で完成!
手順2から解説Model作成
コンソールでモデル作成
rails g model Product rails g model Item_image
マイグレーションファイルにカラムの記述を行う
create_products.rb
class CreateProducts < ActiveRecord::Migration[5.0] def change create_table :products do |t| t.string :name, null: false, default: "" t.references :user, foreign_key: true t.timestamps null: false, foreign_key: true end end endcreate_item__images.rb
class CreateItemImages < ActiveRecord::Migration[5.0] def change create_table :item_images do |t| t.string :name, null: false, default: "" t.references :product, foreign_key: true t.timestamps end end endrake db:migrate各モデルにアソシエーションを記述
product.rb
accepts_nested_attributes_forと記述することで、親レコードの作成と共に子レコードの作成が可能となる。class Product < ApplicationRecord has_many :item_images, :dependent => :destroy accepts_nested_attributes_for :item_images, allow_destroy: true enditem_image.rb
uploaderをマウントするコードを記述class ItemImage < ApplicationRecord mount_uploader :name, ImageUploader belongs_to :product, optional: true endView作成
view側でアップローダーを作成する。
1つのアップローダーで画像の複数選択を出来るようにするためには、multiple: trueを記述する必要がある。
multiple: trueを記述した場合、paramsには配列として渡されるので、コントローラー側で配列の中身を取り出す処理が必要になる。= f.fields_for :item_images do |i| = i.file_field :name, multiple: true, type: 'file', name: "item_images[name][]"Controller作成
ストロングパラメーターの記述
private def product_params params.require(:product).permit( :name, item_images_attributes: [:name]) endnewアクションでインスタンス生成し、createアクションで保存の処理を行う。
この際、params[:item_images]['name']の中身は配列なので、それぞれ@item_images取り出す処理を行う。def new @product = Product.new @item_image = @product.item_images.build end def create @product = Product.new(product_params) if @product.save binding.pry params[:item_images]['name'].each do |a| @item_image = @product.item_images.create!(name: a) end redirect_to root_path, notice: '出品しました。' else render :new end end以上で完成。
- 投稿日:2019-02-09T10:40:09+09:00
deviseを日本語化する
devise.ja.ymlの導入
gemを導入
gem 'devise-i18n' gem 'devise-i18n-views'上記のgemを導入。devise-i18nはdeviseのメッセージなどを日本語にする。devise-i18n-viewsは下記の記述があっていらないのかなと思ったが
devise-i18n-views has been merged into devise-i18n. You should stop using devise-i18n-views and start using devise-i18n 1.0.0 or later. There will be no further releases of devise-i18n-views.
なぜか
$rails g devise:views:locale jaこれが通らなかったので導入。いれたら通った。
devise.ja.ymlの編集
この記事のdevise.ja.ymlを拝借
https://remonote.jp/rails-devise-i18n-locale-ja
ja: activerecord: errors: models: user: attributes: email: taken: "は既に使用されています。" blank: "が入力されていません。" too_short: "は%{count}文字以上に設定して下さい。" too_long: "は%{count}文字以下に設定して下さい。" invalid: "は有効でありません。" password: taken: "は既に使用されています。" blank: "が入力されていません。" too_short: "は%{count}文字以上に設定して下さい。" too_long: "は%{count}文字以下に設定して下さい。" invalid: "は有効でありません。" confirmation: "が内容とあっていません。" attributes: user: current_password: "現在のパスワード" name: 名前 email: "メールアドレス" password: "パスワード" password_confirmation: "確認用パスワード" remember_me: "次回から自動的にログイン" models: user: "ユーザ" devise: confirmations: new: resend_confirmation_instructions: "アカウント確認メール再送" mailer: confirmation_instructions: action: "アカウント確認" greeting: "ようこそ、%{recipient}さん!" instruction: "次のリンクでメールアドレスの確認が完了します:" reset_password_instructions: action: "パスワード変更" greeting: "こんにちは、%{recipient}さん!" instruction: "誰かがパスワードの再設定を希望しました。次のリンクでパスワードの再設定が出来ます。" instruction_2: "あなたが希望したのではないのなら、このメールは無視してください。" instruction_3: "上のリンクにアクセスして新しいパスワードを設定するまで、パスワードは変更されません。" unlock_instructions: action: "アカウントのロック解除" greeting: "こんにちは、%{recipient}さん!" instruction: "アカウントのロックを解除するには下のリンクをクリックしてください。" message: "ログイン失敗が繰り返されたため、アカウントはロックされています。" passwords: edit: change_my_password: "パスワードを変更する" change_your_password: "パスワードを変更" confirm_new_password: "確認用新しいパスワード" new_password: "新しいパスワード" new: forgot_your_password: "パスワードを忘れましたか?" send_me_reset_password_instructions: "パスワードの再設定方法を送信する" registrations: edit: are_you_sure: "本当に良いですか?" cancel_my_account: "アカウント削除" currently_waiting_confirmation_for_email: "%{email} の確認待ち" leave_blank_if_you_don_t_want_to_change_it: "空欄のままなら変更しません" title: "%{resource}編集" unhappy: "気に入りません" update: "更新" we_need_your_current_password_to_confirm_your_changes: "変更を反映するには現在のパスワードを入力してください" new: sign_up: "アカウント登録" sessions: new: sign_in: "ログイン" shared: links: back: "戻る" didn_t_receive_confirmation_instructions: "アカウント確認のメールを受け取っていませんか?" didn_t_receive_unlock_instructions: "アカウントの凍結解除方法のメールを受け取っていませんか?" forgot_your_password: "パスワードを忘れましたか?" sign_in: "ログイン" sign_in_with_provider: "%{provider}でログイン" sign_up: "アカウント登録" unlocks: new: resend_unlock_instructions: "アカウントの凍結解除方法を再送する"viewの文言を日本語化する
devise.ja.ymlではエラーメッセージなど動的な部分の日本語化でviewに書かれているものは個別に対応しないといけない。
rails全体の日本語化
上記はdeviseのみrails全体を日本語化したい場合は下記の記事から
- 投稿日:2019-02-09T09:58:19+09:00
Herokuへpushしたら"Detected sqlite3 gem which is not supported on Heroku"になったので、Gemfile直したけど、反映されないやつ
タイトルの通りです。
エラー文で検索するとGemfile直してbundle install
とgit push heroku
し直せば
いけるよって書いてあるの多いんですが、初学者さんはgit commit忘れてハマることが
ありそうなので書いておきます。(はい、地味に私がハマりました)では最初から。
1. エラー内容
railsアプリなどをHerokuへpushしたときに下記のようなエラーがでることがあります。
# 省略 remote: ! remote: ! Failed to install gems via Bundler. remote: ! Detected sqlite3 gem which is not supported on Heroku: remote: ! https://devcenter.heroku.com/articles/sqlite3 remote: ! # 省略Gemfileにsqlite3のgemを適用する環境を絞らずに記載していると出ます。
HerokuはSQLite3をサポートしていないので、代わりにサポートしているPostgresqlを
Gemfileに追加してあげる必要があります。2. Gemfileの修正
変更前Gemfilegem 'sqlite3'変更後Gemfile# gemのバージョンは適宜変えてください # 開発・テスト環境ではSQLite3を使う group :development, :test do gem 'sqlite3' end # 本番環境ではPostgresqlを使う group :production do gem 'pg', '0.20.0' end3. bundle install
Herokuにpushするときには、Gemfile.lockの中身を参照されるので、bundle installし直して
Gemfile.lockを最新化します。$ bundle install4. git commit
git使い始めだと忘れがち。Herokuへアプリをアップロードするときは、gitで管理しているファイルを
アップロードします。Gemfile.lockもその管理対象なので、コミットしてあげないとHerokuに適用されません。$ git add -A $ git commit -m "[Update] Gemfile修正"5. Herokuへpush
$ git push heroku master
以上です。
- 投稿日:2019-02-09T05:12:36+09:00
Ruby on Rails チュートリアル 第14章 データモデルの関連付け(フォロー フォロー解除)フィードの実装など 演習 解答
著者略歴
YUUKI
ポートフォリオサイト:Pooks
RailsTutorial2周目14章 ユーザーをフォローする
この章では、他のユーザーをフォローしたり、フォロー解除したりするソーシャル的な仕組みと、
フォローしているユーザーの投稿をステータスフィード(いわゆるタイムライン)
に表示する仕組みを追加する。そのために、まずはユーザー間の関係性をどうモデリングするかについて学ぶ。
その後、モデリング結果に対応するWebインターフェースを実装していく。
Webインターフェースの例としてAjaxについても後に詳解する。
最後に、ステータスフィードの完成版を実装する。
この最終章では、本書の中で最も難易度の高い手法をいくつか使っている。
その中には、ステータスフィードの作成のためにRuby/SQLを騙すテクニックも含まれる。この章の例全体にわたって、これまでよりも複雑なデータモデルを使います。
ここで学んだデータモデルは、今後自分用のWebアプリケーションを開発するときに必ず役に立つ。
この章で学ぶことは今まで最も難易度が高いため、
コードを書く前に一旦インターフェースの流れを理解する。モックアップはこちら
出典:図 14.3: ユーザーのプロフィール画面に [Follow] ボタンが表示されている
出典:図 14.4: プロフィールに [Unfollow] ボタンが表示され、フォロワーのカウントが1つ増えた
出典:図 14.5: Homeページにステータスフィードが表示され、フォローのカウントが1増えた
ページ操作の全体的なフローは次の通りとなる。
①あるユーザーは(John Calvin)は自分のプロフィールページを最初に表示する
②フォローするユーザーを選択するためにUsersページに移動する
③Calvinは2番目のThomas Hobbesを表示し、Followボタンを押してフォローする
④Homeページに戻ると、followingカウントが1人増える
⑤Hobbesのマイクロポストがステータスフィードに表示されるになる14.1 Relationshipモデル
ユーザーをフォローする機能を実装する第一歩は、データモデルを構成する。
今回のデータモデルは単純ではなく、
has_many(1対多)の関連付けを用いて
「1人のユーザーが複数のユーザーをhas_manyとしてフォローし、1人のユーザーに複数のフォロワーがいることをhas_manyで表す」
といった方法でも実装できる。しかし、この方法ではたちまち壁に突き当たってしまう。
これを解決する為の
has_many_through
についても解説する。Gitユーザーはこれまで同様新しいトピックブランチを作成する
$ git checkout -b following-users14.1.1 データモデルの問題(および解決策)
ユーザーをフォローするデータモデル構成のための第一歩として、典型的な場合を検討してみる。
あるユーザーが、別のユーザーをフォローしているところを考えてみる。
- CavinはHobbesをフォローしている。 - 逆から見ればHobbesはCalvinからフォローされている。 - CalvinはHobbesから見ればフォロワーであり、Calvinがhobbesをフォローしたことになる。 - Railsにおけるデフォルトの複数形の慣習に従えば、あるユーザーをフォローしているすべてのユーザーの集合(フォローされてる人数)はfollowersとなり、user.followersはそれらのユーザーの配列を表すことになる。 - しかし、これを逆で考えた場合(フォローしている人数)、英語の文法的`followeds`となり、英語の文法からも外れてしまう - そこで、Railsではフォロー人数を`following`という呼称を採用している。 - したがって、あるユーザーがフォローしている全てのユーザーの集合は`calvin.following`となるつまり、
followers
がフォロワー人数で、following
がフォロー人数を表すデータの表となる。まずは
following
テーブル(フォロー人数)を見ていく。followingテーブルとhas_many関連付けを使って、フオローしているユーザーのモデリングができる。
user.following
はユーザーの集合でなければならないため、followingテーブルのそれぞれの行は、followed_idで識別可能なユーザーでなければならない。さらに、それぞれの行はユーザーなので、これらのユーザーに名前(name)やパスワード(password)などの属性を追加する。
出典:図 14.6: フォローしているユーザーの素朴な実装例
上記のデータモデルの問題点は非常に無駄が多いこと。
各行には、フォローしているユーザーのidのみならず、名前やメールアドレスまである。
これらはいずれも
usersテーブル
に既にあるものばかり。さらによくないことに、followersの方をモデリングする時にも、
同じくらい無駄の多いfollowersテーブルを別に作成しなければならなくなってしまう。結論としては、このデータモデルはメンテナンスの観点から見て悪夢。
というのも、ユーザー名を変更するたびに、usersテーブルのそのレコードだけでなく、followingテーブルとfollowersテーブルの両方について、そのユーザーを含む全ての行を更新しなければならなくなる。この問題の根本は、必要な抽象化を行なっていないことである。
正しいモデルを見つけ出す方法の1つは、
Webアプリケーションにおけるfollowingの動作をどのように実装するかをじっくり考えることにある。7章において、RESTアーキテクチャは、作成されたり削除されたりするリソースに関連していたことを思い出してみる。
ここで2つの疑問点が挙げられる。
①あるユーザーが別のユーザーをフォローする時、何が作成されるか?
②あるユーザーが別のユーザーをフォロー解除する時、何が削除されるか?この点を踏まえて考えると、この場合アプリケーションによって作成または削除されるのは
2人のユーザーの関係(リレーションシップ)であることがわかる。つまり、1人のユーザーは1対多の関係を持つことができ、さらにユーザーはリレーションシップを経由して多くのfollowing(またはfollowers)と関係を持つことができるということ。
このデータモデルには他にも解決しなくてはいけない問題がある。
Facebookのような友好関係(Friendships)では、本質的に左右対称のデータモデルが成り立つが、
Twitterのようなフォロー関係では左右非対称の性質がある。すなわち、CalvinはHobbesをフォローしていても、HobbesはCalvinをフォローしていないといった関係性が成り立つ。
このような左右非対称な関係性を見分けるために、それぞれを
能動的関係(Active Relationship)と
受動的関係(Passive Relationship)と呼ぶことにする。例えば先ほどの事例のような、CalvinがHobbesをフォローしているが、hobbesはCalvinをフォローしていない場合では、CalvinはHobbesに対して能動的関係を持っていることになる。
逆に、HobbesはCalvinに対して受動的関係を持っていることになる。
まずは、フォローしているユーザーを生成するために、能動的関係に焦点を当てていく。
(受動的関係についてはのちに考える)先ほどのfollowingデータモデルは実装のヒントにして考える。
フォローしているユーザーは
followed_id
があれば識別することができるので、先ほどのfollowing
テーブルをactive_relationships
(能動的関係)テーブルと見立ててみる。ただし、ユーザー情報は無駄なので、ユーザーid以外の情報は削除する。
そして、followed_idを通して、usersテーブルのフォローされているユーザーを見つけるようにする。このデータモデルを模式図にすると、以下のようになる。
出典:図 14.7: 能動的関係をとおしてフォローしているユーザーを取得する模式図
間に
active_relationships
を挟むことで、フォローとフォロワーの関係性がスムーズに繋がっている。能動的関係も受動的関係も、最終的にはデータベースの同じテーブルを使うことになる。
したがって、テーブル名にはこの「関係」を表す「relationships」を使う。
モデル名はRailsの慣習にならって、Relationshipとする。
作成したRelationshipデータモデルを以下に示す。1つのrelationshipsテーブルを使って2つのモデル(能動的関係と受動的関係)をシミュレートする方法については後に説明する。
このデータモデルを実装するために、まずは上記のデータモデルに対応したマイグレーションを生成する。
$ rails g model Relationship follower_id:integer followed_id:integerこのリレーションシップは今後
follower_id
とfollowed_id
で頻繁に検索することになるので、
それぞれのカラムインデックスを追加する。[timestamp]_create_relationships.rbclass CreateRelationships < ActiveRecord::Migration[5.1] def change create_table :relationships do |t| t.integer :follower_id t.integer :followed_id t.timestamps end add_index :relationships, :follower_id add_index :relationships, :followed_id add_index :relationships, [:follower_id, :followed_id], unique: true # 複合キーインデックスにし、お互いがユニークであることを保証 end end複合キーインデックスで、
follower_id
とfollowed_id
の組み合わせが必ずユニークであることを保証する仕組みを作っている。これにより、あるユーザーが同じユーザーを2回以上フォローすることを防いでいる。
もちろん、このような重複が起きないよう、インタフェース側の実装でも注意を払う。
しかし、ユーザーが何らかの方法で(例えばcurlなどのコマンドラインツール)Relationshipのデータを操作するようなことも起こり得る。
そのような場合でも、一意なインデックスを追加していれば、エラーを発生させて重複を防ぐことができる。
relationshipsテーブルを作成するために、いつものようにデータベースのマイグレーションを行う。
$ rails db:migrate演習
1:id=1のユーザーに対して
user.following.map(&:id)
を実行すると、結果はどのようになるか?引数で受け取ったid=1にフォローされているユーザー(id: 2,7,10,8)のidをそれぞれ1つずつ返す
>> user.following.map(id:1) 2 7 10 82:id=2のユーザーに対して
user.following
を実行すると、結果はどうなるか?
また、同じユーザーに対してuser.following.map(&:id)
を実行すると、結果はどのようになるか?>> user.following(id:2) => [id:1,name:Michael Hartl,email:mhartl@example.com] >> user.following.map(id:2) => 114.1.2 User/Relationshipの関連付け
フォローしているユーザーとフォロワーを実装する前に、UserとRelationshipの関連付けを行う。
1人のユーザーには
has_many
(1対多)のリレーションシップがあり、このリレーションシップは2人のユーザーの間の関係なので、フォローしているユーザーとフォロワーの両方に属している。(belongs_to)マイクロポスト作成の時と同様、下記のようなユーザー関連付けのコードを使って新しいリレーションシップを作成する。
user.active_relationships.build(followed_id: ...) #user.active_relationshipsをデータモデルとして引数で受け取った値と関連付けて、カラムを生成するこの時点では、User/Micropostの関連付けのモデルのようにはならない。
1つ目の違いとして、以前、ユーザーとマイクロポストの関連付けをした時は
class User < ApplicationRecord has_many :microposts endこのように書いた。
引数の
:microposts
シンボルから、Railsはこれに対応するMicropostモデルを探し出し、見つけることができた。しかし、今回のケースで同じように書くと
has_many :active_relationshipsとなってしまい、ActiveRelationshipモデルを探してしまうので、
相互にフォローユーザーを繋ぐRelationshipモデルを見つけることができない。このため、今回のケースではRailsに探して欲しいモデルのクラス名を明示的に伝える必要がある。
2つ目の違いは、先ほどの逆のケースについて。
以前はMicropostモデルで
class Micropost < ApplicationRecord belongs_to :user endこのように書いた。
micropostsテーブルにはuser_id属性があるので、
これを辿って対応するユーザーを特定できた。DBの2つのテーブルを繋ぐとき、このようなidは外部キー(foreign key)と呼ぶ。
すなわち、Userモデルに繋げる外部キーが、Micropostモデルのuser_id属性ということ。
この外部キーの名前を使って、Railsは関連付けの推測をしている。
具体的には、Railsはデフォルトでは外部キーの名前を_idといったパターンとして理解し、
に当たる部分からクラス名(正確には小文字に変換されたクラス名)を推測する。class Micropost < ApplicationRecord belongs_to :user #Micropostはmicropostモデルのuser_id属性が外部キーと自動で推測する)ただし、マイクロポストではユーザーを例として扱ったが、
今回のケースでは
フォローしているユーザーをfollower_id
という外部キーを持って特定しなくてはならないまた、followerというクラス名は存在しないので、ここでもRailsに正しいクラス名を伝える必要が発生する。
先ほどの説明をコードにまとめると、UserとRelationshipの関連付けは以下のようになる。
user.rbclass User < ApplicationRecord # 関連付け has_many :microposts, dependent: :destroy has_many :active_relationships, class_name: "Relationship", foreign_key: "follower_id", dependent: :destroy明示的にclass名や外部キー、destroyも追加している。
(ユーザーを削除したら、ユーザーのリレーションシップも同時に削除される必要があるため)relationship.rbclass Relationship < ApplicationRecord # 1対1の関連付け belongs_to :follower, class_name: "User" belongs_to :followed, class_name: "User" endなお、followerの関連付けはまだ使わない。
(書いておくことで構造の理解の手助けになる)
user.rb
とrelationship.rb
で定義した関連付けにより、13.1で以前紹介したような多くのメソッドが使えるようになった。出典:表 14.1: ユーザーと能動的関係の関連付けによって使えるようになったメソッドのまとめ
これらのメソッドを使えば、フォロワーを返したり、フォローしているユーザーを返したりできる。
演習
1:コンソールを開き、上記表のcreateメソッドを使って、ActiveRelationshipを作ってみる。
DB上に2人以上のユーザーを用意し、最初のユーザーが2人目のユーザーをフォローしている状態を作ってみる。>> user = User.first User Load (0.2ms) SELECT "users".* FROM "users" ORDER BY "users"."id" ASC LIMIT ? [["LIMIT", 1]] => #<User id: 1, name: "Example User", email: "example@railstutorial.org", created_at: "2019-01-31 11:25:14", updated_at: "2019-01-31 11:25:14", password_digest: "$2a$10$ufvz2x2ljsYgknbfQaQrNOF5uG5PP.1YP2jIXom1qCU...", remember_digest: nil, admin: true, activation_digest: "$2a$10$YtXwZx1hETpK66tpv23VJO7a47hav3sFWdwHIpfQDLy...", activated: true, activated_at: "2019-01-31 11:25:14", reset_digest: nil, reset_sent_at: nil> >> user_second = User.second User Load (0.1ms) SELECT "users".* FROM "users" ORDER BY "users"."id" ASC LIMIT ? OFFSET ? [["LIMIT", 1], ["OFFSET", 1]] => #<User id: 2, name: "Jordyn Heaney", email: "example-1@railstutorial.org", created_at: "2019-01-31 11:25:14", updated_at: "2019-01-31 11:25:14", password_digest: "$2a$10$3IGSHyPf/ofme0Fump8NF.4kP13rVb9UmiSRnNJkiLt...", remember_digest: nil, admin: false, activation_digest: "$2a$10$vJENgJrXHkTu3.d8/Bpz8OKUK.AJUW1objabSVuoWHE...", activated: true, activated_at: "2019-01-31 11:25:14", reset_digest: nil, reset_sent_at: nil> >> user.active_relationships.create(followed_id: user_second.id) (0.1ms) begin transaction User Load (0.2ms) SELECT "users".* FROM "users" WHERE "users"."id" = ? LIMIT ? [["id", 1], ["LIMIT", 1]] User Load (0.1ms) SELECT "users".* FROM "users" WHERE "users"."id" = ? LIMIT ? [["id", 2], ["LIMIT", 1]] SQL (2.1ms) INSERT INTO "relationships" ("follower_id", "followed_id", "created_at", "updated_at") VALUES (?, ?, ?, ?) [["follower_id", 1], ["followed_id", 2], ["created_at", "2019-02-04 18:18:22.015668"], ["updated_at", "2019-02-04 18:18:22.015668"]] (6.2ms) commit transaction => #<Relationship id: 1, follower_id: 1, followed_id: 2, created_at: "2019-02-04 18:18:22", updated_at: "2019-02-04 18:18:22">2:active_relationships.followerとactive_relationships.followedの値がそれぞれ正しいことを確認。
フォローしてるのが1で、フォローされてるのが2だと確認できる。
=> #<Relationship id: 1, follower_id: 1, followed_id: 2, created_at: "2019-02-04 18:18:22", updated_at: "2019-02-04 18:18:22">14.1.3 Relationshipのバリデーション
ここでRelationshipモデルの検証を追加して完全なものにしておく。
テストコードとアプリケーションコードを作って実装していく。
ただし、User用のfixtureファイルと同じように、生成されたRelationship用のfixtureでは、
マイグレーションで制約させた一意性を満たすことができない。このままだと正しくテストを行えないので、今の時点では、
生成されたRelationship用のfixtureファイルを空にしておく。fixtures/relationships.yml# 空にする
早速、簡単なテストとバリデーションを記入する。
relationship_test.rbrequire 'test_helper' class RelationshipTest < ActiveSupport::TestCase def setup @relationship = Relationship.new(follower_id: users(:michael).id, followed_id: users(:archer).id ) end test "should be valid" do assert @relationship.valid? end test "should require a follwer_id" do @relationship.follower_id = nil assert_not @relationship.valid? end test "should require a followed_id" do @relationship.followed_id = nil assert_not @relationship.valid? end endrelationship.rbclass Relationship < ApplicationRecord # 1対1の関連付け belongs_to :follower, class_name: "User" belongs_to :followed, class_name: "User" validates :follower_id, presence: true validates :followed_id, presence: true endこれでテストはパスする。
$ rails t演習
1:Relationshipモデルのvalidatesをコメントアウトしてもテストが成功することを確認。
relationship.rb# validates :follower_id, presence: true # validates :followed_id, presence: true end $ rails t 3 tests, 3 assertions, 0 failures, 0 errors, 0 skipsテストが成功する理由は、Rails5だと初期の時点でバリデーションが掛かってるから。
14.1.4 フォローしているユーザー
Relationshipの関連付けの核心
following
とfollowers
に取りかかる。今回は
has_many through
を使う。1人のユーザーにはいくつもの「フォローする」「フォローされる」といった関係性がある。
この関係性を多対多と呼ぶ。デフォルトの
has_many through
という関連付けでは、
Railsはモデル名(単数形)に対応する外部キーを探す。has_many :followeds, through: :active_relationshipsRailsは
followeds
というシンボルを見て、
これをfollowed
単数形に変え、
relationships
テーブルのfollowed_id
を使って対象のユーザーを取得してくる。しかし、
user.followeds
という使い方は英語としては不適切。代わりに、
user.following
という名前を使う。そのためには、Railsのデフォルトを上書きする必要がある。
ここでは:source
パラメーターを使って、following配列の元はfollowed idの集合である
ということを明示的にRailsに伝える。user.rbclass User < ApplicationRecord # 関連付け has_many :microposts, dependent: :destroy # 1対多の関連付け has_many :active_relationships, class_name: "Relationship", foreign_key: "follower_id", dependent: :destroy has_many :following, through: :active_relationships, source: :followed上記で定義した関連付けにより、フォローしているユーザーを配列の様に扱える様になった。
例えば、
include?
メソッドを使ってフォローしているユーザーの集合を調べてみたり、
関連付けを通してオブジェクトを探しだせるようになる。user.following.include?(other_user) user.following.find(other_user)
following
で取得したオブジェクトは、配列の様に要素を追加したり削除したりすることができる。user.following << other_user user.following.delete(other_user)<<演算子で配列の最後に追記することができる。
followingメソッドで配列の様に扱えるだけでも便利だが、
Railsは単純な配列ではなく、もっと賢くこの集合を扱っている。例えば次のようなコードではfollowing.include?(other_user)フォローしている全てのユーザーをDBから取得し、その集合に対して
include?
メソッドを時実行しているように見えるが、実際はDBの中で直接比較をするように配慮している。(other_userがいるかどうかの比較を行なっている)なお、次のようなコードでは
user.microposts.countDBの中で合計を計算した方が高速になる点に注意する。
次に、followingで取得した場合をより簡単に取り扱うために、
follow
やunfollow
といった便利メソッドを追加する。これらのメソッドは、例えば
user.follow(other_user)
といった具合に使う。さらに、これに関連する
following?
論理値メソッドも追加し、あるユーザーが誰かをフォローしているかどうかを確認できるようにする。今回は、こういったメソッドはテストから先に書いていく。
と言うのも、Webインターフェイスなどで便利メソッドを使うのはまだ先なので、すぐに使える場面がなく、実装した手応えを得にくいから。一方で、Usermモデルに対するテストは書くのは簡単かつ今すぐできるので、
先に書いていく。具体的には、
following?
メソッドであるユーザーをまだフォロしていないことを確認follow
メソッドを使ってそのユーザーをフォローできたことを確認unfollow
メソッドでフォロー解除できたことを確認といった具合でテストしていく。
user_test.rbtest "should follow and unfollow a user" do michael = users(:michael) archer = users(:archer) assert_not michael.following?(archer) michael.follow(archer) assert michael.following?(archer) michael.unfollow(archer) assert_not michael.following?(archer) end
following
による関連付けを使ってfollow
、unfollow
、following?
メソッドを実装していく。このとき、可能な限り
self
を省略している点に注目。user.rb# ユーザーをフォローする def follow(other_user) following << other_user end # ユーザーをフォロー解除する def unfollow(other_user) active_relationships.find_by(followed_id: other_user.id).destroy end # 現在のユーザーがフォローしてたらtrueを返す def following?(other_user) following.include?(other_user) end private上記コードを追加することで、テストはパスする。
13 tests, 19 assertions, 0 failures, 0 errors, 0 skips演習
1:コンソールを開き、
user_test.rb
のコードを順々に実行してみる。>> michael = User.find(3) User Load (0.3ms) SELECT "users".* FROM "users" WHERE "users"."id" = ? LIMIT ? [["id", 3], ["LIMIT", 1]] => #<User id: 3, name: "Brittany Schiller", email: "example-2@railstutorial.org", created_at: "2019-01-31 11:25:15", updated_at: "2019-01-31 11:25:15", password_digest: "$2a$10$/KeYm3kd5PfnTaPWl.o/q.yf4I.Q5iXW7K3oSqywWb0...", remember_digest: nil, admin: false, activation_digest: "$2a$10$QctyRqieo7GHcwqke8DSZOm/bbSlBeJ/66VLUF6eukO...", activated: true, activated_at: "2019-01-31 11:25:14", reset_digest: nil, reset_sent_at: nil> >> archer = User.find(4) User Load (0.2ms) SELECT "users".* FROM "users" WHERE "users"."id" = ? LIMIT ? [["id", 4], ["LIMIT", 1]] => #<User id: 4, name: "Zula Wilkinson", email: "example-3@railstutorial.org", created_at: "2019-01-31 11:25:15", updated_at: "2019-01-31 11:25:15", password_digest: "$2a$10$16RQ.clWpUqPfhNsAvTJmekbHJiU9fNmjpJ8Ar7FYEX...", remember_digest: nil, admin: false, activation_digest: "$2a$10$TwjgTo5YKF7QJKiT1aKJ2eliFXquMvkNXLwBvqRh/jg...", activated: true, activated_at: "2019-01-31 11:25:15", reset_digest: nil, reset_sent_at: nil> >> michael.following?(archer) User Exists (0.2ms) SELECT 1 AS one FROM "users" INNER JOIN "relationships" ON "users"."id" = "relationships"."followed_id" WHERE "relationships"."follower_id" = ? AND "users"."id" = ? LIMIT ? [["follower_id", 3], ["id", 4], ["LIMIT", 1]] => false >> michael.follow(archer) (0.1ms) begin transaction User Load (0.1ms) SELECT "users".* FROM "users" WHERE "users"."id" = ? LIMIT ? [["id", 3], ["LIMIT", 1]] SQL (5.7ms) INSERT INTO "relationships" ("follower_id", "followed_id", "created_at", "updated_at") VALUES (?, ?, ?, ?) [["follower_id", 3], ["followed_id", 4], ["created_at", "2019-02-05 16:36:58.158969"], ["updated_at", "2019-02-05 16:36:58.158969"]] (9.8ms) commit transaction User Load (0.2ms) SELECT "users".* FROM "users" INNER JOIN "relationships" ON "users"."id" = "relationships"."followed_id" WHERE "relationships"."follower_id" = ? LIMIT ? [["follower_id", 3], ["LIMIT", 11]] => #<ActiveRecord::Associations::CollectionProxy [#<User id: 4, name: "Zula Wilkinson", email: "example-3@railstutorial.org", created_at: "2019-01-31 11:25:15", updated_at: "2019-01-31 11:25:15", password_digest: "$2a$10$16RQ.clWpUqPfhNsAvTJmekbHJiU9fNmjpJ8Ar7FYEX...", remember_digest: nil, admin: false, activation_digest: "$2a$10$TwjgTo5YKF7QJKiT1aKJ2eliFXquMvkNXLwBvqRh/jg...", activated: true, activated_at: "2019-01-31 11:25:15", reset_digest: nil, reset_sent_at: nil>]> >> michael.following?(archer) User Exists (0.2ms) SELECT 1 AS one FROM "users" INNER JOIN "relationships" ON "users"."id" = "relationships"."followed_id" WHERE "relationships"."follower_id" = ? AND "users"."id" = ? LIMIT ? [["follower_id", 3], ["id", 4], ["LIMIT", 1]] => true >> michael.unfollow(archer) Relationship Load (0.3ms) SELECT "relationships".* FROM "relationships" WHERE "relationships"."follower_id" = ? AND "relationships"."followed_id" = ? LIMIT ? [["follower_id", 3], ["followed_id", 4], ["LIMIT", 1]] (0.1ms) begin transaction SQL (3.3ms) DELETE FROM "relationships" WHERE "relationships"."id" = ? [["id", 2]] (10.6ms) commit transaction => #<Relationship id: 2, follower_id: 3, followed_id: 4, created_at: "2019-02-05 16:36:58", updated_at: "2019-02-05 16:36:58"> >> michael.following?(archer) User Exists (0.3ms) SELECT 1 AS one FROM "users" INNER JOIN "relationships" ON "users"."id" = "relationships"."followed_id" WHERE "relationships"."follower_id" = ? AND "users"."id" = ? LIMIT ? [["follower_id", 3], ["id", 4], ["LIMIT", 1]] => false2:先ほどの演習の各コマンド実行時の結果を見返してみて、実際にはどんなSQLが出力されたのか確認してみる。
上記で確認できる。
14.1.5 フォロワー
リレーションシップに
user.followers
メソッドを追加する。
user.following
はフォローしている人数
user.followers
はフォローされてる人数(フォロワー)である。フォロワーの配列を展開するために必要な情報は、relationshipsテーブルに既にあり、
active_relationships
テーブルを再利用することで出来る。実際、
follower_id
とfollowed_id
を入れ替えるだけで、
フォロワーについてもフォローする場合と全く同じ活用が出来る。データモデルは以下。
出典:図 14.9: Relationshipモデルのカラムを入れ替えて作った、フォロワーのモデル
要は、
active_relationships
をpassive_relationships
に入れ替えて、
followed_id
とfollower_id
を入れ替えるだけ。上記のデータモデルの実装を
user.rb
にhas_manyを使って行う。user.rbclass User < ApplicationRecord # 関連付け has_many :microposts, dependent: :destroy # 1対多の関連付け has_many :active_relationships, class_name: "Relationship", foreign_key: "follower_id", dependent: :destroy has_many :passive_relationships, class_name: "Relationship", foreign_key: "followed_id", dependent: :destroy # 多対多の関連付け has_many :following, through: :active_relationships, source: :followed has_many :followers, through: :passive_relationships, source: :follower一点、上記で注意すべき箇所は次の様に参照先(followers)を指定するための
:source
キーを省略してもよかった点。has_many :followers, through: :passive_relationshipsこれは、
:followers
属性の場合、
Railsがfollowers
を単数形にして自動的に外部キーfollower_id
を探してくれるから。ただ、必要がないが
has_many :following
との類似性を強調させるために書いている。次に、
followers.include?
メソッドを使って先ほどのデータモデルをテストしていく。テストコードは以下の通り。ちなみに
following?
と対照的なfollowed_by?
メソッドを定義してもよかったが、
サンプルアプリケーションで実際に使う場面がなかったのでこのメソッドは省略している。user_test.rbtest "should follow and unfollow a user" do michael = users(:michael) archer = users(:archer) assert_not michael.following?(archer) michael.follow(archer) assert michael.following?(archer) assert archer.followers.include?(michael) michael.unfollow(archer) assert_not michael.following?(archer) end↑archerのフォロワーにmichaelは含まれているかどうかテストしている。
上記のテストは実際には多くの処理が正しく動いていなければパスしない。
つまり、受動的関係に対するテストは実装の影響を受けやすい。この時点で、全てのテストはパスする。
13 tests, 20 assertions, 0 failures, 0 errors, 0 skips演習
1:コンソールで、何人かのユーザーが最初のユーザーをフォローしている状況作ってみる。
最初のユーザーをuser
とすると、user.followers.map(&:id)
のidの値はどのようになっているか?>> user = User.first >> user_second = User.second >> user.followers.map(&:id) " = ? [["followed_id", 1]] => [2, 3]2:
user.followers.count
の実行結果が、先ほどフォローさせたユーザー数と一致していることを確認。>> user.followers.count (0.3ms) SELECT COUNT(*) FROM "users" INNER JOIN "relationships" ON "users"."id" = "relationships"."follower_id" WHERE "relationships"."followed_id" = ? [["followed_id", 1]] => 23:
user.followers.count
を実行した結果、出力されるSQL文はどのような内容になっているか?
また、user.followers.to_a.count
の実行結果と違っている箇所はあるか?
100万人ユーザーがフォロワーにいた場合はどうなるか?(0.3ms) SELECT COUNT(*) FROM "users" INNER JOIN "relationships" ON "users"."id" = "relationships"."follower_id" WHERE "relationships"."followed_id" = ? [["followed_id", 1]] >> user.followers.to_a.count => 2フォロワーが100万人いたらそのまま100万と言う数値が返されるが配列を生成する為、時間が掛かるしDBにも負担が掛かる。
14.2 FollowのWebインターフェイス
これまでやや複雑なデータモデリングの技術を駆使して実装した。
次は、モックアップで示したようにフォロー/フォロー解除の基本的なインターフェイスを実装する。
また、フォローしているユーザーと、フォロワーにそれぞれ表示用のページを作成する。後に、ユーザーのステータスフィードを追加して、サンプルアプリケーションを完成させる。
14.2.1 フォローのサンプルデータ
前章と同じように、サンプルデータを自動生成する
rails db:seed
を使って、DBにサンプルデータを登録できるとやや便利。先にサンプルデータを自動生成出来るようにしておけば、Webページの見た目のデザインから先に取り掛かることができ、バックエンド機能の実装を後に回すことが出来る。
リレーションシップのサンプルデータを生成するためのコードをseedに書いていく。
ここでは、最初のユーザーにユーザー3からユーザー51をフォローさせ、それから逆にユーザー4からユーザー41に最初のユーザーをフォローさせる。seed.rb$ rails db:migrate:reset $ rails db:seed演習
1:コンソールを開き、
User.first.followers.count
の結果がリスト14.14で期待している結果と合致していることを確認。>> User.first.followers.count User Load (0.2ms) SELECT "users".* FROM "users" ORDER BY "users"."id" ASC LIMIT ? [["LIMIT", 1]] (0.3ms) SELECT COUNT(*) FROM "users" INNER JOIN "relationships" ON "users"."id" = "relationships"."follower_id" WHERE "relationships"."followed_id" = ? [["followed_id", 1]] => 382:
User.first.following.count
の結果も合致していることを確認。>> User.first.following.count User Load (0.2ms) SELECT "users".* FROM "users" ORDER BY "users"."id" ASC LIMIT ? [["LIMIT", 1]] (0.3ms) SELECT COUNT(*) FROM "users" INNER JOIN "relationships" ON "users"."id" = "relationships"."followed_id" WHERE "relationships"."follower_id" = ? [["follower_id", 1]] => 4914.2.2 統計と[Follow]フォーム
これでサンプルユーザに、フォローしているユーザーとフォロワーができました。
プロフィールページとHomeページを更新して、これを反映する。最初に、プロフィールページとHomeページに、
フォローしているユーザーとフォロワーの統計情報を表示するためのパーシャルを作成する。次に、フォローしているユーザーの一覧(following)と
フォロワーの一覧(followers)を表示する専用のページを作成する。Twitterの慣習にしたがってフォロー数の単位には
following
を使い、
例えば50 following
といった具合に表示する。上記の統計情報には、現在のユーザーがフォローしている人数と、
現在のフォロワーの人数が表示されている。それぞれの表示はリンクになっており、専用の表示ページに移動できる。
これらのリンクはダミーテキスト
#
を使って無効にしていた。
しかし、ルーティングについての知識もだいぶ増えてきたので、今回は実装することにする。実際のページ作成は後にルーティングは今実装する。
このコードでは、
resources
ブロックの内側で:member
メソッドが使っている。
これは初登場のメソッドだが、まずはどんな動作するのか推測してみる。routes.rbRails.application.routes.draw do root 'static_pages#home' get '/help', to: 'static_pages#help' get '/about', to: 'static_pages#about' get '/contact', to: 'static_pages#contact' get '/signup', to: 'users#new' post '/signup', to: 'users#create' get '/login', to: 'sessions#new' post '/login', to: 'sessions#create' delete '/logout', to: 'sessions#destroy' resources :users do # usersリソースをRESTfullな構造にするためのコード。 member do get :following, :followers end end resources :account_activations, only: [:edit] # editアクションのみaccount_activationsリソースを適用 resources :password_resets, only: [:new, :create, :edit, :update] # password再設定用のリソースを適用 resources :microposts, only: [:create, :destroy] # micropostsリソースをcreateとdestroyアクションにのみ適用 endこの場合のURLは
/users/1/following
や/users/1/followers
のようになるのではないかと推測。また、どちらもデータを表示するページなので、適切なHTTPメソッドはGETリクエストになる。
したがって、getメソッドを使って適切なレスポンスを返す。
ちなみに、memberメソッドを使うとユーザーidが含まれているURLを扱うようになる。
idを指定せずに全てのメンバーを表示するには、次のように
collection
メソッドを使う。resources :users do collection do get :tigers end endこのコードは
/users/tiggers
というURLに応答する。
生成されるルーティングテーブルは以下。この表で示したフォロー用とフォロワー用の名前付きルートを、今後の実装で使う。
HTTPリクエスト URL アクション 名前付きルート
GET /users/1/following following following_user_path(1)
GET /users/1/followers followers followers_user_path(1)出典:表 14.2: カスタムルールで提供するリスト 14.15のRESTfulルート
ルーティングを定義したので、統計情報のパーシャルを実装する準備が整った。
このパーシャルでは、divタグの中に2つのリンクを含めるようにする。_stats.html.erb<% @user ||= current_user %> <div class="stats"> <a href="<%= following_user_path(@user) %>"> <strong id="following" class="stat"> <%= @user.following.count %> </strong> following </a> <a href="<%= followers_user_path(@user) %>"> <strong id="followers" class="stat"> <%= @user.followers.count %> </strong> followers </a> </div>このパーシャルはプロフィールページとHomeページの両方に表示されるので、
最初の行では、次のコードで現在のユーザーを取得する。<% @user ||= current_user %>@userがnilでない場合(つまりプロフィールページ)は何もせず、
nilの場合には@userにcurrent_userに代入するコードである。その後、フォローしているユーザーの人数を、次のように関連付けを使って計算する。
@user.following.countこれはフォロワーについても同様。
@user.microposts.countなお、今回も以前と同様に、Railsは高速化のためにDB内で合計を計算している点に注意。
一部の要素で、次のようにCSS idを指定していることにも注目。
<strong id="following" class="stat"> </strong>こうしておくと、Ajaxを実装するときに便利です。
そこでは、一意のidを指定してページ要素にアクセスしている。これで統計情報パーシャルが出来上がる。Homeページにこの統計情報を表示するには、以下のようにすると良い。
home.html.erb<% @user ||= current_user %> <div class="stats"> <a href="<%= following_user_path(@user) %>"> <strong id="following" class="stat"> <%= @user.following.count %> </strong> following </a> <a href="<%= followers_user_path(@user) %>"> <strong id="followers" class="stat"> <%= @user.followers.count %> </strong> followers </a> </div>統計情報にスタイルを与えるために、SCSSを追加する。
変更の結果、Homeページは以下のようにする。
custom.scss/* sidebar */ .gravatar { float: left; margin-right: 10px; } .gravatar_edit { margin-top: 15px; } .stats { overflow: auto; margin-top: 0; padding: 0; a { float: left; padding: 0 10px; border-left: 1px solid &gray-lighter; color: gray; &:first-child { padding-left: 0; border: 0; } $:hover { text-decoration: none; color: blue; } } strong { display: block; } } .user_avatars { overflow: auto; margin-top: 10px; .gravatar { margin: 1px 1px; } a { padding: 0; } } .users.follow { padding: 0; }出典:図 14.11: Homeページにフォロー関連の統計情報を表示する
この後すぐ、プロフィールにも統計情報パーシャルを表示するが、
今のうちに[Follow]/[Unfollow]ボタン用のパーシャルを作成する。_follow_form.html.erb<!--現在のユーザーがURLのユーザーとは違う場合--> <% unless current_user?(@user) %> <div id="follow_form"> <% if current_user.following?(@user) %> <%= render 'unfollow' %> <% else %> <%= render 'follow' %> <% end %> </div> <% end %>このコードは、followとunfollowのパーシャルに作業を振っているだけ。
(urlのユーザーをログインユーザーがフォローしていればunfollow,フォローしていなければfollowをレンダリング)パーシャルでは、Relationshipsリソース用の新しいルーティングが必要。
これを、Micropostsリソースの例に従って作成する。routes.rbRails.application.routes.draw do root 'static_pages#home' get '/help', to: 'static_pages#help' get '/about', to: 'static_pages#about' get '/contact', to: 'static_pages#contact' get '/signup', to: 'users#new' post '/signup', to: 'users#create' get '/login', to: 'sessions#new' post '/login', to: 'sessions#create' delete '/logout', to: 'sessions#destroy' resources :users do member do get :following, :followers end end resources :users # usersリソースをRESTfullな構造にするためのコード。 resources :account_activations, only: [:edit] # editアクションのみaccount_activationsリソースを適用 resources :password_resets, only: [:new, :create, :edit, :update] # password再設定用のリソースを適用 resources :microposts, only: [:create, :destroy] # micropostsリソースをcreateとdestroyアクションにのみ適用 resources :relationships, only: [:create, :destroy] # endフォロー/フォロー解除用のパーシャルも書く。
_follow.html.erb<%= form_for(current_user.active_relationships.build) do |f| %> <div><%= hidden_field_tag :followed_id, @user.id %></div> <%= f.submit "Follow", class: "btn btn-primary" %> <% end %>_unfollow.html.erb<%= form_for(current_user.active_relationships.find_by(followed_id: @user.id), html: { method: :delete }) do |f| %> <%= f.submit "Unfollow", class: "btn" %> <% end %>これら2つのフォームでは、いずれも
form_for
を使ってRelationshipモデルオブジェクトを操作している。これらの2つのフォームの主な違いは、フォローフォームでは新しいリレーションシップを作成するのに対し、
アンフォローフォームでは既存のリレーションシップを見つけ出すという点。すなわち、前者はPOSTリクエストをRelationshipsコントローラに送信してリレーションシップをcreate(作成)し、
後者はDELETEリクエストを送信してリレーションシップをdestroy(削除)するということ。最終的に、このフォロー/アンフォローフォームにはボタンしかないことが理解できる。
しかし、それでもフォローフォームでは
followed_id
をコントローラに送信する必要がある。
これを行うために、hidden_field_tag
メソッドを使う。このメソッドは、次のフォーム用HTMLを生成する。
<input id="followed_id" name="followed_id" type="hidden" value="3" />12章で見たように、隠しフィールドのinputタグを使うことで、ブラウザ上に表示させずに適切な情報を含めることができる。
これでパーシャルとしてフォロー用フォームをプロフィールページに表示できるようになった。
showビューに表示用のhtmlを書く。
show.html.erb</section> <section class="stats"> <%= render 'shared/stats' %> </section> </aside> <div class="col-md-8"> <%= render 'follow_form' if logged_in? %> <% if @user.microposts.any? %>プロフィールには、それぞれ[Follow][Unfollow]ボタンが表示される。
これらのボタンを実装するには、二通りの方法がある。
1つは標準的な方法、もう1つはAjaxを使う方法。でもその前に、フォローしているユーザーとフォロワーを表示するページをそれぞれ作成してHTMLインターフェイスを完成させる。
演習
1:ブラウザから
/users/2
にアクセスし、フォローボタンが表示されていることを確認する。
同様に、/users/5ではUnfollow]ボタンが表示されているはず。さて、
/users/1
にアクセスすると、どのような結果が表示されるか?users/1はログインユーザーなのでボタンが消える。
2:ブラウザからHomeページとプロフィールページを表示してみて、統計情報が正しく表示されているか確認。
確認済み。
3:Homeページに表示されている統計情報に対してテストを書いてみる。
同様にして、プロフィールページにもテストを追加してみる。site_layout_test.rbtest "count relationships" do log_in_as(@user) get root_path assert_match @user.active_relationships.count.to_s, response.body assert_match @user.passive_relationships.count.to_s, response.body endusers_profile_test.rbend assert_select @user.microposts.count assert_match @user.active_relationships.to_s, response.body assert_match @user.passive_relationships.to_s, response.body endテストがパスしたのでOK。
14.2.3 [Following][Followers]ページ
フォローしているユーザーを表示するページと、フォロワーを表示するページは、
いずれもプロフィールページとユーザー一覧ページを合わせたような作りになるという点で似ている。どちらにもフォローの統計情報などのユーザー情報を表示するサイドバーと、ユーザーのリストがある。
さらに、サイドバーには小さめのユーザープロフィール画像のリンクを格子状に並べて表示する。
出典:図 14.14: フォローしているユーザー用ページのモックアップ
出典:図 14.15: ユーザーのフォロワー用ページのモックアップ
ここでの最初の作業は、フォローしているユーザーのリンクとフォロワーのリンクを動くようにすること。
Twitterに倣って、どちらのページでもユーザーのログインを要求するようにする。
そこで前回のアクセス制御と同様に、まずはテストから書いていく。
今回使うテストは以下の通り。
上記コードでは
following/followers
の名前付きルートを使っている点に注意。users_controller_test.rbtest "should redirect following when not logged in" do get following_user_path(@user) assert_redirected_to login_url end test "should redirect followers when not logged in" do get followers_user_path(@user) assert_redirected_to login_url endこの実装には1つだけトリッキーな部分がある。
それはUsersコントローラに2つの新しいアクションを追加する必要があるということ。これはroutesで定義した2つのルーティングに基づいており、これらはそれぞれ
following
およびfollowers
と呼ぶ必要がある。それぞれのアクションでは、タイトルを認定し、ユーザーを検索し、@user.followingまたは
@user.follower
sからデータを取り出し、ページネーションを行なって、ページを出力する必要がある。users_controller.rbdef following @title = "Following" @user = User.find(params[:id]) @users = @user.following.paginate(page: params[:page]) render 'show_follow' end def followers @title = "Followers" @user = User.find(params[:id]) @users = @user.followers.paginate(page: params[:page]) render 'show_follow' endこれまで見てきたように、
Railsは慣習に従って、アクションに対応するビューを暗黙的に呼び出す。例えば、
show
アクションの最後でshow.html.erb
を呼び出す、といった具合。一方で、上記のいずれのアクションも
render
を明示的に呼び出し、show_follow
という同じビューを出力している。
したがって、作成が必要なビューはこれ1つ。renderで呼び出しているビューが同じである理由は、このERBはどちらの場合でもほぼ同じであり、
1つのファイルで両方の場合をカバーできるから。show_follow.html.erb<% provide(:title, @title) %> <div class="row"> <aside class="col-md-4"> <section class="user_info"> <%= gravatar_for @user %> <h1><%= @user.name %></h1> <span><%= link_to "view my profile", @user %></span> <span><b>Microposts:</b> <%= @user.microposts.count %></span> </section> <section class="stats"> <%= render 'shared/stats' %> <% if @users.any? %> <div class="user_avatars"> <% @users.each do |user| %> <%= link_to gravatar_for(user, size: 30), user %> <% end %> </div> <% end %> </section> </aside> <div class="col-md-8"> <h3><%= @title %></h3> <% if @users.any? %> <ul class="users follow"> <%= render @users %> </ul> <%= will_paginate %> <% end %> </div> </div>
users_controller
では、
- followingアクションで
following
を通してshow_follow
ビューを呼び出し、- followersアクションでは
followers
を通してshow_follow
ビューを呼び出す。この時、上記コードでは現在のユーザーを一切使っていないので、
他のユーザーのフォロワー一覧ページもうまく動く。beforeフィルターを既に実装しているため、テストはパスする。
12 tests, 21 assertions, 0 failures, 0 errors, 0 skips次に、
show_follow
の描画結果を確認するため、統合テストを書いていく。
ただし、今回は基本的なテストだけに留めておき、網羅的なテストにはしない。これはHTML構造を網羅的にチェックするテストは壊れやすく、生産性を落としかねないから。
したがって今回は、
正しい数が表示されているかどうかと、正しいURLが表示されているかどうかの2つのテストを書く。いつものように統合テストを生成するところから始める。
$ rails g integration_test following Running via Spring preloader in process 8224 invoke test_unit create test/integration/following_test.rb次に、テストデータをいくつか揃える。
リレーションシップ用のfixtureにデータを追加する。次のように書くことで
orange: content: "I just ate an orange!" created_at: <%= 10.minutes.ago %> user: michaelユーザーとマイクロポストは関連付けできる。
ユーザー名を書かずに
user: michaelではなく
user_id: 1このようなユーザーidを指定しても関連付けできる。
この例を参考に、Relationship用のfixtureにテストデータを追加する。relationships.ymlone: follower: michael followed: lana two: follower: michael followed: malory three: follower: lana followed: michael four: follower: archer followed: michael上記のfixtureでは、
前半の2つでMichaelがLanaとMaloryをフォローし、
後半の2つでLanaとArcherがMichaelをフォローしている。あとは、正しい数かどうかを確認するために、
assert_match
メソッドを使ってプロフィール画面のマイクロポスト数をテストする。さらに、正しいURLかどうかをテストするコードも加えると、以下のようになる。
following_test.rbdef setup @user = users(:michael) log_in_as(@user) end test "following page" do get following_user_path(@user) assert_not @user.following.empty? assert_match @user.following.count.to_s, response.body @user.following.each do |user| assert_select "a[href=?]", user_path(user) end end test "followers page" do get followers_user_path(@user) assert_not @user.followers.empty? assert_match @user.followers.count.to_s, response.body @user.followers.each do |user| assert_select "a[href=?]", user_path(user) end end上記では
assert_not @user.following.empty?このようなコードを書いているが、これは次のコードを確かめる為のテスト。
@user.following.each do |user| assert_select "a[href=?]", user_path(user) endつまり、
@user.following
の結果がtrueであれば、上記のブロックが実行できなくなる為、
その場合においてテストが適切なセキュリティモデルを確認できなくなることを防いでいる。上の変更を加えるとテストが成功する筈。
66 tests, 324 assertions, 0 failures, 0 errors, 0 skips演習
1:ブラウザから
/users/1/followers
と/users/1/following
を開き、それぞれが適切に表示されていることを確認。
サイドバーにある画像は、リンクとしてうまく機能しているか?OK
2:
following_test
のassert_select
関連のコードをコメントアウトしてみて、正しくテストが失敗することを確認。show_html.erb<% @users.each do |user| %> <%= #link_to gravatar_for(user, size: 30), user %> <% end %> $ rails t app/controllers/users_controller.rb:61:in `followers' test/integration/following_test.rb:20:in `block in <class:FollowingTest>' app/controllers/users_controller.rb:54:in `following' test/integration/following_test.rb:11:in `block in <class:FollowingTest>' 66/66: [=================================================================================================================] 100% Time: 00:00:02, Time: 00:00:02 Finished in 2.39352s 66 tests, 318 assertions, 0 failures, 2 errors, 0 skips12.2.4 [Follow]ボタン(基本編)
ビューが整ってきた。
いよいよ[Follow]/[Unfollow]ボタンを動作させる。フォローとフォロー解除はそれぞれリレーション湿布の作成と削除に対応しているため、まずはRelationshipsコントローラが必要。
いつものようにコントローラを生成させる。$ rails g controller RelationshipsRelationshipsコントローラのアクションでアクセス制御することはそこまで難しくない。
しかし、前回のアクセス制御の時と同様に最初にテストを書き、それをパスするように実装することでセキュリティモデルを確立させていく。
今回はまず、コントローラのアクションにアクセスする時、ログイン済みのユーザーであるかどうかをチェックする。もしログインしていなければログインページにリダイレクトされるので、Relationshipのカウントが変わっていないことを確認する。
relationships_controller_test.rbrequire 'test_helper' class RelationshipsControllerTest < ActionDispatch::IntegrationTest test "create should require logged-in user" do assert_no_difference 'Relationship.count' do post relationships_path end assert_redirected_to login_url end test "destroy should require logged-in user" do assert_no_difference 'Relationship.count' do delete relationship_path(relationships(:one)) end assert_redirected_to login_url end end次に、上記のテストをパスさせるために、
logged_in_user
フィルターを
Relationshipsコントローラのアクションに対して追加する。relationships_controller.rbclass RelationshipsController < ApplicationController before_action :logged_in_user def create end def destroy end end[Follow]/[Unfollow]ボタンを動作させるためには、フォームから送信されたパラメータを使って、
followed_id
に対応するユーザーを見つけてくる必要がある。その後、見つけてきたユーザーに対して適切に
follow/unfollow
メソッド(Userモデルで定義した)を使う。relationships_controller.rbclass RelationshipsController < ApplicationController before_action :logged_in_user def create user = User.find(params[:followed_id]) current_user.follow(user) redirect_to user end def destroy user = Relationship.find(params[:id]).followed current_user.unfollow(user) redirect_to user end end上記をみてれば、先ほどのセキュリティ問題が実はそれほど重要なものではないことを理解できる。
もし、ログインしていないユーザーが(curlなどのコマンドラインツールなどを使って)これらのアクションに直接アクセスするようなことがあれば、
current_user
はnil
になり、
どちらのメソッドでも2行目で例外が発生する。エラーにはなるが、アプリケーションやデータに影響は生じない。
このままでも支障はないが、このような例外には頼らない方がいいので、セキュリティの為のレイヤーを追加した。
これで、フォロー/フォロー解除の機能が完成した。
どのユーザーも、他のユーザーをフォローしたりリフォローしたりできる。
ブラウザ上でボタンをクリックして、確かめてみる。
(振る舞いを検証する統合テストはのちに実装する)フォローしていないユーザーの画面
ユーザーをフォローした結果
演習
1:ブラウザ上から/users/2のFollow/Unfollow実行して動いているか確認
確認済み。
2:先ほどの演習を終えたら、Railsサーバーのログを見てみる。
フォロー/フォロー解除が実行されると、それぞれどのテンプレートが描画されているか?Started GET "/users/2" for 122.50.45.13 at 2019-02-07 19:55:41 +0000 Cannot render console from 122.50.45.13! Allowed networks: 127.0.0.1, ::1, 127.0.0.0/127.255.255.255 Processing by UsersController#show as HTML Parameters: {"id"=>"2"} User Load (0.1ms) SELECT "users".* FROM "users" WHERE "users"."id" = ? LIMIT ? [["id", 2], ["LIMIT", 1]] Rendering users/show.html.erb within layouts/applicationフォローした場合、
/users/2
のビューが描画されているStarted GET "/users/2" for 122.50.45.13 at 2019-02-07 19:56:44 +0000 Cannot render console from 122.50.45.13! Allowed networks: 127.0.0.1, ::1, 127.0.0.0/127.255.255.255 Processing by UsersController#show as HTML Parameters: {"id"=>"2"} User Load (0.1ms) SELECT "users".* FROM "users" WHERE "users"."id" = ? LIMIT ? [["id", 2], ["LIMIT", 1]] Rendering users/show.html.erb within layouts/applicationフォロー解除した場合、
/users/2
のビューが描画されている。14.2.5 [Follow]ボタン(Ajax編)
フォロー関連の機能の実装は完了したが、ステータスフィードに取りかかる前にもう1つだけ機能を洗練させてみる。
先ほどは
Relationships
コントローラのcreateアクション
とdestroyアクション
を単に元のプロフィールにリダイレクトしていた。
つまり、①ユーザーはプロフィールページを最初に表示
②ユーザーをフォロー
③すぐ元のページにリダイレクトという流れになる。
ユーザーをフォローした後、本当にそのページから離れて元のページに戻らないといけないのか。
答えは否(ハンターハンター風)で、同じページにリダイレクトさせる必要はない。この問題は、
Ajax
を使えば解決できる。Ajaxを使うことで、Webページからサーバーに「非同期」でページを遷移させることなくリクエストを送信することができる。
WebフォームにAjaxを採用するのは今や当たり前で、RailsでもAjaxを簡単に実装できるようになっている。
フォロー用とフォロー解除用のパーシャルをこれに沿って更新するのは簡単。
例えば、次のコードがあるとすると
form_for
上のコードを次のように置き換えるだけ
form_for ..., remote: trueこれだけでRailsは自動的にAjaxを使うようになる。
_follow.html.erb<%= form_for(current_user.active_relationships.build, remote: true) do |f| %> <div><%= hidden_field_tag :followed_id, @user.id %></div> <%= f.submit "Follow", class: "btn btn-primary" %> <% end %>_unfollow.html.erb<%= form_for(current_user.active_relationships.find_by(followed_id: @user.id), html: { method: :delete }, remote: true) do |f| %> <%= f.submit "Unfollow", class: "btn" %> <% end %>ERbによって実際に生成されるHTMLはこちら
<form action="/relationships/117" class="edit_relationship" data-remote="true" id="edit_relationship_117" method="post" > </form>ここでは、formタグの内部で
data-remote="true"
を設定している。これは、JavaScriptによるフォーム操作を許可することをRailsに知らせるためのもの。
(現在のRailsではHTMLプロパティを使って簡単にAjaxが扱えるようになっている)フォームの更新が終わったので、今度はこれに対応するRelationshipsコントローラを改造して、
Ajaxリクエストに応答できるようにする。こういったリクエストの種類によって応答を場合分けする時は、
respond_to
メソッドを使う。respond_to do |format| format.html { redirect_to user } format.js end上記のブロック内のコードのうち、いずれかの1行が実行されるという点が重要。
このため、
respond_to
メソッドは、上から順に逐次処理(シリアル)というより、
:if文を使った分岐処理に近いイメージ*RelationshipsコントローラでAjaxに対応させるために、
respond_to
メソッドをcreateアクションとdestroyアクションにそれぞれ追加してみる。この時、ユーザーのローカル変数(user)を@userに変更している点に注目。
これは、
_follow.html.erb
と_unfollow.html.erb
を実装したことにより、
ビューで変数を使うインスタンス変数が必要になったからである。
(Ajaxによる)relationships_controller.rbclass RelationshipsController < ApplicationController before_action :logged_in_user def create @user = User.find(params[:followed_id]) current_user.follow(@user) respond_to do |format| format.html { redirect_to @user } format.js end end def destroy @user = Relationship.find(params[:id]).followed current_user.unfollow(@user) respond_to do |format| format.html { redirect_to @user } format.js end end endAjaxリクエストに対応したので、今度はブラウザ側でJavaScriptが無効になっていた場合でもうまく動くようにする。
application.rbclass Application < Rails::Application # Initialize configuration defaults for originally generated Rails version. config.load_defaults 5.1 # Settings in config/environments/* take precedence over those specified here. # Application configuration should go into files in config/initializers # -- all .rb files in that directory are automatically loaded. # 認証トークンをremoteフォームに埋め込む config.action_view.embed_authenticity_token_in_remote_forms = true end end一方で、JavaScriptが有効になっていても、まだ十分に対応できていない部分がある。
というのも、Ajaxリクエストを受診した場合は、Railsが自動的にアクションと同じ名前を持つ
JavaScript用の埋め込みRubyファイル(create.js.erb destroy.js.erb)などを呼び出す為、これらのファイルを作成する必要がある。.js.erbでは、JSと埋め込みRubyをミックスして現在のページに対するアクションを実行することができる。
ユーザーをフォローしたときや、フォロー解除した時にプロフィールページを更新するために、
これらのファイルが使われる。
JS-ERb
ファイルの内部では、DOM(Document Object Model)を使ってページ操作するため、
RailsがjQuery JavaScriptヘルパーを自動的に提供している。これによりjQueryライブラリの膨大なDOM操作用メソッドが使えるようになるが、
今回使うのはわずか2つ。まず1つ目は、$とCSS idを使って、DOM要素にアクセスする文法について知る必要がある。
例えば、follow_formの要素をjQueryで操作するには、次のようにアクセスする。$("#follow_form")これはフォームを囲むdivタグであり、フォームそのものではない。
jQueryの文法はCSSの記法から影響を受けており、#シンボルを使ってCSSのidを指定する。
jQueryはCSSと同様、ドット.を使ってCSSクラスを操作できる。
次に必要なメソッドはhtml。
これは、引数の中で指定された要素の内側にあるHTMLを更新する。例えば、フォロー用フォーム全体を"foobar"という文字列で置き換えたい場合は、次のようなコードになる。
$("#follow_form").html("foobar")純粋なJavaScriptと異なり、JS-ERbファイルでは組み込みRuby(ERb)が使える。
create.js.erb
ファイルでは、フォロー用のフォームをunfollowパーシャルで更新し、フォロワーのカウントを更新するのにERbを使っている。このコードでは
escape_javascript
メソッドを使っている点に注目。このメソッドは、
JavaScriptファイル内にHTMLを挿入する時に実行結果をエスケープするために必要。create.js.erb$("#follow_form").html("<%= escape_javascript(render('users/unfollow')) %>"); $("#followers").html('<%= @user.followers.count %>');各行の末尾にセミコロン;があることに注目。
(これは1950年代中頃に開発されたALGOLまで遡るらしい)
destroy.js.erb
ファイルの方も同様です。create.js.erb$("#follow_form").html("<%= escape_javascript(render(`users/unfollow`)) %>"); $("#followers").html('<%= @user.followers.count %>');destroy.js.erb$("#follow_form").html("<%= escape_javascript(render(`users/follow`)) %>"); $("#followers").html('<%= @user.followers.count %>');これらのコードにより、プロフィールページを更新させずにフォローとフォロー解除できるようになった筈。
演習
1:ブラウザから
/users/2
にアクセスし、うまく動いているかどうか確認。確認済み。
2:先ほどの演習で確認が終わったら、Railsサーバーのログを閲覧し、フォロー/フォロー解除を実行した直後のテンプレートがどうなっているか確認。
(0.0ms) begin transaction CACHE User Load (0.0ms) SELECT "users".* FROM "users" WHERE "users"."id" = ? LIMIT ? [["id", 1], ["LIMIT", 1]] SQL (2.2ms) INSERT INTO "relationships" ("follower_id", "followed_id", "created_at", "updated_at") VALUES (?, ?, ?, ?) [["follower_id", 1], ["followed_id", 2], ["created_at", "2019-02-08 00:24:36.494125"], ["updated_at", "2019-02-08 00:24:36.494125"]] (8.0ms) commit transaction Rendering relationships/create.js.erb Relationship Load (0.1ms) SELECT "relationships".* FROM "relationships" WHERE "relationships"."follower_id" = ? AND "relationships"."followed_id" = ? LIMIT ? [["follower_id", 1], ["followed_id", 2], ["LIMIT", 1]] Rendered users/_unfollow.html.erb (2.0ms) (0.1ms) begin transaction SQL (2.8ms) DELETE FROM "relationships" WHERE "relationships"."id" = ? [["id", 100]] (8.5ms) commit transaction Rendering relationships/destroy.js.erb Rendered users/_follow.html.erb (1.2ms)きちんと
js.erb
ファイルがレンダリングされている。14.2.6 フォローをテストする
フォローボタンが動くようになったので、バグを検知する為のシンプルなテストを書いていく。
ユーザーのフォローに対するテストでは、
/relationships
に対してPOSTリクエストを送り、
フォローされたユーザーが1人増えたことをチェックする。具体的なコードは
assert_difference '@user.following.count', 1 do post relationships_path, params: { followed_id: @other.id } endこれは標準的なフォローに対するテスト。
ただ、Ajax版もやり方はだいたい同じ。Ajaxのテストでは、xhr :trueオプションを使うようにするだけ。
assert_difference '@user.following.count', 1 do post relationships_path, params: { followed_id: @other.id }, xhr: true endここで使っているxhr(XlHttpRequest)というオプションをtrueにすると
Ajaxでリクエストを発行するよに変わる。したがって、
respond_to
では、JavaScriptに対応した行が実行されるようになる。また、ユーザーをフォロー解除する時も構造は殆ど同じで、postメソッドをdeleteメソッドに置き換えてテストする。
つまり、そのユーザーのidとリレーションシップのidを使ってDELETEリクエストを送信し、フォローしている数が1つ減ることを確認する。
したがって、実際に加えるテストは
assert_difference '@user.following.count', -1 do delete relationship_path(relationship) end上の従来通りのテストと、下のAjax用のテストの2つになる。
assert_difference '@user.following.count', -1 do delete relationship_path(relationship), xhr: true endこれらのテストをまとめた結果
following_test.rbrequire 'test_helper' class FollowingTest < ActionDispatch::IntegrationTest def setup @user = users(:michael) @other = users(:archer) log_in_as(@user) end test "following page" do get following_user_path(@user) assert_not @user.following.empty? assert_match @user.following.count.to_s, response.body @user.following.each do |user| assert_select "a[href=?]", user_path(user) end end test "followers page" do get followers_user_path(@user) assert_not @user.followers.empty? assert_match @user.followers.count.to_s, response.body @user.followers.each do |user| assert_select "a[href=?]", user_path(user) end end test "should follow a user standard way" do assert_difference '@user.following.count', 1 do post relationships_path, params: { followed_id: @other.id } end end test "should follow a user with Ajax" do assert_difference '@user.following.count', 1 do post relationships_path, xhr: true, params: { followed_id: @other.id } end end test "should unfollow a user the standard way" do @user.follow(@other) relationship = @user.active_relationships.find_by(followed_id: @other.id ) assert_difference '@user.following.count', -1 do delete relationship_path(relationship) end end test "should unfollow a user with Ajax" do @user.follow(@other) relationship = @user.active_relationships.find_by(followed_id: @other.id) assert_difference '@user.following.count', -1 do delete relationship_path(relationship), xhr: true end end endこの時点でテストはパスする。
演習
1:
relationships_controller.rb
のrespond_to
ブロック内の各行を順にコメントアウトしていき、テストが正しくエラーを検知できるかどうか確認。relationships_controller.rbrespond_to do |format| # format.html { redirect_to @user } format.js endERROR["test_should_follow_a_user_standard_way", FollowingTest, 1.5789846269981354] test_should_follow_a_user_standard_way#FollowingTest (1.58s) ActionController::UnknownFormat: ActionController::UnknownFormat: ActionController::UnknownFormat app/controllers/relationships_controller.rb:7:in `create' test/integration/following_test.rb:31:in `block (2 levels) in <class:FollowingTest>' test/integration/following_test.rb:30:in `block in <class:FollowingTest>' ERROR["test_should_unfollow_a_user_the_standard_way", FollowingTest, 1.6338956370018423] test_should_unfollow_a_user_the_standard_way#FollowingTest (1.63s) ActionController::UnknownFormat: ActionController::UnknownFormat: ActionController::UnknownFormat app/controllers/relationships_controller.rb:16:in `destroy' test/integration/following_test.rb:45:in `block (2 levels) in <class:FollowingTest>' test/integration/following_test.rb:44:in `block in <class:FollowingTest>' 72/72: [=================================================================================================================] 100% Time: 00:00:02, Time: 00:00:02 Finished in 2.13223s 72 tests, 334 assertions, 0 failures, 2 errors, 0 skips2:
xhr: true
がある行のうち、片方のみを削除するとどういった結果になるか?
このとき発生する問題の原因と、なぜ先ほどの演習で確認したテストがこの問題を検知できたのかを感がてみる。following_test.rbtest "should follow a user with Ajax" do assert_difference '@user.following.count', 1 do # post relationships_path, xhr: true, params: { followed_id: @other.id } end endFAIL["test_should_follow_a_user_with_Ajax", FollowingTest, 1.2568579829967348] test_should_follow_a_user_with_Ajax#FollowingTest (1.26s) "@user.following.count" didn't change by 1. Expected: 3 Actual: 2 test/integration/following_test.rb:36:in `block in <class:FollowingTest>'Ajaxを用いたフォローで、postリクエストを送信していない為、フォロー数が変化せずテストが失敗する。
14.3 ステータスフィード
ステータスフィードの実装では、現在のユーザーにフォローされているユーザーのマイクロポストの配列を作成し、
現在のユーザー自身のマイクロポストと合わせて表示する。このセクションを通して、複雑さを増したフィードの実装に進んでいく。
これを実現するためには、RailsとRubyの高度な機能の他に、SQLプログラミングの技術も必要。
ステータスフィードの最終形のモックアップがこれ
出典:図 14.21: ステータスフィード付きのHomeページのモックアップ
14.3.1 動機と計画
ステータスフィードの基本的なアイデアはシンプル。
以下の図の矢印で示されているように、この目的は、現在のユーザーによってフォローされているユーザーに対応するユーザーidを持つマイクロポストを取り出し、同時に現在のユーザー自身のマイクロポストも一緒に取り出すこと。
出典:図 14.22: id 1のユーザーがid 2、7、8、10をフォローしているときのフィード
どのようにフィードを実装するのかはまだ明確ではないが、テストについてはやや明確そうなので、
まずはテストから書いていく。このテストで重要なことは、以下の3つの条件を満たすこと。
- フォローしているユーザーのマイクロポストがフィードに含まれている
- 自分自身のマイクロポストもフィードに含まれている
- フォローしていないユーザーのマイクロポストがフィードに含まれていない
まずは、MichaelがLanaをフォローしていて、Archerをフォローしていないという状況を作ってみる。
この状況のMichaelのフィードでは、Lanaと自分自身の投稿が見えていて、Archerの投稿は見えないことになる。
先ほどの3つの条件をアサーションに変換して、Userモデルにfeedメソッドがあることに注意しながら、
更新したUserモデルに対するテストを書いていく。user_test.rbtest "feed should have the right posts" do michael = users(:michael) archer = users(:archer) lana = users(:lana) # フォローしているユーザーの投稿を確認 lana.microposts.each do |post_following| assert michael.feed.include?(post_following) end # 自分自身の投稿を確認 michael.microposts.each do |post_self| assert michael.feed.include?(post_self) end # フォローしていないユーザーの投稿を確認 archer.microposts.each do |post_unfollowed| assert_not michael.feed.include?(post_unfollowed) end endfeedメソッドはまだ定義していないのでテストは失敗する。
$ rails t演習
1:マイクロポストのidが正しく並んでいると仮定(昇順ソート)して、データセットで
user.feed.map($:id)
を実行すると、どのような結果が表示されるか?考える。user.feed.map($:id) =>[1,2,7,8,10]このように、引数として受け取った自分のidと、フォローしているidが組み合わさって表示される。
14.3.2 フィードを初めて実装する
ステータスフィードに対する要件定義は、先ほどのテストで明確になったので、
早速フィードの実装に着手する。最終的なフィードの実装はやや込み入っているため、細かい部品を1つずつ確かめながら導入していく。
最初に、このフィードで必要なクエリについて考える。
ここで必要なのは、microposts
テーブルから、
あるユーザーがフォローしているユーザーに対応するidを持つマイクロポストを全て選択すること。このクエリを模式的に書くと
SELECT * FROM microposts WHERE user_id IN (<list of ids>) OR user_id = <user id>上記のコードを書く際に、SQLが
IN
というキーワードをサポートしていることを前提にしている。
(Railsではサポートされている)このキーワードを使うことで、idの集合を内包(setinclusion)に対してテストを行える。
13章のプロトフィードでは、上のような選択を行うために
Active Recordのwhere
メソッドを使っていることを思い出す。この時に選択すべき対象はシンプルで、
現在のユーザーに対応するユーザーidを持つマイクロポストを選択すればよかった。Micropost.where("user_id = ?", id)今回必要になる選択は、上よりも少し複雑で、例えば次のような形になる。
Micropost.where("user_id IN (?) OR user_id = ?", following_ids, id)これらの条件から、フォローされているユーザーに対応するidの配列が必要であることがわかってきた。
これを行う方法の1つは、Rubyのmapメソッドを使うこと。
このメソッドはすべての「列挙可能」なオブジェクト
(配列やハッシュなど、要素の集合で構成されたあらゆるオブジェクト)
で使える。なお、このメソッドは四章でも出てきた。
他の例題として、mapメソッドを使って配列を文字列に変換すると、以下のようになる。$ rails console >> [1,2,3,4].map { |i| i.to_s } => ["1","2","3","4"]上記に示したような状況では、各要素に対して同じメソッドが実行される。
これは非常によく使われる方法であり、次のようにアンバサンド(&)と、メソッドに対応するシンボルを使った短縮表記が使える。
この短縮表記であれば、変数iを使わずに済む。>> [1,2,3,4].map(&:to_s) => ["1","2","3","4"]この結果に対してjoinメソッドを使うと、idの集合をカンマ区切りの文字列として繋げることができる。
>> [1,2,3,4].map(&:to_s).join(', ') => "1,2,3,4"上記のコードを使えば、
user.following
にある各要素のidを呼び出し、フォローしているユーザーのidを配列として扱うことができる。例えばDBの最初のユーザーに対して実行すると、次のような結果になる。
>> User.first.following.map(&:id) User Load (0.9ms) SELECT "users".* FROM "users" ORDER BY "users"."id" ASC LIMIT ? [["LIMIT", 1]] User Load (1.0ms) SELECT "users".* FROM "users" INNER JOIN "relationships" ON "users"."id" = "relationships"."followed_id" WHERE "relationships"."follower_id" = ? [["follower_id", 1]] => [3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 27, 28, 29, 30, 31, 32, 33, 34, 35, 36, 37, 38, 39, 40, 41, 42, 43, 44, 45, 46, 47, 48, 49, 50, 51]実際、この手法は実に便利なので、Active Recordでは次のようなメソッドも用意されている。
>> User.first.following.ids User Load (0.1ms) SELECT "users".* FROM "users" ORDER BY "users"."id" ASC LIMIT ? [["LIMIT", 1]] (0.5ms) SELECT "users"."id" FROM "users" INNER JOIN "relationships" ON "users"."id" = "relationships"."followed_id" WHERE "relationships"."follower_id" = ? [["follower_id", 1]] => [3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 27, 28, 29, 30, 31, 32, 33, 34, 35, 36, 37, 38, 39, 40, 41, 42, 43, 44, 45, 46, 47, 48, 49, 50, 51] >>この
following_ids
メソッドは、has_many :following
の関連付けをした時に、
Active Recordが自動生成したもの。これにより、user.followingコレクション対応するidを得るためには、
関連付けの名前の末尾に_ids
を付け足すだけで済む。結果として、フォローしているユーザーidの文字列は次のようにして取得することができる。
>> User.first.following_ids.join(', ') User Load (0.1ms) SELECT "users".* FROM "users" ORDER BY "users"."id" ASC LIMIT ? [["LIMIT", 1]] (0.2ms) SELECT "users"."id" FROM "users" INNER JOIN "relationships" ON "users"."id" = "relationships"."followed_id" WHERE "relationships"."follower_id" = ? [["follower_id", 1]] => "3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 27, 28, 29, 30, 31, 32, 33, 34, 35, 36, 37, 38, 39, 40, 41, 42, 43, 44, 45, 46, 47, 48, 49, 50, 51"ただ、実際のSQL文字列に挿入するときは、このように記述する必要はない。
実は、?を挿入すると自動的にこのあたりの面倒を見てくれる。
さらに、DBに依存する一部の非互換性まで解消してくれる。
つまり、ここではfollowing_ids
メソッドをそのまま使えば良いだけ。Micropost.where("user_id IN (?) OR user_id = ?", following_ids, id)というコードが無事に動いた。
作成したコードはこれ
user.rb# パスワード再設定の期限が切れている場合はtrueを返す def password_reset_expired? reset_sent_at < 2.hours.ago end # ユーザーのステータスフィードを渡す def feed Micropost.where("user_id IN (?) OR user_id = ?", following_ids, id) end # ユーザーをフォローする def follow(other_user) following << other_user end演習
1:Userモデルにおいて、現在のユーザー自身の投稿を含めないようにするにはどうすれば良いか?
また、そのような変更を加えると、user_test.rbのど部分のテストが失敗するか?$ user = User.first $ user.feed Micropost Load (0.9ms) SELECT "microposts".* FROM "microposts" WHERE (user_id IN (3,4,5,6,7,8,9,10,11,12,13,14,15,16,17,18,19,20,21,22,23,24,25,26,27,28,29,30,31,32,33,34,35,36,37,38,39,40,41,42,43,44,45,46,47,48,49,50,51) OR user_id = 1) ORDER BY "microposts"."created_at" DESC LIMIT ? [["LIMIT", 11]]OR user_id = 1にて、自分自身のユーザーidを渡している点に注目。
この渡す為の処理をメソッドから削除する。
user.rb# ユーザーのステータスフィードを渡す def feed Micropost.where("user_id IN (?) ", following_ids, id) endテストの失敗箇所
app/models/user.rb:90:in `feed' test/models/user_test.rb:97:in `block (2 levels) in <class:UserTest>' test/models/user_test.rb:96:in `block in <class:UserTest>'2:Userモデルにて、フォローしているユーザーの投稿を含めないように内容にするには?
また、テストの失敗箇所を見てみる。user.rb# ユーザーのステータスフィードを渡す def feed Micropost.where("user_id = ?", following_ids, id) endapp/models/user.rb:90:in `feed' test/models/user_test.rb:97:in `block (2 levels) in <class:UserTest>' test/models/user_test.rb:96:in `block in <class:UserTest>'3:フォローしていないユーザーの投稿を含めるためにはどうすればいいか?
また、そのような変更を加えると、テストがどう失敗するか?user.rb# ユーザーのステータスフィードを渡す def feed Micropost.all end全部含めてみる。
Expected true to be nil or false test/models/user_test.rb:105:in `block (2 levels) in <class:UserTest>' test/models/user_test.rb:104:in `block in <class:UserTest>'falseだけど〜って怒られてる。
14.3.3 サブセレクト
先ほどのフィードの実装は、投稿されたマイクロポストの数が膨大になった時にうまくスケールしない。
つまり、フォローしているユーザーが5000人程度になると、
Webサービス全体が遅くなる可能性がある。この節では、フォローしているユーザー数に応じてスケールできるように、ステータスフィードを改善していく。
following_idsでフォローしている全てのユーザーをDBに問い合わせし、
さらに、フォローしているユーザーの完全な配列を作るために再度DBに問い合わせしているのは問題である。。feedメソッドでは、集合に内包されているかどうかだけしかチェックされていない為、この部分はもっと効率的なコードに置き換えられるはず。
また、SQLは本来このような集合の操作に最適化されている。
実際、このような問題は、SQLのサブセレクト(subselect)を使うと解決できる。まずは、Userモデルのコードを若干修正し、フィードをリファクタリングすることから始める。
user.rb# ユーザーのステータスフィードを渡す def feed Micropost.where("user_id IN (:following_ids) OR user_id = :user_id", following_ids: following_ids, user_id: id) end上記の実装では、これまでのコード
Micropost.where("user_id IN (?) OR user_id = ?", following_ids, id)次のように置き換えた。
Micropost.where("user_id IN (:following_ids) OR user_id = :user_id", following_ids: following_ids, user_id: id)疑問符を使った文法も便利だが、
同じ変数を複数の場所に挿入したい場合は、後者の置き換え後の文法を使う方がより便利。上記の説明が示すように、これからSQLクエリにもう1つのuser_idを追加する。
特に、次のコードはfollowing_ids
このようなSQLに置き換えることができる。
following_ids = "SELECT followed id FROM relationships WHERE follower_id = :user_id"このコードをSQLのサブセレクトとして使う。
SELECT * FROM microposts WHERE user_id IN (SELECT followed_id FROM relationships WHERE follower_id = 1) OR user_id = 1つまり、このような階層構造になっている。
- user_id
- 「ユーザー1がフォローしているユーザー全てを選択する」(サブセレクト)
このように、SELECT文を入れ子の中に内包させる形を「サブセレクト」と言う。
このサブセレクトは、集合のロジックをDBに保存するので、より効率的にデータを取得できる。
上記を元に、効率的なフィードを実装する。
user.rb# ユーザーのステータスフィードを渡す def feed following_ids = "SELECT followed_id FROM relationships WHERE follower_id = :user_id" Micropost.where("user_id IN (#{following_ids}) OR user_id = :user_id", user_id: id) endこのコードは、Rails+Ruby+SQLのコードが複雑に絡み合っているが、きちんと動作する。
14 tests, 58 assertions, 0 failures, 0 errors, 0 skips大規模なWebサービスでは、バックグラウンド処理を使ってフィードを非同期生成するなどのさらなる改善が必要だが、Railsチュートリアルではここまでの改善にしておく。(Railsの入門書なので)
これで、ステータスフィードの実装が完了した。
いつも通り、masterブランチに変更を取り込む。
$ rails t $ git add -A $ git commit -m "Add user following" $ git checkout master $ git merge following-usersあとはコードをリポジトリにpushして、本番環境にデプロイ
$ git push $ git push heroku $ source <(curl -sL https://cdn.learnenough.com/heroku_install) $ heroku pg:reset DATABASE $ heroku run rails db:migrate $ heroku run rails db:seed新規作成アカウントの有効化ができない場合は、以下のid、passでログインしてみてください。
email:example-2@railstutorial.org
pass:password演習
1:Homeページで表示される1ページ目のフィードに対して、統合テストを書いてみる。
following_test.rbtest "feed on Home page" do get root_path @user.feed.paginate(page: 1).each do |micropost| assert_match CGI.escapeHTML(micropost.content), response.body end end2:上記コードでは、期待されるHTMLを
CGI.escapeHTML
メソッドでエスケープしている。
その理由は?
また、試しにエスケープ処理を外して、得られるHTMLの内容を調べてみて、マイクロポストの内容がおかしい点を挙げよ。following_test.rbtest "feed on Home page" do get root_path @user.feed.paginate(page: 1).each do |micropost| assert_match micropost.content, response.body end endA:contentをエスケープしている為、CGI.esapeHTMLを加える必要がある。
14.4 最後に(サンプルアプリケーションで学んだこと)
サンプルappで学んだことをまとめてみる。
- MVCモデル
- テンプレート
- パーシャル
- beforeフィルター
- バリデーション
- コールバック
- データモデルの関連付け(has_many/belongs_to/has_many through)
- セキュリティ
- テスティング
- デプロイ
今後はサンプルAppに
- 返信機能
- メッセージ機能
- フォロワーの通知
- RSSフィード
- RESTAPI
- 検索機能
- いいね機能
- シェア機能
などを加えてオリジナルアプリケーションを完成させていくと良い。
終わったー
やっとRailsチュートリアルを終えることができました。
長かったですねー。全部Qiitaにメモったおかげで、流し読みせずに熟読できました。
お陰で、全体を通してWebアプリケーション制作の基礎を理解できたような気がします。正直、1周目の理解度は30パーセントぐらいでした。
今回の2周目で理解度は80パーセントぐらいまで上がった気がします。これからはオリジナルのWebアプリケーションを作成していくことで、
今回覚えた内容を自分の物にしていきたいと思います!みなさんお疲れ様でした!
YUUKI.
単語集
- has_many through
多対多の関係性を定義する関連付けメソッド。
- source
has_manyに対してパラメータを与えるオプション。
sourceオブションで与えた値は配列の元を表しているので、実際の配列のインデックスは変わらない。
- collection
コレクションルーティングを追加するメソッド。
idを指定せずに全てのメンバーを表示したりできる
- Ajax(エイジャックス)
Asynchronous(非同期な) JS + XMLで作られている非同期通信を行いながらインターフェイスの構築を行うプログラミング手法のこと。
サーバーからのレスポンスを待たずにクライアント側の表示を変更させることができる。例えば、Ajaxを使用することで画面遷移せずにHTMLを更新することが可能で、ユーザビリティの向上やサーバー負荷の軽減に繋がる。
Railsでは、
remote: true
で使える。
- respond_to
リクエストの種類によって応答を場合分けするメソッド。
処理内に書いたいずれかの1行が実行されるよう書くことができる。書き方の例
respond_to do |format| format.htlm {...} format.json {...}
- xhr
Xmlhttprequestの略で、Ajax通信かどうかを判定するオプション。
trueを渡すことでAjaxでリクエストを発行出来る。Ajax
- DOM
Document Object Modelの略で、JSファイル内で別のHTMLファイルなどを読み込む時の役割のこと。
- following_ids
followしているユーザーのidをそれぞれ文字列に変換して、,で区切る値として返すメソッド。
- サブセレクト
SQLのSELECT文を入れ子にしたものを指す。
入れ子構造にすることで、より効率的にデータを取得できる。