20190209のRailsに関する記事は11件です。

Railsでページ遷移時にPay.jpのフォームが表示されない問題の解決方法

Turbolinksを切らないと、Pay.jpのフォームは表示されない

https://pay.jp/docs/checkout の注意事項にも書かれているように、
RailsでPay.jpのチェックアウトを利用しようとすると、Turbolinksに邪魔されてフォームがうまく表示されません。
具体的にいうと、ページ遷移時にチェックアウトのフォームが表示されません。
スクリーンショット 2019-02-09 20.53.33.png

ページ遷移時に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

スクリーンショット 2019-02-09 21.44.35.png

多分、turbolinksを切らずに、htmlにjsを埋め込みたい場合に全般的に役に立つ方法だと思います。

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

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:text

modelは必ず単数形で作成しましょう

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

Rails における rake タスクの :environment について

rails generate task [namespace] [task_name] で rake タスクを生成するとき、デフォルトで以下のようなファイルがされます。

sample.rake
namespace :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#L509
    def 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
    end

https://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#L335
    def require_environment! #:nodoc:
      environment = paths["config/environment"].existent.first
      require environment if environment
    end

https://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.rake
namespace :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 コマンドで行儀よく作った方が思わぬミスは少なくなりそうです。

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

RailsのCoffeeScriptで共通の処理を別のファイルへ切り出したい

タイトルの通りのことをやろうとしたときの、解決方法を見つけるまでに
調べたことを自分用のまとめも兼ねて書いておきます。Rails初学者向けです。
解決策は1番下にあります。

CoffeeScriptとは?

前提知識として。
CoffeeScriptとはすごく簡単に言うと、JavaScriptをより書きやすくするためのものです。
Rubyっぽくかけます。Railsで採用されています。CoffeeScriptはCoffeeScriptのままでは
使えません。CoffeeScriptで書かれたものを普通のJavaScriptに変換して使用します。
そのあたりの変換はRailsが勝手にやってくれます。
なお、RailsはCoffeeScriptもJavaScriptも併用できます
ぐぐるとすぐどんなものかわかると思いますが、書きやすさの片鱗を1ミリだけ見せると以下です。

javascript
alert("Hello");
coffeescript
# 引数をとる関数を呼び出すときの括弧いらない。
# 行末のセミコロンいらない。
alert "Hello"

CoffeeScriptで書くと、他にも色々書きやすくなる点があります。
調べてみてください。

以下本題です。

やりたいこと

やりたいことは単純です。

test1.coffee
sayHello = ->
  alert "Hello"
test2.coffee
sayHello = ->
  alert "Hello"
(なお、上記をJavaScriptで書くとこう)
function sayHello() {
  alert("Hello");
}

test1.coffeeとtest2.coffeeには同じ関数が定義されています。
同じことを2回書いているので、下記のように共通処理をまとめた
common.coffeeを作り、そこから呼び出せるようにしたい。

common.coffee
sayHello = ->
  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.js
function 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 hogevarを取ると、グローバルな変数とすることもできます。
ただ、varの有る無しでローカススコープのエリアで定義しているのに、
グローバルとローカルが切り替わってしまうため、基本的にはvarは付けたほうがいい。
なお、CoffeeScriptにはvarはもともと存在せず、グローバル変数にはできません。

JavaScriptだとなぜできるのか(再掲)

ではもう一度、JavaScriptだとなぜできるのか。
再度、JavaScriptのファイルを見てみます。

common.js
function 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への変換処理は、
変換するときに、中身を丸ごと即時・無名関数として包みます。

変換前CoffeeScript
sayHello = ->
  alert "Hello"
変換後JavaScript
(function () {
  sayHello = function() {
    return alert("Hello");
  };
}());

関数の内側に自分が書いたコードが包まれるので、変換後は全ての変数や関数は
ローカルスコープに閉じ込められるということです。なぜこんなことをするのか。
それは、前述のローカルスコープのところで説明した効果を得たいためです。

ローカルスコープとは、「そこに置いたものは、その場所でしか呼び出せない」エリアです。
関数の内側で定義された変数や関数が、このエリアに所属することになります。
このエリアで定義された変数や関数は、関数の内側以外では、上書きしたり、呼び出すことはできません。

もしたくさんのCoffeeScriptファイルを取り扱うようになった時、あるファイルでせっかく
定義した関数や変数を、別のファイルの中で同じ名前の関数や変数を使って全く違う定義を
誤ってしてしまうかもしれません。それを防ぐ効果があります。

この効果のために、共通処理を切り出せなくなっています。

解決策

いくつか方法があるようです。

方法1

JavaScriptにはwindowオブジェクトという特殊なオブジェクトがあります。
先程から良く使っているalert関数も実はwindowオブジェクトが持つ関数の一つです。
つまりこう書けます。このwindowは省略できるため、alertだけで普段使えています。

window.alert("Hello");

このwindowオブジェクトはグローバルスコープに属しています。
グローバルスコープに属しているものはどこからでも上書き・呼び出しができます。
上書きしてしまうとalertもなにも使えなくなってしまうので、このオブジェクトに
関数を追加します。

common.coffee
window.sayHello = ->
  alert "Hello"
test1.coffee
sayHello()
test2.coffee
sayHello()

方法2

@を使います。@はCoffeeScriptに用意されたJavaScriptのthisの別名です。
JavaScriptのthisは、グローバルスコープのエリアでは、windowオブジェクトを指します。

よって方法1の下記は・・・

common.coffee
window.sayHello = ->
  alert "Hello"

下記のように書けます。

common.coffee
@sayHello = ->
  alert "Hello"
test1.coffee
sayHello()
test2.coffee
sayHello()

終わり

以上で終わりです。
新しいバージョンのJavaScriptだとちゃんと関数の外に定義した変数や関数も
グローバルにならないように宣言する方法があるみたいです。
ただ、どこまで今存在するWebブラウザが、そのバージョンに対応しているかは
調べられていません。「ECMAScript 6」とかでぐぐると出てきます。

なにかこう・・・JavaScriptって、昔から、こう、、、アレなんですよね。。。

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

ドットインストール  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初期画面は表示されず以下のエラー画面が出力されます。
スクリーンショット 2019-02-09 13.37.15.png

※エラー文
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

Railsの初期画面が表示されました!!
スクリーンショット 2019-02-09 13.14.27.png

あとがき

新人のプログラム初学者からの質問により今回のエラーが発生/解決しました。
今後の新人プログラマーたちに少しでも役立ててもらえれば幸いです。

参考URL

Error loading the 'sqlite3' Active Record adapter. Missing a gem it depends on?エラーで困ってます

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

【Rails】RailsAPIテスト - ベストプラクティス

概要

Railsで作成されたAPIのテストについてこちらを参考に必要最低限の情報をまとめました。
実際にこちらの記事で作成したAPIに対してテストを書いていきます。

何をテストする?

適切なAPIはHTTPのレスポンスのステータスコードと実際のデータを含んだレスポンスボディを返します。
よって主にその2つをテストしていきます。

ステータスコード

通常APIによって返されるステータスコードは以下の4つに分類されます。
APIの動きによってこちらのコードと照らし合わせる事でテストします。

  • 200: OK - リクエストは成功し、レスポンスとともに要求に応じた情報が返される。
  • 401: Unauthorized - 認証失敗。認証が必要である。
  • 403: Forbidden - 禁止されている。リソースにアクセスすることを拒否された。
  • 404: Not Found - 未検出。リソースが見つからなかった。

レスポンスボディ

GETリクエストの場合は単にリクエストボディに要求したデータが入っているかをテストします。
POSTPUTDELETE リクエストの際にはそれぞれ要求したデータ通りの動きをするかテストします。

APIのテストはインテグレーションテスト

APIのテストは特定のURLに対してデータの要求等をした際の動きをテストするので、インテグレーションテスト(結合テスト)として扱います。

※通常Railsで結合テストを行う際にはcapybaraというgemがよく使われますが、こちらはAPIのテストをする際には適していません。詳しくはこちらから

RspecのRequest Specsを使用する。

APIだからといって特別なテストツール等は使用する必要はなく通常のrailsアプリで頻繁に使用されるrspecを使用してテストします。
APIに対するリクエストに関してテストですのでrspec/requests/ディレクトリ下にテストを作成していきます。

実装

上記で挙げたポイントを以下にまとめましたので、こちらを意識して実際に簡易的なテストを書いていきます。
(※1番上にも挙げましたがこちらのAPIにテストを書いていきます。)
(※あくまでも簡易的なテストですので、とてもシンプルな構造で書いていますが、実際には状況に応じてここからテストを充実させる必要が考えられます。)

ここまでのポイント

  1. ステータスコードとレスポンスボディをテストする。
  2. APIのテストはインテグレーションテスト
  3. rspecを使用しrspec/requests/ディレクトリ下にテストを作成する。

下準備

factory_botを使用してデータを作成しますので以下の手順でセットアップします。

$ gem install factory_bot_rails
Gemfile
group :development, :test do
  gem 'factory_bot_rails'
end
spec/factories/posts.rb
FactoryBot.define do
  factory :post do
    title { 'title' }
  end
end

GET リクエスト

GETリクエストでは正しくデータを取得した際にステータスコード200が返るか、正しく要求したデータが取得できたかをテストします。

/api/v1/postsに対するテスト。

spec/requests/api/v1/posts_spec.rb
require '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.rb
require '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

参考

Rails API Testing Best Practices

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

英語と日本語で考える、整理された 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
end

describe は説明する・描写する・記述するという意味だが、説明する対象を目的語にとる。

「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 は依存関係

私はそんなことを意識して書いてる。もちろんいつでも適用できるわけではないけれど。
ちなみになるべく「こと」は書かない主義。

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

1つの親レコードに対して複数の子レコードである画像をアップロードしたい。

5日間くらい詰まって進まなかったので、同じところで詰まる人がいないようメモ

やりたいこと

フリマアプリのように、出品するアイテムに複数の画像をアップロードする機能を作りたい。

環境

Rails 5.0.7.1

使用するgem

  • carrierwave
  • mini_magick

手順

  1. gemをGemfileに記述し、bundle install
  2. モデル作成
  3. アップロードのためのviewを作成
  4. コントローラー作成

以上で完成!
手順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
end

create_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
end
rake 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
end

item_image.rb
uploaderをマウントするコードを記述

class ItemImage < ApplicationRecord
  mount_uploader :name, ImageUploader
  belongs_to :product, optional: true
end

View作成

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])
end

newアクションでインスタンス生成し、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

以上で完成。

参考記事
CarrierWave Upload Multiple Images [2018 Update]

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

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全体を日本語化したい場合は下記の記事から

https://qiita.com/kusu_tweet/items/b534c808ac1ee0382f05

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

Herokuへpushしたら"Detected sqlite3 gem which is not supported on Heroku"になったので、Gemfile直したけど、反映されないやつ

タイトルの通りです。
エラー文で検索するとGemfile直してbundle installgit 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の修正

変更前Gemfile
gem 'sqlite3'
変更後Gemfile
# gemのバージョンは適宜変えてください

# 開発・テスト環境ではSQLite3を使う
group :development, :test do
  gem 'sqlite3'
end

# 本番環境ではPostgresqlを使う
group :production do
  gem 'pg', '0.20.0'
end

3. bundle install

Herokuにpushするときには、Gemfile.lockの中身を参照されるので、bundle installし直して
Gemfile.lockを最新化します。

$ bundle install

4. git commit

git使い始めだと忘れがち。Herokuへアプリをアップロードするときは、gitで管理しているファイルを
アップロードします。Gemfile.lockもその管理対象なので、コミットしてあげないとHerokuに適用されません。

$ git add -A
$ git commit -m "[Update] Gemfile修正"

5. Herokuへpush

$ git push heroku master

以上です。

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

Ruby on Rails チュートリアル 第14章 データモデルの関連付け(フォロー フォロー解除)フィードの実装など 演習 解答

前回の続き

著者略歴
YUUKI
ポートフォリオサイト:Pooks
RailsTutorial2周目

14章 ユーザーをフォローする

この章では、他のユーザーをフォローしたり、フォロー解除したりするソーシャル的な仕組みと、
フォローしているユーザーの投稿をステータスフィード(いわゆるタイムライン)
に表示する仕組みを追加する。

そのために、まずはユーザー間の関係性をどうモデリングするかについて学ぶ。

その後、モデリング結果に対応するWebインターフェースを実装していく。

Webインターフェースの例としてAjaxについても後に詳解する。

最後に、ステータスフィードの完成版を実装する。

この最終章では、本書の中で最も難易度の高い手法をいくつか使っている。
その中には、ステータスフィードの作成のためにRuby/SQLを騙すテクニックも含まれる。

この章の例全体にわたって、これまでよりも複雑なデータモデルを使います。

ここで学んだデータモデルは、今後自分用のWebアプリケーションを開発するときに必ず役に立つ。

この章で学ぶことは今まで最も難易度が高いため、
コードを書く前に一旦インターフェースの流れを理解する。

モックアップはこちら

image.png

出典:図 14.1: 現在のプロフィールページ

image.png

出典:図 14.2: フォローする相手を見つける

image.png

出典:図 14.3: ユーザーのプロフィール画面に [Follow] ボタンが表示されている

image.png

出典:図 14.4: プロフィールに [Unfollow] ボタンが表示され、フォロワーのカウントが1つ増えた

image.png

出典:図 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-users

14.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)などの属性を追加する。

image.png

出典:図 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テーブルのフォローされているユーザーを見つけるようにする。

このデータモデルを模式図にすると、以下のようになる。

image.png

出典:図 14.7: 能動的関係をとおしてフォローしているユーザーを取得する模式図

間にactive_relationshipsを挟むことで、フォローとフォロワーの関係性がスムーズに繋がっている。

能動的関係も受動的関係も、最終的にはデータベースの同じテーブルを使うことになる。

したがって、テーブル名にはこの「関係」を表す「relationships」を使う。

モデル名はRailsの慣習にならって、Relationshipとする。
作成したRelationshipデータモデルを以下に示す。

1つのrelationshipsテーブルを使って2つのモデル(能動的関係と受動的関係)をシミュレートする方法については後に説明する。

image.png

出典:図 14.8: Relationshipデータモデル

このデータモデルを実装するために、まずは上記のデータモデルに対応したマイグレーションを生成する。

$ rails g model Relationship follower_id:integer followed_id:integer

このリレーションシップは今後follower_idfollowed_idで頻繁に検索することになるので、
それぞれのカラムインデックスを追加する。

[timestamp]_create_relationships.rb
class 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_idfollowed_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
8 

2: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)
=> 1

14.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.rb
class User < ApplicationRecord
  # 関連付け
  has_many :microposts, dependent: :destroy
  has_many :active_relationships, class_name:     "Relationship",
                                  foreign_key:    "follower_id",
                                  dependent:      :destroy

明示的にclass名や外部キー、destroyも追加している。
(ユーザーを削除したら、ユーザーのリレーションシップも同時に削除される必要があるため)

relationship.rb
class Relationship < ApplicationRecord
  # 1対1の関連付け
  belongs_to :follower, class_name: "User"
  belongs_to :followed, class_name: "User"
end

なお、followerの関連付けはまだ使わない。
(書いておくことで構造の理解の手助けになる)

user.rbrelationship.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.rb
require '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
end
relationship.rb
class 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の関連付けの核心followingfollowersに取りかかる。

今回はhas_many throughを使う。

1人のユーザーにはいくつもの「フォローする」「フォローされる」といった関係性がある。
この関係性を多対多と呼ぶ。

デフォルトのhas_many throughという関連付けでは、
Railsはモデル名(単数形)に対応する外部キーを探す。

has_many :followeds, through: :active_relationships

Railsはfollowedsというシンボルを見て、
これをfollowed単数形に変え、
relationshipsテーブルのfollowed_idを使って対象のユーザーを取得してくる。

しかし、
user.followedsという使い方は英語としては不適切。

代わりに、user.followingという名前を使う。

そのためには、Railsのデフォルトを上書きする必要がある。
ここでは:sourceパラメーターを使って、following配列の元はfollowed idの集合である
ということを明示的にRailsに伝える。

user.rb
class 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.count

DBの中で合計を計算した方が高速になる点に注意する。

次に、followingで取得した場合をより簡単に取り扱うために、
followunfollowといった便利メソッドを追加する。

これらのメソッドは、例えばuser.follow(other_user)といった具合に使う。

さらに、これに関連するfollowing?論理値メソッドも追加し、あるユーザーが誰かをフォローしているかどうかを確認できるようにする。

今回は、こういったメソッドはテストから先に書いていく。
と言うのも、Webインターフェイスなどで便利メソッドを使うのはまだ先なので、すぐに使える場面がなく、実装した手応えを得にくいから。

一方で、Usermモデルに対するテストは書くのは簡単かつ今すぐできるので、
先に書いていく。

具体的には、

  • following?メソッドであるユーザーをまだフォロしていないことを確認
  • followメソッドを使ってそのユーザーをフォローできたことを確認
  • unfollowメソッドでフォロー解除できたことを確認

といった具合でテストしていく。

user_test.rb
  test "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による関連付けを使ってfollowunfollowfollowing?メソッドを実装していく。

このとき、可能な限り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]]
=> false

2:先ほどの演習の各コマンド実行時の結果を見返してみて、実際にはどんなSQLが出力されたのか確認してみる。

上記で確認できる。

14.1.5 フォロワー

リレーションシップにuser.followersメソッドを追加する。

user.followingはフォローしている人数
user.followersはフォローされてる人数(フォロワー)である。

フォロワーの配列を展開するために必要な情報は、relationshipsテーブルに既にあり、
active_relationshipsテーブルを再利用することで出来る。

実際、follower_idfollowed_idを入れ替えるだけで、
フォロワーについてもフォローする場合と全く同じ活用が出来る。

データモデルは以下。

image.png

出典:図 14.9: Relationshipモデルのカラムを入れ替えて作った、フォロワーのモデル

要は、active_relationshipspassive_relationshipsに入れ替えて、
followed_idfollower_idを入れ替えるだけ。

上記のデータモデルの実装をuser.rbにhas_manyを使って行う。

user.rb
class 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.rb
  test "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]]
=> 2

3: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]]
=> 38

2: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]]
=> 49

14.2.2 統計と[Follow]フォーム

これでサンプルユーザに、フォローしているユーザーとフォロワーができました。
プロフィールページとHomeページを更新して、これを反映する。

最初に、プロフィールページとHomeページに、
フォローしているユーザーとフォロワーの統計情報を表示するためのパーシャルを作成する。

次に、フォローしているユーザーの一覧(following)と
フォロワーの一覧(followers)を表示する専用のページを作成する。

Twitterの慣習にしたがってフォロー数の単位にはfollowingを使い、
例えば50 followingといった具合に表示する。

image.png

出典:図 14.10: 統計情報パーシャルのモックアップ

上記の統計情報には、現在のユーザーがフォローしている人数と、
現在のフォロワーの人数が表示されている。

それぞれの表示はリンクになっており、専用の表示ページに移動できる。

これらのリンクはダミーテキスト#を使って無効にしていた。
しかし、ルーティングについての知識もだいぶ増えてきたので、今回は実装することにする。

実際のページ作成は後にルーティングは今実装する。

このコードでは、resourcesブロックの内側で:memberメソッドが使っている。
これは初登場のメソッドだが、まずはどんな動作するのか推測してみる。

routes.rb
Rails.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;
}

image.png

出典:図 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.rb
Rails.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]ボタンが表示される。

スクリーンショット 2019-02-07 15.56.13.png

これらのボタンを実装するには、二通りの方法がある。
1つは標準的な方法、もう1つはAjaxを使う方法。

でもその前に、フォローしているユーザーとフォロワーを表示するページをそれぞれ作成してHTMLインターフェイスを完成させる。

演習

1:ブラウザから/users/2にアクセスし、フォローボタンが表示されていることを確認する。
同様に、/users/5ではUnfollow]ボタンが表示されているはず。

さて、/users/1にアクセスすると、どのような結果が表示されるか?

users/1はログインユーザーなのでボタンが消える。

スクリーンショット 2019-02-07 16.08.47.png

2:ブラウザからHomeページとプロフィールページを表示してみて、統計情報が正しく表示されているか確認。

確認済み。

3:Homeページに表示されている統計情報に対してテストを書いてみる。
同様にして、プロフィールページにもテストを追加してみる。

site_layout_test.rb
  test "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
  end
users_profile_test.rb
    end
    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]ページ

フォローしているユーザーを表示するページと、フォロワーを表示するページは、
いずれもプロフィールページとユーザー一覧ページを合わせたような作りになるという点で似ている。

どちらにもフォローの統計情報などのユーザー情報を表示するサイドバーと、ユーザーのリストがある。

さらに、サイドバーには小さめのユーザープロフィール画像のリンクを格子状に並べて表示する。

image.png

出典:図 14.14: フォローしているユーザー用ページのモックアップ

image.png

出典:図 14.15: ユーザーのフォロワー用ページのモックアップ

ここでの最初の作業は、フォローしているユーザーのリンクとフォロワーのリンクを動くようにすること。

Twitterに倣って、どちらのページでもユーザーのログインを要求するようにする。

そこで前回のアクセス制御と同様に、まずはテストから書いていく。

今回使うテストは以下の通り。

上記コードではfollowing/followersの名前付きルートを使っている点に注意。

users_controller_test.rb
  test "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.followersからデータを取り出し、ページネーションを行なって、ページを出力する必要がある。

users_controller.rb
  def 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ビューを呼び出す。

この時、上記コードでは現在のユーザーを一切使っていないので、
他のユーザーのフォロワー一覧ページもうまく動く。

スクリーンショット 2019-02-07 21.00.33.png

スクリーンショット 2019-02-07 21.01.00.png

スクリーンショット 2019-02-07 21.01.47.png

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.yml
one:
  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.rb
  def 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を開き、それぞれが適切に表示されていることを確認。
サイドバーにある画像は、リンクとしてうまく機能しているか?

スクリーンショット 2019-02-07 22.27.08.png

スクリーンショット 2019-02-07 22.27.58.png

スクリーンショット 2019-02-07 22.28.16.png

OK

2:following_testassert_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 skips

12.2.4 [Follow]ボタン(基本編)

ビューが整ってきた。
いよいよ[Follow]/[Unfollow]ボタンを動作させる。

フォローとフォロー解除はそれぞれリレーション湿布の作成と削除に対応しているため、まずはRelationshipsコントローラが必要。
いつものようにコントローラを生成させる。

$ rails g controller Relationships

Relationshipsコントローラのアクションでアクセス制御することはそこまで難しくない。

しかし、前回のアクセス制御の時と同様に最初にテストを書き、それをパスするように実装することでセキュリティモデルを確立させていく。
今回はまず、コントローラのアクションにアクセスする時、ログイン済みのユーザーであるかどうかをチェックする。

もしログインしていなければログインページにリダイレクトされるので、Relationshipのカウントが変わっていないことを確認する。

relationships_controller_test.rb
require '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.rb
class RelationshipsController < ApplicationController
  before_action :logged_in_user

  def create
  end

  def destroy
  end
end

[Follow]/[Unfollow]ボタンを動作させるためには、フォームから送信されたパラメータを使って、followed_idに対応するユーザーを見つけてくる必要がある。

その後、見つけてきたユーザーに対して適切にfollow/unfollowメソッド(Userモデルで定義した)を使う。

relationships_controller.rb
class 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_usernilになり、
どちらのメソッドでも2行目で例外が発生する。

エラーにはなるが、アプリケーションやデータに影響は生じない。

このままでも支障はないが、このような例外には頼らない方がいいので、セキュリティの為のレイヤーを追加した。

これで、フォロー/フォロー解除の機能が完成した。

どのユーザーも、他のユーザーをフォローしたりリフォローしたりできる。
ブラウザ上でボタンをクリックして、確かめてみる。
(振る舞いを検証する統合テストはのちに実装する)

スクリーンショット 2019-02-08 4.49.01.png

フォローしていないユーザーの画面

スクリーンショット 2019-02-08 4.49.26.png

ユーザーをフォローした結果

演習

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.rb
class 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
end

Ajaxリクエストに対応したので、今度はブラウザ側でJavaScriptが無効になっていた場合でもうまく動くようにする。

application.rb
  class 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.rb
require '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.rbrespond_toブロック内の各行を順にコメントアウトしていき、テストが正しくエラーを検知できるかどうか確認。

relationships_controller.rb
    respond_to do |format|
      # format.html { redirect_to @user }
      format.js
    end
ERROR["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 skips

2:xhr: trueがある行のうち、片方のみを削除するとどういった結果になるか?
このとき発生する問題の原因と、なぜ先ほどの演習で確認したテストがこの問題を検知できたのかを感がてみる。

following_test.rb
  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
 FAIL["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プログラミングの技術も必要。

ステータスフィードの最終形のモックアップがこれ

image.png

出典:図 14.21: ステータスフィード付きのHomeページのモックアップ

14.3.1 動機と計画

ステータスフィードの基本的なアイデアはシンプル。

以下の図の矢印で示されているように、この目的は、現在のユーザーによってフォローされているユーザーに対応するユーザーidを持つマイクロポストを取り出し、同時に現在のユーザー自身のマイクロポストも一緒に取り出すこと。

image.png

出典:図 14.22: id 1のユーザーがid 2、7、8、10をフォローしているときのフィード

どのようにフィードを実装するのかはまだ明確ではないが、テストについてはやや明確そうなので、
まずはテストから書いていく。

このテストで重要なことは、以下の3つの条件を満たすこと。

  • フォローしているユーザーのマイクロポストがフィードに含まれている
  • 自分自身のマイクロポストもフィードに含まれている
  • フォローしていないユーザーのマイクロポストがフィードに含まれていない

まずは、MichaelがLanaをフォローしていて、Archerをフォローしていないという状況を作ってみる。

この状況のMichaelのフィードでは、Lanaと自分自身の投稿が見えていて、Archerの投稿は見えないことになる。

先ほどの3つの条件をアサーションに変換して、Userモデルにfeedメソッドがあることに注意しながら、
更新したUserモデルに対するテストを書いていく。

user_test.rb
  test "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
  end

feedメソッドはまだ定義していないのでテストは失敗する。

$ 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)
  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>'

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の入門書なので)

これで、ステータスフィードの実装が完了した。

スクリーンショット 2019-02-09 3.14.21.png

いつも通り、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

スクリーンショット 2019-02-09 4.11.13.png

完成したサンプルAppはこちら

新規作成アカウントの有効化ができない場合は、以下のid、passでログインしてみてください。

email:example-2@railstutorial.org
pass:password

演習

1:Homeページで表示される1ページ目のフィードに対して、統合テストを書いてみる。

following_test.rb
  test "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
  end

2:上記コードでは、期待されるHTMLをCGI.escapeHTMLメソッドでエスケープしている。
その理由は?
また、試しにエスケープ処理を外して、得られるHTMLの内容を調べてみて、マイクロポストの内容がおかしい点を挙げよ。

following_test.rb
  test "feed on Home page" do
    get root_path
    @user.feed.paginate(page: 1).each do |micropost|
      assert_match micropost.content, response.body
    end
  end

A: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を指定せずに全てのメンバーを表示したりできる

スクリーンショット 2019-02-07 17.05.39.png

  • 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文を入れ子にしたものを指す。
入れ子構造にすることで、より効率的にデータを取得できる。

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む