普段SQLを使うことが多いのですが、JavaScriptで配列のデータを扱っている時、JavaScriptの配列に対してもSQLでクエリが書けたらいいのになと思うことがあります。
一方、C#には配列をSQLライクなクエリで処理できる、LINQという言語が備わっています。
JavaScriptにもLINQを移植したライブラリがあったので今回使ってみました。
使ってみたところ、当初SQLでやりたかったことはLINQで一通りできそうだったので、これから使っていきたいと思います。
ユーティリティライブラリは、思いついた時にさっと使いたいのですが、何も見ないで書くには、JavaScriptのLINQはちょっと複雑でした。
そこで、SQLでよくやる操作をJavaScriptのLINQで書く方法をまとめておこうと思います。
LINQとは?
LINQとは、C#で配列をSQLライクなクエリで処理できる、C#の組み込み言語。
ここでは、JavaScriptでもLINQを使えるようにしたlinq
ライブラリを使用。ただし、マイクロソフトオフィシャルではない。
インストール
ES module版とCommonJS版がある。CommonJSは下記の方法でバージョン指定してインストールする必要がある。
npm install linq@3
基本的な使い方
import * as Enumerable from 'linq'; (async ()=>{ try{ const user = [ { user_id: 1, name: "AAA" }, { user_id: 2, name: "BBB" }, { user_id: 3, name: "CCC" }, ]; const res = Enumerable .from(user) .where(row => return row.user_id <= 2) .orderByDescending(row => row.user_id) .select((row) => { return { ...row, id_name: `${row.name}(${row.user_id})`, }; }) .toArray(); /* [ { user_id: 2, name: 'BBB', id_name: 'BBB(2)' }, { user_id: 1, name: 'AAA', id_name: 'AAA(1)' } ] */ console.log(res); }catch(err){ console.error(err); } })();
解説
- C#のLINQには、SQLライクな文で書く
クエリ式
と、関数で書くメソッド構文
の2種類の書き方があるが、JavaScriptのlinq
で利用できるのはメソッド構文
のみ - データは
Enumerable
オブジェクトとして扱う Enumerable
はデータベースのテーブルのようなものEnumerable
にwhere()
やorderBy()
などの関数が定義されていて、Enumerable
を変換するEnumerable
の関数は変換後のEnumerable
を返す。それにより、メソッドチェーンで連続して変換が行えるfrom()
は、JavaScriptの配列からEnumerable
を生成するEnumerable
はJavaScriptの配列ではないので、最後にtoArray()
で配列に戻しているEnumerable
のメソッドチェーンに過ぎないので、関数の記述順序や回数は不問orderBy()
の後にwhere()
を呼び出したり、where()
を複数回呼び出しても問題ないEnumerable
を出力するのであればselect()
も不要
使い方一覧
サンプルデータ
const purchase = [ { event_date: "2023-01-01", user_id: 1, sales: 100 }, { event_date: "2023-01-01", user_id: 1, sales: 200 }, { event_date: "2023-01-01", user_id: 2, sales: 1000 }, { event_date: "2023-01-02", user_id: 999, sales: 10000 }, ]; const user = [ { user_id: 1, name: "AAA" }, { user_id: 2, name: "BBB" }, ]; const filter = [ { event_date: "2023-01-01", user_id: 1 }, { event_date: "2023-01-01", user_id: 2 }, ];
select()
- データを加工する
const res = Enumerable .from(purchase) .select(row => { return { event_date: row.event_date, user_id: row.user_id, salesX100: row.sales * 100, } }) .toArray(); /* [ { event_date: '2023-01-01', user_id: 1, salesX100: 10000 }, { event_date: '2023-01-01', user_id: 1, salesX100: 20000 }, { event_date: '2023-01-01', user_id: 2, salesX100: 100000 }, { event_date: '2023-01-02', user_id: 999, salesX100: 1000000 } ] */ console.log(res);
そのまま出力
- データを加工しないのなら
select()
は不要
const res = Enumerable .from(purchase) .toArray(); /* [ { event_date: '2023-01-01', user_id: 1, sales: 100 }, { event_date: '2023-01-01', user_id: 1, sales: 200 }, { event_date: '2023-01-01', user_id: 2, sales: 1000 }, { event_date: '2023-01-02', user_id: 999, sales: 10000 } ] */ console.log(res);
where()
- 取り出す条件を指定する
const res = Enumerable .from(purchase) .where(row => { return ( row.sales >= 1000 && row.event_date === "2023-01-01" ); }) .toArray(); /* [ { event_date: '2023-01-01', user_id: 2, sales: 1000 } ] */ console.log(res);
複数回呼び出し
- 条件を一度で指定せず、複数回指定して絞り込んでもOK
const res = Enumerable .from(purchase) .where(row => row.sales >= 1000) .where(row => row.event_date === "2023-01-01") .toArray(); /* [ { event_date: '2023-01-01', user_id: 2, sales: 1000 } ] */ console.log(res);
orderBy()
- オーダーキーを指定すると、そのキーでソートしてくれる
- 複数のキーでソートする場合はメソッドチェーンでつなげる
- ただし、ソート確定後に次のソートに入るので、記述順序はSQLとは逆になる
- 降順は
orderByDescending()
const res = Enumerable .from(purchase) .orderByDescending(row => row.sales) .orderBy(row => row.event_date) .toArray(); /* [ { event_date: '2023-01-01', user_id: 2, sales: 1000 }, { event_date: '2023-01-01', user_id: 1, sales: 200 }, { event_date: '2023-01-01', user_id: 1, sales: 100 }, { event_date: '2023-01-02', user_id: 999, sales: 10000 } ] */ console.log(res);
groupBy()
- グループキーを指定して、そのキーでグルーピングする
- 引数
- 1番目
- グループキー
- 2番目
- 3番目の関数の引数の
rows
の要素。null
にしておいて問題ない
- 3番目の関数の引数の
- 3番目
- 結果
key
は1番目の引数で作成したグループキー。rows
はそのグループキーでグルーピングした時に含まれる要素一覧- 結果は1行なので、
rows
を集計する必要がある
- 結果
- 1番目
- 演算結果をグループキーにしてもOK
const res = Enumerable .from(purchase) .groupBy((row) => row.event_date, null, (key, rows) => { return { event_date: key, count: rows.count(), } } ) .toArray(); /* [ { event_date: '2023-01-01', count: 3 }, { event_date: '2023-01-02', count: 1 } ] */ console.log(res);
複合キー
- キーに複数要素を持たせて複合キーとする
- キーの識別は
===
演算子で行うので、値で比較できるよう、JSON.stringify()
でシリアライズした値で比較する
const res = Enumerable .from(purchase) .groupBy((row) => { return JSON.stringify({ event_date: row.event_date, sales_threshold: row.sales >= 500, }); }, null, (key, rows) => { return { ...JSON.parse(key), count: rows.count(), } }, (key) => JSON.stringify(key), ) .toArray(); /* [ { event_date: '2023-01-01', sales_threshold: false, count: 2 }, { event_date: '2023-01-01', sales_threshold: true, count: 1 }, { event_date: '2023-01-02', sales_threshold: true, count: 1 } ] */ console.log(res);
INNER JOIN
join()
を使う- 引数
- 1番目
right
テーブル
- 2番目
left
の結合キー
- 3番目
right
の結合キー
- 4番目
- 結果
- 1番目
const res = Enumerable .from(purchase) .join(user, left => left.user_id, right => right.user_id, (left, right) => { return { ...left, user_name: right.name, } } ) .toArray(); /* [ { event_date: '2023-01-01', user_id: 1, sales: 100, user_name: 'AAA' }, { event_date: '2023-01-01', user_id: 1, sales: 200, user_name: 'AAA' }, { event_date: '2023-01-01', user_id: 2, sales: 1000, user_name: 'BBB' } ] */ console.log(res);
複合キー
groupB()
の時と同様- キーに複数要素を持たせて複合キーとする
- キーの識別は
===
演算子で行うので、値で比較できるよう、JSON.stringify()
でシリアライズした値で比較する
LEFT JOIN
- LINQにはLeft Joinの機能はない
groupJoin()
とselectMany()
を使って実装する
groupJoin()
- Leftテーブルに対応するRightテーブルの行を配列で結合する
- Left Joinのように配列の展開はしないが、使い勝手がいいので、
groupJoin()
は覚えておいて損はない - 配列は
Enumerable
で渡されるので、実際に利用する場合はtoArray()
でJavaScriptの配列に変換しておく - 引数
- 1番目
right
テーブル
- 2番目
left
テーブルの結合キー
- 3番目
right
テーブルの結合キー
- 4番目
- 結果
- 第1引数が
left
テーブルの行 - 第2引数が
left
テーブルの行にジョインするright
テーブルの行の配列- (1つの
left
テーブルの行に対し、複数のright
テーブルの行が結合するケースがある)
- (1つの
- 第1引数が
- 結果
- 1番目
const res = Enumerable .from(purchase) .groupJoin(user, left => left.user_id, right => right.user_id, (left, rightRows) => { return { ...left, right: rightRows.toArray(), }; } ) .toArray(); /* [ { event_date: '2023-01-01', user_id: 1, sales: 100, right: [ { user_id: 1, name: 'AAA' } ] }, { event_date: '2023-01-01', user_id: 1, sales: 200, right: [ { user_id: 1, name: 'AAA' } ] }, { event_date: '2023-01-01', user_id: 2, sales: 1000, right: [ { user_id: 2, name: 'BBB' } ] }, { event_date: '2023-01-02', user_id: 999, sales: 10000, right: [] } ] */ console.dir(res, { depth: null });
複合キー
groupBy()
・join()
の時と同様- キーに複数要素を持たせて複合キーとする
- キーの識別は
===
演算子で行うので、値で比較できるよう、JSON.stringify()
でシリアライズした値で比較する
selectMany()
- 行の要素に配列が含まれている時、配列を展開する
- 引数
- 1番目
- どの要素が配列かを指定
- 配列が空の場合、展開が行われない。空配列でも展開されるようにするには
defaultIfEmpty()
を付ける
- 2番目
- 結果
- 引数には、
left
テーブルの1行と、right
テーブルの行の配列が展開されて、そのうちの1行が渡される。
- 1番目
groupJoin() と selectMany() を使った LEFT JOIN の実装
- 注意点
groupJoin()
のrightRows
はtoArray()
しないでEnumerable
のまま渡すdefaultIfEmpty()
は必須defaultIfEmpty()
がないと、left
テーブルの行にマッチするright
テーブルの行がない場合、left
テーブルのレコードそのものも作られない。つまり Inner Join と同じ挙動になる
defaultIfEmpty()
に引数を渡すと、引数値がright
の空行の値として使われる- 下記例の場合
defaultIfEmpty({ user_id: null, name: null })
などとする
- 下記例の場合
const res = Enumerable .from(purchase) .groupJoin(user, left => left.user_id, right => right.user_id, (left, rightRows) => { return { ...left, rightRows, }; } ) .selectMany((row) => row.rightRows.defaultIfEmpty(), (left, right) => { delete left.rightRows; return { ...left, right, } } ) .toArray(); /* [ { event_date: '2023-01-01', user_id: 1, sales: 100, right: { user_id: 1, name: 'AAA' } }, { event_date: '2023-01-01', user_id: 1, sales: 200, right: { user_id: 1, name: 'AAA' } }, { event_date: '2023-01-01', user_id: 2, sales: 1000, right: { user_id: 2, name: 'BBB' } }, { event_date: '2023-01-02', user_id: 999, sales: 10000, right: null } ]*/ console.log(res);
CROSS JOIN
selectMany()
の第1引数にright
テーブルを渡すleft
テーブルの行が呼ばれる度に、right
テーブルの全行が返される
const res = Enumerable .from(purchase) .selectMany(row => user, (left, right) => { return { left, right, }; } ) .toArray(); /* [ { left: { event_date: '2023-01-01', user_id: 1, sales: 100 }, right: { user_id: 1, name: 'AAA' } }, { left: { event_date: '2023-01-01', user_id: 1, sales: 100 }, right: { user_id: 2, name: 'BBB' } }, ... ... ... ]*/ console.log(res);
UNION ALL
concat()
で2つの配列を合わせる
const userAdd = Enumerable .from(user); const res = Enumerable .from(user) .concat(userAdd) .toArray(); /* [ { user_id: 1, name: 'AAA' }, { user_id: 2, name: 'BBB' }, { user_id: 1, name: 'AAA' }, { user_id: 2, name: 'BBB' } ] */ console.log(res);
LIMIT & TOP
take()
で最初の指定行数を取り出す
const res = Enumerable .from(purchase) .take(2) .toArray(); /* [ { event_date: '2023-01-01', user_id: 1, sales: 100 }, { event_date: '2023-01-01', user_id: 1, sales: 200 } ] */ console.log(res);
DISTINCT
distinct()
- 重複行を取り除く
- 第1引数にキーを指定した場合、キーに一致する行を1行だけ取り出す
COUNT(DISTINCT)
実装例
distinct(row=>row.column)
してから行数をcount()
で数える
const res = Enumerable .from(purchase) .groupBy(row => row.event_date, null, (key, rows) => { return { event_date: key, unique_user: rows.distinct((row: any) => row.user_id).count() }; } ) .toArray(); /* [ { event_date: '2023-01-01', unique_user: 2 }, { event_date: '2023-01-02', unique_user: 1 } ] */ console.log(res);
GROUP BY
の各グループの最初の1行を取得する
orderBy()
でグループ内の要素をソートfirst()
で最初の1行を取得
const res = Enumerable .from(purchase) .groupBy(row => row.event_date , null , (key, rows) => { const row:any = rows .orderByDescending((row:any) => row.sales) .first(); return row; } ) .toArray(); /* [ { event_date: '2023-01-01', user_id: 2, sales: 1000 }, { event_date: '2023-01-02', user_id: 999, sales: 10000 } ] */ console.log(res);
その他関数
sum(row=>row.column)
average(row=>row.column)
max(row=>row.column)
min(row=>row.column)
count()
null
値許容なので注意
ドキュメント
- 本家のLINQの
Enumerable
のリファレンスで機能を調べる - JavaScriptの
linq
にはリファレンスがないので、どういった関数があるかは型定義ファイルlinq.d.ts
を直接見る
参考記事
感想など
SQLライクかと言われると独特な部分も多いですね。
メソット構文で書くしかないので、パッと見とっつきにくいのですが、Enumerable
のチェーンを意識すると理解しやすくなるかと思います。
慣れてくるとより複雑なこともできるようになり楽しくなってくるのですが、どんどんSQLとはかけ離れたものになるので、あくまでSQL的な使い方に留めておきます。