ここから本文です

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

Impress Watch 4/18(火) 6:00配信

 筆者のブログは、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でどうなるのか見れば容易に見当が付くと思う。

SQLの構文例SELECT*FROMpostsWHEREid=1273;
SELECTid,dateFROMpostsWHEREid=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のインストールと確認は、

$sudoapt-getinstallmongodb

$mongo-version
MongoDBshellversion:2.6.10

$mongod-version
dbversionv2.6.10
2017-04-11T12:00:53.045+0900gitversion:nogitversion
2017-04-11T12:00:53.046+0900OpenSSLversion:OpenSSL1.0.2g1Mar2016

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

$sudo/etc/init.d/mongodbstart
$sudo/etc/init.d/mongodbstop

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

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

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

$curlhttp://blog.iwh12.jp/test/tags.json-otags.json
$mongoimport--dbtest--collectiontags--jsonArray--typejson--filetags.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
>showdatabases
admin(empty)
local0.078GB
test0.078GB
※存在するdb名の一覧表示

>usetest
switchedtodbtest
※db:testを選択

>showcollections
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":"PCWatch"}
{"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":"PCWatch"}
※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":"PCWatch"}
{"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-Lgit.io/nodebrew|perl-setup

 もしくは、

$wgetgit.io/nodebrew
$perlnodebrewsetup

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

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

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

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

$nodebrewinstall-binaryv7.8.0
$nodebrewusev7.8.0
usev7.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を入力)。

$npminit
Thisutilitywillwalkyouthroughcreatingapackage.jsonfile.
Itonlycoversthemostcommonitems,andtriestoguesssensibledefaults.

See`npmhelpjson`fordefinitivedocumentationonthesefields
andexactlywhattheydo.

Use`npminstall--save`afterwardstoinstallapackageand
saveitasadependencyinthepackage.jsonfile.

Press^Catanytimetoquit.
name:(pcwatch)
version:(1.0.0)
description:
entrypoint:(index.js)
testcommand:
gitrepository:
keywords:
author:
license:(ISC)
Abouttowriteto/mnt/c/Users/knish/Documents/work/pcwatch/package.json:

{
"name":"pcwatch",
"version":"1.0.0",
"description":"",
"main":"index.js",
"scripts":{
"test":"echo\"Error:notestspecified\"&&exit1"
},
"author":"",
"license":"ISC"
}

Isthisok?(yes)y

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

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

 まず手始めに、index.jsへ

console.log('test');

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

$nodeindex.js
test

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

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

$npminstallmongodb--save

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

varmongoClient=require('mongodb').MongoClient;
varmongodb='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プロトコルを扱うモジュールを先にインストールする。

$npminstallhttp--save

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

varmongoClient=require('mongodb').MongoClient;
varmongodb='mongodb://localhost:27017/test';

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

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

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

json.on('end',()=>{
vard=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に感謝!

PC Watch,西川 和久

最終更新:4/18(火) 6:00

Impress Watch