いいね機能の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してあるtweetService
のupdateLikeCount
メソッドに
ツイートの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
でいいね情報の保存、更新、削除を行います。RedisModel
でRedis
の処理を定義しているので、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をどんどん活用していきましょう!
次回は、「通報機能(フロント側)」を実装し、モーダル画面の実装方法も学べます!
楽しみにしていてください!
それではまた!
コメント