こんにちわ。はじぴー(hajipy)です。
NeDBはJavaScriptで使用できるデータベースです。アプリケーションに組み込み可能であるため、別途データベースプロセスを起動する必要がありません。そのため、Electronで作成したデスクトップアプリケーションでデータベースを利用したい場合などに使うことができます。
この記事ではNeDBの基本的な使い方を記載します。
しかし、NeDBのAPIはMongoDBのAPIのサブセットであるため、MongoDBに慣れている方はデータベースインスタンスさえ作成してしまえば、今までの知識で使うことができると思います。
この記事では以下のバージョンを使用して動作を検証しました。
- Node.js 8.11.3
- NeDB 1.8.0
またサンプルコードはGitHubで公開してあります。
データベースの作成
NeDBではインメモリかファイルのいずれかのデータベースを作成できます。
インメモリの場合、プロセス内のメモリにデータが保存されますので、プロセス終了時にすべてのデータが消えてしまいます。ユニットテストを行う場合などにはいちいち初期化する必要がなく便利ですが、長期的にデータを保存することはできません。
長期的なデータ保存が必要な場合、ファイルに永続化する必要があります。。ファイルはテキストファイルで1行が1ドキュメントをJSONにシリアライズしたものになっているようです。テキストエディタで確認や編集でき便利です。データ更新時は古いドキュメントは削除されることはなく、更新後のドキュメントが追記され、同じドキュメントが複数ある状態になります。ある程度ファイルサイズが肥大化した時点で不要になった古いデータを削除し、ファイルサイズを小さくするようになっていいます。今回はこの機能については掘り下げませんので、詳細は公式ドキュメントを参照してください。
インメモリ・ファイルどちらのデータベースを作るかは、Databaseクラスのインスタンスを生成する際にfilenameを指定するかどうかで決まります。指定しなかった場合はインメモリ、指定した場合はファイルになります。
const Database = require("nedb");
// インメモリデータベース
const db = new Database();
// 永続化する場合はfilenameを指定する
const db = new Database({ filename: "example.db" });
ファイルから以前のデータをロード必要がある場合、loadDatabaseを使います。loadDatabaseは引数としてロード完了コールバック関数を受け取ります。
// データベースをロードする。ロード完了後にコールバックが呼ばれる
db.loadDatabase((error) => {
if (error !== null) {
console.error(error);
}
console.log("load database completed.");
});
ドキュメントの保存
NeDBはドキュメント指向データベースなので、データはドキュメント単位で扱います。ドキュメントはJavaScriptのオブジェクトして表現でき、文字列・数字・日付・配列・オブジェクトを含めることができます。
// 保存したいドキュメント
const doc = {
// 文字列
someString: "hello, world",
// 数字
someNumber: 37,
// 日付
someDate: new Date(),
// 配列
someArray: [1, 2, 3],
// オブジェクト
someObject: {
key: "value",
}
};
保存したいオブジェクトの用意ができたら、insertを使って保存します。insertは引数として保存したいドキュメントと保存完了時コールバック関数を受け取ります。この関数は第1引数にErrorオブジェクト、第2引数に保存したドキュメントを返します。
返されるドキュメントには必ず_idというフィールドが含まれます。これはドキュメントの識別子でデータベース内で一意である必要があります(自動的に一意制約付きのインデックスが設定されています)。保存したい_idというフィールドが含まれていた場合、その値が使用されます。含まれていなかった場合、NeDBが16文字の文字列の値を生成します。
// 新規ドキュメントをデータベースに保存する
db.insert(doc, (error, newDoc) => {
if (error !== null) {
console.error(error);
}
// newDocにはアルファベット16文字の値を持つ_idフィールドが追加されている
console.log(newDoc);
});
ドキュメントの取得
ドキュメントの取得にはfindを使います。
サンプルコードは以下のドキュメントが保存されているデータベースを対象としています。みんな大好き家庭用ゲーム機です。
[
{
_id: 'id1',
name: "Play Station 4",
developer: { name: "Sony", country: "JP" },
releaseDate: new Date(2014, 2, 22),
price: 39980,
media: "Blu-ray",
portable: false,
connectivity: ["HDMI", "USB", "Ethernet", "Wi-Fi", "Bluetooth"],
peripheral: ["Play Station VR"],
},
{
_id: 'id2',
name: "Play Station Vita",
developer: { name: "Sony", country: "JP" },
releaseDate: new Date(2011, 12, 17),
price: 24980,
media: "Card",
portable: true,
connectivity: ["Wi-Fi", "Bluetooth", "3G"],
},
{
_id: 'id3',
name: "Nintendo 3DS",
developer: { name: "Nintendo", country: "JP" },
releaseDate: new Date(2011, 2, 26),
price: 25000,
media: "Card",
portable: true,
connectivity: ["Wi-Fi"],
},
{
_id: 'id4',
name: "Nintendo Switch",
developer: { name: "Nintendo", country: "JP" },
releaseDate: new Date(2017, 3, 3),
price: 29980,
media: "Card",
portable: true,
connectivity: ["HDMI", "USB", "Wi-Fi", "Bluetooth"],
},
{
_id: 'id5',
name: "Xbox One",
developer: { name: "Microsoft", country: "US" },
releaseDate: new Date(2013, 11, 22),
price: 39980,
media: "Blu-ray",
portable: false,
connectivity: ["HDMI", "USB", "Ethernet", "Wi-Fi"],
peripheral: ["Kinect"],
},
]
まずは一番シンプルな使い方の例を挙げます。
// find()はクエリに一致したすべてのドキュメントを返す
db.find({ media: "Blu-ray" }, (error, docs) => {
// Play Station 4とXbox Oneが返る
});
第1引数にはクエリーを、第2引数には取得完了時のコールバック関数を渡します。コールバック関数はErrorオブジェクトと取得結果ドキュメントの配列を引数として受け取ります。
クエリーにマッチするドキュメントのいずれか1件のみが取得できればいい場合、findの代わりにfindOneを使います。この場合、取得結果ドキュメントは配列ではなく単体が返ります。
// findOne()はクエリに一致したドキュメントのいずれか1つを返す
// どれが返ってくるかは不定なので注意
db.findOne({ media: "Blu-ray" }, (error, doc) => {
// Play Station 4もしくはXbox Oneのいずれか1つが返る
});
クエリ
クエリーは、検索フィールド名がキーで検索条件が値のオブジェクトとして表現します。一番簡単なのは前述のドキュメントのトップレベルフィールドの値が完全一致する検索方法です。少し複雑な例は以下の通りです。
// ドットで連結することで子ドキュメントのフィールドを指定できる
db.find({ "developer.name": "Sony" }, (error, docs) => {
// Play Station 4とPlayStation Vitaが返る
});
// 複数のフィールドを指定するとすべて一致したドキュメントのみを返す
db.find({ media: "Blu-ray", "developer.name": "Sony" }, (error, docs) => {
// Play Station 4のみが返る
});
// 空オブジェクトを指定することで全ドキュメントを返す
db.find({}, (error, docs) => {
// 全ドキュメントが返る
});
比較演算子も使えます。クエリオブジェクトは{ 検索フィールド: { 比較演算子: 比較対象値 } }というネストしたオブジェクトになります。
- $lt
- 〜より小さい
- $lte
- 〜より小さいか、等しい
- $gt
- 〜より大きい
- $gte
- 〜と等しいか、大きい
// $lt, $lte, $gt, $gteで比較条件が指定できる
db.find({ releaseDate: { $gte: new Date(2014, 1, 1) }}, (error, docs) => {
// Play Station 4とNintendo Switchが返る
});
比較演算子と似た感じで使えるいくつかの演算子もあります。
// $inで指定した値のいずれかに一致したドキュメントを返す
db.find({ name: { $in: ["Play Station 4", "Nintendo 3DS"] }}, (error, docs) => {
// Play Station 4とNintendo 3DSが返る
});
// $neで指定した値に一致しないドキュメントを返す
db.find({ name: { $ne: "Xbox One" }}, (error, docs) => {
// Play Station 4とPlay Station VitaとNintendo 3DSとNintendo Switchが返る
});
// $ninで指定した値のすべてに一致しなかったドキュメントを返す
db.find({ name: { $nin: ["Play Station 4", "Nintendo 3DS"] }}, (error, docs) => {
// Play Station VitaとNintendo SwitchとXbox Oneが返る
});
// $existsはフィールドの存在有無をbooleanで指定する
db.find({ peripheral: { $exists: true }}, (error, docs) => {
// Play Station 4とXbox Oneが返る
});
$whereは値に指定したコールバック関数が各ドキュメントに対して呼び出されます。コールバック関数は、そのドキュメントをfindの結果として返したいかどうかのbooleanを返してください。コールバック関数の引数にドキュメントが渡されるわけではなく、コールバック関数のthisがドキュメントに束縛されています。なので、ES 2015のアロー関数をコールバック関数に指定すると、アロー関数はthisが束縛されないため、正しく機能しません。通常の無名関数を指定しましょう。
// $whereはbooleanを返す関数を指定し、その関数がtrueを返したドキュメントを返す
// 関数のthisには判定対象のドキュメントが束縛されている
db.find({ $where: function() { return this.connectivity.length >= 4; } }, (error, docs) => {
// Play Station 4とNintendo SwitchとXbox Oneが返る
});
続いて、配列フィールド専用のクエリーです。
// $sizeは配列フィールドの要素数が一致するドキュメントを返す
db.find({ connectivity: { $size: 4 } }, (error, docs) => {
// Nintendo SwitchとXbox Oneが返る
});
// $elemMatchは配列の要素1つ以上が一致するドキュメントを返す
db.find({ connectivity: { $elemMatch: "Ethernet" } }, (error, docs) => {
// Play Station 4とXbox Oneが返る
});
ここまでに登場したすべてのクエリは論理演算子を使うことでさらに複雑な条件を記述することができます。
// $and, $orはクエリの配列を指定し、そのすべてもしくはいずれかに一致するドキュメントを返す
db.find({ $or: [{ "developer.name": "Sony"}, { portable: true }] }, (error, docs) => {
// Play Station 4とPlay Station VitaとNintendo 3DSとNintendo Switchが返る
});
// $notはクエリに一致しないドキュメントを返す
db.find({ $not: { connectivity: { $size: 4 } } }, (error, docs) => {
// Play Station 4とPlay Station VitaとNintendo 3DSが返る
});
ソート・ページネーション
ドキュメントを取得する際に特定のキーで並び替えたい場合はsortを使用します。いままではfindの第1引数にクエリー、第2引数にコールバックを指定していました。第1引数のみを指定してfindを呼び出すと戻り値としてクエリーオブジェクトが返ってきます。クエリーオブジェクトに対して、sortを呼び出します。sortの引数にはソート条件を記述したオブジェクトを渡します。{ ソートするフィールド名: 1 or -1 }という形式です。昇順の場合は1、降順の場合は-1を渡します。sortの戻り値もクエリーオブジェクトですが、指定したソート条件が反映された新たなクエリーオブジェクトになっています。最後にクエリーオブジェクトに対してexecを呼び出します。execの第1引数はfindの第2引数と同様です。
// ソートを行う場合にはfind()の戻り値に対してsort(), exec()をメソッドチェーンする
// sort()には並び替えフィールドと昇順(1)と降順(-1)を指定する
// exec()にはfind()の第2引数と同じコールバックを指定する
db.find({}).sort({ releaseDate: 1 }).exec((error, docs) => {
// 以下の順に並んだドキュメントが返る
// 1. Nintendo 3DS (releaseDate: 2011-03-25)
// 2. Play Station Vita (releaseDate: 2012-01-16)
// 3. Xbox One (releaseDate: 2013-12-21)
// 4. Play Station 4 (releaseDate: 2014-03-21)
// 5. Nintendo Switch (releaseDate: 2017-04-02)
});
ここまでの例はクエリーにマッチしたすべてのドキュメントを返していました。実際の利用時には「1ページ20件表示なので、3ページ目に表示すべき41件目から60件目のドキュメントを取得したい」という場合もあると思います。その場合はskipとlimitを使用し、一部のドキュメントを返すようにできます。これをページネーションと言います。使用方法はsortと同じく戻り値としてクエリーオブジェクトが返ってきますので、順次設定していき最後にexecを呼び出します。ページネーションを行う場合はsortも合わせて使うことになると思います。メソッドチェーンで書くとシンプルになります。
// skip(), limit()でページネーションが行える
// skip()には読み飛ばすドキュメント数を指定する
// limit()には返すドキュメント数を指定する
db.find({}).sort({ releaseDate: 1 }).skip(1).limit(2).exec((error, docs) => {
// 以下の順に並んだドキュメントが返る(ーは返らないドキュメント)
// - Nintendo 3DS (releaseDate: 2011-03-25)
// 1. Play Station Vita (releaseDate: 2012-01-16)
// 2. Xbox One (releaseDate: 2013-12-21)
// - Play Station 4 (releaseDate: 2014-03-21)
// - Nintendo Switch (releaseDate: 2017-04-02)
});
カウント
クエリーにマッチするドキュメントの件数のみが分かればいい場合はcountを使うと、不要なドキュメント生成が発生しないため、効率的です。countは第1引数にfindと同様にクエリーを指定すると、カウント完了時に第2引数のコールバック関数の件数が返ります。
// findの代わりにcountを使うとドキュメント数のみを返す
db.count({}, (error, numOfDocs) => {
// 5が返る
});
ドキュメントの更新
ドキュメントの更新にはupdateを使います。第1引数にfindと同じクエリ、第2引数に更新内容、第3引数にオプション、第4引数に完了コールバックを指定します。更新内容はオブジェクトで表現し、フィールドに後述するmodifierが含まれている場合、modifierに従いドキュメントの一部を更新します。modifierが含まれていない場合、ドキュメント全体を置き換えます。以下はmodifierを含まない例です。
// queryはfind(), findOne()と同じものが使える
const query = { _id: "id1" };
// updateにmodifier(後述)を含まないオブジェクトを指定した場合、ドキュメントの内容すべてを置き換える
const update = {
_id: 'id1',
name: "Play Station 4 Pro",
developer: { name: "Sony", country: "JP" },
releaseDate: new Date(2016, 11, 10),
media: "UHD Blu-ray",
portable: false,
connectivity: ["HDMI 2.0b", "USB", "Ethernet", "Wi-Fi", "Bluetooth"],
peripheral: ["Play Station VR"],
};
// optionsについては後述
const options = {};
// update()はqueryに一致したドキュメントをupdateに従って更新する
db.update(query, update, options, (error, numOfDocs) => {
// numOfDocsには更新した件数が返る
// ドキュメント全体がupdateの内容に更新されている
});
基本的はmodifierは以下の通りです。
- $set
- 指定したフィールドの値を更新
- $unset
- 指定したフィールドを削除
- $inc
- 指定したフィールドの値を増減
- $push
- 指定した配列フィールドに値を追加
- $pop
- 指定した配列フィールドの先頭か末尾から値を削除
const query = { _id: "id5" };
// updateに$set modifierを指定した場合、そのフィールドのみ更新し、それ以外のフィールドは以前のままになる
const update = {
$set: {
name: "Xbox One X",
releaseDate: new Date(2017, 11, 7),
media: "UHD Blu-ray",
},
};
const options = {};
db.update(query, update, options, (error, numOfDocs) => {
// name, releaseDate, mediaフィールドのみが更新されている
});
const query = { _id: "id1" };
// updateに$unset modifierを指定した場合、そのフィールドを削除し、それ以外のフィールドは以前のままになる
const update = {
$unset: {
peripheral: true,
},
};
const options = {};
db.update(query, update, options, (error, numOfDocs) => {
// peripheralフィールドが削除されている
});
const query = { _id: "id1" };
// updateに$inc modifierを指定した場合、そのフィールドの値をインクリメントし、それ以外のフィールドは以前のままになる
const update = {
$inc: { price: 10000 }
};
const options = {};
db.update(query, update, options, (error, numOfDocs) => {
// priceが39980+10000=49980に更新されている
});
const query = { _id: "id1" };
// updateに$push modifierを指定した場合、配列フィールドに値を追加し、それ以外のフィールドは以前のままになる
const update = {
$push: { peripheral: "Play Station Move" }
};
const options = {};
db.update(query, update, options, (error, numOfDocs) => {
// peripheralがPlay Station VRとPlay Station Moveに更新されている
});
const query = { _id: "id1" };
// updateに$pop modifierを指定した場合、配列フィールドの先頭(-1)か末尾(1)から値を取り除き、それ以外のフィールドは以前のままになる
const update = {
$pop: { connectivity: 1 }
};
const options = {};
db.update(query, update, options, (error, numOfDocs) => {
// connectivityの末尾のBluetoothが削除される(-は削除された値)
// 1. HDMI
// 2. USB
// 3. Ethernet
// 4. Wi-Fi
// - Bluetooth
});
オプション
第3引数のオプションを指定することで、複数ドキュメントをまとめて更新したり、更新対象が見つからなかった場合、更新の代わりにドキュメントを追加したりすることができます。
const query = { "developer.name": "Nintendo" };
const update = {
$set: {
"developer.japaneseName": "任天堂"
}
};
// optionsのmultiにtrueを指定した場合、複数のドキュメントを更新する(デフォルトはfalse)
const options = {
multi: true,
};
db.update(query, update, options, (error, numOfDocs) => {
// developer.nameがNintendoのドキュメントすべてが更新されている
});
const query = { "name": "Play Station 3" };
const update = {
_id: 'id6',
name: "Play Station 3",
developer: { name: "Sony", country: "JP" },
releaseDate: new Date(2006, 11, 11),
price: 62790,
media: "Blu-ray",
portable: false,
connectivity: ["HDMI", "USB", "Ethernet", "Wi-Fi", "Bluetooth"],
};
// optionsのupsertにtrueを指定した場合、queryに一致するドキュメントがなかった場合、insertを行う(デフォルトはfalse)
const options = {
upsert: true,
};
db.update(query, update, options, (error, numOfDocs) => {
// nameがPlay Station 3のドキュメントはないので、insertされる
});
const query = { _id: "id1" };
const update = {
_id: 'id1',
name: "Play Station 4 Pro",
developer: { name: "Sony", country: "JP" },
releaseDate: new Date(2016, 11, 10),
media: "UHD Blu-ray",
portable: false,
connectivity: ["HDMI 2.0b", "USB", "Ethernet", "Wi-Fi", "Bluetooth"],
peripheral: ["Play Station VR"],
};
// optionsのreturnUpdatedDocsにtrueを指定した場合、更新したドキュメントを返す(デフォルトはfalse)
// multiがfalseの場合はドキュメント、trueの場合はドキュメントの配列になる
const options = {
returnUpdatedDocs: true,
};
// update()はqueryに一致したドキュメントをupdateに従って更新する
db.update(query, update, options, (error, numOfDocs, updatedDocs) => {
// updatedDocsにはドキュメントが返る
});
ドキュメントの削除
ドキュメントの削除にはremoveを使います。第1引数にクエリー、第2引数にオプション、第3引数に完了時のコールバック関数を指定します。オプションにはmultiのみを指定できます。
// queryはfind(), findOne()と同じものが使える
const query = { _id: "id1" };
// optionsについては後述
const options = {};
// remove()はqueryに一致したドキュメントを削除する
db.remove(query, options, (error, numOfDocs) => {
// numOfDocsには削除した件数が返る
// queryに一致したドキュメントのうち1件が削除されている
});
const query = { "developer.name": "Sony" };
// optionsはmultiのみを受け付ける
// trueを指定した場合、複数のドキュメントを削除する(デフォルトはfalse)
const options = { multi: true };
db.remove(query, options, (error, numOfDocs) => {
// queryに一致したドキュメントがすべて削除されている
});
インデックス
インデックスを作ることでデータベースの検索を高速化することや、一意制約を課すことができます。検索が高速化されるのは基本的な条件や比較演算子を使った場合のみです。配列フィールドへインデックスを作成することはできません。
インデックスの作成にはensureIndexを使います。第1引数にオプション、第2引数に完了時のコールバック関数を指定します。すでにドキュメントが登録されているデータベースに一意制約ありのインデックスを作成しようとした場合、エラーになる可能性があります。
オプションにはfieldNameの指定が必須です。インデックスを作成するフィールド名を指定してください。ドットで連結することでトップレベルフィールド以外を指定することができます。uniqueをtrueに指定した場合、一意制約付きインデックスになります(デフォルトはfalse)。
// filedNameで指定したフィールドにインデックスを作成する
const options = {
fieldName: "name"
};
db.ensureIndex(options, (error) => {
});
// ドットで連結することで子ドキュメントのフィールドを指定できる
const options = {
fieldName: "developer.name"
};
db.ensureIndex(options, (error) => {
});
// uniqueにtrueを指定すると一意制約を設けることができる(デフォルトはfalse)
const options = {
fieldName: "name",
unique: true,
};
db.ensureIndex(options, (error) => {
});
インデックスがあるデータベースを永続化し、ロードし直した場合、インデックスも合わせてロードされます。しかし、同じインデックスを2回作ってもムダもエラーも発生しませんので、データベース初期化処理の一環としてインデックスを作成を行って問題ないと思います。
最後にインデックスの削除にはremoveIndexを使います。
const options = {
fieldName: "name"
};
// removeIndex()を呼び出すことで作成済みインデックスを削除できる
db.removeIndex(options, (error2) => {
});
終わりに
これでNeDBの基本の説明はおしまいです。説明を省略した機能や詳細などもありますので、不明点がありましたら、公式ドキュメントを参照してください。最後まで読んでいただき、ありがとうございました。