MERNスタックの代わりにGERNスタックでクエリビルダを作る

このブログでは、FacebookのReactとGridDBのWeb APIを使ってCSVファイルを取り込み、そのデータを可視化するまでの手順について説明した前回のブログのフォローアップです。今回もReactを使いますが、Web APIの代わりにGridDBのNode.jsコネクタを使ってデータを取り込み、フロントエンドに提供します。この構成は、GridDB、Express、React、node.js から成るGERNスタックと考えることができます。これは、非常に人気の高い MERN stack と似た構成となっています。

MERNスタックとGERNスタックの主な違いは、デプロイされるデータベースです。MERNは、MongoDB、Express.js、React.js、node.jsの頭文字をとったものです。MongoDBをご存じない方のために説明すると、ドキュメントベースのデータベースで、NoSQLスキーマにより、先見性や事前計画なしに非常に流動的にデータを取り込むことができるため、迅速なプロトタイピングが可能になります。GERNスタックとMERNスタックは用途に応じて使い分けます。もし、より高いパフォーマンスが必要であったり、時系列データを保存する必要があったり、あるいはIoTセンサーからのデータであるなら、MongoDBではなくGridDBを選択する方が適切であると言えます。

プロジェクトの概要

これらの製品を紹介するために、ユーザがドロップダウンメニューで選択したデータを表示するシンプルなクエリビルダのアプリを作成します。GridDB node.js クライアントを npm 経由でインストールし、Kaggle からオープンソースデータを取り込み、GridDB に接続した node.js サーバ(バックエンド)で動作する React フロントエンドをセットアップするプロセスを説明します。また、静的なReactアセットを構築し、この種のプロジェクトをWebサイトにデプロイできるように設定します(2台のnode.jsサーバを使用しないようにするため)。

3つのドロップダウンを使って、取り込まれたデータセットに関連するいくつかのデータポイントを見つけることができます。次に、バックエンドにクエリ文字列を送信してDBに対して実行し、その結果をフロントエンドにプッシュバックする方法を紹介します。

ソースコードの全文はこちら

ここでは、その完成例を簡単に紹介します。

前提条件

このプロジェクトを実行するには、次の前提条件が必要となります。

  • GridDB
  • node.js
  • GridDB c-client
  • GridDB node.js client

.toc_container {
    border: 1px solid #aaa !important;
    display: table !important;
    font-size: 95%;
    margin-bottom: 1em;
    padding: 20px;
    width: auto;
}

.toc_title {
    font-weight: 700;
    text-align: center;
}

.toc_container li,
.toc_container ul,
.toc_container ul li,
.toc_container ol li {
    list-style: outside none none !important;
}

.single-post .post>.entry-content { font-size: 14px !important }

技術情報

アーキテクチャ概要

以上のように、ここで使われている技術は、GridDB、Express、React、node.jsの頭文字をとってGERNと呼べるでしょう。筆者個人の環境は、CentOS7でGridDBがベアメタルで動作しています。バックエンドは、GridDB node.jsコネクタ6とnode.jsサーバ、express.jsフレームワークを使用しました。フロントエンドは、React.jsをreact bundlerツールで実行し、独自のフロントエンド・サーバで構成します。バックエンドとフロントエンドの間では、APIエンドポイントを通じてデータを共有します。最終的な目標は、Reactフロントエンドを静的アセットにビルドアウトし、それをバックエンドで表示させることです。

フロントエンドサーバ部分は、npx integrated toolchainを使ってセットアップしました。バックエンドは、express.jsのコードを作成し、app.js ファイルに追加するだけで構築できました。

フロントエンドとバックエンドを連携させる

興味深い点は、フロントエンド部分にプロキシ用の行を追加していることです。フロントエンドとバックエンドはそれぞれ別のサーバを使用するため、実行に必要なパッケージもそれぞれ異なるので、バックエンドとフロントエンドの両方で npm install を実行する必要があります。これはまた、それぞれのサーバが独自の package.json を持つことを意味します。そこで、フロントエンドのファイルでは、フロントエンドのプロキシ URL を示す行を追加しています。"proxy": "http://localhost:5000" このアドレスとポートの組み合わせは、nodejs/griddb/express を使って構築したバックエンドに対応しています。

この行をここに追加する目的は、フロントエンドのフェッチ API エンドポイントが、アドレス全体を呼び出すのではなく、単にエンドポイントを呼び出すことができるということです(例えば、http://localhost:5000/query を呼び出すのではなく、/query を直接呼び出すなど)。これは、CORS の問題を完全に解決し、物事をシンプルに保つことができるため、開発環境で実行するのに最適な方法です。

もう一つの注目点は、1台のサーバで動作するようにワークフローを設定することです。このコンテンツを開発環境で実行する場合、おそらく2つのサーバ(バックエンドとフロントエンド)を同時に実行することになりますが、本番環境にデプロイする場合やデモを確認する場合は、もう1つのステップを実行します。

ルートディレクトリの package.json に、npm run build スクリプトを追加する必要があります。

"build": "cd frontend && npm install && npm run build"

これにより、Reactフロントエンドは、frontend/buildディレクトリ内の静的ファイルにビルドされます。

そして、バックエンドサーバにコードのスニペットを追加します(app.js)。

const path = require('path');
app.use(express.static(path.resolve(__dirname, 'frontend/build')));

app.get('*', (req, res) => {
    res.sendFile(path.resolve(__dirname, 'frontend/build', 'index.html'));
  });
  

これは、frontend/buildディレクトリにある、新しくビルドしたReactのコンテンツをFrontendに提供するようにサーバに指示します。

構築と実行

はじめに

フロントエンドとバックエンドのコードに入る前に、これをローカルマシンで実行する正確な手順について簡単に説明します。

まずは、プロジェクト全体のソースコードを git clone します。

$ git clone --branch query_builder https://github.com/griddbnet/Blogs.git

次に、GridDBをインストールし、サービスとして実行します。

$ wget --no-check-certificate https://github.com/griddb/griddb/releases/download/v5.0.0/griddb_5.0.0_amd64.deb
$ sudo dpkg -i griddb_5.0.0_amd64.deb
$ sudo systemctl start gridstore

こちら は、すべてのディストロと最新のリリースのためのGitHubのリリースページへのリンクです。

次のステップでは、Node.jsクライアントをインストールし、デモデータセットのインジェストをセットアップして実行します。

GridDB node.js Connectorをインストールする(npm経由)

node.js コネクタをインストールするためには、まず GridDB c-client をインストールする必要があります。そのためには、GitHub page から適切なパッケージファイルを取得します。

Ubuntuではこのようにインストールすることができます。

$ wget https://github.com/griddb/c_client/releases/download/v5.0.0/griddb-c-client_5.0.0_amd64.deb
$ sudo dpkg -i griddb-c-client_5.0.0_amd64.deb

GridDB c-clientをインストールした後は、npmを使用してnodejsパッケージを取得します。

$ npm i griddb-node-api

これで、すべてが実行されるはずです。これでインジェストを実行して cereal.csv ファイルをインジェストし、プロジェクト自体を実行することができます。

Node.jsでIngestingスクリプトを作成する

まずは、Kaggleから提供されたcsvファイルからデータを取り込むためのシンプルなnode.jsスクリプトを作成しましょう。Python GridDB Clientに慣れていれば、node.jsのイテレーションは非常に分かりやすいでしょう。

まず、csvパーサーと一緒にgriddbライブラリをインポートします。パーサーをインストールするには、npm を使用します。

npm install --save csv-parse

次に、griddb factory get storeでクレデンシャルを設定します。

const griddb = require('griddb-node-api');
var { parse } = require('csv-parse');

var fs = require('fs');
var factory = griddb.StoreFactory.getInstance();
var store = factory.getStore({
    "host": process.argv[2],
    "port": parseInt(process.argv[3]),
    "clusterName": process.argv[4],
    "username": process.argv[5],
    "password": process.argv[6]
});

インポートするデータのスキーマをjavascriptの変数として設定します。

var containerName = "Cereal"
const conInfo = new griddb.ContainerInfo({
    'name': containerName,
    'columnInfoList': [
        ["name", griddb.Type.STRING],
        ["mfr", griddb.Type.STRING],
        ["type", griddb.Type.STRING],
        ["calories", griddb.Type.INTEGER],
        ["protein", griddb.Type.INTEGER],
        ["fat", griddb.Type.INTEGER],
        ["sodium", griddb.Type.INTEGER],
        ["fiber", griddb.Type.FLOAT],
        ["carbo", griddb.Type.FLOAT],
        ["sugars", griddb.Type.INTEGER],
        ["potass", griddb.Type.INTEGER],
        ["vitamins", griddb.Type.INTEGER],
        ["shelf", griddb.Type.INTEGER],
        ["weight", griddb.Type.FLOAT],
        ["cups", griddb.Type.FLOAT],
        ["rating", griddb.Type.FLOAT]
    ],
    'type': griddb.ContainerType.COLLECTION,
    'rowKey': false
});

提案するスキーマを設定したら、ネイティブの node.js ファイルシステムリーダーを使って csv ファイルを読み込み、各行の内容をループして、データベースに挿入する適切な値を取得します。

var arr = []
fs.createReadStream(__dirname + '/cereal.csv')
    .pipe(parse({ columns: true }))
    .on('data', (row) => {
        arr.push(row)
    })
    .on('end', () => {
        store.dropContainer(containerName)
            .then(() => {
                return store.putContainer(conInfo)
            })
            .then(col => {
                arr.forEach(row => {
                    return col.put([
                        row['name'],
                        row['mfr'],
                        row['type'],
                        parseInt(row['calories']),
                        parseInt(row['protein']),
                        parseInt(row['fat']),
                        parseInt(row['sodium']),
                        parseFloat(row['fiber']),
                        parseFloat(row['carbo']),
                        parseInt(row['sugars']),
                        parseInt(row["potass"]),
                        parseInt(row["vitamins"]),
                        parseInt(row["shelf"]),
                        parseFloat(row["weight"]),
                        parseFloat(row["cups"]),
                        parseFloat(row["rating"])
                    ]);
                })
            })
            .then(() => {
                console.log("Success!");
                return true;
            })
            .catch(err => {
                console.log(err);
            });
    })

インジェストを実行する

cereal.csv ファイルを取り込むには、以下のコードを実行します。

$ node ingest.js 127.0.0.1:10001 myCluster admin admin

これを実行すると、GridDB サーバに Cereal というコンテナ名で全データが利用可能になります。

確認するには、GridDBのシェルにドロップして、以下のように確認します。

$ sudo su gsadm
  $ gs_sh
  gs[public]> showcontainer Cereal
Database    : public
Name        : Cereal
Type        : COLLECTION
Partition ID: 35
DataAffinity: -

Columns:
No  Name                  Type            CSTR  RowKey
------------------------------------------------------------------------------
 0  name                  STRING                
 1  mfr                   STRING                
 2  type                  STRING                
 3  calories              INTEGER               
 4  protein               INTEGER               
 5  fat                   INTEGER               
 6  sodium                INTEGER               
 7  fiber                 FLOAT                 
 8  carbo                 FLOAT                 
 9  sugars                INTEGER               
10  potass                INTEGER               
11  vitamins              INTEGER               
12  shelf                 INTEGER               
13  weight                FLOAT                 
14  cups                  FLOAT                 
15  rating                FLOAT

フロントエンドとバックエンドを実行する

このプロジェクトを実行するには、開発モードで実行するか、サーバを1台だけ実行するかの2つの選択肢があります。開発環境で実行するには、まずフロントエンドサーバを実行する必要があります。

$ cd frontend && npm install && npm run start

そして、別のターミナルでバックエンドを実行する必要があります。$ npm installを実行した後、実行します。

$ npm run start 127.0.0.1:10001 myCluster admin admin

注意事項: GridDB v5.0をサービスとして実行する場合、GridDBは(Multicastではなく)FIXED_LISTモードで実行されます。詳しくはこちらを参照してください。

GridDBをサービスとして動作させている場合は、上記のコマンドで動作します。GridDBをMulticastモードで動作させている場合は、IPアドレスとポートが異なる場合があります。ご自身の設定に合った認証情報を利用してください。

もちろん、実行コマンドと一緒に自分の認証情報を入力する必要があります。これらはすべてGridDBのデフォルト値です。

しかし、このプロジェクトを1つのターミナルだけで実行する、よりシンプルな方法を希望する場合は、Reactの静的資産を構築し、バックエンドサーバを実行するだけでよいでしょう。

$ npm run build # builds out the frontend into frontend/build
  $ npm install # installs backend packages
  $ npm run start 127.0.0.1:10001 myCluster admin admin #command line arguments for GridDB server creds

プロジェクトコードの設定

はじめに

まず、GridDBインスタンスに接続するために、上記のengestセクションで扱ったのと同じ方法で接続する必要があります。もちろん、アプリのフロントエンド部分をホストするために、Reactフロントエンドアプリをホストするフロントエンドサーバをセットアップする必要があります。また、node.js のコード (app.js) 内にいくつかのエンドポイントをセットアップする必要があります。エンドポイントを簡単に設定するために、よく使われる express ウェブフレームワークをインストールします。

GridDBのnode.jsコネクタでクエリを実行する

コンテナに問い合わせるとき、結果はプロミスの形になり、実行が終わると解決するか拒否されます。したがって、このコードを適切に実行するには、promise chaining を利用するか、またはプロミスを処理する新しい形式を使用することを選択する必要があります。JavaScript 非同期関数です。

コンテナへのクエリは以下のようになります。

containerName = 'Cereal';

const queryCont = async (queryStr) => {

    var data = []
    try {
        const col = await store.getContainer(containerName)
        const query = await col.query(queryStr)
        const rs = await query.fetch(query)
        while(rs.hasNext()) {
            data.push(rs.next())
        }
        return data
    } catch (error) {
        console.log("error: ", error)
    }
}

上記のコードを /all のエンドポイントで呼び出すと、select * のクエリが実行され、利用可能なすべてのデータが取得されます。つまり、/all からデータを取得するたびに、queryCont 関数を実行し、その結果を REQUEST を作成したエージェント(通常はブラウザと React フロントエンド)に返します。

app.get('/all', async (req, res) => {
    try {
        let queryStr = "select *"
        var results = await queryCont(queryStr)
        res.json({
            results
        });
    } catch (error) {
        console.log("try error: ", error)
    }
});

ここで注目すべき点がいくつかあります。1つ目は、エンドポイントを設定するために express フレームワークを使用しているため、もし /all のエンドポイントで HTTP GET Request を実行すると、このコードが実行されて json ファイルが応答されます。2つ目は、 queryCont 関数を呼び出すときは、await キーワードを使用して呼び出す必要があります。

Reactでは、ページロード時にこのクエリを実行し、HTTP GETリクエストですべてのデータを取得します。これは単にデモのために、ドロップダウンメニューにデータを入力する簡単な方法として実行されています。本番コードでは、これを実行することはありません。

GridDBでReactを使う

これでバックエンドの基本ができたので、フロントエンドをセットアップします。新しいReactアプリを構築する場合、通常はFacebook/Metaによって維持されている統合バンドルツールを使用するのが最も簡単です。既製のプロジェクトにショートカットするには、単純に実行するだけです。

$ npx create-react-app frontend

先に述べたように、フロントエンドの package.json ファイルに、どのプロキシを使用するかについての行を追加するのが賢明です。これにより、アドレスを直接指し示すエンドポイントを使用することができ、IPアドレスやポートなどを示す必要がなくなります。

コードを編集するには、 src/App.js ファイルを編集します。このプロジェクトでは、単純にフロントエンドのコードをすべてこのファイルに貼り付けました。

App.jsファイルは基本的に1つのJavaScript関数で、(ファイルの一番下にある)エクスポートされます。この関数は、アプリをビルドするJSX` を返します。この場合、フロントエンドアプリに表示されるすべての要素を生成します。

これで、実際のReactのコードを書くことができるようになりました。

Reactでクエリビルダを作成する

このプロジェクトの全コードはGithubで公開されていますので、この部分がどのように作られたかを詳しく見ることができます 。このブログでは掘り下げた説明は省略します。しかし、その基本的な考え方は、ユーザーが様々なオプションを選択するために、3つの別々のドロップダウンメニューがあるということです。ユーザーは様々な栄養素から選択し、より大きい、より小さい、または等しいを選択し、最後にクエリを実行するために特定の穀物を選択することができます。

例えば、どのシリアルがFrosted Mini-Wheats よりも食物繊維が多いかを調べたい場合、Fiber、Greater Than、そして最後にシリアル名を選択します。このクエリはnodejsサーバに送られ、クエリを実行した後、エンドポイント経由でReactのコードに戻されます。

node.js サーバへのデータ送信は、HTTP POST リクエストで行われます。このリクエストでは、GridDB の SQL クエリを構築するために使用するデータのペイロード(この場合 JSON)を送り返すことができます。

ユーザーがパラメータを設定したら、送信ボタンをクリックし、HTTPリクエストが発行されます。

 const handleSubmit = async (event) => {
            fetch('/query', {
                method: 'POST',
                headers: {
                    'Content-Type': 'application/json'
                },
                body: JSON.stringify({'list': list, 'comp': comp, 'name': nameDropDown})
            }).then(function(response) {
                console.log(response)
                return response.json();
            });

本体には、ユーザーが選択したドロップダウンリストのパラメータが含まれています。バックエンド、node.js側では、これらの値を取得し、クエリを形成します。

var userResult = {"null": "null"}
var userVal;

app.post('/query', jsonParser, async (req, res) => {
    const {list, comp, name} = req.body 

    try {
        var results = await querySpecific(name)
        let type = checkType(list) //grabs array position of proper value
        let compVal = checkComp(comp)
        let val = results[0][type] // the specific value being queried against
  
        userVal = val
        let specificRes = await queryVal(list, compVal, val)
        userResult = specificRes
        res.status(200).json(userResult);
    } catch (error) {
        console.log("try error: ", error)
    }
});

つまり、ユーザーがパラメータを設定すると、このエンドポイントにREQUESTが戻ってくるというわけです。まず、適切な値を取得します。まず、 listcompname という適切な値を取得します。次に、選択されたシリアル名に基づいてクエリを生成する querySpecific 関数を使用します。

const querySpecific = async (cerealName) => {

    var data = []
    let q = `SELECT * WHERE name='${cerealName}'`
    try {
        const col = await store.getContainer(containerName)
        const query = await col.query(q)
        const rs = await query.fetch(query)
        while(rs.hasNext()) {
            data.push(rs.next())
        }
        return data
    } catch (error) {
        console.log("error: ", error)
    }
}

したがって、もしユーザーが Frosted Mini Wheats よりも食物繊維の多いシリアルをすべて知りたければ、 まず名前が Frosted Mini Wheats となっている行全体を取得しなければなりません。そこから、どの値が食物繊維なのかを表す配列の位置を取得し、2 番目のクエリを実行して、取得した値よりも食物繊維が多いシリアルをすべて見つけることができます。

checkTypecheckComp 関数は、ユーザーが選択したパラメータを、栄養素の種類 (カロリー、食物繊維など) と、等しい、またはより小さいなどの適切な符号の両方について、適切な配列位置に変換するだけのものです。

ユーザーの完全なパラメータと、比較する項目の値を取得したので、 queryVal を使用して完全なクエリを実行することができます。この関数は前の2つの関数と非常によく似ていますが、その代わりに、より複雑なクエリを構築するために、より多くのパラメータを取ります。

const queryVal = async (list, comp, val) => {

    var data = []
    let q = `SELECT * WHERE ${list} ${comp} ${val} `

    try {
        const col = await store.getContainer(containerName)
        const query = await col.query(q)
        const rs = await query.fetch(query)
        while(rs.hasNext()) {
            data.push(rs.next())
        }
        return data
    } catch (error) {
        console.log("error: ", error)
    }
}

本体とクエリをコンソールに出力すると、次のようになります。

query val string:  SELECT * WHERE fiber > 3 
req body:  { list: 'fiber', comp: 'Greater Than', name: 'Frosted Mini-Wheats' }

そして、データが取得できたので、もう一回HTTP GET Requestを実行して、クエリの行を取得し、それらを初歩的なHTMLテーブルに追加することができます。

app.get("/data", (req, res) => {
    res.json({
        userVal,
        userResult
    })
});

そして、フロントエンドに以下を行います。

let response = await fetch(`/data`)
            let result = await response.text()
            let resp = await JSON.parse(result)
            // the Specific react state will set off other functions to form our table rows and columns to be inserted into our table with all the relevant information
            setSpecific(resp)

この状態で、テーブルの行を適切に構築して、HTMLテーブルに挿入し、ユーザーに表示させることができます。

その簡単なデモをもう一度ご紹介します。

まとめ

今回のブログでは、最新のGridDB node.jsコネクタ(とc_client)のインストール方法、node.jsコネクタを使ったCSVファイルの取り込み方法、GridDBデータを提供するシンプルなexpressサーバの構築方法、ReactとGridDBやそのバンドルツールの使い方、最後にGridDBサーバでReactアプリに送信できるクエリの実行方法について説明しました。

ブログの内容について疑問や質問がある場合は Q&A サイトである Stack Overflow に質問を投稿しましょう。 GridDB 開発者やエンジニアから速やかな回答が得られるようにするためにも "griddb" タグをつけることをお忘れなく。 https://stackoverflow.com/questions/ask?tags=griddb

Leave a Reply

Your email address will not be published. Required fields are marked *