Step by Step Ruby on Rails

Ruby on Railsで実際にWebサイトを構築する手順をまとめています。

Ruby on Rails Tutorial フォロアーの数を表示

「Ruby on Rails Tutorial」のサンプルアプリをAngularJSとBootstrap3を使う形にして作成します。今回からフォロアーの機能を追加しています。まずフォロアーに関するモデル、アソシエーションを定義し、AngularJSのビューでフォロアーの数を表示します。

(1)フォロアーのモデル、アソシエーション、バリデーション定義作成

フォローしている人(follower_id)とフォローされている人(followed_id)を結びつけるRelationshipモデルを作成します。

1)モデルの作成

$ rails generate model Relationship follower_id:integer followed_id:integer

2)relationshipsテーブルを定義

各項目のインデックスと複合インデックスを追加します。

$ vi db/migrate/20150724080930_create_relationships.rb

class CreateRelationships < ActiveRecord::Migration
  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

3)マイグレーション実行

$ bundle exec rake db:migrate

4)relationshipsモデルにバリデーションチェック追加

$ vi app/models/relationship.rb

validates :follower_id, presence: true
validates :followed_id, presence: true

(2)ユーザーとフォローしているユーザーとのアソシエーション定義

そのユーザーがフォローしているユーザーである"followed_users"の関連を定義します。

1)Userモデルにアソシエーション追加

Relationshipテーブルは、follower_idとfollowed_idのカラムから構成されています。

ユーザーとそのユーザーがフォローしている人との関連を定義します
has_many throughを使って定義します。

①relationshipsモデルにアソシエーション定義

relationshipsは、二人のユーザー間の関係を示すので、フォローしているユーザーとフォローされている人の両方に属します。

Userモデルとのアソシエーションを"followed"というアソシエーション名で定義します。"follower"のアソシエーションは、フォロアーのアソシエーション定義に使用します。

$ vi app/models/relationship.rb

belongs_to :follower, class_name: "User"
belongs_to :followed, class_name: "User"

"follower"と"followed"はモデルではないので、class_nameでクラスを記述します。

②Userモデルにアソシエーションを定義

"followed_users"というアソシエーション名でhas_many :throughを使って定義します。

これによってrelationshipsテーブルのfollowed_idを使用してそのユーザがフォローしているユーザーを表すfollowed_users配列を作成できます。

$ vi app/models/user.rb

has_many :relationships, foreign_key: "follower_id", dependent: :destroy
has_many :followed_users, through: :relationships, source: :followed

・Railsは、外部キーを"<class>_id"の形式で記述されていると期待していますが、この場合は"relationship"テーブル内の"follower_id"を外部キーとして使用するので明示的に指定します。

・relationshipモデルには、"followed"と"follower"の2つのアソシエーションを定義しているのでどちらを使用するのか指定する必要があります。sourceを使って指定します。

・通常は下記のように指定しますが、"followed"の複数形がおかしくなるので、"followed_users"に変更し、"source:"パラメータで配列の元を明示しています。

has_many :followeds, through: :relationships

2)Userモデルにメソッド追加

上記で定義したhas_many :relationshipsのアソシエーションを使ってユーザー定義のメソッドを追加します。

$ vi app/models/user.rb

  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

  def unfollow!(other_user)
    relationships.find_by(followed_id: other_user.id).destroy
  end

①following?(other_user)

・"other_user"をすでにフォローしているか確認する。

・データベース上に存在するかチェックする。

②follow!(other_user)

・"other_user"をフォローするため"relationship"テーブルにレコードを作成する。

・感嘆符"!"をつけ、作成に失敗した場合は例外を発生するようにする。

③unfollow!(other_user)

・"other_user"のフォローを"relationship"テーブルから削除する。

(3)ユーザーとフォロアーのアソシエーション定義

そのユーザーをフォローしているユーザー"followers"の関連を定義します。

1)Userモデルにアソシエーション追加

①relationshipsモデルのアソシエーション定義を確認

$ more app/models/relationship.rb

belongs_to :follower, class_name: "User"
belongs_to :followed, class_name: "User"

②Userモデルにアソシエーション追加

"followers"というアソシエーション名でhas_many :throughを使って定義します。

これによってrelationshipsテーブルのfollower_idを使用してそのユーザーのフォロアーを表すfollowers配列を作成できます。

すでに定義済みの"followed_users"のアソシエーションの定義と対になる関係なので、逆リレーションシップによって定義します。

"reverse_relationships"に関する定義を2行追加します。

$ vi app/models/user.rb

  has_many :reverse_relationships, foreign_key: "followed_id",
                                   class_name:  "Relationship",
                                   dependent:   :destroy
  has_many :followers, through: :reverse_relationships, source: :follower

2)コンソールで確認

ユーザーとフォローしている、フォロアーのリレーション定義が一通り出来たので、コンソールで確認します。

①ユーザーインスタンス作成

2.0.0-p247 :005 > user2=User.find_by(id: 2)
2.0.0-p247 :006 > user6=User.find_by(id: 6)

②"following?"メソッドを使ってお互いをフォローしているか確認

2.0.0p247 :021 > user2.following?(user6)
 => nil
2.0.0p247 :022 > user6.following?(user2)
 => nil

③"follow!"メソッドを使ってお互いフォロー

2.0.0p247 :023 > user2.follow!(user6)
   (0.2ms)  begin transaction
  SQL (0.3ms)  INSERT INTO "relationships" ("created_at", "followed_id", "follower_id", "updated_at") VALUES (?, ?, ?, ?)  [["created_at", "2015-07-24 08:17:13.716171"], ["followed_id", 6], ["follower_id", 2], ["updated_at", "2015-07-24 08:17:13.716171"]]
   (3.4ms)  commit transaction
 => #<Relationship id: 1, follower_id: 2, followed_id: 6, created_at: "2015-07-24 08:17:13", updated_at: "2015-07-24 08:17:13">

2.0.0p247 :024 > user6.follow!(user2)

④relationships、reverse_relationshipsのアソシエーション

2.0.0p247 :009 > user2.relationships
 => #<ActiveRecord::Associations::CollectionProxy [#<Relationship id: 1, follower_id: 2, followed_id: 6, created_at: "2015-07-24 08:17:13", updated_at: "2015-07-24 08:17:13">]>

2.0.0p247 :010 > user2.reverse_relationships
  Relationship Load (0.2ms)  SELECT "relationships".* FROM "relationships"  WHERE "relationships"."followed_id" = ?  [["followed_id", 2]]
 => #<ActiveRecord::Associations::CollectionProxy [#<Relationship id: 2, follower_id: 6, followed_id: 2, created_at: "2015-07-24 08:17:54", updated_at: "2015-07-24 08:17:54">]>

2.0.0p247 :015 > Relationship.all
  Relationship Load (0.2ms)  SELECT "relationships".* FROM "relationships"
 => #<ActiveRecord::Relation [#<Relationship id: 1, follower_id: 2, followed_id: 6, created_at: "2015-07-24 08:17:13", updated_at: "2015-07-24 08:17:13">, #<Relationship id: 2, follower_id: 6, followed_id: 2, created_at: "2015-07-24 08:17:54", updated_at: "2015-07-24 08:17:54">]>

2.0.0p247 :016 > user2.relationships.find_by(followed_id: user6)
  Relationship Load (0.1ms)  SELECT  "relationships".* FROM "relationships"  WHERE "relationships"."follower_id" = ? AND "relationships"."followed_id" = 6 LIMIT 1  [["follower_id", 2]]
 => #<Relationship id: 1, follower_id: 2, followed_id: 6, created_at: "2015-07-24 08:17:13", updated_at: "2015-07-24 08:17:13">

⑤followed_users、followersのアソシエーション

下記アソシエーションを通じて取得します。
has_many :followers, through: :reverse_relationships, source: :follower
has_many :followed_users, through: :relationships, source: :followed

2.0.0p247 :025 > user2.followed_users.count
 => 1
2.0.0p247 :026 > user2.followers.count
 => 1
2.0.0p247 :027 > user6.followed_users.count
 => 1
2.0.0p247 :029 > user6.followers.count
 => 1
2.0.0p247 :032 > user2.followers.all

2.0.0p247 :033 > user2.followed_users.all

(4)フォローしている人、フォロアーのカウント数を表示

対象ユーザーがフォローしている人、フォロアーの人数を計算し、ホームページに表示します。

1)Railsコントローラのアクションにfollowed_users、followersを追加

$ vi app/controllers/sessions_controller.rb

  def create
    user = User.find_by(email: session_params[:email].downcase)
    if user && user.authenticate(session_params[:password])
      remember_token = User.new_remember_token
      cookies.permanent[:remember_token] = remember_token
      user.update_attribute(:remember_token, User.encrypt(remember_token))
      gravatar_id = Digest::MD5::hexdigest(user.email.downcase)
      microposts = user.microposts
      @user_info = {
        user: user,
        gravatar_url: "https://secure.gravatar.com/avatar/#{gravatar_id}",
        microposts: microposts,
        feed: user.feed,
        followed_users: user.followed_users,
        followers:  user.followers
      }
      render json: @user_info, status: :accepted, location: user
    else
  :
  def current_user
    remember_token = User.encrypt(cookies[:remember_token])
    current_user ||= User.find_by(remember_token: remember_token)
    if current_user
      gravatar_id = Digest::MD5::hexdigest(current_user.email.downcase)
      microposts = current_user.microposts
      @user_info = {
        user: current_user,
        gravatar_url: "https://secure.gravatar.com/avatar/#{gravatar_id}",
        microposts: microposts,
        feed: current_user.feed,
        followed_users: current_user.followed_users,
        followers:  current_user.followers
      }
      render json: @user_info, status: :accepted
    else
      head :no_content
    end
  end

2)AngularJSのビューを修整

homeビューにフォローしている人、フォロアー数の表示を追加します。
$ vi app/assets/templates/static_pages/home.html.erb

<div ng-controller="HomeCtrl">
  <div ng-show="chkSignin().user.id > 0" class="row">
   :
    <div class="col-sm-4">
   :
      <div class="stats">
        <a href="#">
          <strong id="following" class="stat">
            {\{chkSignin().followed_users.length}}
          </strong>
          following
        </a>
        <a href="#">
          <strong id="followers" class="stat">
            {\{chkSignin().followers.length}}
          </strong>
          followers
        </a>
      </div>

3)CSSの設定追加

$ vi app/assets/stylesheets/custom.css.scss

.stats {
  overflow: auto;
  border-top: 1px solid $grayLighter;
  a {
    float: left;
    padding: 0 10px;
    border-left: 1px solid $grayLighter;
    color: gray;
    &:first-child {
      padding-left: 0;
      border: 0;
    }
    &:hover {
      text-decoration: none;
      color: blue;
    }
  }
  strong {
    display: block;
  }
}