railstutorial.jp 11章メモ

  • テーマ
    • has_many throughアソシエーション
    • member, collection ルーティング
    • コレクションとアンパサンドシンボルメソッド
    • Ajaxリクエストとレスポンス

ユーザ has_many リレーションシップ
リレーションシップ has_many ユーザ

リレーションシップをリンクテーブルとしてユーザとユーザが多対多の関係にある

$ rails generate model Relationship follower_id:integer followed_id:integer

ユニークインデックスを張る

add_index :relationships, [:follower_id, :followed_id], unique: true

リレーションの関連づけ
Railsはデフォルト、クラス名_idという名前のカラムは外部キーとして認識する
そうでない場合には外部キーforeign_keyを明示的に指定する

class User < ActiveRecord::Base
  # relationshipsにあるusersへの参照は、user_idではなくfollower_id。
  has_many :relationships, foreign_key: "follower_id", dependent: :destroy
関連 モデル Relationshipで持つ参照
belongs_to :follower Follower follower_id
belongs_to :followed Followed followed_id

を推測するが、FollowerもFollowedモデルも存在しない。ここではUserモデルへの参照を持つ必要がある。
明示的にクラス名class_nameを明示的に指定する。

class Relationship < ActiveRecord::Base
  belongs_to :follower, class_name: "User"
  belongs_to :followed, class_name: "User"
end

has_many through関連付け

User と Userで多対多
user.followed_usersで取得できる用に設定 sourceパラメータで、followed_usersfollowedのidの集合であることを明示的に指定

class User < ActiveRecord::Base
  has_many :followed_users, through: :relationships, source: :followed

つまり、こんなじょうたいの表現

User.id <- has_many -> Relationship.follower_id Relationship.followed_id <- has_many -> User.id

# 逆リレーションシップを使ってuser.followersを実装する
# クラスを明示的に指定してReverseRelationshipクラスを探しに行かないようにする
has_many :reverse_relationships, foreign_key: "followed_id", class_name: "Relationship", dependent: :destroy
has_many :followers, through: :reverse_relationships, source: :follower

?をつけたメソッドは、慣習的に論理値を返す
!をつけたメソッドは、慣習的に失敗した場合には例外を発生する(create!とかsave!)

def following?(other_user)
  relationships.find_by(followed_id: other_user.id)
end

def follow!(other_user)
  relationships.create!(followed_id: other_user.id)
end

最初のユーザに3から51までをフォローさせ、
4から14のユーザは最初のユーザをフォローさせる

def make_relationships
  users = User.all
  user  = users.first
  followed_users = users[2..50]
  followers      = users[3..40]
  followed_users.each { |followed| user.follow!(followed) }
  followers.each      { |follower| follower.follow!(user) }
end

ルーティング
memberメソッドのルーティングはidを含むURLに対応するURLを生成する
collectionメソッドのルーティングはidを指定せずにURLに対応するURLを生成する

resources :users do

  member do
    get :following, :followers
  end

  collection do
    get :tigers
  end
end
$ rake routes
>         Prefix Verb   URI Pattern                    Controller#Action
> following_user GET    /users/:id/following(.:format) users#following
> followers_user GET    /users/:id/followers(.:format) users#followers
>   tigers_users GET    /users/tigers(.:format)        users#tigers
> .
> .
> .

カレントユーザと特定ユーザIDの両立

<% @user ||= current_user %>

have_xpathメソッドを使ってFollowしたときにUnfollowボタンに切り替わったことをテスト

describe "toggling the button" do
  before { click_button "Follow" }
  it { should have_xpath("//input[@value='Unfollow']") }
end

フォームでAjaxを使用する方法 form_forform_for ..., remote:trueにする。
内部的にはformタグにdata-remote="true"が加えられる

<%= form_for(current_user.relationships.build(followed_id: @user.id), remote: true) do |f| %>

xhrメソッドAjaxのテスト
xhr, HTTPリクエスト, アクション, paramsの内容を指定する。

it "should increment the Relationship count" do
  expect do
    xhr :post, :create, relationship: { followed_id: other_user.id }
  end.to change(Relationship, :count).by(1)
end

it "should respond with success" do
  xhr :post, :create, relationship: { followed_id: other_user.id }
  expect(response).to be_success
end

controller側でAjaxレスポンスをする
実際下記の例だと、リクエストの種類に応じて、続く行の中から1つだけが実行される

# respond_toはRspecのメソッドとは別物なので注意
respond_to do |format|
  format.html { redirect_to @user } # htmlが要求されていたらこっち
  format.js # jsが要求されていたらこっち
end

format.jsの場合、js.erbファイルが使われる
app/views/relationships/create.js.erb
app/views/relationships/destroy.js.erb
js.erbファイルのなかではjQueryJavaScriptヘルパーが使える
escape_javascript関数でHTMLをエスケープできる

列挙可能(enumerable)オブジェクト(配列やハッシュなど)
アンパサンド(&)とメソッド名に対応するシンボルで短縮表記

$ rails console

> [1,2,3,4].map { |i| i.to_s }
=> ["1", "2", "3", "4"]

> [1,2,3,4].map(&:to_s)
=> ["1", "2", "3", "4"]

> [1,2,3,4].map(&:to_s).join(',')
=> "1,2,3,4"

これを利用して、フォローしているユーザのID配列を取得する

> User.first.followed_users.map(&:id)

> User.first.followed_user_ids
=> 上と同じ結果!

ActiveRecordでは、:followed_usersアソシエーションから、followed_user_idsメソッドが使えるようになっている
さらにSQL文字列に挿入するときには?プレースホルダに配列を突っ込むと.join(',')を自動で行って文字列にしてくれる

relationships_controller_spec.rbがこける

Failures:

  1) RelationshipsController creating a relationship with Ajax should increment the Relationship count
     Failure/Error: before { sign_in user, no_capybara: true }
     NameError:
       undefined local variable or method `cookies' for #<RSpec::ExampleGroups::RelationshipsController::CreatingARelationshipWithAjax:0x007f9901433e28>
     # ./spec/support/utilities.rb:30:in `sign_in'
     # ./spec/controllers/relationships_controller_spec.rb:8:in `block (2 levels) in <top (required)>'

requireを追記したらうごいた

require 'rails_helper'
require 'support/utilities.rb'
require 'spec_helper'

describe RelationshipsController do

名前付きプレースホルダ

where("user_id IN (:followed_user_ids) OR user_id = :user_id",
      followed_user_ids: followed_user_ids, user_id: user)