当ブログ人気記事!
転職体験談
Laravelで学ぶWebアプリ開発
誰でも作れるチャットアプリ
未経験への勧め

【Laravel初心者向け講座】いいね機能を実装してみる(API編:Redisでいいねデータ管理)

プログラミング

いいね機能のAPIはどうやって実装するんだろう?
データ管理するんだろう?
Redisって何?

こういった方に対して書きました!


この記事を書いている私(@Shoot58153748)は、
2020年2月現在メガベンチャーの社内スタートアップの部署でエンジニア(1年目)をしており、

プログラミング未経験からメガベンチャーへの転職を成功させた経験・ノウハウ
Webエンジニアになってから学んだこと

をブログにまとめています。


前回は、いいね機能のフロント側の実装
lodashを用いたリクエスト制御、vueによるいいねボタン制御を行いました。

今回はいいね機能のAPI(サーバーサイド側)の実装
前回の内容と合わせてついにいいね機能の完成です!

いいねデータの管理はMySQL(RDB)ではなくRedisを使っていきます。
データ設計も合わせて行うので、ぜひ参考にしてください。


今回のポイント

  • Redisのデータ型設計
  • Redisでデータを格納
  • フロント側と結合
スポンサーリンク

Redisとは?

Redisはインメモリ型のKVS(key value store)です。
インメモリ型データベース(NoSQL)とも呼ばれたりします。

インメモリとは、データをハードディスクに書き込まず、メモリ上で管理する手法で、
通常のDB(mysqlなどのRDB)と比べ、高速にデータを出し入れできるという特徴をもちます。

本来の用途としては、セッション管理やデータキャッシュに使われます。

RDBのようにデータを永続的に置いておくのではなく、高速にアクセス可能なデータの一時的な格納場所というイメージですね。


今回の開発中のツイッター風SNSアプリでは、

いいね機能や通報機能、impression機能などのユーザーアクションは
データの更新が頻繁に起こります。


それらを全てtweetsテーブルで管理するとDB負荷が大きくなってしまうので、
Redisを使ってユーザーアクションのデータを管理することにしました。


ちなみにツイッターの公式ブログでも、timelineは全てKVSで管理しており、RDBはデータのバックアップというような記述があったのでそれを真似することにします。


ただ、先ほど話したようにredis本来の用途的には、
永続的なデータを置いとくのは少々気が引けます。

そのためユーザーアクションのデータを定期的にDBに入れるバッチなどが必要になるかと思います。
ですが今回バッチ開発は後日においといて、

さっそくいいね機能を開発していきましょう!

画面イメージ

データ設計

Redisのデータ構造は5種類(String型、List型、Set型、SortedSet型、Hash型)ありますが、

今回使用したのは、SortedSet型とHash型です。


SortedSet型

Key

“laravel_database_shoot_tweet:like”

Score

{likeCount}

Member

{tweetId}

Hash型

Key

“laravel_database_shoot_tweet:{userId}:like”

Field

{tweetId}

Value

true or false

いいね機能API実装

今回の制作物は以下になります。

  • コントローラークラス(+ルーティング)
  • サービスクラス
  • Tweetクラス
  • Redisクラス
  • Configファイル


ルーティングにパスを定義するところから始めます。

ルーティング

ファイル名:routes/web.php

Route::post('/tweet/like', 'TweetController@postLike');

コントローラークラス

ファイル名:app/Http/Controllers/TweetController.php

    public function postLike(Request $request)
    {
        $this->tweetService->updateLikeCount($request->tweetId, $request->likePushed);
        return $request->tweetId;
    }

コントローラーはデータの入出力の流れが分かりやすいように出来るだけシンプルに、スマートに。サービスクラスにビジネスロジックを投げることが望ましいです。

DIしてあるtweetServiceupdateLikeCountメソッドに
ツイートのIDといいねのプッシュ情報を渡し、データの更新を行います。

サービスクラス

ファイル名:app/Http/Services/TweetService.php

    public function extractShowTweets($fetchedTweetIdList, $page)
    {
        $limit = 10;
        $offset = $page * $limit;
        $tweets = Tweet::orderBy('created_at', 'desc')->offset($offset)->take($limit)->get();
        if (is_null($tweets)) {
            return [];
        }

        if (is_null($fetchedTweetIdList)) {
            return $tweets;
        }

        $user = Auth::user();
        $tweetIdList = $tweets->pluck('id')->toArray();
        // Redisからアクセスユーザーのいいね情報を取得する
        $likedTweetIdList = RedisModel::getTweetLikePerUser($user->id, $tweetIdList);
        $showableTweets = [];
        foreach ($tweets as $index => $tweet) {
            if (!in_array($tweet->id, $fetchedTweetIdList)) {
                // オブジェクトにアクセスユーザーのいいね情報を格納する
                $tweet->is_liked = $likedTweetIdList[$index];
                $showableTweets[] = $tweet;
            }
        }

        return $showableTweets;
    }

    public function updateLikeCount($tweetId, $likePushed)
    {
        $user = Auth::user();
        
        // いいねした場合
        // いいね情報を新規保存できたら、回答の総いいね数をインクリメントする
        if ($likePushed) {
            if (RedisModel::setTweetLikePerUser($user->id, $tweetId, $likePushed)) {
                RedisModel::incrTweetLikeCount($tweetId);
            }
        }

        // いいねを取り消した場合
        // いいね情報を削除できたら、ツイートの総いいね数をデクリメントする
        if (!$likePushed) {
            if (RedisModel::delTweetLikePerUser($user->id, $tweetId, $likePushed)) {
                RedisModel::decrTweetLikeCount($tweetId);
            }
        }

        return $likePushed;
    }

updateLikeCountでいいね情報の保存、更新、削除を行います。

RedisModelRedisの処理を定義しているので、
use App\RedisModel;を忘れずに記載しましょう。

データを更新した内容を、ツイートを取得する際にデータを埋め込んで
画面に渡す必要があるので、

無限スクロールの際に実装したextractShowTweetsメソッドを修正します。

Redisに格納してあるいいねデータを取得し、埋め込んであげます。

ここで、Tweet.phpに少し手を加えます。
というのは、既存のままだとtweetsテーブルのカラムに存在する属性しかありません。
いいねデータを格納するis_likedという属性を定義してあげます。

Tweetクラス

ファイル名:app/Tweet.php

<?php

namespace App;

use Illuminate\Database\Eloquent\Model;

class Tweet extends Model
{
    /**
     * モデルの配列形態に追加するアクセサ
     *
     * @var array
     */
    protected $appends = ['is_liked'];

    public function getIsLikedAttribute() {
        return $this->attributes['is_liked'];
    }
}

データベースに対応するカラムがない属性の配列を追加するためには、
まずはじめに値のアクセサ(getIsLikedAttribute)を定義します。

アクセサの定義後は、
$appendsというプロパティに属性名を追加してあげることでモデルの配列とJSON形式両方に含まれるようになります。

残りは、Redisの実装です!

Redisクラス

ファイル名:app/RedisModel.php

<?php

namespace App;

use Illuminate\Support\Facades\Redis;

class RedisModel
{

    public static function setTweetLikePerUser ($userId, $tweetId, $likePushed)
    {
        $baseKey = implode(':', [config('tweet.TWEET_BASE_KEY'), $userId, config('tweet.USER_ACTION.LIKE')]);
        $expireDays = 60;
        Redis::hset($baseKey, $tweetId, $likePushed);
        Redis::expire($baseKey, 60 * 60 * 24 * $expireDays);
    }

    public static function getTweetLikePerUser ($userId, $tweetIdList)
    {
        $baseKey = implode(':', [config('tweet.TWEET_BASE_KEY'), $userId, config('tweet.USER_ACTION.LIKE')]);
        return Redis::hMGet($baseKey, $tweetIdList);
    }


    public static function delTweetLikePerUser ($userId, $tweetId)
    {
        $baseKey = implode(':', [config('tweet.TWEET_BASE_KEY'), $userId, config('tweet.USER_ACTION.LIKE')]);
        $expireDays = 60;
        Redis::hdel($baseKey, $tweetId);
    }

    public static function incrTweetLikeCount($tweetId)
    {
        $baseKey = implode(':', [config('tweet.TWEET_BASE_KEY'), config('tweet.USER_ACTION.LIKE')]);
        $expireDays = 60;
        Redis::zincrby($baseKey, 1, $tweetId);
        Redis::expire($baseKey, 60 * 60 * 24 * $expireDays);
    }

    public static function decrTweetLikeCount($tweetId)
    {
        $baseKey = implode(':', [config('tweet.TWEET_BASE_KEY'), config('tweet.USER_ACTION.LIKE')]);
        $expireDays = 60;
        Redis::zincrby($baseKey, -1, $tweetId);
    }

}

上述したデータ構造に対して、
期限60日に設定、以下のコマンドを実装しました。

Hash型
作成 ⇒ hset
key に対応するハッシュの指定されたフィールド field(TweetId) に値 value(”1″(true)) をセットします。
keyが存在しない場合は、 field と value のハッシュを持つ新しいキーが生成。
値を更新しただけの場合は0、
新しいフィールドが作成された場合は1が返ります。

削除 ⇒ hdel
キー key に対応するハッシュ内のフィールド field を削除します。
もしフィールドがハッシュ内に存在する場合は、そのフィールドは削除され1が返ります。それ以外の場合は0が返り、なんの操作も行われません。

取得 ⇒ hmget
指定した複数のキー keyN に対応するハッシュ内のフィールド field に保持された値を取得します。
もし指定したキーの内いくつかが存在しない場合、nilが返ります。

hmgetを用いることで一度に複数ツイートのいいね情報を取得できます。

SortedSet型
インクリメント、デクリメント ⇒ zincrby
member(TweetId) がすでにkey に対応するソート済みセット内に存在する場合、increment 分だけインクリメントします。
今回の場合、インクリメントに1, -1を渡すことで増減させます。

Configファイル

ファイル名:configs/tweet.php

<?php

return [

    'TWEET_BASE_KEY' => 'shootTweet',
    'USER_ACTION' => [
        'LIKE' => 'like'
    ]

];

ソースコードで極力マジックナンバーや生の文字列はNGです。

今回の場合、RedisのKeyは生の文字列が含むので、
コンフィグファイルに定義を切り出してあげたほうが良いでしょう。

Laravelの場合、configsフォルダに格納すれば、

config('tweet.USER_ACTION.LIKE')

といった風にアプリのどこからでも呼び出せるようになっています。

便利ですね!

まとめ

以上、いいね機能のAPI実装でした。
これでいいね機能が完成しました!!

今回のポイントと成果物をまとめます。

ポイント

  • Redisのデータ型設計
  • Redisでデータを格納
  • フロント側と結合

制作物

  • コントローラークラス
  • サービスクラス
  • Redisクラス
  • Tweetクラス



いいね機能を全2回にわたって実装例を紹介しましたが、
あくまで1例なので、もっとよいフロント設計、データ設計があると思います。

例えば、リクエスト内容をもっとまとめるなどもっと改善できるはずです。
Redisでのデータ管理ももっとシンプルにできるかもしれません。

改善点が見つかったらむしろ知らせていただけるとありがたいです!


そして今回Redisを強調した記事を書きました。
Redisは、他にもセッション管理や頻繁に使用されるデータをキャッシュしたりなど、
速度が求められる昨今のwebアプリ開発において重宝されます。

ぜひともRedisをどんどん活用していきましょう!


次回は、「通報機能(フロント側)」を実装し、モーダル画面の実装方法も学べます!



楽しみにしていてください!
それではまた!

プログラミング
スポンサーリンク
シェアする
SHOOTをフォローする
WebエンジニアSHOOTのブログ

コメント

タイトルとURLをコピーしました