単一テーブル継承を使って複数のコンテンツへのLikeをいい感じにまとめる

  • 7
    いいね
  • 0
    コメント

仕事で開発しているサービスで、

  • ユーザーは複数種類のコンテンツを投稿出来る(開発済み)
  • 複数種類投稿されるコンテンツに対してそれぞれ「いいね!」が出来る(開発済み)
  • マイページ上で、自分がつけられた「いいね!」の合算数や、1週間の純増数などを表示させる(新規開発)
  • マイページ上で、自分がつけた「いいね!」のコンテンツを時系列順に表示させる(新規開発)

の様な仕様に取り組む必要があった。
開発着手時点でのテーブルの構造は下の様に、
それぞれのコンテンツのデータが格納されているテーブル(左側のarticles等)に対応する、
「いいね!」のデータを格納するテーブル(右側のarticle_likes等)を持つ設計で実装されていた。

スクリーンショット 2016-11-17 17.13.44.png

しかし、

  • マイページ上で、自分がつけられた「いいね!」の合算数や、1週間の純増数などを表示させる(新規開発)
  • マイページ上で、自分がつけた「いいね!」のコンテンツを時系列順に表示させる(新規開発)

を実現させるうえで、hoge_likesテーブルがコンテンツの数だけある為、
直接合計数を取得する為にはhoge_likesテーブルの数分だけjoinするなり、
非正規化したカラムにデータを集計させていくなりと、一手間加えないと実現させるのが辛い自体に直面したので、
「いいね!」の関連のデータはlikesテーブル一つに集約して行くことにした。

likesテーブルへのデータの集約をするにあたって、RailsのActiveRecordの単一テーブル継承の機能を利用する事で、
以下のようにデータベースの世界では、テーブルは一つに集約され、
Railsのモデルを対象とするコンテンツに合わせて分ける事が出来るので、
今回満たしたい要件を、ビジネスロジック部分を殆ど弄ること無く実現することが出来る。

スクリーンショット 2016-11-17 20.17.52.png

Railsの単一テーブル継承については以下が参考になる

http://qiita.com/kidach1/items/789c2e7aebbcfbd2583e

また具体的な作業内容としては、以下の様にLikeモデルを定義し

$ rails g model Like user_id:integer content_id:integer type:string

Like全体で共通のassociation・validationなどをLikeモデルに実装し

class Like < ActiveRecord::Base
  belongs_to :user
  validates :user, presence: true, uniqueness: { scope: [:content_id, :type] }
  validates :content_id, presence: true
end

Likeモデルクラスを継承する形で各コンテンツに対応したLikeのクラスを作成し、それぞれのクラスに必要なコードを記述していく
普通のオブジェクト指向の継承と同じなので、Likeモデルクラスで定義したvalidationassociationは当然継承先のクラスでも適用される。
associationを作成する際に、外部キーをちゃんと指定してあげないと、「article_id」無いぞと怒られるので注意

class ArticleLike < Like
  belongs_to :article, foreign_key: 'content_id'
  # 以下何かしらクラスごとに必要なコードを記述していく...
end

class ArticleCommentLike < Like
  belongs_to :article_comment, foreign_key: 'content_id'
end

これだけで、あたかもArticleLikeArticleCommentLikeが別のテーブルに対応したモデルかのように振る舞ってくれる。
実際に、コンソールでそれぞれのクラスからActiveRecordのメソッドを呼び出してみると、
それぞれ別のモデルクラスから新しいオブジェクトを作成しているが、データベースのレコード上は同じテーブル内にインサートされ、
Likeモデルから継承したサブクラスのオブジェクトをそれぞれ取得することが出来る。

> ArticleLike.create(user_id: 1, content_id: 1)
  => #<ArticleLike id: 1, user_id: 1, content_id: 1, type: "ArticleLike", created_at: "2016-11-17 03:20:56", updated_at: "2016-11-17 03:20:56">

> ArticleCommentLike.create(user_id: 1, content_id: 1)
  => #<ArticleCommentLike id: 2, user_id: 1, content_id: 1, type: "ArticleCommentLike", created_at: "2016-11-17 04:20:56", updated_at: "2016-11-17 04:20:56">

> Like.all
  => #<ActiveRecord::Relation [#<ArticleLike id: 1, user_id: 1, content_id: 1, created_at: "2016-11-17 03:20:56", updated_at: "2016-11-17 03:20:56">, 
#<ArticleCommentLike id: 2, user_id: 1, content_id: 1, created_at: "2016-11-17 04:25:21", updated_at: "2016-11-17 04:25:21">]>

この特性を利用する事で、自分につけられた「いいね!」の総数や、自分が「いいね!」したコンテンツを時系列順に取得して表示する
等の機能を開発する事が非常に簡単に出来る様になる。