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 サービスを試している時に頻繁に遭遇した。

天下の AWS が提供する Amazon DynamoDB でも遭遇したことがあるが、DynamoDB はその対策として、強整合性のある読み込みオプションが提供されていたりする。その代わり性能と利用料が犠牲となるのだが。

それから、自分は Java 言語からプログラミングに入門したこともあり、型情報のない JSON 形式を多用するのは避けたいのだ。

個人製作アプリのレベルであれば、自分一人でモデルを設計して実装するので、TypeScript を使わずに実装しても「ココはこういうデータをやり取りするから~」と想定で実装しても何とかなったりする。しかし、業務でチーム開発するコードとなると型情報が大事になってくるので、TypeORM のような TypeScript で型情報を扱える O/R マッパーを使いたいし、生の JSON (連想配列) をそのまま多用はしたくないのである。

NoSQL だと行ごとに異なるカラムを持たせたりもできちゃうんでしょ?RDBMS はカラム定義の柔軟性には欠けるかもしれないけど、データにそんな柔軟性持たせちゃってアプリケーションが実装できるのけ?っていうのがずっと引っかかっている。

非同期処理・結果整合性モデルで、スキーマ・型定義がなくていい仕組みって、自分は信用ならなくて、ずっと使って来なかった。今でもこの生理的な毛嫌いは残っている。

…個人的に腑に落ちない部分があるとはいえ、日頃「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 the root user. Replace myFirstDatabase 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 形式に対応しているようだったので、asyncawait でコーディングできた。

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 の用語と照らし合わせると理解がしやすいだろう。

だもんで、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