仕事で開発しているサービスで、
- ユーザーは複数種類のコンテンツを投稿出来る(開発済み)
- 複数種類投稿されるコンテンツに対してそれぞれ「いいね!」が出来る(開発済み)
- マイページ上で、自分がつけられた「いいね!」の合算数や、1週間の純増数などを表示させる(新規開発)
- マイページ上で、自分がつけた「いいね!」のコンテンツを時系列順に表示させる(新規開発)
の様な仕様に取り組む必要があった。
開発着手時点でのテーブルの構造は下の様に、
それぞれのコンテンツのデータが格納されているテーブル(左側のarticles
等)に対応する、
「いいね!」のデータを格納するテーブル(右側のarticle_likes
等)を持つ設計で実装されていた。
しかし、
- マイページ上で、自分がつけられた「いいね!」の合算数や、1週間の純増数などを表示させる(新規開発)
- マイページ上で、自分がつけた「いいね!」のコンテンツを時系列順に表示させる(新規開発)
を実現させるうえで、hoge_likes
テーブルがコンテンツの数だけある為、
直接合計数を取得する為にはhoge_likes
テーブルの数分だけjoinするなり、
非正規化したカラムにデータを集計させていくなりと、一手間加えないと実現させるのが辛い自体に直面したので、
「いいね!」の関連のデータはlikes
テーブル一つに集約して行くことにした。
likes
テーブルへのデータの集約をするにあたって、RailsのActiveRecordの単一テーブル継承の機能を利用する事で、
以下のようにデータベースの世界では、テーブルは一つに集約され、
Railsのモデルを対象とするコンテンツに合わせて分ける事が出来るので、
今回満たしたい要件を、ビジネスロジック部分を殆ど弄ること無く実現することが出来る。
Railsの単一テーブル継承については以下が参考になる
また具体的な作業内容としては、以下の様に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モデルクラスで定義したvalidation
やassociation
は当然継承先のクラスでも適用される。
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
これだけで、あたかもArticleLike
とArticleCommentLike
が別のテーブルに対応したモデルかのように振る舞ってくれる。
実際に、コンソールでそれぞれのクラスから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">]>
この特性を利用する事で、自分につけられた「いいね!」の総数や、自分が「いいね!」したコンテンツを時系列順に取得して表示する
等の機能を開発する事が非常に簡単に出来る様になる。