新しいことにはウェルカム

技術 | 電子工作 | ガジェット | ゲーム のメモ書き

JavaScriptの配列をSQLで扱いたい

普段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はデータベースのテーブルのようなもの
  • Enumerablewhere()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番目
      • 結果
        • keyは1番目の引数で作成したグループキー。rowsはそのグループキーでグルーピングした時に含まれる要素一覧
        • 結果は1行なので、rowsを集計する必要がある
  • 演算結果をグループキーにしても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番目
      • 結果
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テーブルの行が結合するケースがある)
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行が渡される。

groupJoin() と selectMany() を使った LEFT JOIN の実装

  • 注意点
    • groupJoin()rightRowstoArray()しないで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値許容なので注意

ドキュメント

参考記事

感想など

SQLライクかと言われると独特な部分も多いですね。

メソット構文で書くしかないので、パッと見とっつきにくいのですが、Enumerableのチェーンを意識すると理解しやすくなるかと思います。

慣れてくるとより複雑なこともできるようになり楽しくなってくるのですが、どんどんSQLとはかけ離れたものになるので、あくまでSQL的な使い方に留めておきます。

関連カテゴリー記事

www.kwbtblog.com

www.kwbtblog.com

www.kwbtblog.com

www.kwbtblog.com

www.kwbtblog.com

www.kwbtblog.com

www.kwbtblog.com

www.kwbtblog.com

www.kwbtblog.com

www.kwbtblog.com

www.kwbtblog.com

www.kwbtblog.com

www.kwbtblog.com

www.kwbtblog.com

www.kwbtblog.com

www.kwbtblog.com

www.kwbtblog.com

www.kwbtblog.com

www.kwbtblog.com

www.kwbtblog.com

www.kwbtblog.com

www.kwbtblog.com

www.kwbtblog.com

www.kwbtblog.com

www.kwbtblog.com

www.kwbtblog.com

www.kwbtblog.com

www.kwbtblog.com

www.kwbtblog.com

www.kwbtblog.com

www.kwbtblog.com

www.kwbtblog.com

www.kwbtblog.com

www.kwbtblog.com

www.kwbtblog.com

www.kwbtblog.com

www.kwbtblog.com

www.kwbtblog.com

www.kwbtblog.com

www.kwbtblog.com

www.kwbtblog.com

www.kwbtblog.com

www.kwbtblog.com

www.kwbtblog.com

www.kwbtblog.com

www.kwbtblog.com

www.kwbtblog.com

www.kwbtblog.com

www.kwbtblog.com

www.kwbtblog.com

www.kwbtblog.com

www.kwbtblog.com

www.kwbtblog.com

www.kwbtblog.com

www.kwbtblog.com

www.kwbtblog.com

www.kwbtblog.com

www.kwbtblog.com

www.kwbtblog.com

www.kwbtblog.com

www.kwbtblog.com

www.kwbtblog.com

www.kwbtblog.com

www.kwbtblog.com

www.kwbtblog.com

www.kwbtblog.com

www.kwbtblog.com

www.kwbtblog.com

www.kwbtblog.com

www.kwbtblog.com

www.kwbtblog.com

www.kwbtblog.com

www.kwbtblog.com

www.kwbtblog.com

www.kwbtblog.com

www.kwbtblog.com

www.kwbtblog.com

www.kwbtblog.com