キャッシュの利用については、次のように考えています。
- キャッシュドライバハーはDBとはしない。なぜなら、DBアクセスの負荷を落とすためのキャッシュなのだから。
- 単純な構造で、いろいろな局面で利用可能で、かつキャッシュの内容が十分少数となるものをキャッシュする。自分の中では、3桁のオーダーを越えるようなものはキャッシュしないという大枠で考えています。
- 例外で再帰SQLが出るような局面(例えば階層化されたカテゴリー)なんてのは、キャッシュできないか考える。
この実装を作成中のモデルに付加します。
//Cache 生成削除基底
public static function forget($cacheName):void
{
Cache::forget($cacheName);
}
public static function remember($cacheName, $seconds,
$sqlstring ='', $column = '',$cacheData = null): array
{
if($sqlstring<>""){
return Cache::remember($cacheName, $seconds,
function () use ($cacheName,$sqlstring, $column) {
Log::debug('re-cache:'.$cacheName);
return
$column != '' ?
array_column(json_decode(json_encode(DB::select($sqlstring)), true), $column) :
json_decode(json_encode(DB::select($sqlstring)), true);
});
}else{
return Cache::remember($cacheName, $seconds,
function () use ($cacheName, $cacheData) {
Log::debug('re-cache:'.$cacheName);
return json_decode(json_encode($cacheData), true);
});
}
}
forgetはいらないかもですが、ラッパーのような関数 remember を作る意味は、
- この関数でキャッシュする事で、キャッシュの中味は、常に何らかの配列であることにする。DB::select は、オブジェクトの配列というちょっと中途半端な形の結果を返す。検証してませんが、オブジェクトの配列って無駄にデカイのでは?
- この関数を使わない場合、コードのいくつかのところで、 素の Cache::remember .. という記述を繰り返すことになる。
からです。
次に、このキャッシュ、どのあたりで使えるかなのですが、すぐ思いつくのが、[年度][締日]です。
- その量からいって適当。
- アプリケーションが稼働中にほぼ変化無し。
- この二つの要素は、SQLのパラメーターとして利用される。つまり、稼働するアプリの中では、利用ユーザーの入力値となってやってくる。
- したがって、このリストがあると、バリデーション時のホワイトリスト的な利用法もできるし、ユーザーの入力時のselectBoxの素としても利用できかも。
- [会計日付]は、ホワイトリストバリデーション的には利用できるかも。selectBoxの素としては量が多すぎ。
検討の結果、年度と締日のキャッシュ管理部分を次の通り追加。
//会計年度キャッシュ
public static function forgetYearList(): void
{
self::forget(config('cachename.yearlist'));
}
public static function getYearList(): array
{
$cacheName = config('cachename.yearlist');
$seconds = config('cachename.lifetime');
$sqlstring = 'select distinct 年度 from kaikei.work_仕訳_明細_増減 order by 年度 DESC';
return self::remember($cacheName, $seconds, $sqlstring, '年度');
}
//締日キャッシュ
public static function forgetCutoffDateList(): void
{
self::forget(config('cachename.cutoffdatelist'));
}
public static function getCutoffDateList(): array
{
$cacheName = config('cachename.cutoffdatelist');
$seconds = config('cachename.lifetime');
$sqlstring = "
--アプリとDB間の日付の区切り文字調整
select distinct replace(締日::text,'-','/') as 締日
from kaikei.work_仕訳_明細_増減
order by 締日 DESC
";
return self::remember($cacheName, $seconds, $sqlstring, '締日');
}
cacheNameとseconds をconfig経由で取得しているのは、プロジェクト単位でのキャッシュ名の名前衝突回避策、デバックから実運用に変わっていく時に、キャッシュ時間を調整したいからです。
作ったキャッシュから、ホワイトリストバリデーションを行う関数を次のように。
public static function isExistCutoffDate(string $cutoffDate): bool
{
return in_array($cutoffDate, self::getCutoffDateList(), true);
}
public static function isExistYear(int $year): bool
{
return in_array($year, self::getYearList(), true);
}
dryRunで確かめていた会計年度での試算表取得を関数として、
public static function getTrialBalanceByYear(int $year): array|bool
{
try {
if (! self::isExistYear($year)) {
throw new \Exception($year.' is not business year');
}
$sqlstring = str_replace(':QUERYTYPE', '年度', self::$sqlstring);
$sqlstring = str_replace(':WPARAM1', 'where 年度 < ? ', $sqlstring);
$sqlstring = str_replace(':WPARAM2', 'where 年度 <= ? ', $sqlstring);
$sqlstring = str_replace(':WPARAM3', '?::integer as 年度,', $sqlstring);
$sqlstring = str_replace(':WPARAM4', 'where 年度 = ? ', $sqlstring);
$sqlstring = str_replace(':WPARAM5', 'where 年度 = ? ', $sqlstring);
$result = DB::select($sqlstring, [$year, $year, $year, $year, $year]);
return $result;
} catch (\Exception $e) {
Log::error($e->getMessage());
return false;
}
}
結果、このgetTrialBalanceByYear()だって、十分キャッシュ対象ではと考え出していますが、取り敢えず、締日での実装を優先します。
テストに追加した部分は、以下。テストというより、動作確認用にテストの仕組みを使っているだけです。
public function test_forget_year_list(): void
{
// 関数呼び出しができることだけ
KaikeiWorkModel::forgetYearList();
$this->assertTrue(true);
}
public function test_forget_cutoff_date_list(): void
{
// 関数呼び出しができることだけ
KaikeiWorkModel::forgetCutoffDateList();
$this->assertTrue(true);
}
public function test_is_exist_year(): void
{
// Error がおこることを確認確認してコメントアウト
// $response = KaikeiWorkModel::isExistYear();
// $response = KaikeiWorkModel::isExistYear(null);
// $response = KaikeiWorkModel::isExistYear('hoge');
$response = KaikeiWorkModel::isExistYear(2024);
$this->assertTrue($response);
$response = KaikeiWorkModel::isExistYear(2050);
$this->assertFalse($response);
}
public function test_get_trial_balance_by_year(): void
{
$response = KaikeiWorkModel::getTrialBalanceByYear(1000);
$this->assertFalse($response);
$response = KaikeiWorkModel::getTrialBalanceByYear(2024);
$this->assertTrue(is_array($response));
if (is_array($response)) {
echo $response[0]->年度; //一行目の中味を確認
}
}
試算表SQLの元になる部分も、書き換えています。
private static $sqlstring = "
with
cte_zenki
as(
select 科目コード, sum(
case
when kaikei.work_仕訳_明細_増減.要素名 ='収益' and 科目貸借 = '借方' then - 税抜増減
when kaikei.work_仕訳_明細_増減.要素名 ='費用' and 科目貸借 = '貸方' then - 税抜増減
else 税抜増減 end
) as 前残
from kaikei.work_仕訳_明細_増減
:WPARAM1
group by 科目コード),
cte_touki
as(
select 科目コード, sum(
case
when kaikei.work_仕訳_明細_増減.要素名 ='収益' and 科目貸借 = '借方' then - 税抜増減
when kaikei.work_仕訳_明細_増減.要素名 ='費用' and 科目貸借 = '貸方' then - 税抜増減
else 税抜増減 end
) as 当期残
from kaikei.work_仕訳_明細_増減
:WPARAM2
group by 科目コード),
cte_zenki_touki
as(
select distinct :WPARAM3
科目コード,
case when 要素名 in('収益', '費用') then 0 else 前残 end as 前残,
case when 要素名 in('収益', '費用') then 当期残 -前残 else 当期残 end as 残高
from cte_zenki
join kaikei.view_勘定科目補助科目 kk on 科目コード = kk.勘定科目コード
full outer join cte_touki using( 科目コード)
),
cte_kari
as(
select :QUERYTYPE,
科目コード, sum(金額-内消費税) as 借方
from kaikei.work_仕訳_明細_税集計
:WPARAM4
and 仕訳貸借 in('借方')
group by :QUERYTYPE, 科目コード),
cte_kasi
as(
select :QUERYTYPE,
科目コード, sum(金額-内消費税) as 貸方
from kaikei.work_仕訳_明細_税集計
:WPARAM5
and 仕訳貸借 in('貸方')
group by :QUERYTYPE, 科目コード
)
select
COALESCE(cte_zenki_touki.:QUERYTYPE, COALESCE(cte_zenki_touki.:QUERYTYPE, COALESCE(cte_kasi.:QUERYTYPE, cte_kari.:QUERYTYPE))) as :QUERYTYPE,
COALESCE(cte_zenki_touki.科目コード, COALESCE(cte_kasi.科目コード, cte_kari.科目コード)) as 科目コード,
勘定科目名,
COALESCE(cte_zenki_touki.前残, 0) as 前残,
COALESCE(借方, 0) as 借方,
COALESCE(貸方, 0) as 貸方,
COALESCE(cte_zenki_touki.残高, 0) as 残高,
case m.要素コード
when '1' then COALESCE(cte_zenki_touki.残高, 0)
when '2' then (COALESCE(貸方, 0) - COALESCE(借方, 0))
when '3' then COALESCE(cte_zenki_touki.残高, 0)
when '4' then COALESCE(cte_zenki_touki.残高, 0)
when '5' then COALESCE(貸方, 0) - COALESCE(借方, 0)
when 'X' then 0
end 評価
from kaikei.ma_勘定科目 m
left outer join cte_kari on m.勘定科目コード = cte_kari.科目コード
left outer join cte_kasi on m.勘定科目コード = cte_kasi.科目コード
left outer join cte_zenki_touki on m.勘定科目コード = cte_zenki_touki.科目コード
where not (cte_kasi.:QUERYTYPE is null and cte_kari.:QUERYTYPE is null and cte_zenki_touki.:QUERYTYPE is null)
and not ( 前残 = 0 and 借方 = 0 and 貸方 = 0 and 残高 = 0)
order by 科目コード
会計年度以外で集計する場合に必要な:QUERYTYPEという部分が加わっています。