はじめに
この間別のアプリを作っていて、「いいね」機能を作ろうとして、ポリモーフィック関連とモデルのconcernを使ってみました。
モデルに共有な処理を扱うconcernといろんなモデルと関連を作れるポリモーフィック関連は仲が良さそうだと思い、少しサンプルを作ってみました。
サンプル
ここでは、ポリモーフィック関連をmodelのconcernを使って、
記事とコメントに対して、「いいね」できるようにしてみようと思います。
gemとしては、activerecord-reputation-systemがあり、Rails Castsの記事などもあってよいのですが、ちょっと大仰なので自作することにします。
モデル
下のような、ユーザ、記事とコメントがあるとします。
ユーザ
$ rails g model user name:string
関連
- 記事をいくつも書くことができます。
- コメントをいくつも書くことができます。
class User < ActiveRecord::Base
has_many :articles
has_many :comments
end
記事(article)
$ rails g model article title:string body:text user_id:integer
関連
関連としては、記事には必ずユーザに書かれ、コメントが複数寄せられます。
class Article < ActiveRecord::Base
belongs_to :user
has_many :comments
end
コメント(comment)
$ rails g model comments body:text user_id:integer article_id:integer
間違えたら訂正されたよ!
rails g model comments body:text user_id:integer
[WARNING] The model name 'comments' was recognized as a plural, using the singular 'comment' instead. Override with --force-plural or setup custom inflection rules for this noun before running the generator.
invoke active_record
:
:
正しくは…
$ rails g model comment body:text user_id:integer
関連
class Comment < ActiveRecord::Base
belongs_to :user
belongs_to :article
end
いいね
ユーザが「いいね」を記事とコメントに対して付けられるようにします。
いいねを記事いいねとコメントいいねの2つのモデルを作ってもよいのですが、
もしもまた別の写真などのような新しいいいねの対象が出てきたときに、
写真いいねを作るのは面倒だし、もっと出てきたら更に面倒くさそうです。
なので、抽象化された「いいね」にしました。
$ rails g model like likable:references{polymorphic}:index user_id:integer
いいねとの関連
いいね
いいねにポリモーフィック関連でいいねできるものを追加します。
class Like < ActiveRecord::Base
belongs_to :likable, polymorphic: true
belongs_to :user
end
記事とコメント
class Article < ActiveRecord::Base
belongs_to :user
has_many :comments
has_many :likes, as: :likable
end
class Comment < ActiveRecord::Base
belongs_to :user
belongs_to :article
has_many :likes, as: :likable
end
ポリモーフィック関連を使わなかったら?
- 記事いいねとコメントいいねを別に作る
- 記事とコメントに関連を持つようにいいねを作る
自分は2つ目で最初作ったのですが…煩雑…でした。
いいねを使うモデルの共通メソッド
いいねを使うモデルにはざっくり考えて下のようなメソッドがありそうです。
- いいねされる
- いいねを解除される
- いいねされた数
- このユーザにいいねされてる?
これをmodelのconcernにモジュールとして実装して…
module Liked
def liked_by (user)
likes.where(user: user).first_or_create
end
def unliked_by (user)
like = likes.where(user: user).first
like.destroy if like.present?
end
def liked_count
likes.count
end
def liked_by? (user)
likes.where(user: user).present?
end
end
記事とコメントで読み込むようにします。
class Article < ActiveRecord::Base
belongs_to :user
has_many :comments
has_many :likes, as: :likable
include Liked
end
class Comment < ActiveRecord::Base
belongs_to :user
belongs_to :article
has_many :likes, as: :likable
include Liked
end
concernを使わなかったら?
これを各モデルにそれぞれ書くと…ちょっと冗長かなぁ。
動作確認
記事を準備。
> author = User.create(name: "test_author")
> article = Article.create(title: "test_title", body: "test_body", user: author)
コメントを準備。
> commenter = User.create(name: "test_commenter")
> comment = Comment.create(article: article, body: "test_body", user: commenter)
いいねするユーザを準備。
> liker = []
> (0..1).each {|i| liker[i] = User.create(name: "test_liker#{i}")}
いいねせずに記事とコメントのいいねを数える。
記事もコメントもいいねが0ならいい。
irb(main):060:0* article.liked_count
(0.2ms) SELECT COUNT(*) FROM "likes" WHERE "likes"."likable_id" = ? AND "likes"."likable_type" = ? [["likable_id", 2], ["likable_type", "Article"]]
=> 0
irb(main):061:0> comment.liked_count
(0.2ms) SELECT COUNT(*) FROM "likes" WHERE "likes"."likable_id" = ? AND "likes"."likable_type" = ? [["likable_id", 1], ["likable_type", "Comment"]]
=> 0
irb(main):062:0>
記事にいいねして、いいねを数える。
記事のいいね数だけが1になってればいい。
irb(main):062:0> article.liked_by(liker[0])
Like Load (0.3ms) SELECT "likes".* FROM "likes" WHERE "likes"."likable_id" = ? AND "likes"."likable_type" = ? AND "likes"."user_id" = 5 ORDER BY "likes"."id" ASC LIMIT 1 [["likable_id", 2], ["likable_type", "Article"]]
(0.2ms) begin transaction
SQL (0.9ms) INSERT INTO "likes" ("likable_id", "likable_type", "user_id", "created_at", "updated_at") VALUES (?, ?, ?, ?, ?) [["likable_id", 2], ["likable_type", "Article"], ["user_id", 5], ["created_at", "2015-06-15 04:20:13.828906"], ["updated_at", "2015-06-15 04:20:13.828906"]]
(1.7ms) commit transaction
=> #<Like id: 2, likable_id: 2, likable_type: "Article", user_id: 5, created_at: "2015-06-15 04:20:13", updated_at: "2015-06-15 04:20:13">
irb(main):063:0> article.liked_count
(0.4ms) SELECT COUNT(*) FROM "likes" WHERE "likes"."likable_id" = ? AND "likes"."likable_type" = ? [["likable_id", 2], ["likable_type", "Article"]]
=> 1
irb(main):064:0> comment.liked_count
(0.3ms) SELECT COUNT(*) FROM "likes" WHERE "likes"."likable_id" = ? AND "likes"."likable_type" = ? [["likable_id", 1], ["likable_type", "Comment"]]
=> 0
irb(main):065:0>
記事のいいねを取り消して、コメントにいいねして数える。
記事のいいねが0、コメントのいいねが1になってればいい。
irb(main):068:0* article.unliked_by(liker[0])
Like Load (0.3ms) SELECT "likes".* FROM "likes" WHERE "likes"."likable_id" = ? AND "likes"."likable_type" = ? AND "likes"."user_id" = 5 ORDER BY "likes"."id" ASC LIMIT 1 [["likable_id", 2], ["likable_type", "Article"]]
(0.1ms) begin transaction
SQL (0.7ms) DELETE FROM "likes" WHERE "likes"."id" = ? [["id", 2]]
(3.0ms) commit transaction
=> #<Like id: 2, likable_id: 2, likable_type: "Article", user_id: 5, created_at: "2015-06-15 04:20:13", updated_at: "2015-06-15 04:20:13">
irb(main):069:0> comment.liked_by(liker[0])
Like Load (0.2ms) SELECT "likes".* FROM "likes" WHERE "likes"."likable_id" = ? AND "likes"."likable_type" = ? AND "likes"."user_id" = 5 ORDER BY "likes"."id" ASC LIMIT 1 [["likable_id", 1], ["likable_type", "Comment"]]
(0.1ms) begin transaction
SQL (0.5ms) INSERT INTO "likes" ("likable_id", "likable_type", "user_id", "created_at", "updated_at") VALUES (?, ?, ?, ?, ?) [["likable_id", 1], ["likable_type", "Comment"], ["user_id", 5], ["created_at", "2015-06-15 04:23:44.448579"], ["updated_at", "2015-06-15 04:23:44.448579"]]
(7.9ms) commit transaction
=> #<Like id: 3, likable_id: 1, likable_type: "Comment", user_id: 5, created_at: "2015-06-15 04:23:44", updated_at: "2015-06-15 04:23:44">
irb(main):070:0> article.liked_count
(0.3ms) SELECT COUNT(*) FROM "likes" WHERE "likes"."likable_id" = ? AND "likes"."likable_type" = ? [["likable_id", 2], ["likable_type", "Article"]]
=> 0
irb(main):071:0> comment.liked_count
(0.3ms) SELECT COUNT(*) FROM "likes" WHERE "likes"."likable_id" = ? AND "likes"."likable_type" = ? [["likable_id", 1], ["likable_type", "Comment"]]
=> 1
irb(main):072:0>
記事とコメントに別のユーザでいいねして数える。
記事のいいねが1、コメントのいいねが2になっていればいい。
irb(main):077:0* article.liked_by(liker[1])
Like Load (0.3ms) SELECT "likes".* FROM "likes" WHERE "likes"."likable_id" = ? AND "likes"."likable_type" = ? AND "likes"."user_id" = 6 ORDER BY "likes"."id" ASC LIMIT 1 [["likable_id", 2], ["likable_type", "Article"]]
(0.1ms) begin transaction
SQL (0.5ms) INSERT INTO "likes" ("likable_id", "likable_type", "user_id", "created_at", "updated_at") VALUES (?, ?, ?, ?, ?) [["likable_id", 2], ["likable_type", "Article"], ["user_id", 6], ["created_at", "2015-06-15 04:28:09.669492"], ["updated_at", "2015-06-15 04:28:09.669492"]]
(5.2ms) commit transaction
=> #<Like id: 4, likable_id: 2, likable_type: "Article", user_id: 6, created_at: "2015-06-15 04:28:09", updated_at: "2015-06-15 04:28:09">
irb(main):078:0> comment.liked_by(liker[1])
Like Load (0.1ms) SELECT "likes".* FROM "likes" WHERE "likes"."likable_id" = ? AND "likes"."likable_type" = ? AND "likes"."user_id" = 6 ORDER BY "likes"."id" ASC LIMIT 1 [["likable_id", 1], ["likable_type", "Comment"]]
(0.1ms) begin transaction
SQL (0.4ms) INSERT INTO "likes" ("likable_id", "likable_type", "user_id", "created_at", "updated_at") VALUES (?, ?, ?, ?, ?) [["likable_id", 1], ["likable_type", "Comment"], ["user_id", 6], ["created_at", "2015-06-15 04:28:15.808948"], ["updated_at", "2015-06-15 04:28:15.808948"]]
(41.0ms) commit transaction
=> #<Like id: 5, likable_id: 1, likable_type: "Comment", user_id: 6, created_at: "2015-06-15 04:28:15", updated_at: "2015-06-15 04:28:15">
irb(main):079:0> article.liked_count
(0.3ms) SELECT COUNT(*) FROM "likes" WHERE "likes"."likable_id" = ? AND "likes"."likable_type" = ? [["likable_id", 2], ["likable_type", "Article"]]
=> 1
irb(main):080:0> comment.liked_count
(0.2ms) SELECT COUNT(*) FROM "likes" WHERE "likes"."likable_id" = ? AND "likes"."likable_type" = ? [["likable_id", 1], ["likable_type", "Comment"]]
=> 2
irb(main):081:0>
問題ないっぽい。
まとめ
いろいろと突っ込みどころはありそうですが…
ポリモーフィック関連とmodelのconcernにモジュールを作ることで、
だいぶスッキリとなったように感じました。