#1 の続きです。機能を増やしていきましょう。

今回の学習

前回は「ブログ」としてよくある機能に着目したので、今回はSNSによくある次の機能を考えてみようと思います。

  • フォロー機能
  • fav機能

各機能の詳細

フォロー機能・fav機能をそれぞれ以下のように定義します:

フォロー機能

利用者は自分のタイムラインに他アカウントのトゥートを流すことができます。利用者は自分のタイムラインに流したい他アカウントを「フォロー」することで他アカウントのトゥートを流すことができます。

fav機能

利用者は自分が気に入ったトゥートにマーキング(fav)を行うことができます。各アカウントは自分がfavしたトゥートをトゥートが削除されていない場合に限りあとから閲覧することができます。

ER図

愚直に書いてみます。

2つのアカウントがあればフォロー/フォロワー関係は描写できて、また、アカウントとトゥートとがあればfav機能はできるかな...?と思って書いてみたものです。

さてこれだと気になるのが、あるユーザーの「フォロー数・フォロワー数」を取得するときにわざわざ次のようなSQLでも書くんですか?ってところです。

SELECT COUNT(*) FROM follows
WHERE follow_id = ?;

ある特定の利用者のプロフィールを見るときに同時に参照されるような値ですから、 accounts にフォロー数・フォロワー数をサマリーデータとして持たせるべきなのかなとも思います。

次のER図はマストドンの実際のテーブル構成を元に修正を加えたものです。

予想通りというか、 accounts テーブルがサマリーデータを持っています。フォロー数・フォロワー数、そしてトゥート数ですね。

あと気づいたところは全てのテーブルに created_atupdated_at があることです。 updated_at がすべてのテーブルに本当にいるんか...?という気持ちはあるのですが、そういやLaravelで何も考えずテーブル作るとcreated_at, updated_at のカラムは存在しましたし、そして存在することが前提のようにもなっていたような覚えがあります。

1レコードあたり4バイト(?)減らすために updated_at 削るくらいならフレームワークを便利に使える方がいいのかもしれませんね...。

ところでこういうサマリーデータの更新っていつ、何をトリガーに行われるのでしょう?例えば「フォロー」を行うということになったら次のようなSQLが動いているんでしょうか?

BEGIN
SELECT following_count FROM accounts WHERE id = ? FOR UPDATE;
SELECT follower_count FROM accounts WHERE id = ? FOR UPDATE;
INSERT INTO follows(account_id, target_account_id) VALUES (?, ?);
UPDATE accounts SET following_count = following_count + 1
WHERE account_id = ? AND following_count = ?;
UPDATE accounts SET follower_count = follower_count + 1
WHERE account_id = ? AND follower_count = ?;
COMMIT

一定時間にあるユーザーにフォローが集中するといったことがあると、上のような方法だと資源が解放されるのを待たなければいけないという観点でパフォーマンスが怪しいのではないか...?と思うところです。

正直、フォロー数・フォロワー数といったサマリーデータは「あるユーザーがフォローしたらその次の瞬間にはフォロー数が1増えている」ということが望まれるような厳密なデータではありませんから、上のようなSQLは叩かれる必要がないように思います。

ということで「フォローする」というタイミングでは次のSQLだけで済ませているのではないでしょうか?

INSERT INTO follows(account_id, target_account_id) VALUES (?, ?);

検証のためアカウントをフォローする部分に相当するコードをリポジトリから探してみたのですが自分の力では follow! が見つからず、代わりに unfollow! がすぐに見つかったのでコードを見てみます。

(フォローを外す部分のみ抜粋 引用元: tootsuite/mastodon)

 def unfollow!
    follow = Follow.find_by(account: @source_account, target_account: @target_account)

    return unless follow

    follow.destroy!

    create_notification(follow) if !@target_account.local? && @target_account.activitypub?
    create_reject_notification(follow) if @target_account.local? && !@source_account.local? && @source_account.activitypub?
    UnmergeWorker.perform_async(@target_account.id, @source_account.id) unless @options[:skip_unmerge]

    follow
  end

Follow モデルから外したいフォローを見つけてきて抹消してるんだろうなってのが見てわかります。

それでFollowモデルを見てみると次のような記述がありました。

  before_validation :set_uri, only: :create
  after_create :increment_cache_counters
  after_destroy :remove_endorsements
  after_destroy :decrement_cache_counters

あぁこれ数減らされてますわ...でも "cache" ですか...?

データベースのレコードをフォローの度、毎回アップデートする代わりに、アプリケーションキャッシュかどこかで一度数値の増減を行っておき、キャッシュから追い出されるときにそれを契機にDBの値を変更する...とかでしょうかね?それなら行をロックして解放して...に起因するようなパフォーマンス低下ってのは避けられそうです。

おわりに

何気なく使っているだけだったSNSの思いもよらない「大きさ」に驚くばかりです。あぁ、サービスを作るのって難しいんだろうな...と思う次第です。

何か作っているものがあるよーって人はぜひ手伝わせて欲しいなと思ってきました。