20190209のRubyに関する記事は13件です。

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で続きを読む

いまさらながらのSOAP処理 ruby gem Savon

誰向け記事

  1. rubyでSOAPを扱う必要がある人
  2. ちょっとrubyを触ったことがある人

なぜいまさら、SOAP

お仕事の関係で、SOAPを扱う必要がでてきました。BtoBではまだ現役で使われているところもあるかと思います。
なお、私はSOAPについて未経験。

SOAPとは

SOAP(ソープ)は、コンピュータネットワーク内のWebサービスの実装において、構造化された情報を交換するための通信プロトコルの仕様である。拡張性、中立性、独立性を導入することを目的とする。XML-RPCから発展した、XML Webサービスのための、XMLベースのRPCプロトコルである。

出典:wikipedia

言うならば、昔のAPIプロトコルですね。RESTでやるのが現代のスタンダードだと思います。

いいGemはないかしら、と探しているとありました。

Heavy metal SOAP client

基本的な使い方

client = Savon.client(wsdl: 'http://service.example.com?wsdl')
client.operations # 使えるオペレーションの一覧がでる
response = client.call(:find_user, message: { id: 42 }) # find_userというオペレーションでid 42をとってこいみたいな。
response.body # HTTPレスポンスのボディ、hashで値が入っています。

渡すパラメータに型を明示する

SOAPではmessageで渡すパラメータに型を明示させることができるみたいで、その指定方法にはまりました。

やり方は下記のように

client = Savon.client(
  wsdl: 'http://service.example.com?wsdl', 
  namespaces: { "xmlns:xs": "http://www.w3.org/2001/XMLSchema" } # 名前空間どれ使うかを指定できる。
) 
client.call(:update, message: {id: 42, attributes!: { id: "xsi:type": "xs:int" } } # idのタイプ指定を追加
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

Shingeki no Kyojin Manga 114 Español Online

shingeki.jpg

Holas ya esta disponible Shingeki no Kyojin Manga 114 Español, para descargar y ver online en HeavenManga

http://heavenmanga.com/shingeki-no-kyojin-114.html

http://heavenmanga.com/shingeki-no-kyojin-114.html

  • このエントリーをはてなブックマークに追加
  • 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で続きを読む

ChatworkAPIでメッセージを投稿(Ruby)

ChatworkAPIを使用してメッセージの投稿を行ってみたのでメモ。
ChatworkAPIトークンの発行からrubyで作ったプログラムでメッセージ投稿、cronでの操作を行ってみました。
ここから改造して色々やる予定。

投稿方法

ChatworkAPI公式ドキュメント

http://developer.chatwork.com/ja/

APIトークンの発行

https://help.chatwork.com/hc/ja/articles/115000172402-API%E3%83%88%E3%83%BC%E3%82%AF%E3%83%B3%E3%82%92%E7%99%BA%E8%A1%8C%E3%81%99%E3%82%8B

メッセージ投稿プログラム

chatwork.rb
class Chatwork
  require 'net/http'
  require 'uri'

  ROOMID = #ルームID
  POSTURL = 'https://api.chatwork.com/v2/rooms/#[ルームID]/messages'
  TOKEN = #発行したAPIトークン

  def main
    uri = URI.parse(POSTURL)
    https = Net::HTTP.new(uri.host, uri.port) 
    https.use_ssl = true

    req = Net::HTTP::Post.new(uri.request_uri)
    req['X-ChatWorkToken'] = TOKEN
    req.set_form_data({'body' => 'メッセージ', 'self_unread' => 0}) # bodyは必須

    res = https.request(req)
  end
end

Chatwork.new.main

実行

ruby ./chatwork.rb

投稿結果

ちゃんとメッセージが投稿されました!

chatwork.png

cronで回してみる

cronに追記

プログラムを配置してる場所を指定。

cron
*/1 * * * * ruby /Users/[ユーザ名]/Documents/git/chatwork/chatwork.rb

実行結果

1分ごとに投稿されることを確認。
chatwork2.png

  • このエントリーをはてなブックマークに追加
  • 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で続きを読む

英語と日本語で考える、整理された 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で続きを読む

Rubyで深いHashが書きまくれるやつが欲しくなった

Rubyの深いハッシュ、例えば地球>日本>東京>目黒区>大岡山は

{
  earth: {
    japan: {
      tokyo: {
        meguro: {
          o_okayama: {
            # something
          }
        }
      }
    }
  }
}

こういう感じのHashになって、{}を書くことに疲れてしまいそうとおもった。
ネストしたHashを書きまくれるNというクラスを定義してみた

class N < Hash
  def initialize(*args)
    return self if args.length == 0
    key = args.shift
    case args.length
    when 0
      self[key] = nil
    when 1
      self[key] = args.first
    else
      self[key] = N.new(*args)
    end
  end
end
oookayama = %i(earth japan tokyo meguro o_okayama)
#=> [:earth, :japan, :tokyo, :meguro, :o_okayama]
N.new(*oookayama, 'something')
#=> {:earth=>{:japan=>{:tokyo=>{:meguro=>{:o_okayama=>"something"}}}}}

階層構造を変数にしてそのまま渡せるのでなんかわかりやすいかもと思ったけど、
使い所が思い浮かばなかった。

おわり。

  • このエントリーをはてなブックマークに追加
  • 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で続きを読む

bundle installがエラーになると思ったらMacOS Mojaveのアップグレードが原因だった話。

bundle installがエラーに!

bundle installができない! エラー文の指示にしたがってもまた指示でてきてループする!
と言う感じのエラーにはまって、半日潰してしまったので備忘録として残しておきます...。

環境はこちら

ruby 2.3.1p112 (2016-04-26 revision 54768) [x86_64-darwin17]
Rails 4.2.6
MacOS Mojave 10.14

ことの発端は、RailsのPaperclipで縦横比がおかしいためImageMagickのバージョン確認をしていたこと。
rmagickとの互換性がないことがわかり、以下の記事を参考に、ImageMagickを7からImageMagick6へダウングレードしました。

imagemagick と rmagick のインストールでつまづいたときに確認する手順
https://qiita.com/DriftwoodJP/items/56e9f9265022ba7a9802

記事の指示通り進め、
imagemagick6をinstallしました。

command
$ brew install imagemagick@6

以下結果です。

command-result
Updating Homebrew...
xcrun: error: invalid active developer path (/Library/Developer/CommandLineTools), missing xcrun at: /Library/Developer/CommandLineTools/usr/bin/xcrun
\以下?と黄色い文字のダウンロード内容が延々と続く

ここの「xcrun: error:」エラーは、MacOS Mojaveにアップデートしたことに関するXcodeのエラーのようです。このエラーが原因だった模様。後ほど言及します。

無限ループのエラーに遭遇

ダウングレードには成功した(バージョン確認できた)のですが、rmagickをアンインストールして以下のコマンドを打ったところ思わぬエラーが...。

command
PKG_CONFIG_PATH=/usr/local/opt/imagemagick@6/lib/pkgconfig bundle install --path vendor/bundle

以下表示された結果

command-result
Fetching gem metadata from https://rubygems.org/.......
Fetching rake 12.3.2
Installing rake 12.3.2
Fetching concurrent-ruby 1.1.4
Installing concurrent-ruby 1.1.4
Fetching i18n 0.9.5
Installing i18n 0.9.5
Fetching json 1.8.6
Installing json 1.8.6 with native extensions
Gem::Ext::BuildError: ERROR: Failed to build gem native extension.

    current directory: /Users/***/projects/***/vendor/bundle/ruby/2.3.0/gems/json-1.8.6/ext/json/ext/generator
/Users/***/.rbenv/versions/2.3.1/bin/ruby -r ./siteconf20190208-54374-blljf9.rb extconf.rb
creating Makefile

current directory: /Users/***/projects/***/vendor/bundle/ruby/2.3.0/gems/json-1.8.6/ext/json/ext/generator
make "DESTDIR=" clean
xcrun: error: invalid active developer path (/Library/Developer/CommandLineTools), missing xcrun at:
/Library/Developer/CommandLineTools/usr/bin/xcrun

current directory: /Users/***/projects/***/vendor/bundle/ruby/2.3.0/gems/json-1.8.6/ext/json/ext/generator
make "DESTDIR="
xcrun: error: invalid active developer path (/Library/Developer/CommandLineTools), missing xcrun at:
/Library/Developer/CommandLineTools/usr/bin/xcrun

make failed, exit code 1

Gem files will remain installed in /Users/***/projects/***/vendor/bundle/ruby/2.3.0/gems/json-1.8.6 for inspection.
Results logged to /Users/***/projects/***/vendor/bundle/ruby/2.3.0/extensions/x86_64-darwin-17/2.3.0-static/json-1.8.6/gem_make.out

An error occurred while installing json (1.8.6), and Bundler cannot continue.
Make sure that `gem install json -v '1.8.6' --source 'https://rubygems.org/'` succeeds before bundling.

In Gemfile:
  rails was resolved to 4.2.6, which depends on
    actionmailer was resolved to 4.2.6, which depends on
      actionpack was resolved to 4.2.6, which depends on
        actionview was resolved to 4.2.6, which depends on
          rails-dom-testing was resolved to 1.0.9, which depends on
            rails-deprecated_sanitizer was resolved to 1.0.3, which depends on
              activesupport was resolved to 4.2.6, which depends on
                json

上のエラー文の中の最後のこちらのエラーに悩まされました。

command
An error occurred while installing json (1.8.6), and Bundler cannot continue.
Make sure that `gem install json -v '1.8.6' --source 'https://rubygems.org/'` succeeds before bundling.

json1.8.6をインストールしろという指示

json 1.8.6をinstallしろとのこと。よく分からんけど、とりあえず指示にしたがっておこう。ということで、この指示通りにinstallしようと以下を実行

command
$ gem install json -v '1.8.6' --source 'https://rubygems.org/'

すると結果がこちら。

command-result
ERROR:  While executing gem ... (Errno::EPERM)
    Operation not permitted @ chmod_internal - /Users/***/.rbenv/versions/2.3.1/lib/ruby/gems/2.3.0/gems/json-1.8.6/tests/test_json.rb

ん?、permittionがない?
bundle doctorで確認してみると、すべてのgemがmissingになってしまっている。
よし、sudo で無理やりするしかないか、と思い以下を実行すると、

command
$ sudo gem install json -v '1.8.6' --source 'https://rubygems.org/'
commad-result
Password:
Fetching json-1.8.1.gem
Building native extensions. This could take a while...
ERROR:  Error installing json:
    ERROR: Failed to build gem native extension.

/以下最初のエラー文と同じ/
An error occurred while installing json (1.8.6), and Bundler cannot continue.
Make sure that `gem install json -v '1.8.6' --source 'https://rubygems.org/'` succeeds before bundling.
/以下も同じ/

またもやgem install json しろという無限ループに。

この時点で、bundle installbundle updateもできなくなり、rails sもできなくなりました。 sudo でも動かなくなり。。。やばい、何もできない。。。

ちなみに、Rails sをすると、、、

command-result
Could not find json-1.8.6 in any of the sources
Run `bundle install` to install missing gems.

とのこと。bundle installができないんですよ〜エラーさん。。。

いろいろと試す

ということでいろいろとエラー文でググりまくり解決策を見つけようと試みましたがどれもうまくいかず。

これも試す

Can't install JSON gem
https://stackoverflow.com/questions/5216278/cant-install-json-gem

bundle execも試す

Why am I not able to install JSON gem?
https://stackoverflow.com/questions/42409173/why-am-i-not-able-to-install-json-gem?rq=1

こちらも確認するも問題なし

bundle install しようとしたら ruby のバージョン違くてコケた話
https://qiita.com/white_aspara25/items/d5e19b82be17048d9215

仕方なくgit resetして一つ前のコミットに戻るも、環境は変わらずエラーは解消せず。

MacOS Mojaveのアップグレードが関係する?

英語でググってみたりStack overflow探したりするなかでこちらの記事を発見。

Rails: macOSをMojaveにアップグレード後bundle installがエラーになった場合の対応方法
https://techracho.bpsinc.jp/hachi8833/2018_10_10/62963

ん、そういや最近Mojaveにアップグレードしたぞ。。。
Xcode普段使わないのにこんなところでエラーの原因になってるとは。。

まさにこの記事の通りの状態でした。
/Library/Developer/CommandLineToolsをローカルで探すもフォルダがないことがわかり、この記事の指示の前に以下のコマンドでインストール。

command
$ xcode-select --install

そして記事の通り以下を実行すると無事インストール画面のポップアップがでてきました。

command
$ sudo open /Library/Developer/CommandLineTools/Packages/macOS_SDK_headers_for_macOS_10.14.pkg

インストール終了後、sudo bundle installをすると、見事成功。

json1.8.6というversionのエラーと、Mojaveのアップグレードがどう関係していたのかは未だ謎なのですが、MacOS Mojaveのアップグレード後に起きうるエラーのようなので試してみてください。

  • このエントリーをはてなブックマークに追加
  • 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で続きを読む

Ruby 2.6.1 に含まれる Bundler 1.17.2 に不具合があるという話

不具合

先日知った以下の件。

この問題は gem update --system によって default gems としての Bundler を 1.17.3 にすれば回避可能でしょうか?

Ruby 2.6.1 は Ruby 2.5.3 と比較して、メモリ使用量が大きく(数百 MB 単位で)削減される(ケースがある)ことが確認できているため、簡単な Workaround で回避可能であれば個人的には積極的に利用したいところです。

比較

default gems としての Bundler のバージョンと、通常の gem としての Bundler のバージョンの組み合わせについて、実行に問題がなさそうなのかを確認。確認内容があっているのか、あまり自信がない。

Bundler 1.17.3 で作った Gemfile.lock

# default gems gem install ruby -rbundler/setup bundle exec ruby -rbundler/setup
A 1.17.2 None 指定バージョンを利用可能 default gems のものを利用してしまう
B 1.17.2 1.17.3 bundler の取り違い 指定バージョンを利用可能
C 1.17.3 None 指定バージョンを利用可能 指定バージョンを利用可能
D 1.17.3 2.0.1 指定バージョンを利用可能 指定バージョンを利用可能

Bundler 2.0.1 で作った Gemfile.lock

# default gems gem install ruby -rbundler/setup bundle exec ruby -rbundler/setup
E 1.17.2 None bundle install --deployment 不可 bundle install --deployment 不可
F 1.17.2 2.0.1 You must use Bundler 2 or greater with this lockfile. 指定バージョンを利用可能
G 1.17.3 2.0.1 指定バージョンを利用可能 指定バージョンを利用可能

補足(1): Bundler 1.17.3 で作った Gemfile.lock

$ bundle -v
Bundler version 1.17.3
$ bundle init
$ echo 'gem "csv", "3.0.3"' >> Gemfile
$ bundle install
$ cat Gemfile.lock
GEM
  remote: https://rubygems.org/
  specs:
    csv (3.0.3)

PLATFORMS
  ruby

DEPENDENCIES
  csv (= 3.0.3)

BUNDLED WITH
   1.17.3

A

$ docker run --rm -it -v(pwd):/mnt centos:7 bash
# yum install -y https://github.com/feedforce/ruby-rpm/releases/download/2.6.1/ruby-2.6.1-1.el7.centos.x86_64.rpm
# ruby -v
ruby 2.6.1p33 (2019-01-30 revision 66950) [x86_64-linux]
# gem -v
3.0.1
# bundle -v
Bundler version 1.17.2
# gem list bundler

*** LOCAL GEMS ***

bundler (default: 1.17.2)
# cp /mnt/Gemfile* .
# bundle install --deployment
# ruby -rbundler/setup -rcsv -e 'puts CSV::VERSION'
3.0.3
# bundle exec ruby -rbundler/setup -rcsv -e 'puts CSV::VERSION'
3.0.4

B

$ docker run --rm -it -v(pwd):/mnt centos:7 bash
# yum install -y https://github.com/feedforce/ruby-rpm/releases/download/2.6.1/ruby-2.6.1-1.el7.centos.x86_64.rpm
# gem install bundler -v 1.17.3 --no-document
# bundle -v
Bundler version 1.17.3
# gem list bundler

*** LOCAL GEMS ***

bundler (1.17.3, default: 1.17.2)
# cp /mnt/Gemfile* .
# bundle install --deployment
# ruby -rbundler/setup -rcsv -e 'puts CSV::VERSION'
Traceback (most recent call last):
        9: from /usr/lib64/ruby/2.6.0/rubygems/core_ext/kernel_require.rb:54:in `require'
        8: from /usr/lib64/ruby/2.6.0/rubygems/core_ext/kernel_require.rb:54:in `require'
        7: from /usr/lib64/ruby/2.6.0/bundler/setup.rb:10:in `<top (required)>'
        6: from /usr/lib64/ruby/gems/2.6.0/gems/bundler-1.17.3/lib/bundler.rb:107:in `setup'
        5: from /usr/lib64/ruby/gems/2.6.0/gems/bundler-1.17.3/lib/bundler/runtime.rb:26:in `setup'
        4: from /usr/lib64/ruby/gems/2.6.0/gems/bundler-1.17.3/lib/bundler/runtime.rb:26:in `map'
        3: from /usr/lib64/ruby/2.6.0/forwardable.rb:230:in `each'
        2: from /usr/lib64/ruby/2.6.0/forwardable.rb:230:in `each'
        1: from /usr/lib64/ruby/gems/2.6.0/gems/bundler-1.17.3/lib/bundler/runtime.rb:31:in `block in setup'
/usr/lib64/ruby/gems/2.6.0/gems/bundler-1.17.3/lib/bundler/runtime.rb:319:in `check_for_activated_spec!': You have already activated bundler 1.17.3, but your Gemfile requires bundler 1.17.2. Prepending `bundle exec` to your command may solve this. (Gem::LoadError)
# bundle exec ruby -rbundler/setup -e 'puts Bundler::VERSION'
1.17.3
# bundle exec ruby -rbundler/setup -rcsv -e 'puts CSV::VERSION'
3.0.3

C

$ docker run --rm -it -v(pwd):/mnt centos:7 bash
# yum install -y https://github.com/feedforce/ruby-rpm/releases/download/2.6.1/ruby-2.6.1-1.el7.centos.x86_64.rpm
# gem update --system
# bundle -v
Bundler version 1.17.3
# gem list bundler

*** LOCAL GEMS ***

bundler (default: 1.17.3)
# cp /mnt/Gemfile* .
# bundle install --deployment
# ruby -rbundler/setup -e 'puts Bundler::VERSION'
1.17.3
# ruby -rbundler/setup -rcsv -e 'puts CSV::VERSION'
3.0.3
# bundle exec ruby -rbundler/setup -e 'puts Bundler::VERSION'
1.17.3
# bundle exec ruby -rbundler/setup -rcsv -e 'puts CSV::VERSION'
3.0.3

D

$ docker run --rm -it -v(pwd):/mnt centos:7 bash
# yum install -y https://github.com/feedforce/ruby-rpm/releases/download/2.6.1/ruby-2.6.1-1.el7.centos.x86_64.rpm
# gem install bundler --no-document
# gem update --system
# gem -v
3.0.2
# bundle -v
Bundler version 2.0.1
# gem list bundler

*** LOCAL GEMS ***

bundler (2.0.1, default: 1.17.3)
# cp /mnt/Gemfile* .
# bundle install --deployment
# ruby -rbundler/setup -e 'puts Bundler::VERSION'
1.17.3
# ruby -rbundler/setup -rcsv -e 'puts CSV::VERSION'
3.0.3
# bundle exec ruby -rbundler/setup -e 'puts Bundler::VERSION'
1.17.3
# bundle exec ruby -rbundler/setup -rcsv -e 'puts CSV::VERSION'
3.0.3

補足: Bundler 2.0.1 で作った Gemfile.lock

$ bundle -v
Bundler version 2.0.1
$ bundle init
$ echo 'gem "csv", "3.0.3"' >> Gemfile
$ bundle install
$ cat Gemfile.lock
GEM
  remote: https://rubygems.org/
  specs:
    csv (3.0.3)

PLATFORMS
  ruby

DEPENDENCIES
  csv (= 3.0.3)

BUNDLED WITH
   2.0.1

E

$ docker run --rm -it -v(pwd):/mnt centos:7 bash
# yum install -y https://github.com/feedforce/ruby-rpm/releases/download/2.6.1/ruby-2.6.1-1.el7.centos.x86_64.rpm
# ruby -v
ruby 2.6.1p33 (2019-01-30 revision 66950) [x86_64-linux]
# gem -v
3.0.1
# bundle -v
Bundler version 1.17.2
# gem list bundler

*** LOCAL GEMS ***

bundler (default: 1.17.2)
# cp /mnt/Gemfile* .
# bundle install --deployment
Traceback (most recent call last):
        2: from /usr/bin/bundle:23:in `<main>'
        1: from /usr/lib64/ruby/2.6.0/rubygems.rb:302:in `activate_bin_path'
/usr/lib64/ruby/2.6.0/rubygems.rb:283:in `find_spec_for_exe': Could not find 'bundler' (2.0.1) required by your /Gemfile.lock. (Gem::GemNotFoundException)
To update to the latest version installed on your system, run `bundle update --bundler`.
To install the missing version, run `gem install bundler:2.0.1`

F

$ docker run --rm -it -v(pwd):/mnt centos:7 bash
# yum install -y https://github.com/feedforce/ruby-rpm/releases/download/2.6.1/ruby-2.6.1-1.el7.centos.x86_64.rpm
# gem install bundler -v 2.0.1 --no-document
# bundle -v
Bundler version 2.0.1
# gem list bundler

*** LOCAL GEMS ***

bundler (2.0.1, default: 1.17.2)
# cp /mnt/Gemfile* .
# bundle install --deployment
# ruby -rbundler/setup -e 'puts Bundler::VERSION'
You must use Bundler 2 or greater with this lockfile.
# ruby -rbundler/setup -rcsv -e 'puts CSV::VERSION'
You must use Bundler 2 or greater with this lockfile.
# bundle exec ruby -rbundler/setup -e 'puts Bundler::VERSION'
2.0.1
# bundle exec ruby -rbundler/setup -rcsv -e 'puts CSV::VERSION'
3.0.3

G

$ docker run --rm -it -v(pwd):/mnt centos:7 bash
# yum install -y https://github.com/feedforce/ruby-rpm/releases/download/2.6.1/ruby-2.6.1-1.el7.centos.x86_64.rpm
# gem install bundler --no-document
# gem update --system
# gem -v
3.0.2
# bundle -v
Bundler version 2.0.1
# gem list bundler

*** LOCAL GEMS ***

bundler (2.0.1, default: 1.17.3)
# cp /mnt/Gemfile* .
# bundle install --deployment
# ruby -rbundler/setup -e 'puts Bundler::VERSION'
2.0.1
# ruby -rbundler/setup -rcsv -e 'puts CSV::VERSION'
3.0.3
# bundle exec ruby -rbundler/setup -e 'puts Bundler::VERSION'
2.0.1
# bundle exec ruby -rbundler/setup -rcsv -e 'puts CSV::VERSION'
3.0.3

おまけ

gem install bundlergem update --system の実行順序によって、実行ファイル(ラッパー) bundle が conflict する様です。

bundler → rubygems

特に警告が出ることなく、また /usr/bin/bundle の内容が変わることはなかった。

# gem install bundler --no-document
...
# gem update --system
...

rubygems → bundler

gem update --system によって /usr/bin/bundle が更新され、 gem install bundler で上書きすると Ruby 同梱同等の内容に戻った。

# gem update --system
...
# gem install bundler --no-document
...
bundler's executable "bundle" conflicts with /usr/bin/bundle
Overwrite the executable? [yN]  y
...

RPM でインストールした /usr/bin/bundle

#!/usr/bin/ruby
#
# This file was generated by RubyGems.
#
# The application 'bundler' is installed as part of a gem, and
# this file is here to facilitate running it.
#

require 'rubygems'

version = ">= 0.a"

str = ARGV.first
if str
  str = str.b[/\A_(.*)_\z/, 1]
  if str and Gem::Version.correct?(str)
    version = str
    ARGV.shift
  end
end

if Gem.respond_to?(:activate_bin_path)
load Gem.activate_bin_path('bundler', 'bundle', version)
else
gem "bundler", version
load Gem.bin_path("bundler", "bundle", version)
end

gem update --system で暗黙的に上書きされた /usr/bin/bundle

Bundler の exe/bundle とほぼ同等。違いは shebang のみ。

#!/usr/bin/ruby
# frozen_string_literal: true

# Exit cleanly from an early interrupt
Signal.trap("INT") do
  Bundler.ui.debug("\n#{caller.join("\n")}") if defined?(Bundler)
  exit 1
end

require "bundler"
# Check if an older version of bundler is installed
$LOAD_PATH.each do |path|
  next unless path =~ %r{/bundler-0\.(\d+)} && $1.to_i < 9
  err = String.new
  err << "Looks like you have a version of bundler that's older than 0.9.\n"
  err << "Please remove your old versions.\n"
  err << "An easy way to do this is by running `gem cleanup bundler`."
  abort(err)
end

require "bundler/friendly_errors"
Bundler.with_friendly_errors do
  require "bundler/cli"

  # Allow any command to use --help flag to show help for that command
  help_flags = %w[--help -h]
  help_flag_used = ARGV.any? {|a| help_flags.include? a }
  args = help_flag_used ? Bundler::CLI.reformatted_help_args(ARGV) : ARGV

  Bundler::CLI.start(args, :debug => true)
end

gem install bundler で上書きした /usr/bin/bundle

#!/usr/bin/ruby
#
# This file was generated by RubyGems.
#
# The application 'bundler' is installed as part of a gem, and
# this file is here to facilitate running it.
#

require 'rubygems'

version = ">= 0.a"

str = ARGV.first
if str
  str = str.b[/\A_(.*)_\z/, 1]
  if str and Gem::Version.correct?(str)
    version = str
    ARGV.shift
  end
end

if Gem.respond_to?(:activate_bin_path)
load Gem.activate_bin_path('bundler', 'bundle', version)
else
gem "bundler", version
load Gem.bin_path("bundler", "bundle", version)
end

rbenv install 2.6.1 で作られた versions/2.6.1/bin/bundle

#!/home/koshigoe/.rbenv/versions/2.6.1/bin/ruby
#
# This file was generated by RubyGems.
#
# The application 'bundler' is installed as part of a gem, and
# this file is here to facilitate running it.
#

require 'rubygems'

version = ">= 0.a"

str = ARGV.first
if str
  str = str.b[/\A_(.*)_\z/, 1]
  if str and Gem::Version.correct?(str)
    version = str
    ARGV.shift
  end
end

if Gem.respond_to?(:activate_bin_path)
load Gem.activate_bin_path('bundler', 'bundle', version)
else
gem "bundler", version
load Gem.bin_path("bundler", "bundle", version)
end
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む