Help us understand the problem. What is going on with this article?

BEAR.SundayのCQRSを時限公開のコンテンツに使う

はじめに

BEAR.Sundayには破壊的メソッド(onPut/onPost/onDelete)を実行した際にキャッシュを更新または破棄する機能があります。キャッシュの参照とキャッシュの生成が分離できます。

注)CQRSは本来ソフトウェアパターンの名前ですが、本記事では便宜的にキャッシュ生成およびキャッシュ破棄に関するBEAR.Sundayの一機能の名前として言及しています。

時限公開コンテンツ

@Cacheableアノテーションが指定されたResourceは標準で有効期限が存在しないキャッシュが生成されます。
これは未来永劫公開され続けるものには有効ですが、指定時間に公開したい、もしくは指定時間に公開を終了したいようなコンテンツに対しては不利な面があります。
そのResourceに対して更新処理が行われない限りキャッシュが破棄されず、記事が公開されない・記事の公開が終了しないといった問題が発生します。

CacheableアノテーションのexpiryAt

@Cacheabeアノテーションは$expiryAtという引数を受け入れます。この引数を指定すると、無期限もしくは決められた時間(TTL)の指定ではなく、キャッシュの有効期限を日時指定で柔軟にカスタマイズできます。

これを利用して、予約公開のコンテンツには公開開始日時を指定するように、公開終了日時が決まっているコンテンツには公開終了日時が指定されるようになれば一応意図する動作になりそうですね。

var/db/sql/getArticleById.sql

記事を取得するSQLです。
今回キャッシュ操作に扱う公開開始日(release_date)は一応専用のキーで取得しておきます。

SELECT
  *,
  release_date AS __release_date
FROM
  article
WEHERE
  id = :id

src/Resource/App/Article.php

@CacheableアノテーションにexpiryAtが指定された場合、$this->body内に指定されたそのキーで何かしらの有効日時を示す値が必ず存在している必要があります。

ですので、有効期限も公開日時も指定されていない、もしくは記事が存在しないなどといった、本来であれば無期限でキャッシュできそうな場合でも日時を指定する必要があります。今回はそのような場合とりあえず2999-12-31にしてしまいます。

<?php
namespace Vendor\Project\Resource\App;

/**
 * @Cacheable(expiryAt="__expiry_at")
 */
class Article extends ResourceObject
{
    use ResourceInject;

    private const EXPIRY_NEVER = '2999-12-31';

    /**
     * @var callable
     */
    private $getArticle;

    /**
     * @var NowInterface
     */
    private $now;

    /**
     * @param RowInterface $getArticle
     * @param NowInterface $now
     * @Named("getArticle=getArticle")
     */
    public function __construct(RowInterface $getArticle, NowInterface $now)
    {
        $this->getArticle = $getArticle;
        $this->now = $now;
    }

    /**
     * @param int $id
     *
     * @return ResourceObject
     */
    public function onGet(int $id) : ResourceObject
    {
        $article = ($this->getArticle)([
            'id' => $id,
            'now' => (string) $this->now
        ]);

        // 記事自体が見つからない
        if (empty($article)) {
            $this->code = Code::NOT_FOUND;
            $this->body = ['__expiry_at' => self::EXPIRY_NEVER];

            return $this;
        }

        $this->body = $article;
        $this->body['__expiry_at'] = empty($article['expiration_date']) ? self::EXPIRY_NEVER : $article['expiration_date'];
        // 公開開始前の記事
        if (strtotime($article['__release_date']) > strtotime($this->now)) {
            $this->body['__expiry_at'] = $article['release_date'];
            $this->code = Code::NOT_FOUND;
        }

        return $this;
    }
}

まず有効期限設定の有無に関わらず、キャッシュの有効期限を設定します。次にもしその記事が公開開始前のものであれば、その公開開始日時でキャッシュの有効期限を上書きします。

これで

  • 公開開始日前の記事であれば公開開始日にキャッシュが破棄される
  • すでに公開されている記事であれば公開終了日にキャッシュが破棄される
  • 公開終了日が存在しない記事は2999年にキャッシュが破棄される(自分はもう死んでいるのでそんな先のことは知らない)

という挙動が実現できるはずです。

最後に

お気づきになられた方も多いと思いますが、こちらの実装では厳密なCQRSになっていません。(キャッシュの有効期限が切れたあとのユーザアクセスでキャッシュが生成されてしまう)
キャッシュが存在しないものが公開されて困るほど高負荷のサービスでは他に対策が必要になると思います。

Why not register and get more from Qiita?
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away
Comments
No comments
Sign up for free and join this conversation.
If you already have a Qiita account
Why do not you register as a user and use Qiita more conveniently?
You need to log in to use this function. Qiita can be used more conveniently after logging in.
You seem to be reading articles frequently this month. Qiita can be used more conveniently after logging in.
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away
ユーザーは見つかりませんでした