FirestoreでGROUP BYを実現するObject.groupBy
状況
firestoreからのデータ取得時はGROUP BY句と同等の機能が存在しません。
Remixとfirebaseを使ってtodoリストを作成していて、firestoreからのデータ読み出し時にtodoをグループに分類して管理したいと思った時の話です。
データ取得後にfilterメソッドなどを使ってグループごとにまとめ直すしかないのか、と思って探していたら、JavaScriptのObject.groupByを見つけました。
しかし、TypeScriptを使っていたのでVSCode上でTypeScriptの型エラーが出て困ったり、出力方法を悩んだり勉強になったので備忘録です。
Object.groupByとは
簡単にいうと、2024年7月現在ESNextで使用できる、オブジェクトをオブジェクト内の任意のプロパティでグルーピングできる関数です。
Object.groupBy() 静的メソッドは、指定されたコールバック関数によって返された文字列値に従って、指定された反復可能な要素をグループ化します。返されるオブジェクトには、グループごとに個別のプロパティがあり、グループ内の要素を含む配列が格納されます。
以下は今回の使い方イメージで、tasksオブジェクトをgroupの種類によって振り分けることができます。
const tasks = [
{ name: "鰹", group: "魚" },
{ name: "トマト", group: "野菜" },
{ name: "冷凍うどん", group: "主食" },
{ name: "白菜", group: "野菜" },
{ name: "レモン", group: "野菜" },
];
const result = Object.groupBy(tasks, (t) => t.group);
console.log(JSON.stringify(result));
/* 結果:
{
魚: [
{ name: "鰹", type: "魚"}
],
野菜: [
{ name: "トマト", type: "野菜"},
{ name: "白菜", type: "野菜"},
{ name: "レモン", type: "野菜"}
],
主食: [
{ name: "冷凍うどん", type: "主食"}
]
}
*/
TypeScriptも対応
TypeScriptはバージョン5.4からObject.groupByに対応しています。
書き方もJavaScriptと変わらないみたいなので使ってみます。
すると、consoleに期待する出力結果が表示されましたが、VSCode上では型エラーが出ました。
原因はTypeScriptの設定
tsconfig.jsonを確認すると、libに設定されているJavaScriptのversionがES2022になっています。
compilerOptionsの項目については、こんな感じです。
・target
TypeScriptからJavaScriptにコンパイルされる際、どのバージョンのJavaScriptで出力するか指定できる。
・lib
VSCode上でコードを書いている時のJavaScriptのversion指定ができる。
指定したtargetに使いたい機能がないときに指定する。
Object.groupByは2024年7月現在ESNextでしか使えません。
targetを"ESNext"にすると少し古いバージョンのブラウザ側が対応できないため、
libの"ES2022"を"ESNext"にすると解決しました。
グループごとに分類したオブジェクトを出力する
出力はObject.entriesを使ってなんとか捻り出した感じになりました。
Object.entries() は静的メソッドで、与えられたオブジェクトが所有する、文字列をキーとした列挙可能なプロパティのキーと値の組の配列を返します。
Object.groupByはグループごとにまとめてObjectを返してくれるだけなので、TodoListの表示順序は意図しないものになることがあります。
そのため、Object.entriesを使って、グループ名ごとに意図した順序で並ぶようにしました。
const groupDivide = () => {
const groupDivide = Object.groupBy(todos, (t) => t.group);
return (
<>
{Object.entries(groupDivide)
//group名のリストをindexで比較して並べ替える
.sort(([a], [b]) =>
groupLabelList.findIndex((value) => value === a) >
groupLabelList.findIndex((value) => value === b)
? 1
: -1
)
//mapしてグループ名とタスク名を出力する
.map(([label, todos]) => (
<Fragment key={label}>
<h3>{label}</h3>
{todos?.map((todo) => (
<Label
key={todo.id}
className={`pb-5 pt-1 pl-2 flex text-base break-all ${
todo.isChecked ? "line-through" : ""
}`}
>
<Checkbox
checked={todo.isChecked}
onChange={(e) => changeIsChecked(todo, e.target.checked)}
></Checkbox>
{todo.todo}
</Label>
))}
</Fragment>
))}
</>
);
cssは少し違いますが、出力イメージはこの形です。
まとめ
Object.groupBy使って型エラーが起きたら、tsConfigのcompilerOptionsのlibをESNextに変更すると解決しそうです。
出力部分はObject.entriesとsortして、map使えばなんとか期待通りになりました!
もっといい方法がありそうととても思います。
そしてfirestoreでgroupbyできたらいいな、が本音です。
余談 ESNextとは
ESNextってなんだ?と私がなったので、余談で記述しておきます。
JavaScriptは、ECMAScriptという仕様に基づいて実装されているプログラム言語です。
ECMAScriptは毎年更新が続けられており、1年に1回更新するという意味でES2023のように西暦表記されています。
ESNextは2024年中に追加された変更をES2024が出る前に、先取りで使えるNextバージョンのJavaScriptで、おそらくES2024が出たあとは2025年中に追加されたメソッドなどがまとまってESNextになります。
つまり、ESNextは最新版のJavaScriptのバージョンと言えます。
TypeScriptもそれに合わせて進化しているようです。
TypeScriptはESNextと言われる次のECMAScriptに導入される言語仕様、別の言い方をすれば、未来のJavaScriptで使用可能になることがほぼ確定している言語仕様も先取りして取り入れています。
ECMAScriptへの追従に加え、ESNextの先取りによりプログラマは最新のJavaScript構文を使いながら、古い環境のコードにも対応できるようになります。
参考
Discussion