西川和久の不定期コラム

Windows 10 Creators Updateで「Bash」がバージョンアップ【後編】

~NoSQLのMongoDBで遊んでみる。インストールからNode.jsでプログラミングまで

Visual Studio CodeでコーディングしつつBashで動作確認

 筆者のブログは、WordPress REST APIからみでいろいろプログラミングネタを載せているが、さすがにここでそのまま扱うには場違いのような気がするので、まずNoSQLのMongoDBで遊んでみたい。

 というのも、クライアントサイド(WebブラウザとJavaScript)だけで処理できるものであれば、わざわざ「Bash on Ubuntu on Windows」を使う必要がないからだ。

 またMySQLなどSQLなら少し知っているが、NoSQLに関しては話は聞いているものの、使ったことがない方も多いのではないだろうか(実際、筆者もそうだった)。そこで「Bash on Ubuntu on Windows」をきっかけとして、NoSQLのMongoDBを触る機会になればと思った次第だ。

NoSQLのMongoDBをインストール

 SQLはご存知のように関係データベース(RDB)へ問い合わせなどを行なう言語だ。RDBは複数のテーブルを持ち、それらを結合して利用することができる。加えて、スキーマと呼ばれる、データ定義や関連性を厳密に定義する。

 (物凄く)大雑把なイメージとしては、Excelに複数の表を持ち、各々がLOOKUPしつつ、各カラムは、整数とかテキストとか日付とかが定義されている感じで、それをSQLで問い合わせる……という形になるだろうか。

 対してNoSQLは、いくつかのタイプがあるのだが、基本的にRDBのスキーマに相当する部分がなく、XMLやJSONなどのデータ構造をそのまま扱える(ドキュメント型の場合)のが特徴だ。

 MongoDBのデータ構造はキーと値のペアを要素としたシンプルなバイナリデータ配列で、検索速度を稼ぐためのインデックスも構築できる。

 SQL(RDB)とMongoDB(NoSQL)の関係をざっくり書くと、

SQLとMongoDBのデータ構造の呼称の違い
SQLdbtablerowcolumn
MongoDBdbcollectiondocumentfield

 こんな感じだろうか。SQLの分かる人であれば、MongoDBでどうなるのか見れば容易に見当が付くと思う。

SQLの構文例

SELECT * FROM posts WHERE id=1273;
SELECT id,date FROM posts WHERE id=1273;

MongoDBの構文例

db.posts.find({id:1273})
db.posts.find({id:1273},{id:1,date:1})

 上はどちらもpostsというtable or collectionからid=1273を探し、column or field全部、もしくはidとdateを表示する……となる。何となく相違点がお分かりいただけるだろうか。

 MongoDBのインストールと確認は、

$ sudo apt-get install mongodb

$ mongo -version
MongoDB shell version: 2.6.10

$ mongod -version
db version v2.6.10
2017-04-11T12:00:53.045+0900 git version: nogitversion
2017-04-11T12:00:53.046+0900 OpenSSL version: OpenSSL 1.0.2g  1 Mar 2016

となる。ただ最新は3.4系なのでBash on Ubuntu on Windowsでインストールされるものは若干古いが、試すだけなら問題ないだろう。実際筆者が実験しているサイトも、ほぼ同じバージョンだ。起動と終了は、

$ sudo /etc/init.d/mongodb start
$ sudo /etc/init.d/mongodb stop

 これでOK。ただし、「Bash on Ubuntu on Windows」は、デーモンの自動起動などができず、また「Bash on Ubuntu on Windows」を終了すると、すべてのプロセスが落ちるところが、本物のUbuntuとの違いとなる(つまりBash起動時にMongoDBも毎回起動しなければならない)。

 さて、実際MongoDBを操作するのに何かデータがないと面白くない。ちょうど筆者が別件で、WordPress REST APIで得たtags(BLOGのタグ情報でJSONフォーマット)の情報をサイトに置いてあるので、今回はそれを例として使うことにする。

 Bashのローカルにそのファイルを保存して、MongoDBへインポートする方法は以下の通り。

$ curl http://blog.iwh12.jp/test/tags.json -o tags.json
$ mongoimport --db test --collection tags --jsonArray --type json --file tags.json
※db: test、collection: tagsへtags.jsonからインポートする

 curlコマンドもmongoimportコマンドもすでにインストールされているので、このまま操作できる。tags.jsonの中身は、more tags.json として確認すれば分かるが、[]{}:,を使ったデータの羅列で、「:」の左側がKey、右側がValueとなっており、「,」で区切られ並んでいるフォーマット=JSONとなる。

 MongoDBにデータが入ったので、mongoDBのシェルを使って、実際にデータベースを操作してみよう。

 まず、MongoDB内にあるデータベース名の確認。testが存在している。次にこれから操作するデータベースをuseで指定。以降、db: testでの操作となる。collectionは、system.indexesとtagsの2つ。タグのデータが入っているのは、tagsとなる。

$ mongo
> show databases
admin  (empty)
local  0.078GB
test   0.078GB
※存在するdb名の一覧表示

> use test
switched to db test
※db: testを選択

> show collections
system.indexes
tags
※db: testにcollectionが2つある。tagsが今回使うcollection

 以下、検索などありがちなパターンを掲載したのでご覧いただきたい("> "はプロンプトなので入力する必要はない)。

> db.tags.find({}).count()
19
※collection: tagsの件数

> db.tags.find({},{id:1,name:1,_id:0})
{ "id" : 21, "name" : "2in1" }
{ "id" : 30, "name" : "Android" }
{ "id" : 14, "name" : "Audio" }
{ "id" : 13, "name" : "BABYMETAL" }
{ "id" : 12, "name" : "Blog" }
{ "id" : 20, "name" : "Desktop" }
{ "id" : 15, "name" : "Event" }
{ "id" : 27, "name" : "Hardware" }
{ "id" : 26, "name" : "Information" }
{ "id" : 28, "name" : "Music" }
{ "id" : 25, "name" : "Note" }
{ "id" : 9, "name" : "Nutube" }
{ "id" : 22, "name" : "OS" }
{ "id" : 8, "name" : "PC Watch" }
{ "id" : 18, "name" : "Photo" }
{ "id" : 23, "name" : "Program" }
{ "id" : 19, "name" : "Smartphone" }
{ "id" : 24, "name" : "Software" }
{ "id" : 16, "name" : "Windows" }
※collection: tags、全件のidとnameを表示。_idはMongoDBが自動的に追加するもので本体のデータとは無関係。非表示=_id:0とする(:1が表示)

> db.tags.find({},{id:1,name:1,_id:0}).sort({id:-1})
{ "id" : 30, "name" : "Android" }
{ "id" : 28, "name" : "Music" }
{ "id" : 27, "name" : "Hardware" }
{ "id" : 26, "name" : "Information" }
{ "id" : 25, "name" : "Note" }
{ "id" : 24, "name" : "Software" }
{ "id" : 23, "name" : "Program" }
{ "id" : 22, "name" : "OS" }
{ "id" : 21, "name" : "2in1" }
{ "id" : 20, "name" : "Desktop" }
{ "id" : 19, "name" : "Smartphone" }
{ "id" : 18, "name" : "Photo" }
{ "id" : 16, "name" : "Windows" }
{ "id" : 15, "name" : "Event" }
{ "id" : 14, "name" : "Audio" }
{ "id" : 13, "name" : "BABYMETAL" }
{ "id" : 12, "name" : "Blog" }
{ "id" : 9, "name" : "Nutube" }
{ "id" : 8, "name" : "PC Watch" }
※collection: tags、全件のidとnameをidで降順ソートして表示(昇順ソートは1)

> db.tags.find({id:12},{id:1,name:1,_id:0})
{ "id" : 12, "name" : "Blog" }
※collection: tags、id=12のidとnameを表示

> db.tags.find({count:{$gt:10}},{id:1,name:1,count:1,_id:0})
{ "id" : 8, "count" : 13, "name" : "PC Watch" }
{ "id" : 23, "count" : 13, "name" : "Program" }
{ "id" : 16, "count" : 11, "name" : "Windows" }
※collection: tags、count>10 のidとname、countを表示(このcountは参照している記事の数)

> db.dropDatabase()
{ "dropped" : "test", "ok" : 1 }
※db: testを削除。この記事の後半で使うので、この段階で実行しなくても大丈夫だ。

 ちなみに、mongoDBのシェルから抜け出すのはexitコマンドだ。

 いかがだろうか。ハッシュそのままなので、わりと分かりやすい操作系のように思う。例に挙げた比較だけでなく、正規表現を使ったり演算なども可能だ。MongoDBでWeb検索すれば、いろいろ情報が見つかるので、興味のある人は調べて試して欲しい。

Node.jsでMongoDBをコントロールする

 さて、ここまではプログラミングで扱うデータの下準備。ここからが本番だ。

 Node.jsは、聞き覚えのある方も多いと思うが、ザックリ説明すると、サーバーサイドでのJavaScript実行環境となる。

 「え?Webブラウザでのクライアント環境でなく!?」と筆者も当初は思っていた。すでにPHPやPerl、Ruby、Pythonなどがあるので、今更JavaScriptをサーバーで動かす意味が分からない……と思っていた。

 しかし実際使ってみると、すでに言語仕様は分かっているので、取り立てて勉強する必要もなく、デバッグもしやすく意外と使い勝手が良いことがわかった。まだ仕事としての実践経験はないものの、暇(で天気が悪い日)な時は適当にプログラミングして遊んでいる。

 まず環境の構築から。MongoDBと同じように、apt-getでもパッケージをインストール可能だが、さすがにバージョンが古いので、今回はnodebrewと呼ばれている専用のマネージャを使いってみよう。インストールは簡単だ。

$ curl -L git.io/nodebrew | perl - setup

 もしくは、

$ wget git.io/nodebrew
$ perl nodebrew setup

 でインストールできる。仕上げはエディタなどで.bashrcの最後に

export PATH=$HOME/.nodebrew/current/bin:$PATH

 を追加(vi ~/.bashrcでファイルを編集できる)。一度Bashを終了して、再度Bashを起動するとパスが通るので(ほかにも方法はあるが)、

$ nodebrew ls-remote
※インストール可能なNode.jsのバージョン一覧が表示される。ここではv7.8.0を使用

$ nodebrew install-binary v7.8.0
$ nodebrew use v7.8.0
use v7.8.0
$ node -v
v7.8.0

これで準備は完了。今回は関係ないが、Node.jsでバージョン依存するアプリを使う場合は、install-binaryで該当するバージョンをインストールして、useでそのバージョンを指定すると、バージョンをスイッチングできる。

 これから実際プログラミングを始めるが、まず作業用のフォルダを作る。これはWindowsからもUbuntuからも扱えた方がいいので、/mnt/c以下のどこかにする。通常だと、/mnt/c/Users/[ユーザー名]/Documents下になるだろうか。

 たとえば今回、Documents/pcwatchを作業フォルダにした場合、pcwatchフォルダを作ってカレントディレクトリを移動後、初期設定として、npmコマンドを実行する。いろいろ聞かれるが、取りあえず全て[Enter]で問題ない(最後はyを入力)。

$ npm init
This utility will walk you through creating a package.json file.
It only covers the most common items, and tries to guess sensible defaults.

See `npm help json` for definitive documentation on these fields
and exactly what they do.

Use `npm install  --save` afterwards to install a package and
save it as a dependency in the package.json file.

Press ^C at any time to quit.
name: (pcwatch)
version: (1.0.0)
description:
entry point: (index.js)
test command:
git repository:
keywords:
author:
license: (ISC)
About to write to /mnt/c/Users/knish/Documents/work/pcwatch/package.json:

{
  "name": "pcwatch",
  "version": "1.0.0",
  "description": "",
  "main": "index.js",
  "scripts": {
    "test": "echo \"Error: no test specified\" && exit 1"
  },
  "author": "",
  "license": "ISC"
}

Is this ok? (yes) y

 ソースコードのファイル名はindex.jsとなり、これからここへいろいろプログラミングしていく。もちろん、この段階では作られないので、Windowsのエクスプローラーなのでファイルを新規作成して始めよう。

 Cドライブの下なので、Windowsからも直接アクセスでき、秀丸やVisual Studio Codeなど好きなエディタで編集可能だ。

Visual Studio Codeで、ファイル>基本設定>設定へ { "terminal.integrated.shell.windows": "C:\Windows\sysnative\bash.exe","termnial.integrated.shell.unixlike": "bash.exe" } と書くと、総合ターミナルでダイレクトにBashを起動できる

 まず手始めに、index.jsへ

console.log('test');

 これだけ書いて保存。Bashで

$ node index.js
test

を実行し、testが表示されればNode.jsが正しく作動している証拠だ。

 次に、MongoDBをNode.jsから扱うモジュールをインストールする。HTMLでJavaScriptのライブラリをいろいろ「script type="text/javascript" src="xxx"」とするのと同じ感じだ。

$ npm install mongodb --save

 これでNode.jsでMongoDBが扱えるようになる。index.jsの内容は以下の通り。

var mongoClient = require('mongodb').MongoClient;
var mongodb = 'mongodb://localhost:27017/test';

mongoClient.connect(mongodb, function(err, db) {
    db.collection('tags').find({}).toArray(function (err, tags) {
        console.log(tags);
        db.close();
        process.exit(0);
    });
});
// error処理などは含まれていない

 1行目は、先にnpmしたモジュールを使う宣言。2行目は、MongoDBのDefaultはlocalhostでPort 27017、db: testを指定、という意味となる(もちろんvarを使わず、ダイレクトに該当箇所へ入れれるが便宜上)。

 node index.jsで実行すると、find({})なのでcollection: tagsの内容が全件表示されているはずだ。また、検索条件をcount > 10で、idで降順ソート、id/name/count表示は、このように先のmongoDB shellからの検索と同じとなる。

    db.collection('tags').find({count:{$gt:10}},{id:1,name:1,count:1,_id:0}).sort({id:-1}).toArray(function (err, tags) {

 いかがだろうか。割と簡単にNode.jsでMongoDBが扱えるのがお分かりいただけただろうか。

 加えてJavaScriptなので、(特にLAMP経験者なら)これといって言語仕様を勉強する必要もなく、サラッとプログラミングできてしまうのが魅力的だ。

 次にデータがないと面白くないので、はじめにツールを使いJSONをインポートしたが、もちろんNodo.jsだけでMongoDBへデータを読み込める。先では一旦ファイルをダウンロードしたが、今度は直接ネットからDBへセットしてみたい。

 まずhttpプロトコルを扱うモジュールを先にインストールする。

$ npm install http --save

 そして上記のコードはいったん破棄して、index.jsへ以下のコードを書き込む。

var mongoClient = require('mongodb').MongoClient;
var mongodb = 'mongodb://localhost:27017/test';

mongoClient.connect(mongodb, function(err, db) {
    var http = require('http');

    http.get('http://blog.iwh12.jp/test/tags.json', (json) => {
        var body = '';
        json.setEncoding('utf8');

        json.on('data', (chunk) => {
            body += chunk;
        });

        json.on('end', () => {
            var d = JSON.parse(body);
            db.collection('tags').insertMany(d).then(function(err, r) {
                db.close();
                process.exit(0);
            });
        });
    });
});
// error処理などは含まれていない。実行前にdb.dropDatabase()でdb: testの削除を忘れずに(追記となるため)

 ここでのポイントは、httpを使ってajaxでtags.jsonを読み込んでいるところと、実際MongoDBへ書き込むinsertMany()となる。

 ajaxを使ってファイルを読み込むのは、クライアントでもよくあるパターンなので特に説明の必要はないだろう。非同期で通信して、'end'の部分で読み込んだデータが扱えるようになる。

 そして配列dへ値を入れ、いきなり全部insertMany()でMongoDBへ入れている。中身が整数とかテキストとかスキーマ定義は一切なしなのがNoSQLの所以。またinsert()というメソッドもあるのだが、この場合は、Document(row)単位になるので、件数分ループする必要があり、一発で全てInsertできるinsertMany()の方がシンプルとなる。

 確認はmongoDB shellを使って件数や検索結果を見て、ツールでインポートした時と同じになっていればOK。

 これで読み込みの部分と検索の部分を1本のコードにまとめれば動く……と思ってしまうが、実は単に繋げるだけではうまくいかない。というのも、MongoDBの作動が非同期だからだ。

 同期の場合は、基本的にコードを書いた順番で作動するので分かりやすいが、非同期の場合は、「httpでtags.jsonの読み込みよろしく!」と丸投げして、その結果を待たず先に進んでしまう=検索のコードが動いてしまい、(データ量にもよるので今回に限っては動くかも知れないが)当然DBの中身は空っぽ(もしくは半端な状態)なので、うまく検索できなくなる。

 これを解決するには、読み込みのコールバック関数の中、process.exit(0);の部分へ、検索のコードを入れるか、同期/非同期のコントロールができるasyncモジュールを使う必要がある(今回のケースでは1つの処理だけなので前者で十分)。

 とはいえ、これ以上は本サイトから逸脱しそうなので、ここまでとしたい。以降、もし興味があれば、"Node.js MongoDB"などでWeb検索すれば無数の情報がある。筆者もそれで勉強した。

 Anniversary Updateでは、LAMP環境を作ってphpMyAdminやWordPressを動かし、今回は、MongoDBとNode.jsを動かした。

 筆者のブログでは、これらとNode.jsモジュールのExpress(主にルーティング)+EJS(テンプレートエンジン)を使い、複数のWordPressサイトを1つにまとめて表示する実験サイトも公開しているが、全てBashで開発した。

 Windows 10標準の機能「Bash on Ubuntu on Windows」でこれだけのことができるのだから、そろそろmacOSでの同環境に頼る必要もないかも知れない(加えてmacOSではシステムにいろいろ混ざるが、Bashはいざとなったら簡単に環境全てリセットできる)。実に楽しい時代になったものだ。Microsoftに感謝!