2
6

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 5 years have passed since last update.

Ruby on Rails APIモードでいいね機能を実装する【初学者のReact×Railsアプリ開発 第6回】

Last updated at Posted at 2020-01-13

やったこと

  • ログイン中のユーザーが、ポスト(投稿)に対して、「好き」「嫌い」「興味ない」を投票でき、更新もできるようにした。
  • likesテーブルをpostsテーブル、usersテーブルとリレーションさせた。
  • counter_cultureを使って、関連レコードの集計(postsテーブルのsuki_countなど)を行った
  • PostgreSQL 12の新機能であるGenerated Columnを使用して、関連レコードの集計結果の計算(postsテーブルのall_count, suki_percent)を行わせた。

完成したデータベースのイメージ(dbconsoleを使って確認)

app_development=# select * from likes;
 id | user_id | post_id | suki |         created_at         |         updated_at         
----+---------+---------+------+----------------------------+----------------------------
  1 |       1 |       2 |    1 | 2020-01-12 23:54:28.193197 | 2020-01-12 23:54:28.193197
(1 row)

app_development=# select * from posts;
 id | content | user_id |        created_at        |        updated_at        | suki_count | kirai_count | notinterested_count | all_count | suki_percent 
----+---------+---------+--------------------------+--------------------------+------------+-------------+---------------------+-----------+--------------
  2 | bbbbd   |       1 | 2020-01-12 12:41:03.4942 | 2020-01-12 12:41:03.4942 |          1 |           0 |                   0 |         1 |          100

実装手順

counter_cultureのインストール

counter_cultureは、各ポストに対して何件の「好き」「嫌い」が投票されたのか、など関連レコード数の集計に使うモジュールです。

Gemfile
gem 'counter_culture'

gemfileにcounter_cultureを追加。

$ docker-compose build --no-cache

モデルとコントローラーの作成

$ docker-compose run api rails g model like suki:integer
$ docker-compose run api rails g controller api/v1/likes
XXX_create_likes.rb
class CreateLikes < ActiveRecord::Migration[5.2]
  def change
    create_table :likes do |t|
      t.integer :user_id, null: false
      t.integer :post_id, null: false
      t.integer :suki, null: false

      t.timestamps

      t.index :user_id
      t.index :post_id
      t.index [:user_id, :post_id], unique: true
    end
  end
end
  • suki:0ならば「嫌い」、suki:1ならば「好き」、suki:2なら「興味なし」とする。
  • user_idとpost_idの組み合わせがユニークであることを書く。重複データを避けるため。

追加のマイグレーションファイルの作成

  • 関連レコードの集計のためのカラムやそれをパーセント表記するための列を追加します。(あるポストに対して「好き」が何票か、「嫌い」が何票か、「好き」と「嫌い」の合計は何票か、「好き」は何%か)
$ docker-compose run api rails g migration AddLikesCountToPosts
$ docker-compose run api rails g migration AddAllCountToPosts
$ docker-compose run api rails g migration AddSukipercentToPosts
XXX_add_likes_count_to_posts.rb
class AddLikesCountToPosts < ActiveRecord::Migration[5.2]
  class MigrationUser < ApplicationRecord
    self.table_name = :posts
  end

  def up
    _up
  rescue => e
    _down
    raise e
  end

  def down
    _down
  end

  private

  def _up
    MigrationUser.reset_column_information

    add_column :posts, :suki_count, :integer, null: false, default: 0 unless column_exists? :posts, :suki_count
    add_column :posts, :kirai_count, :integer, null: false, default: 0 unless column_exists? :posts, :kirai_count
    add_column :posts, :notinterested_count, :integer, null: false, default: 0 unless column_exists? :posts, :notinterested_count
  end

  def _down
    MigrationUser.reset_column_information

    remove_column :posts, :suki_count if column_exists? :posts, :suki_count
    remove_column :posts, :kirai_count if column_exists? :posts, :kirai_count
    remove_column :posts, :notinterested_count if column_exists? :posts, :notinterested_count
  end
end
  • suki_count, kirai_count, nointerested_countというカラムをpostsテーブルに追加しています。counter_cultureを使って、likesテーブルに「好き」「嫌い」「興味なし」が投票されたときに、+1され、likesテーブルのデータが更新されたら-1、+1がされます。
XXX_add_all_count_to_posts.rb
class AddAllCountToPosts < ActiveRecord::Migration[5.2]
  def up
    execute "ALTER TABLE posts ADD COLUMN all_count real GENERATED ALWAYS AS (suki_count+kirai_count) STORED;"
    add_index :posts, :all_count, unique: false
  end

  def down
    remove_column :posts, :all_count
  end
end
  • postsテーブルにall_countカラムを追加。
  • PostgreSQL 12の新機能「Generated Column」を使って、suki_countとkirai_countの合計をall_countに自動計算させるように記述しています。

Generated Columnとは

“generated column” を使うと、”(同じテーブル内の) 他の列の値を利用した計算結果” を、特定の列に格納することが可能になります。https://tech-lab.sios.jp/archives/17098

XXX_add_sukipercent_to_posts.rb
class AddSukipercentToPosts < ActiveRecord::Migration[5.2]
  def up
    execute "ALTER TABLE posts ADD COLUMN suki_percent real GENERATED ALWAYS AS (
      CASE WHEN (suki_count+kirai_count) = 0 THEN NULL
      ELSE suki_count*100/(suki_count+kirai_count) END
      ) STORED;"
    add_index :posts, :suki_percent, unique: false
  end

  def down
    remove_column :posts, :suki_percent
  end
end
  • suki_percentカラムをpostsテーブルに追加。
  • ここでもgenerated columnを使って、「好き」の票数の「好き」と「嫌い」の合計に対するパーセンテージを求めてsuki_percentに格納するように記述しています。

モデルの編集

user.rb
class User < ActiveRecord::Base
  has_many :posts, dependent: :destroy
  has_many :likes, dependent: :destroy

  # Include default devise modules. Others available are:
  # :confirmable, :lockable, :timeoutable and :omniauthable
  devise :database_authenticatable, :registerable,
         :recoverable, :rememberable, :trackable, :validatable,
         :omniauthable, omniauth_providers: [:twitter]
  include DeviseTokenAuth::Concerns::User
end
  • テーブル間のリレーションシップの追加
post.rb
class Post < ApplicationRecord
  belongs_to :user
  has_many :likes, dependent: :destroy
end
  • テーブル間のリレーションシップの追加
like.rb
class Like < ApplicationRecord
  belongs_to :user
  belongs_to :post
  validates :user_id, presence: true
  validates :post_id, presence: true
  counter_culture :post, column_name: -> (model) {"#{model.like_type_name}_count"}

  def like_type_name
    if suki == 1 then
      return 'suki'
    elsif suki == 0 then
      return 'kirai'
    elsif suki == 2 then
      return 'notinterested'

    end      
  end
end
  • counter_cultureの実装はここで行っている。
  • likesテーブルにデータがcreateされたとき「好き(suki==1)」ならばsuki_countを+1するようにしている...のような処理を記述している。

likesコントローラーの編集

likes_controller
module Api
  module V1
    class LikesController < ApplicationController
      before_action :authenticate_api_v1_user!
      before_action :set_like, only: [:show, :destroy, :update,]

      def index
        likes = Like.order(created_at: :desc)
        render json: { status: 'SUCCESS', message: 'Loaded posts', data: likes }
      end

      def show
        if @like.nil? then
            data = {
              updated_at: 3,
              suki: 3
            }
          render json: { status: 'SUCCESS', message: 'Loaded the like', data: data }
        else 
          render json: { status: 'SUCCESS', message: 'Loaded the like', data: @like }
        end
        
      end


      def create
        like = Like.new(like_params)
        if like.save
          @post = Post.find(params[:post_id])
          @user = @post.user
          @like = Like.find_by(user_id: @user.id, post_id: params[:post_id])
          json_data = {
            'post': @post,
            'user': {
              'name': @user.name,
              'nickname': @user.nickname,
              'image': @user.image
            },
            'like': @like
          }
          render json: { status: 'SUCCESS', data: json_data}
        else
          render json: { status: 'ERROR', data: like.errors }
        end
      end

      def destroy
        @like.destroy
        render json: { status: 'SUCCESS', message: 'Delete the post', data: @like}
      end

      def update
        data = {
          'user_id': @user.id,
          'post_id': params[:post_id],
          'suki': params[:suki]
        }
        if @like.update(data)
          @post = Post.find(params[:post_id])
          @user = @post.user
          json_data = {
            'post': @post,
            'user': {
              'name': @user.name,
              'nickname': @user.nickname,
              'image': @user.image
            },
            'like': @like
          }
          render json: { status: 'SUCCESS', message: 'Updated the post', data: json_data }
        else
          render json: { status: 'SUCCESS', message: 'Not updated', data: @like.errors }
        end
      end


      private

      def set_like
        @user = User.find_by(id: current_api_v1_user.id)
        @like = Like.find_by(user_id: @user.id, post_id: params[:post_id])
      end

      def like_params
        params.require(:like).permit(:post_id, :user_id, :suki)
      end

    end
  end
end
  • 一般的なCRUD用のAPIの記述かな...
  • showでは、該当するlkeのデータが無かったときはエラーになるのを避けるために「suki: 3」として返している。まだ投票していないのかアクセスエラーなのかを区別するために...

Postmanを使ってテスト

スクリーンショット 2020-01-13 10.08.25.png

  • likeをPOSTしたときに、suki_countが+1され、自動的にsuki_percent, all_countが計算されているのがわかる。
2
6
0

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
  3. You can use dark theme
What you can do with signing up
2
6

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?