

現在Nodejsを使用してJavascriptの勉強をしております。  
クエリ文を実行して得た値を配列に格納したいのですが、非同期になってしまうため、繰り返し２週目以降のデータが格納されません。  
DBのレコードはそれぞれ parent_id カラム(親レコードのidが格納してある)を持っており、子となるレコードは複数ある場合もあります。

実現したいこととしては、Main.jsから受け取ったid(req.body.id)から末端の子要素のidを全て取得し、idArrayへ重複なく格納したいです。  
どのようなコードを書けばよいかご教授お願いいたします。

[code]

    let resultTmp = 0; //繰り返し判定要素
    let parentIdArray = []; //親要素を格納する配列(forEach)
    
    parentIdArray.push(req.body.id); //　Main.jsからpostで来た値を格納
    
    while (resultTmp.length !== 0) {
      parentIdArray.forEach((parentId) => {
        connection.query(
          'SELECT id, folder_name, parent_id FROM folder WHERE parent_id = ?',
          [parentId],
          (error, results) => {
            results.forEach((result) => {
              //重複していなければ追加
              if (idArray.indexOf(result.id) == -1) {
                idArray.push(result.id);
              } //重複していなければ追加
              if (parentIdArray.indexOf(result.id) == -1) {
                parentIdArray.push(result.id);
              }
            });
            //resultsが空であればWhile文終了
            resultTmp = results;
          }
        );
        //一度parentIdArray.forEachで実行した配列要素は削除
        parentIdArray.splice(parentIdArray.indexOf(parentId), 1);
      });
    }
    res.send({ response: idArray });
    
[/code]


CC BY-SA 4.0
2022-12-11T09:03:45.573
92701
2022-12-12T15:23:37.893
2022-12-11T09:30:03.540
3060
30760
question
javascript mysql node.js
NodejsでMySQLのクエリ文の繰り返し処理を同期的に実行したい


今回はPromiseなどを使用せずとも下記で実行できたため、自己解決とさせていただきたいと思います。

[code]

    connection.query('select * from folder', (error, results) => {
            console.log(results);
             while (parentIdArray.length !== 0) {
              parentIdArray.forEach((parentId) => {
                results.forEach((result) => {
                  if (parentId == result.parent_id) {
                    console.log(`id : ${result.id}`);
                    //重複していないなら格納する
                    if (idArray.indexOf(result.id) == -1) {
                      idArray.push(result.id);
                    }
                    if (parentIdArray.indexOf(result.id) == -1) {
                      parentIdArray.push(result.id);
                    }
                  }
                });
                parentIdArray.splice(parentIdArray.indexOf(parentId), 1);
              });
            }
            res.send({ response: idArray });
          });
    
[/code]


CC BY-SA 4.0
2022-12-11T15:09:00.267
92708
2022-12-12T12:40:31.850
2022-12-12T12:40:31.850
30760
30760
92701
answer


階層問い合わせは正解が難しい処理だと思います。なので他の方法を提示するという意味で回答を書いてみました。

合わせて少し回答としては足りない部分があることをお詫びしておきます。

  * 用意したテーブルが仕様と食い違っている可能性があります
  * MySQLはバージョンが分からなかったので最新になっています
  * node.jsのMySQLドライバはmysqlとmysql2を使用しています
  * 環境はLinuxを想定しdocker及びdocker-composeを前提としています
  * 各最新バージョンは2022/12/12時点の最新です(基本安定版の最新)
  * dockerの設定はセキュリティの考慮が皆無なので、間違ってもインターネット環境で動かしてはいけません

※質問だけではよく分からない部分があり、すでに解決済みだったので質問を控えたため

なので参考程度の情報と思ってください。

## 再現環境作成

空ディレクトリで下記ファイルをシェルスクリプトとして実行してください。

[code]

    WAIT_SETUP_MYSQL=10
    cat >docker-compose.yml <<EOF
    version: '3.1'
    services:
      db:
        image: mysql
        command: --default-authentication-plugin=mysql_native_password
        volumes:
          - ./data:/var/lib/mysql
          - ./logs:/var/log/mysql
        ports:
          - 3306:3306
        environment:
          MYSQL_ROOT_PASSWORD: example
      node:
        image: node
        volumes:
          - ./node/:/opt/project/
    EOF
    docker-compose up -d db
    cat <<EOF | docker-compose exec -T db bash
    while sleep 1;do if [ -S /var/run/mysqld/mysqld.sock ];then break;fi;done
    sleep $WAIT_SETUP_MYSQL
    mysql --user=root --password=example
    use mysql
    create user user@node IDENTIFIED BY 'node';
    create table folder(
    id varchar(20) primary key,
    folder_name varchar(1024),
    parent_id varchar(20)
    );
    insert into folder values('1000', '/hoge', '0');
    insert into folder values('1001', '/hoge/piyo', '1000');
    insert into folder values('1002', '/hoge/hoge', '1000');
    insert into folder values('1003', '/hoge/piyo/foo', '1001');
    insert into folder values('1004', '/hoge/hoge/bar', '1002');
    insert into folder values('1005', '/hage', '0');
    insert into folder values('1006', '/hoge/piyo/foo/hoge', '1003');
    exit
    exit
    EOF
    cat <<EOF | docker-compose run -T --rm node bash
    cd /opt/project
    npm init -y
    npm install mysql mysql2
    cat >index.js <<INNER_EOF
    const mysql = require('mysql');
    const connection = mysql.createConnection({
      host     : 'db',
      user     : 'root',
      password : 'example',
      database : 'mysql'
    });
    connection.connect();
    
    let resultTmp = 0; //繰り返し判定要素
    let parentIdArray = []; //親要素を格納する配列(forEach)
    
    parentIdArray.push('1000'); //　Main.jsからpostで来た値を格納
    
    while (resultTmp.length !== 0) {
      parentIdArray.forEach((parentId) => {
        console.log('start query');
        connection.query(
          'SELECT id, folder_name, parent_id FROM folder WHERE parent_id = ?',
          [parentId],
          (error, results) => {
            console.log('resultset callback');
            results.forEach((result) => {
              //重複していなければ追加
              if (idArray.indexOf(result.id) == -1) {
                idArray.push(result.id);
              } //重複していなければ追加
              if (parentIdArray.indexOf(result.id) == -1) {
                parentIdArray.push(result.id);
              }
            });
            //resultsが空であればWhile文終了
            resultTmp = results;
          }
        );
        console.log('async parentIdArray update...orz');
        //一度parentIdArray.forEachで実行した配列要素は削除
        parentIdArray.splice(parentIdArray.indexOf(parentId), 1);
      });
    }
    console.log({ response: idArray });
    connection.end();
    INNER_EOF
    cat >index_promise.js <<INNER_EOF
    (async ()=>{
      const mysql = require('mysql2/promise');
      const config = {
        host     : 'db',
        user     : 'root',
        password : 'example',
        database : 'mysql'
      };
      const connection = await mysql.createConnection(config);
      const parentIdArray = [];
      const idArray = [];
      parentIdArray.push('1000');
      while (parentIdArray.length != 0) {
        const parentId = parentIdArray.pop();
        const [rows, _] = await connection.execute('SELECT id, folder_name, parent_id FROM folder WHERE parent_id = ?', [parentId]);
        const ids = rows.map(e=>e.id);
        idArray.push(...ids);
        parentIdArray.push(...ids);
      }
      console.log({ response: idArray });
      connection.end();
    })();
    INNER_EOF
    cat >index_recursive.js <<INNER_EOF
    (async ()=>{
      const mysql = require('mysql2/promise');
      const config = {
        host     : 'db',
        user     : 'root',
        password : 'example',
        database : 'mysql'
      };
      const connection = await mysql.createConnection(config);
      const parentId = '1000';
      const [rows, _] = await connection.execute('\\\\
    WITH recursive cte AS(\\\\
      SELECT id, folder_name, parent_id FROM folder WHERE parent_id = ? \\\\
    UNION \\\\
      SELECT child.id, child.folder_name, child.parent_id FROM folder as child, cte WHERE cte.id = child.parent_id \\\\
    ) \\\\
    SELECT id, folder_name, parent_id FROM cte \\\\
    ', [parentId]);
      const idArray = rows.map(e=>e.id);
      console.log({ response: idArray });
      connection.end();
    })();
    INNER_EOF
    node index.js&
    sleep 30
    kill %1
    EOF
    docker-compose down
    
[/code]

## 使い方

`docker-compose up -d db`としてMySQLを動作させ、`docker-compose run --rm node bash -c
"cd /opt/project;node index.js"`みたいな感じで使ってください。

※index.jsは再現コードです

# 回答

## Promiseを用いるケース

`./node/index_promise.js`です。構築スクリプト内にありますが、ハイライトがないので、再掲しておきます。

### ソースコード

[code]

    (async ()=>{
      const mysql = require('mysql2/promise');
      const config = {
        host     : 'db',
        user     : 'root',
        password : 'example',
        database : 'mysql'
      };
      const connection = await mysql.createConnection(config);
      const parentIdArray = [];
      const idArray = [];
      parentIdArray.push('1000');
      while (parentIdArray.length != 0) {
        const parentId = parentIdArray.pop();
        const [rows, _] = await connection.execute('SELECT id, folder_name, parent_id FROM folder WHERE parent_id = ?', [parentId]);
        const ids = rows.map(e=>e.id);
        idArray.push(...ids);
        parentIdArray.push(...ids);
      }
      console.log({ response: idArray });
      connection.end();
    })();
    
[/code]

### 実行方法

[code]

    $ docker-compose run --rm node bash -c "cd /opt/project;node index_promise.js"
    
[/code]

### 解説

元のコードと同じ量の同じようなクエリを発行する形かと思います。awaitで待てるので、楽にループし、無駄なCPUの浪費もしません。ただIDが重複しない前提のテーブルなのでロジックはかなり違っています。

見つかった子孫1つに対して必ず1回クエリが発行されるので、とても重たいロジックになっています。少なくともこのままでは実用に耐えません。

## 再帰クエリを使用するケース

MySQL8以上で実装された[再帰クエリ](https://dev.mysql.com/doc/refman/8.0/ja/with.html#common-
table-expressions-recursive)を使用するケースです。  
`./node/index_recursive.js`です。構築スクリプト内にありますが、ハイライトがないので、再掲しておきます。

### ソースコード

[code]

    (async ()=>{
      const mysql = require('mysql2/promise');
      const config = {
        host     : 'db',
        user     : 'root',
        password : 'example',
        database : 'mysql'
      };
      const connection = await mysql.createConnection(config);
      const parentId = '1000';
      const [rows, _] = await connection.execute('\
    WITH recursive cte AS(\
      SELECT id, folder_name, parent_id FROM folder WHERE parent_id = ? \
    UNION \
      SELECT child.id, child.folder_name, child.parent_id FROM folder as child, cte WHERE cte.id = child.parent_id \
    ) \
    SELECT id, folder_name, parent_id FROM cte \
    ', [parentId]);
      const idArray = rows.map(e=>e.id);
      console.log({ response: idArray });
      connection.end();
    })();
    
[/code]

### 解説

MySQLサーバー側で頑張るケースです。階層的な再帰クエリを発行することで子孫をまとめて取ってくることができます。恐らく最も速いと思いますが、データベース負荷はそれなりに高いと思います。ここまで書いてすみませんが、詳しくないのでどの程度かは知りません。

## 余談(個人的見解)

クライアント側で全取得して処理するくらいならDBに入れない方がいいです。かといって、この回答のPromiseみたいなコードは現実的じゃありません。データに強い制限をかけるか、ストアードプログラムにするか、階層問い合わせか、そもそも用途によってテーブル構造を変えるか(そもそもDBに入れない)が現実的な選択肢だと思います。

とはいえ、仕様次第なので、それらを分かって何らかのメリットのために選択しているなら問題ありません。


CC BY-SA 4.0
2022-12-12T15:15:45.480
92729
2022-12-12T15:23:37.893
2022-12-12T15:23:37.893
54957
54957
92701
answer