MongoDB Atlas の無料枠で MongoDB デビューしてみた
MongoDB をようやく触ってみました。
目次
MongoDB および NoSQL の概要
MongoDB という有名な NoSQL データベースがある。NoSQL は固定のスキーマ (カラム定義) を持たないデータベースで、MongoDB はその中でもドキュメント指向データベースと呼ばれている。ドキュメント指向とは、要はデータを JSON 形式とか XML 形式で持てるデータベースのことを指している。
「MEAN スタック」といった言葉は2013年に提唱された。MongoDB・Express.js・AngularJS・Node.js という技術スタックを使えば、クライアントサイドからサーバサイドまで全て JavaScript・JSON で記述できて、Web アプリが作りやすいよね、というワケだ。
NoSQL に対する個人的な雑感
しかし、自分はコレまで、NoSQL というモノをずっと避けてきた。
自分は安全志向が強い性格で、近年主流となりつつある「非同期処理」が苦手だ。だから、NoSQL 全般で採用されている「結果整合性モデル」が嫌いなのである。簡単にいうと、「データ更新した直後にそのデータを取得しようとすると、更新反映が間に合っておらず更新前のデータが取得できてしまうことがある」というのが嫌なのだ。
この現象は割と簡単に引き起こせて、Deta.sh Base や Cloudflare Workers KV などの NoSQL サービスを試している時に頻繁に遭遇した。
- 過去記事 : マイクロサービス・FaaS・NoSQL・Object Storage が作り放題な Deta.sh を試してみた
- 過去記事 : Cloudflare Workers による FaaS・Cloudflare Workers KV による Key-Value Store を試してみた
天下の AWS が提供する Amazon DynamoDB でも遭遇したことがあるが、DynamoDB はその対策として、強整合性のある読み込みオプションが提供されていたりする。その代わり性能と利用料が犠牲となるのだが。
それから、自分は Java 言語からプログラミングに入門したこともあり、型情報のない JSON 形式を多用するのは避けたいのだ。
個人製作アプリのレベルであれば、自分一人でモデルを設計して実装するので、TypeScript を使わずに実装しても「ココはこういうデータをやり取りするから~」と想定で実装しても何とかなったりする。しかし、業務でチーム開発するコードとなると型情報が大事になってくるので、TypeORM のような TypeScript で型情報を扱える O/R マッパーを使いたいし、生の JSON (連想配列) をそのまま多用はしたくないのである。
NoSQL だと行ごとに異なるカラムを持たせたりもできちゃうんでしょ?RDBMS はカラム定義の柔軟性には欠けるかもしれないけど、データにそんな柔軟性持たせちゃってアプリケーションが実装できるのけ?っていうのがずっと引っかかっている。
非同期処理・結果整合性モデルで、スキーマ・型定義がなくていい仕組みって、自分は信用ならなくて、ずっと使って来なかった。今でもこの生理的な毛嫌いは残っている。
- 実際のところ、NoSQL は RDBMS を完全代替する存在ではないことは理解している。今まで RDBMS しかなかったから仕方なく RDB を使っていたところ、NoSQL を使えば色々楽になる分野が一部存在する、という捉え方が正しい
- IoT などからとにかくデータを蓄積するデータストアなら NoSQL で良かったりする。基本は書き込みだけで、リアルタイムな読み取りの整合性が要らない部分で、NoSQL を使えば良いし、厳密なスキーマ定義が要らないのも分かる
- Web アプリケーションの分野でマイクロサービス化が進んだことで、非同期で分散トランザクション処理するのが前提になってくると、NoSQL を活用できる場面もあるのかもしれないが、個人的にはココがあまり納得できてなくて、「何でデータの強整合性を犠牲に出来るの?」というのが未だ腑に落ちない部分がある。更新されたデータは更新されたモノがちゃんと見えてほしくないか?
- 過去記事 : 「サーバレスがもてはやされてるけど RDS 使いたい時もあるじゃん?」について調べた
- NoSQL はスキーマレスな構造のためか、検索処理が苦手 (遅い) といわれているので、NoSQL がデータの一時受けになり、RDBMS に同じデータを入れ直して検索用に活用するとかいう併用パターンも多い
- その理屈は分からんでもないが、NoSQL を併用するメリットよりデメリット (考慮事項) が多い気がしてしまい、「NoSQL を取り入れるための諸々のコストを捨てて、RDBMS のスペック増強に回したら NoSQL 要らずで解決するんじゃね?」とか考えてしまう。誰か僕を説得してくれ…w
…個人的に腑に落ちない部分があるとはいえ、日頃「Node.js チョットデキル」「SPA フレームワークの中では Angular が好きだなー」とか何とか抜かしておきながら、(その他の NoSQL ツールは使ったことあるとはいえ) MEAN スタックの「M」を全く使ったことがないというのはどうかね? と思い、今回ドッグフーディングてみた次第である。
MongoDB Atlas に登録してみる
MEAN スタックを提唱したのは MongoDB の開発者らしい。調べてみると、MongoDB 公式が「MongoDB Atlas」というマネージド・ホスティングサービスを提供しており、ココに無料枠が存在した。今回はこの無料枠で遊んでみることにする。
上述の公式サイトより「無料で始める」ボタンを押下してアカウントを作成していく。アカウント登録の際にクレジットカード情報を入力する必要はないので安心。
無料枠を使う場合、クラスタ構成は「M0 Sandbox」というスペックを選択することになる。容量は 512MB で、同時接続数や性能が低めだが、クラウドプロバイダとリージョンを選択できるのが面白いところだ。AWS と Azure は日本リージョンがないが、Google Cloud なら東京リージョンが選択できるので、GCP の東京リージョンを選ぶことで地理的な優位性を利用してスペック不足を誤魔化せるか期待しよう。w
アカウントを登録しクラスタを作成したら、管理画面の「Network Access」より「Add IP Address」ボタンを押下する。ココで MongoDB にアクセスできる IP アドレスを制限できるのだが、今回は簡単にするため、0.0.0.0/0
、つまりパブリックアクセスを許可するようにしておく。
データベースにアクセスするには、他の RDBMS などと同様に、DB ユーザの情報が必要になる。デフォルトで root
ユーザが作成されており、デフォルトでパスワードが生成されている。パスワードやユーザ情報を変更したい場合は、管理画面の「Database Access」から設定できる。
管理画面の「Databases」から、作成したクラスタのところにある「Connect」ボタンを押すと、MongoDB に接続するための方法が確認できる。自分は今回は Node.js スクリプトを書いて接続してみようと思うので、「Connect your application」を選択し、以下のようなサンプルコードを表示してもらう。
const { MongoClient } = require('mongodb');
const uri = "mongodb+srv://root:<password>@【URL】.mongodb.net/myFirstDatabase?retryWrites=true&w=majority";
const client = new MongoClient(uri, { useNewUrlParser: true, useUnifiedTopology: true });
client.connect(err => {
const collection = client.db("test").collection("devices");
// perform actions on the collection object
client.close();
});
Replace
<password>
with the password for theroot
user. ReplacemyFirstDatabase
with the name of the database that connections will use by default. Ensure any option params are URL encoded.
説明書きもちゃんとあるので分かりやすい。コレを参考にスクリプトを組んでみよう。
Node.js スクリプトを実装する
今回実装したサンプルコードの全量は以下の GitHub リポジトリで公開している。
Windows10 の WSL2 Ubuntu 上に Node.js v14.16.1 をインストールし、次のようにセットアップしていった。
今回は公式のサンプルコードと同じ、公式の mongodb パッケージを使用したが、mongoose という O/R マッパーも有名である。NestJS だと Mongoose 連携プラグインが豊富なので、サーバサイドフレームワークに合わせて検討すると良いだろう。
MongoDB への接続 URL を環境変数で注入するため、dotenv
パッケージをインストールしているが、ココらへんはお好みで。
$ npm init -y
$ npm install --save mongodb dotenv
# ファイルを作成する
$ touch index.js .env
mongodb
パッケージは古くからあるパッケージなので、世の文献を漁るとコールバック関数形式で書かれたサンプルコードが多く見られたが、現在はちゃんと Promise 形式に対応しているようだったので、async
・await
でコーディングできた。
index.js
const { MongoClient } = require('mongodb');
require('dotenv').config();
(async () => {
console.log('Start');
const mongoClient = new MongoClient(process.env.MONGODB_URL, { useNewUrlParser: true, useUnifiedTopology: true });
const databaseName = 'my-database';
const collectionName = 'my-collection';
try {
// Connect
await mongoClient.connect();
// List All Databases
const adminDb = mongoClient.db().admin();
const databases = await adminDb.listDatabases();
console.log('Databases :', databases);
// Use This Database
const db = mongoClient.db(databaseName);
// List All Collections In The Database
const collections = await db.listCollections().toArray();
console.log('Collections :', collections);
// Use This Collection
const collection = db.collection(collectionName); // Like Table
// Delete Documents (Like Row)
const deleteResult = await collection.deleteMany({});
console.log('Deleted Documents :', deleteResult);
// Insert Documents
const insertResult = await collection.insertMany([{ name: 'User 1' }, { name: 'User 2' }, { name: 'User 3' }]);
console.log('Inserted Documents :', insertResult);
// Find Documents
const findResult = await collection.find({}).toArray();
console.log('Found Documents :', findResult);
}
catch(error) {
console.error('Error :');
console.error(error);
console.error('Error -----');
}
finally {
mongoClient.close();
console.log('Finished');
}
})();
簡単な CRUD スクリプトを書いてみた。
MongoDB における用語は、RDBMS の用語と照らし合わせると理解がしやすいだろう。
- MongoDB における Database → RDBMS における Database そのまま
- Collection → RDBMS における「テーブル」のこと
- Document → RDBMS における「行 (Row)」のこと
だもんで、MongoClient#db()
で扱う DB スキーマを選択し、Db#collection()
でその中のテーブルを一つ選択する要領だ。あとは find()
や insertMany()
や updateOne()
やら、CRUD 操作する関数が用意されているので、そいつを使ってやる、と。
上のスクリプトは実行する度に全データを削除し、3件のデータを Insert して取得しているが、特にデータの不整合は発生していなかった。いくら低スペックな無料枠とはいえ、一人の人間が直列実行するだけなら問題ないか。w
MongoClient#close()
は、いつどのように呼び出しても例外がスローされたりしなかったので、finally
で一律呼ぶようにしている。
気軽に使えるっちゃ使える
観た映画のデータベースアプリ FilmDeX を作っていた時に、SQLite でのデータ管理を止めて、NestJS + Mongoose で管理しようかな、と挑戦しかけていた時がある。しかし、Mongoose は TypeORM と同じく、TypeScript で型をガッチリ決めてかかるので、コーディングに苦労した (TypeORM も MongoDB 対応しかけているが、今のところ Experimental なので避けた)。結局、「無料枠の MongoDB Atlas にデータを置くのは、バックアップ機能もないし不安だなー」と思い、色々考えている内に Google スプレッドシート連携すればいいやとなって、NestJS と Mongoose を捨てた経緯があった。
今回は TypeScript を使わず、素の Node.js で書いたので、気軽に書けた。RDBMS と違ってテーブル定義やスキーマ定義が要らないので、Node.js スクリプトからいきなり新規コレクションを作ったり、型定義もなく連想配列で思い思いにドキュメントを登録・更新・参照できたりして、気楽ではある。
前述のように、IoT デバイスからデータを蓄積するだけの場面だとか、小規模なデータストアとしてなら、MongoDB のような NoSQL も全然使えると思う。
ただ、大規模なマイクロサービス・アプリケーションが、大量リクエストをさばくために NoSQL を活用するというのは、理屈は分かってはいるが、自分の経験値が足りず、どうにもまだ慣れない。そういうアプリケーションに対して、どうしても「強整合性を犠牲にして良い場面」が理解できないのだ。処理が全部成功してないのに次の画面に遷移していい場面ってどこ?DynamoDB などは優秀で、書き込み遅延なんかは遅くともせいぜい1・2秒の間のことなのは分かっているのだが、その1・2秒で「データが更新されていない」風の画面が見えちゃうのってどうなんよ?と思っている。4・5秒待たされたとしても、ちゃんと全部成功してから画面遷移した方が、自分は嬉しいし、安心する。
個人的な性にはなかなか合わないが、ひとまずちょいとかじったので、「MEAN スタックチョットワカル」って言うことにする。w