16
16

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.

ポリモーフィック関連とmodelのconcernで「いいね」を作ってみる

Last updated at Posted at 2015-06-15

はじめに

この間別のアプリを作っていて、「いいね」機能を作ろうとして、ポリモーフィック関連とモデルのconcernを使ってみました。
モデルに共有な処理を扱うconcernといろんなモデルと関連を作れるポリモーフィック関連は仲が良さそうだと思い、少しサンプルを作ってみました。

サンプル

ここでは、ポリモーフィック関連をmodelのconcernを使って、
記事とコメントに対して、「いいね」できるようにしてみようと思います。
gemとしては、activerecord-reputation-systemがあり、Rails Castsの記事などもあってよいのですが、ちょっと大仰なので自作することにします。

モデル

下のような、ユーザ、記事とコメントがあるとします。

ユーザ

$ rails g model user name:string

関連

  • 記事をいくつも書くことができます。
  • コメントをいくつも書くことができます。
app/models/user.rb
class User < ActiveRecord::Base
  has_many :articles
  has_many :comments
end

記事(article)

$ rails g model article title:string body:text user_id:integer

関連

関連としては、記事には必ずユーザに書かれ、コメントが複数寄せられます。

app/models/article.rb
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

関連

app/models/comment.rb
class Comment < ActiveRecord::Base
  belongs_to :user
  belongs_to :article
end

いいね

ユーザが「いいね」を記事とコメントに対して付けられるようにします。
いいねを記事いいねとコメントいいねの2つのモデルを作ってもよいのですが、
もしもまた別の写真などのような新しいいいねの対象が出てきたときに、
写真いいねを作るのは面倒だし、もっと出てきたら更に面倒くさそうです。
なので、抽象化された「いいね」にしました。

$ rails g model like likable:references{polymorphic}:index user_id:integer

いいねとの関連

いいね

いいねにポリモーフィック関連でいいねできるものを追加します。

app/model/like.rb
class Like < ActiveRecord::Base
  belongs_to :likable, polymorphic: true
  belongs_to :user
end

記事とコメント

app/model/article.rb
class Article < ActiveRecord::Base
  belongs_to :user
  has_many :comments
  has_many :likes, as: :likable
end
app/model/comment.rb
class Comment < ActiveRecord::Base
  belongs_to :user
  belongs_to :article
  has_many :likes, as: :likable
end

ポリモーフィック関連を使わなかったら?

  • 記事いいねとコメントいいねを別に作る
  • 記事とコメントに関連を持つようにいいねを作る

自分は2つ目で最初作ったのですが…煩雑…でした。

いいねを使うモデルの共通メソッド

いいねを使うモデルにはざっくり考えて下のようなメソッドがありそうです。

  • いいねされる
  • いいねを解除される
  • いいねされた数
  • このユーザにいいねされてる?

これをmodelのconcernにモジュールとして実装して…

app/model/concern/liked.rb
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

記事とコメントで読み込むようにします。

app/models/article.rb
class Article < ActiveRecord::Base
  belongs_to :user
  has_many :comments
  has_many :likes, as: :likable

  include Liked
end
app/models/comment.rb
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にモジュールを作ることで、
だいぶスッキリとなったように感じました。

16
16
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
16
16

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?