search
LoginSignup
121

More than 3 years have passed since last update.

posted at

updated at

Railsでマッチング機能を作ってみる

12月19日のアドベントカレンダーは大野がやります。

毎年盛り上がっているアドベントカレンダーですが、そもそもアドベントってなんだ。

アドベントカレンダー (Advent calendar) は、クリスマスまでの期間に日数を数えるために使用されるカレンダーである。待降節の期間(イエス・キリストの降誕を待ち望む期間)に窓を毎日ひとつずつ開けていくカレンダーである。すべての窓を開け終わるとクリスマスを迎えたことになる。

クリスマスまでの期間に日数を数えるために使用されるカレンダーでしたか。

クリスマス関連で何か作ろうかと思いましたが、発想が貧困な自分はマッチングサイトしか浮かばなかったのでもうこれでいきます。

マッチング機能中心に試すので、それ以外は想像でお願いします。

デザイン

デザインはbootstrapを使って少し整えることとします。

gem 'bootstrap-sass'
gem 'jquery-rails'

デフォルトでgem 'sass-rails', '~> 5.0'は入っているので、上記をインストール。

application.js
//= require jquery
//= require bootstrap-sprockets

.cssを.scssに変更して以下の記述をする。

application.scss
@import "bootstrap-sprockets";
@import "bootstrap";

これでbootstrapを使っていきます。
デザインがあるとやる気がだいぶ変わってくるので、まあそれっぽい感じにしておきましょう。

スクリーンショット 2017-11-23 16.32.58.png

ユーザーをフォローする

お互いがそれぞれフォローし合っていたらマッチング成立とします。そのために、まずはユーザーをフォローする機能をつけていこうと思います。
ユーザー管理はdeviseに任せます。

gem 'devise'
bundle install --path vendor/bundle
rails g devise:install
rails g devise:views
rails g devise user

作ったユーザー間で多対多のアソシエーションを実装します。
ユーザー同士の関係性を保つモデルを作りましょう。

rails g model relationship
class CreateRelationships < ActiveRecord::Migration[5.1]
  def change
    create_table :relationships do |t|
      t.integer :follower_id
      t.integer :following_id
      t.timestamps
    end
    add_index :relationships, :follower_id
    add_index :relationships, :following_id
    add_index :relationships, [:follower_id, :following_id], unique: true
  end
end

indexも貼っておきます。

アソシエーション。とりあえずフォローすることしか考えません。

user.rb
  has_many :active_relationships,class_name:  "Relationship", foreign_key: "follower_id", dependent: :destroy
  has_many :following, through: :active_relationships
relationship.rb
  belongs_to :following, class_name: "User"

いいねのFormを作る

いいねボタンを押すと、そのユーザーをフォローする仕様にしたいと思います。
いいねボタンはユーザーのshowページにおきます。

users_controller.rb
class UsersController < ApplicationController
#省略
  def show
    @user = User.find(params[:id])
    @relationship = Relationship.new
  end
end
show.html.haml-users
.text-center
    = image_tag @user.avatar, class: "avatar"
.text-center#user-description
    = @user.nickname
    - if user_signed_in?
        #follow_form
            - if current_user.following?(@user)
                #liked.btn.btn-default いいね済
            - else
                = render 'follow', {relationship: @relationship}
    %br= @user.profile
User.rb
def following?(other_user)
  following.include?(other_user)
end
_folllow.html.haml
= form_with model: relationship, remote: true do |f|
    %div= hidden_field_tag :following_id, @user.id
    = f.submit "いいね", class: "btn btn-primary"
relationships_controller
class RelationshipsController < ApplicationController
    def create
      current_user.active_relationships.create(create_params)
    end

    private

    def create_params
      params.permit(:following_id)
    end
end
create.js.erb
$("#follow_form").html(`<div class="btn btn-default">いいね済</div>`)

like_you.gif

ボタン押したら、createアクション動いてrelationshipsテーブルにはフォローフォロワーの関係が書かれます。

これあれですね、マッチングしたらなんか出るといいですね。マッチングしたけど、「いいね済」としか表示されなかったらなんの面白みもないわ。

そういうことなので、マッチングしたらちょっとした演出を入れたいと思います。これは後の方でほんのちょっと書きます。

マッチングしているユーザー一覧を取得する

お互いがそれぞれをフォローし合ったらマッチング成立とします。

さっきまででユーザーをいいね(フォロー)することはできました。今のままではいいねするだけで、そのユーザーとマッチングしているかどうかは分かりません。

現在ログイン中のユーザーとマッチングしているユーザーを取得したいので、そのための記述をします。

ActiveRecordの力で取得する

自分がフォローしている && 自分がフォローされているユーザーを取得します。お互いがフォローしている状態なので、マッチングしていますね。

ユーザーのフォロワーを取得してそのユーザーがフォローしている人と照らし合わせたいので、アソシエーションを追加します。

User.rb
has_many :passive_relationships, class_name: "Relationship", foreign_key: "following_id", dependent: :destroy 

ログイン中のユーザーとマッチングしているユーザーを取得します。

User.rb
def matchers
  follower_ids = passive_relationships.pluck(:follower_id)
  active_relationships.eager_load(:following)
  .select{|r|follower_ids.include? r.following_id}
  .map{|r|r.following}
end

なげえ・・・

これでユーザーは取得できるけど、こんなに書きたくない。

要はお互いがお互いのことをフォローしていればいいんです。あるユーザーのフォローとフォロワーを取得して、重なっている人がマッチング済のユーザー。
これを簡単に書くためにさらにもう少しアソシエーションを追加します。

user.rb
has_many :followers, through: :passive_relationships, source: :follower
relationship.rb
belongs_to :follower, class_name: "User"

追加したらあとはもう重なり合っているユーザーを取得すればいいだけではないでしょうか。

def matchers
  following & followers
end

これでいいんじゃない。
ただ、どちらの場合でもこのとき取ったユーザー集団のクラスはArrayになります。

ActiveRecordRelationが良いという場合は、following、followersの考えでこんな感じでできます。

  def matchers
    User.where(id: passive_relationships.select(:follower_id))
     .where(id: active_relationships.select(:following_id))
  end

以下のように、SQLでもActiveRecordRelationにできますね。

Sqlで頑張って取得する

User.matching(current_user)
scope :matching, -> user_id { joins("INNER JOIN relationships ON relationships.follower_id = users.id
  INNER JOIN relationships AS r ON relationships.following_id = r.follower_id AND r.following_id = relationships.follower_id").where('relationships.following_id = ?', user_id) }

こんな感じで取得すれば、classはActiveRecord_Relationとなります。
こっちのクラスの方がこの先Happyな気がしますね。

マッチング済のユーザーを表示する

どちらの方法でもあとは呼び出すだけで、ユーザーにマッチングしているユーザーが表示されるはず。

def index
  @users = current_user.matchers
end
index.html.haml
= render 'partial/navbar'
.container
  .col-md-3
    = render 'partial/verticalnavbar'
  .col-md-9
    .panel.panel-default
      .panel-heading
        .text-center
          %span{style: "font-size:18px;"} ユーザー一覧
      - @users.each do |user|
        .col-sm-4.col-md-3
          .panel.panel-default
            .panel-heading
              %h1.panel-title= link_to "#{user.email}",  user_path(user)
            .panel-body
            .thumbnail
              = image_tag(user.avatar)
            .panel-footer

indexのviewを呼び出してみます。

ezgif.com-video-to-gif (3).gif
現在のユーザーとマッチング済のユーザーが表示されました。

とりあえず、マッチング機能は終了。

マッチングの際の演出を追加する

さっき後で書くと言っていたやつです。
やっぱりマッチングの際には何か欲しい。何もないなんてつまらなすぎる。

マッチングの際に何か動作を入れる前に、いいねしたユーザーが自分をフォローしているかの判定条件を記述する必要があります。その条件を通過した場合にマッチングの演出をやってやりましょう。

マッチング判定の条件を追加する

いいねボタンを押した際にマッチングしたら演出をしたいです。記述を書くのはRelationshipのインスタンスがcreateされた瞬間です。

relationships_controller.rb
User.find(params["following_id"]).following?(current_user)

こんな感じにすれば、いいねしたユーザーが自分をフォローしているかどうか判断できそう。

判定条件を使う

演出をする際に、シンプルにやるなら今のままjs.erbを使っても良さそうですが、やりにくいので普通にjavascriptでAjaxの記述をします。
そのため、ユーザーをフォローするいいねボタンのフォームからremote: trueの記述とcreate.js.erbは消します。

= form_with model: current_user.active_relationships.build, class: 'like_form' do |f|
    %div= hidden_field_tag :following_id, @user.id
    = f.submit "いいね", class: "btn btn-primary"
  $(".like_form").on("submit",function(e){
    e.preventDefault();
    var formData = new FormData(this);
    var url = $(this).attr('action');

    $.ajax({
      url: url,
      type: "POST",
      data: formData,
      dataType: 'json',
      processData: false,
      contentType: false
    })
    .done(function(data){
      //省略

js側で、判定結果を持ちたいので送ります。

following_user = User.find(params["following_id"])
@matching = following_user.following?(current_user)
create.json.jbuilder
#省略
json.set! :matching, @matching

あとは、js側でtrueかfalseか判断するだけです。trueの場合には盛大な演出をかますと良いでしょう。

matching.gif

クリスマス仕様にしておきました。
画面が両サイドから来るやつと花火みたいなやつはmojsを使っています。
http://mojs.io/

おまけ

チャット機能もつけると、ちょっとそれっぽくなりますね。
chat_sample.gif
違うユーザーでログインして、マッチングしたユーザーに対してメッセージを送っています。

やっていることは主に2つです。
・メッセージ入力→送信ボタンクリック→AjaxでDB保存、表示
・Ajaxでメッセージ取ってきて自動更新

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
What you can do with signing up
121