やったこと
- ログイン中のユーザーが、ポスト(投稿)に対して、「好き」「嫌い」「興味ない」を投票でき、更新もできるようにした。
- 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を使ってテスト
- likeをPOSTしたときに、suki_countが+1され、自動的にsuki_percent, all_countが計算されているのがわかる。