Edited at

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

More than 1 year has passed since last update.

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でメッセージ取ってきて自動更新