娯楽開発

娯楽開発

開発は娯楽。遊び心で開発をすすめるブログ。

【rails】Twitterのツイート機能のモデル構造を考えてみた

みなさん、Twitterやってますか?

自分はそこそこいい歳ですがそこそこのツイ廃具合を発揮させてもらっています。(もし興味あればフォローどうぞ さどはら (@sado_not_maso) | Twitter )

今日は、最近Twitterのツイート機能(つぶやきを投稿する機能)のモデル構成をRailsで考えてみたのでそのことについて簡単にまとめようと思います。

(今回記事内でやたらTwitter, Twitterと言っているためTwitter公式から怒られると怖いのでリンク貼っておきます。Twitterやったことないよ!という方は以下のリンクから簡単に始められるのでどうぞ)

twitter.com

ツイート機能の概要

まずはモデル構成(モデル間のアソシエーション)を考える上で押さえておきたいTwitterのツイート機能の特徴について一度まとめておきます。

  • ひとつのツイートに紐づくユーザーはただ一人のみ
  • ツイートに対してリプライ(返信)ができる
  • 返信も1つのツイートとしてカウントされる
  • 他人のツイートをリツイート(共有・拡散)できる

細かいことを言い出すと「いいね」機能や140字制限などの特徴もありますが、ここではあくまで「モデル構成を考える上で考慮する必要のある機能」を挙げています。

それでは早速これらの機能を備えるためのモデル構成を考えていきたいと思います。

登場するモデル

今回のアソシエーションで登場するモデルとそのカラムをまとめておきます。

(カラムは今回のツイート機能を実装する上で必要となるものだけを記載しています。)

Userモデル

User
id

Tweetモデル

Tweet
id
user_id

ReplyRelationshipモデル

ReplyRelationship
id
main_tweet_id
reply_tweet_id

ShareRelationshipモデル

TweetRetweetでは少しややこしいためShareとしています。

ShareRelationship
id
origin_tweet_id
share_tweet_id

実際のコード

Userモデル

# users.rb
class User < ActiveRecord::Base
  has_many :tweets, dependent: :destroy
end

Tweetモデル

# tweets.rb
class Tweet < ActiveRecord::Base
  belongs_to :user

  has_one :replying, through: :replying_relationships, source: :main_tweet
  has_one :replying_relationships, class_name: 'ReplyRelationship',
                                   foreign_key: 'reply_tweet_id',
                                   dependent: :destroy

  has_many :replied, through: :replied_relationships, source: :reply_tweet
  has_many :replied_relationships, class_name: 'ReplyRelationship',
                                   foreign_key: 'main_tweet_id',
                                   dependent: :destroy

  has_one :sharing, through: :sharing_relationships, source: :origin_tweet
  has_one :sharing_relationships, class_name: 'Share',
                                  foreign_key: 'share_tweet_id',
                                  dependent: :destroy

  has_many :shared, through: :shared_relationships, source: :share_tweet
  has_many :shared_relationships, class_name: 'Share',
                                  foreign_key: 'origin_tweet_id',
                                  dependent: :destroy
end

ReplyRelationshipモデル

# reply_relationships.rb
class ReplyRelationship < ActiveRecord::Base
  belongs_to :main_tweet, class_name: 'Tweet'
  belongs_to :reply_tweet, class_name: 'Tweet'
end

ShareRelationshipモデル

# share_relationships.rb
class ShareRelationship < ActiveRecord::Base
  belongs_to :origin_tweet, class_name: 'Tweet'
  belongs_to :share_tweet, class_name: 'Tweet'
end

解説

TweetUserについて

# users.rb
class User < ActiveRecord::Base
  has_many :tweets, dependent: :destroy
end

# tweets.rb
class Tweet < ActiveRecord::Base
  belongs_to :user
end

これはシンプルなhas_manybelongs_toの関係です。

あるuserに対して、user.tweetsでそのユーザーのツイート一覧を参照できます。

TweetReplyRelationshipについて

# tweets.rb
class Tweet < ActiveRecord::Base
  has_one :replying, through: :replying_relationships, source: :main_tweet
  has_one :replying_relationships, class_name: 'ReplyRelationship',
                                   foreign_key: 'reply_tweet_id',
                                   dependent: :destroy

  has_many :replied, through: :replied_relationships, source: :reply_tweet
  has_many :replied_relationships, class_name: 'ReplyRelationship',
                                   foreign_key: 'main_tweet_id',
                                   dependent: :destroy
end

# reply_relationships.rb
class ReplyRelationship < ActiveRecord::Base
  belongs_to :main_tweet, class_name: 'Tweet'
  belongs_to :reply_tweet, class_name: 'Tweet'
end

ツイートのアソシエーションを考える上で一番特徴的なのはTweetモデルとReplyRelationshipモデルの関係性かなと思います。

Twitterをやっている方はご存知かと思いますが、Twitterでのリプライ(返信)は

  • リプライは主となるツイートに付随するものである
  • リプライそのものも一つのツイートである

という二つの性質を同時に持ちます。ちょうど以下の画像のような感じです。

リプライの例:@◯◯から始まる内容は特定のユーザーに向けた返信 f:id:sado_tech:20180831133307p:plain

これがFacebookなどになると、一つの投稿(Post)に対するコメント(Comment)は主従の関係がはっきりと成立するため

Post -- has_many --> Comments

Comment -- belongs_to --> Post

というわかりやすいアソシエーションとなります。

しかし今回リプライはツイートに付随するコメントとしての性質と、それ自体が一つのツイートであるという性質をあわせ持つため、Tweetモデル間を繋げる役目を持つReplyRelationshipモデルを用意しています。

このようにすることで、以前ユーザーのフォロー機能を解説した記事のようにアソシエーションを考えることができます。

sado-tech.hateblo.jp

TweetShareRelationshipについて

# tweets.rb
class Tweet < ActiveRecord::Base
  has_one :sharing, through: :sharing_relationships, source: :origin_tweet
  has_one :sharing_relationships, class_name: 'Share',
                                  foreign_key: 'share_tweet_id',
                                  dependent: :destroy

  has_many :shared, through: :shared_relationships, source: :share_tweet
  has_many :shared_relationships, class_name: 'Share',
                                  foreign_key: 'origin_tweet_id',
                                  dependent: :destroy
end

# share_relationships.rb
class ShareRelationship < ActiveRecord::Base
  belongs_to :origin_tweet, class_name: 'Tweet'
  belongs_to :share_tweet, class_name: 'Tweet'
end

リツイートについては以下の性質を持っています。

  • 表示上は他人のツイート
  • 所属は自分のTweets群

つまるところ、あるuserリツイートした他人のツイートもuser.tweetsで参照できるtweets群の中に含めたい、という感じです。

これについてはいろいろ方法があるかもしれませんが、僕が考えた方法ではTweets群の中のあるtweetがもしリツイートであればtweet.sharingとして別のツイートを参照できる、というものでした。(もっといい方法があればアドバイスいただけると幸いです。)

それによって必要になったのがShareRelationshipというモデルですね。

アソシエーション自体はTweetReplyRelationshipの場合とほとんど変わりません。

まとめ

今回はTwitterのツイート機能のモデル構成について考えてみました。

個人的にツイート機能まわりはリプライの扱いが少し独特かなーという感じでしたが、なんとなく理解していただけたでしょうか。

とてもざっくりとした内容になりましたが、今回の記事が少しでも何かの参考になれば幸いです。