新卒3カ月目でシニアエンジニアたちを驚かせたくてまだ誰も手を付けていないJiraのタスクに勝手に取り掛かってみた。結論から言えば無駄足だったけど、使えないコードではないので備忘録として残しておきたい。
タイトルにある通り、複数のAPIコールの結果をクライアント側にキャッシュとして残しておくタスクがあった。取得するデータの件数はおよそ6万件。一挙に取得すると時間がかかるので、ユーザーが必要な件数だけ取得し、毎回APIを叩かなくてもいいように、取得したものをキャッシュしておく必要があった。
Angularを使ったプロジェクトだったのでRxjsを使用する。RxjsでAPIのコールをキャッシュする方法は主に二つあった。RxjsのShareReplayオペレーターを使う方法とBehaviourSubjectを使う方法。
今回のケースだと後者が適切だと思った。
参考にしたサイトはこれ
https://medium.com/@deyan.kamburov/caching-data-in-angular-application-using-rxjs-ec8629b23bbb
public getData(startIndex: number, chunkSize: number): any {
const endIndex = startIndex + chunkSize;
let areAllItemsInCache = true;
let cacheData = [];
for (let i = startIndex; i < endIndex; i++) {
if (this._cachedData[i] === null || this._cachedData[i] === undefined) {
areAllItemsInCache = false;
break;
}
cacheData.push(this._cachedData[i]);
}
if (!areAllItemsInCache) {
this._http.get(this._buildDataUrl(startIndex, chunkSize)).subscribe((data: any) => {
this._data.next(data.value);
for (let i = 0; i < data.value.length; i++) {
this._cachedData[i + startIndex] = data.value[i];
}
});
} else {
this._data.next(cacheData);
}
}
要はAPIから新しくデータを取得するたびに配列に格納しておき、データが要求された際に配列の要素を確認し、既存のデータであればそのまま返して、配列に格納されていない新規のデータであればAPIを叩いて取得する。ロジックはシンプルだが、上記のサンプルに倣ってそのまま実装しようとすると、例えば複数のAPIコールをキャッシュする場合、似たようなForループやIf文を書かなきゃいけなくて嫌だ。それに仕様が変更になって、また別に新しくAPIのコールをキャッシュしなきゃいけなくなると、同じような内容の実装をしなきゃいけなくて面倒くさい。
なので……
そうだ!クラスにしてしまおう!
export class CacheData<T> {
private _data$: BehaviorSubject<T[]>;
private readonly _cachedData: T[];
private readonly _getRequest: (pageSize: number, startIndex: number) => Observable<T[]>;
constructor(getRequest: (pageSize: number, startIndex: number) => Observable<T[]) {
this._data$ = new BehaviorSubject(null);
this._cachedData = [] as T[];
this._getRequest = getRequest;
}
get data$() {
return this._data$.asObservable();
}
private fetchData(pageSize: number, startIndex: number) {
this._getRequest(pageSize, startIndex).subscribe((data) => {
this._data$.next(data.arr);
for (let i = 0; i < data.arr.length; i++) {
this._cachedData[i + startIndex] = data[i];
}
});
}
public getData(pageSize: number, startIndex: number) {
const endIndex = startIndex + pageSize;
let cachedData: T[] = [] as T[];
let isAllCached = true;
for (let i = startIndex; i < endIndex; i++) {
if (this._cachedData[i] == null) {
isAllCached = false;
break;
}
cachedData.push(this._cachedData[i]);
}
if (!isAllCached) {
this.fetchData(pageSize, startIndex);
} else {
this._data$.next(cachedData);
}
}
}
使用例は
let pageSize = 5;
let startIndex = 0;
let dataSource:any;
let dataCache = new CacheData<Object>(getApi.bind(this));
dataCache.getData(pageSize, startIndex)
.subscribe( data =>{
dataSource = data;
});
みたいな感じ。(適当です)
こだわった点はコンストラクタでコールバック関数を渡せるようにしたとこ。こうすればObservableの配列を返り値に持つ関数さえあればどんなAPI取得リクエストでもキャッシュできる!(試してない)。bind(this)しないとAPIの取得リクエスト関数がコールバックとして渡せないけど、Arrow functionを使うことが推奨されているっぽいので改善が必要です。AngularもRxjsも初心者なのでこうすればもっと良くなる!この方法のほうが効率的!こうしたらもっと簡潔に書ける!というご意見、歓迎します。
サーバー側でキャッシュしておくから必要ないと言われたのがオチです。じゃあね!