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

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


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

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



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

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

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

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

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

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


```ruby:application.js
//= require jquery
//= require bootstrap-sprockets
```

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

```ruby:application.scss
@import "bootstrap-sprockets";
@import "bootstrap"; 
```

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

<img width="1139" alt="スクリーンショット 2017-11-23 16.32.58.png" src="https://qiita-image-store.s3.amazonaws.com/0/208513/e60d2de4-f045-17e9-aab5-475d885a75da.png">


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

```ruby
gem 'devise'
```

```ruby
bundle install --path vendor/bundle
```

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

```
rails g model relationship
```

```ruby
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も貼っておきます。


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

```ruby:user.rb
  has_many :active_relationships,class_name:  "Relationship", foreign_key: "follower_id", dependent: :destroy
  has_many :following, through: :active_relationships
```

```ruby:relationship.rb
  belongs_to :following, class_name: "User"
```

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

```ruby: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
```

```ruby:User.rb
def following?(other_user)
  following.include?(other_user)
end
```

```ruby:_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"
```

```ruby: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
```

```ruby:create.js.erb
$("#follow_form").html(`<div class="btn btn-default">いいね済</div>`)
```

![like_you.gif](https://qiita-image-store.s3.amazonaws.com/0/208513/83ffb4c9-69fc-da14-3108-eac49d019615.gif)


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



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

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

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

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

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


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

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

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

```ruby:User.rb
has_many :passive_relationships, class_name: "Relationship", foreign_key: "following_id", dependent: :destroy 
```
ログイン中のユーザーとマッチングしているユーザーを取得します。

```ruby: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
```

なげえ・・・

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

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

```ruby:user.rb
has_many :followers, through: :passive_relationships, source: :follower
```

```ruby:relationship.rb
belongs_to :follower, class_name: "User"
```
追加したらあとはもう重なり合っているユーザーを取得すればいいだけではないでしょうか。

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

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

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

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

### Sqlで頑張って取得する
```ruby
User.matching(current_user)
```

```ruby
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な気がしますね。


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

```ruby
def index
  @users = current_user.matchers
end
```

```ruby: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](https://qiita-image-store.s3.amazonaws.com/0/208513/3412dd89-9eda-71ce-97c7-ee32b0b8884a.gif)
現在のユーザーとマッチング済のユーザーが表示されました。

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


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

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

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

```ruby:relationships_controller.rb
User.find(params["following_id"]).following?(current_user)
```
こんな感じにすれば、いいねしたユーザーが自分をフォローしているかどうか判断できそう。

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

```ruby
= 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"
```

```javascript
  $(".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側で、判定結果を持ちたいので送ります。

```ruby
following_user = User.find(params["following_id"])
@matching = following_user.following?(current_user)
```

```ruby:create.json.jbuilder
#省略
json.set! :matching, @matching
```
あとは、js側でtrueかfalseか判断するだけです。trueの場合には盛大な演出をかますと良いでしょう。


![matching.gif](https://qiita-image-store.s3.amazonaws.com/0/208513/407eb332-47dd-cefb-402a-a52a01dcde78.gif)


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


## おまけ
チャット機能もつけると、ちょっとそれっぽくなりますね。
![chat_sample.gif](https://qiita-image-store.s3.amazonaws.com/0/208513/3715c908-d413-c85c-eef0-106b72ac49cb.gif)
違うユーザーでログインして、マッチングしたユーザーに対してメッセージを送っています。

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