[NeDBを使おう]データベースのドキュメントを検索しよう。

このエントリーを Google ブックマーク に追加
Pocket
[`yahoo` not found]

NeDBはJavaScriptで作られたデータベースです。 Node.js、nw.js、エレクトロン、ブラウザ等で動かすことができます。 Github NeDBはドキュメント型データベースでJSONをそのまま保存できます。 そのため、JavaScriptのプログラムと親和性が高く、データの取得、利用が簡単にできます。 また、NeDBはMongoDBと互換性があり、一つのデータベースはMongoDBのコレクションと対応しています。 対応は下記の通りです。
  • データベース→コレクション
  • データロウ→ドキュメント
今回は、NeDBのデータベースに保存されているドキュメントを検索する方法について書いていきます。

NeDBデータベース内のドキュメントの検索

NeDBデータベースに保存したドキュメントはDatastore#findまたはDatastore#findOneを使って検索することができます。 Datastore#findは条件に一致した複数のドキュメントをアレイで取得します。Datastore#findOneは特定のドキュメントを取得できます。 検索時の条件指定には下記の方法があります。
  • 同値判定
  • 比較演算子 ($lt, $lte, $gt, $gte, $in, $nin, $ne)の使用
  • 論理演算子の使用( $or, $and, $not and $where )
  • 基本的な正規表現の使用
  • 正規表現演算子($regex)の使用
  • ソートの使用
  • ページネーションの使用
  • プロジェクションの機能による結果の変更?
上記の条件指定方法は単体で使うこともできますが、複数を組み合わせて使うこともできます。

テスト用のデータベース

本記事で使う検索対象のデータベースとドキュメントをインサートして準備します。 記事内で検索条件指定で得られたドキュメントを説明のために、_idフィールドの値を使います。 例:〇〇という検索条件ため、結果はid1,id2,id3のドキュメントの配列になります。

export function getTestDatabase() {
    let db = new Datastore();
  db.insert([
    { _id: 'id1', planet: 'Mars', system: 'solar', inhabited: false, satellites: ['Phobos', 'Deimos'] },
    { _id: 'id2', planet: 'Earth', system: 'solar', inhabited: true, humans: { genders: 2, eyes: true } },
    { _id: 'id3', planet: 'Jupiter', system: 'solar', inhabited: false },
    { _id: 'id4', planet: 'Omicron Persei 8', system: 'futurama', inhabited: true, humans: { genders: 7 } },
    { _id: 'id5', completeData: { planets: [ { name: 'Earth', number: 3 }, { name: 'Mars', number: 2 }, { name: 'Pluton', number: 9 } ] } },
    { _id: 'id6', completeData: { planets: { name: 'Mars', number: 2} } },
  ], function (err, newDoc) {
    console.log('err', err);
    console.log('newDoc', newDoc);
  })
  return db
}

基本的な検索

Datastoreで検索を行う場合、Datastore#findとDatastore#findOneを使用することができます。 どちらも同じ下記の引数を持ちます。
  • query:クエリを設定したオブジェクトです。
  • callback:検索処理のコールバック関数です。下記の引数を持ちます。
    • err:エラー発生時の情報オブジェクトです。
    • docs:検索結果のドキュメントです。Datastore#findの場合はアレイ、Datastore#findOneの場合はオブジェクトです。
第二引数のcallbackはオプションです、指定した場合と指定しなかった場合で挙動が大きく異なるので注意が必要です。 また、下記のようにqueryに空のオブジェクトを設定するとデータベース内の全てのドキュメントを取得することができます。
export function findData(){
    let db = getTestDatabase()
    // コレクション内のすべてのドキュメントを取得します。
    db.find({}, function (err, docs) {
      // 条件に一致した_idがid1,id2,id3,id4,id5,id6のドキュメントが結果として取得できる。
      console.log('err', err);
      console.log('docs', docs);
    });
}
queryはクエリが設定されたオブジェクトとして定義することができます。 クエリはオブジェクトのkey:value形式で表現され、key:valueは[ドキュメントのフィールド名 or 演算子]:[ドキュメントの値 or queryオブジェクト]となります。 NeDBで使われる検索・アップデート・デリート等の処理は対象のドキュメントをqueryオブジェクトで指定するため、NeDBを使いこなすにはqueryオブジェクトの書き方を覚える必要があります。

同値判定

一番単純なクエリです。 任意のフィールドと任意の値を指定し、データベースの中からクエリに合致するドキュメントを取得できます。 複数のクエリを設定すると、全てのクエリに合致するドキュメントを取得できます。
export function findData(){
    let db = getTestDatabase()
    // systemフィールドの値が'solar'のドキュメントを全て取得する
    db.find({ system: 'solar' }, function (err, docs) {
      // 条件に一致した_idがid1,id2,id3のドキュメントが結果として取得できる
      // もし一致するドキュメントがない場合は空の配列が返却されます。
      console.log('err', err);
      console.log('docs', docs);
    });

    // systemフィールドの値が'solar'、inhabitedがtrueのドキュメントを全て取得する
    db.find({ system: 'solar', inhabited: true }, function (err, docs) {
      // 条件に一致した_idがid2のドキュメントが結果として取得できる
      console.log('err', err);
      console.log('docs', docs);
    });

    // 値にオブジェクトを指定するとオブジェクトの完全一致比較を使う深い比較もできる。ドット記法との違いを理解する必要がある。
    db.find({ humans: { genders: 2 } }, function (err, docs) {
      // id2は条件に一致しないと判断され、検索結果は空の配列となる。
      // id2のhumansフィールドは{ genders: 2, eyes: true }であり、条件はeyesフィールドの値が一致しないため検索条件に当てはまらない
      console.log('err', err);
      console.log('docs', docs);
    });

    // 単一のドキュメントを取得できます。
    db.findOne({ _id: 'id1' }, function (err, doc) {
      // 条件に一致した_idがid1のドキュメントが結果として取得できる。
      // 一致するドキュメントがない場合はdocはnullになります。
      console.log('err', err);
      console.log("doc", doc);
    });
}

基本的な正規表現の使用

クエリでは正規表現リテラルを使うことができます。 データベースから指定したフィールドに保存されている文字列に対して正規表現に合致するドキュメントを取得します。
export function findData(){
    let db = getTestDatabase()
    // 正規表現を使ってplanetsフィールドの値の文字列が'ar'を含むドキュメントを全て取得します。
    db.find({ planet: /ar/ }, function (err, docs) {
      // 条件に一致した_idがid1,id2のドキュメントが結果として取得できる
      console.log('err', err);
      console.log('docs', docs);
    });
}

ドット記法を使った検索(ネステッドドキュメント、ドキュメントアレイ)

指定するフィールド名にドットを入れることで、ネストされたドキュメント・ドキュメントのアレイに対して検索条件を設定することができます。
export function findData(){
    let db = getTestDatabase()
    // ドット記法を使い下記の条件に合致するドキュメントを取得する。
    // - humansフィールドにサブドキュメントがある
    // - サブドキュメントのgendersフィールドの値が2
    db.find({ "humans.genders": 2 }, function (err, docs) {
      // 条件に一致した_idがid2のドキュメントが結果として取得できる、_idがid4のドキュメントは一致しない。
      console.log('err', err);
      console.log('docs', docs);
    });

    // ドット記法を使い下記の条件に合致するドキュメントのアレイを検索する。
    // - completeDataフィールドにサブドキュメントがあり、planetsフィールドがある
    // - planetsフィールドの値がオブジェクトまたはオブジェクトのアレイである。
    // - planetsフィールドの値がオブジェクトの場合、nameフィールドの値が"Mars"であること
    // - planetsフィールドの値がオブジェクトのアレイの場合、アレイの中に一つでもnameフィールドの値が"Mars"であるオブジェクトがあること
    db.find({ "completeData.planets.name": "Mars" }, function (err, docs) {
      // 条件に一致した_idがid5,id6のドキュメントが結果として取得できる。
      console.log('err', err);
      console.log('docs', docs);
    });
    db.find({ "completeData.planets.name": "Jupiter" }, function (err, docs) {
      // 条件に一致するものがないのでdocsは空の配列になる
      console.log('err', err);
      console.log('docs', docs);
    });

    // ドット記法を使い下記の条件に合致するドキュメントのアレイの任意の要素に対してを検索する。
    // - completeDataフィールドにサブドキュメントがあり、planetsフィールドがある
    // - planetsフィールドの値がオブジェクトのアレイである。
    // - planetsフィールドの0番目のオブジェクトのnameフィールドの値が"Earth"であること
    db.find({ "completeData.planets.0.name": "Earth" }, function (err, docs) {
      // 条件に一致した_idがid5のドキュメントが結果として取得できる。
      // 配列の要素を指定しているため、仮に"Earth"ではなく"Mars"で検索した場合、検索結果は空となる。
      console.log('err', err);
      console.log('docs', docs);
    });

}

演算子の使用

NeDBにはいくつかの演算子が定義されています。 演算子は{ field: { $op: value } }を基本構文としている。 $opには後述の演算子を使用することができる。

比較演算子 ($lt, $lte, $gt, $gte, $in, $nin, $ne)の使用

NeDBでは下記の比較演算子が使用できます。
  • $lt: 未満
  • $lte: 以下
  • $gt: 超過
  • $gte: 以下
  • $in: 配列の中に含まれる。この演算子を使う場合、valueは配列である必要がある。
  • $nin: 配列の中に含まれない。この演算子を使う場合、valueは配列である必要がある。
  • $ne: 等しくない
  • $exists: ドキュメントにフィールドが存在するか。この演算子を使う場合、valueはtrueまたはfalseである必要がある
export function findData(){
    let db = getTestDatabase()
  // $lt,$lte,$gt,$gteは数値と文字列に対して使用することができます。
  db.find({ "humans.genders": { $gt: 5 } }, function (err, docs) {
    // humans.gendersフィールドを持つのはid2とid4です。
    // 条件が5より大きいなので、id4のドキュメントが検索結果に入ります。
    console.log('err', err);
    console.log('docs', docs);
  });

  // $lt,$lte,$gt,$gteを文字列に対して使用すると辞書順で判断されます。
  db.find({ planet: { $gt: 'Mercury' }}, function (err, docs) {
    // 条件が辞書順でMercuryより後なので、id4のドキュメントが検索結果に入ります。
    console.log('err', err);
    console.log('docs', docs);
  })

  // $inと$ninは同じ使い方をしますが、判定が反転します。
  db.find({ planet: { $in: ['Earth', 'Jupiter'] }}, function (err, docs) {
    // planetのvalueが['Earth', 'Jupiter']に含まれているものなので、id2,id3が検索結果となります。
    console.log('err', err);
    console.log('docs', docs);
  });

  // $existsはフィールドの有無を判定します。
  db.find({ satellites: { $exists: true } }, function (err, docs) {
    // satellitesフィールドが存在するのはid1だけなので、検索結果はid1になります。
    console.log('err', err);
    console.log('docs', docs);
  });
}

正規表現演算子($regex)の使用

クエリにオブジェクトを使用して検索する場合、$regexを使うことで正規表現を使用することができます。
export function findData(){
    let db = getTestDatabase()
    // $regex演算子は正規表現に適合するかを判定します。また、他の演算子と一緒に使っています。
    db.find({ planet: { $regex: /ar/, $nin: ['Jupiter', 'Earth'] } }, function (err, docs) {
      // planetに'ar'が部分的に含まれているドキュメントはid1,id2です。id1,id2のうち['Jupiter', 'Earth']に含まれないのはid1です。そのため検索結果はid1となります。
      console.log('err', err);
      console.log('docs', docs);
    });
}

論理演算子の使用( $or, $and, $not and $where )

論理演算子で複数の演算子を組み合わせて複雑な演算を行うことができます。
  • $or: クエリの配列をorで繋ぎます。例:{ $or: [query1, query2, …] }
  • $and: クエリの配列をandで繋ぎます。例:{ $and: [query1, query2, …] }
  • $not: クエリの判定を反転させます。例:{ $not: query }
  • $where: ドキュメントの内容に対して関数を適用します。この関数はtrue/falseを返し、trueの場合、ドキュメントは検索結果に含まれます。例:{ $where: function () { // object is “this”, return a boolean } }
export function findData(){
    let db = getTestDatabase()
    // $or演算子を使い、複数条件の論理和で検索します。
    db.find({ $or: [{ planet: 'Earth' }, { planet: 'Mars' }] }, function (err, docs) {
      // コレクションの中からplanetが'Earth'または'Mars'のドキュメントを探します。検索結果はid1,id2の配列になります。
      console.log('err', err);
      console.log('docs', docs);
    });
    
    // $not演算子を使い、条件の否定で検索します。
    db.find({ $not: { planet: 'Earth' } }, function (err, docs) {
      // コレクションの中からplanetが'Earth'以外のドキュメントを探します。検索結果はid2以外のドキュメントの配列になります。
      console.log('err', err);
      console.log('docs', docs);
    });
    
    // $where演算子を使い、ドキュメントの内容に対して条件を設定します。
    db.find({ $where: function () { return Object.keys(this) > 6; } }, function (err, docs) {
      // TODO 結果を調べる
      // コレクションの中からドキュメントのフィールドが6以上のを探します。検索結果はid2以外のドキュメントの配列になります。
      console.log('err', err);
      console.log('docs', docs);
    });
    
    // 基本的なクエリと一緒に使うことができます。
    db.find({ $or: [{ planet: 'Earth' }, { planet: 'Mars' }], inhabited: true }, function (err, docs) {
      // コレクションの中からplanetフィールドが'Earth'または'Mars'でinhabitedフィールドがtrueのドキュメントを探します。検索結果はid2のドキュメントの配列になります。
      console.log('err', err);
      console.log('docs', docs);
    });
}

ソートとページネーションの使用

Datastore#findとDatastore#findOneの第二引数callbackを設定しない場合、メソッドはCursorオブジェクトを返却します。 Cursorオブジェクトは下記のメソッドを持ちます。
  • sort:ソートするフィールドと昇降順を設定します。一つ以上のフィールドに対してそれぞれ昇順(1),降順(-1)を設定することができます。
  • skip:スキップする要素数を設定します。
  • limit:取得するドキュメント数を設定します。
  • exec(callback):Cursorオブジェクトの内容を実行し、コールバックで検索結果を取得します。
上記のメソッドを組み合わせることでソートやデータベースの任意の順番にあるドキュメントを取り出すページネーションを実現することができます。
export function findData(){
    let db = getTestDatabase()
    // クエリを設定せずコレクション全体を取得します。
    db.find({}).sort({ planet: 1 }).skip(1).limit(2).exec(function (err, docs) {
      // コレクション内のドキュメントをplanetを昇順にソートし、2番目のドキュメントから2つのドキュメントを取得します。
      console.log('err', err);
      console.log('docs', docs);
    });
    
    // 降順で取得します。
    db.find({ system: 'solar' }).sort({ planet: -1 }).exec(function (err, docs) {
      // コレクション内のsystemが'solar'のドキュメントを降順にソートして取得します。
      console.log('err', err);
      console.log('docs', docs);
    });
    
    // 複数のフィールドに対してソートを設定することができます。
    db.find({}).sort({ planet: 1, system: -1 }).exec(function (err, docs) {
      // コレクション内のドキュメントをplanetを昇順、systemを降順にソートして取得します。
      console.log('err', err);
      console.log('docs', docs);
    });
}

配列フィールドの検索

配列が条件に指定された場合、NeDBはvalueの配列と完全一致しているかどうかを判定します。 配列固有演算子がある場合はその演算子を適用します。
  • $size: 配列のサイズが一致するかを判定します。
  • $elemMatch: 配列要素内の一つ以上の要素に適合するかを判定します。
export function findData(){
    let db = getTestDatabase()
    // 完全に一致するドキュメントを探します。
    db.find({ satellites: ['Phobos', 'Deimos'] }, function (err, docs) {
      // satellitesフィールドが完全に一致するのはid1だけなので、検索結果はid1になります。
      console.log('err', err);
      console.log('docs', docs);
    })

    db.find({ satellites: ['Deimos', 'Phobos'] }, function (err, docs) {
      // satellitesフィールドの要素は一致しますが、順番が異なるのでid1は検索結果に含まれません。
      console.log('err', err);
      console.log('docs', docs);
    })

    // ドキュメントのフィールドが配列だった場合、クエリは全ての要素に適用され、要素内のいずれかと一致するかを判定します。
    db.find({ satellites: 'Phobos' }, function (err, docs) {
      // コレクションの中からsatellitesの配列内に'Phobos'の要素を持つドキュメントを探します。検索結果にはid5のドキュメントが含まれます。
      // また、 { satellites: 'Deimos' }としても同様の結果が得られます。
      console.log('err', err);
      console.log('docs', docs);
    });

    // 演算子も全ての要素に適用される。
    db.find({ satellites: { $lt: 'Amos' } }, function (err, docs) {
      // コレクションの中からsatellitesの配列内の要素が辞書順で'Amos'より早いドキュメントを探します。
      // PhobosとDeimosはAmosより後なので、検索結果には空になります。
      console.log('err', err);
      console.log('docs', docs);
    });

    // $in演算子と$nin演算子も使うことができます。
    db.find({ satellites: { $in: ['Moon', 'Deimos'] } }, function (err, docs) {
      // docs contains Mars (the Earth document is not complete!)
      // コレクションの中からsatellitesの配列内の要素に'Moon'または'Deimos'を含むドキュメントを探します。
      // satellitesにDeimosを含むので、検索結果にid1が含まれます。
      console.log('err', err);
      console.log('docs', docs);
    });

    // 配列固有演算子の$elemMatch演算子を使いドキュメントを探します。
    // completeData.planetsに格納されている配列の全要素に対して条件の適用を行うため$elemMatch演算子を使用します。
    db.find({ completeData: { planets: { $elemMatch: { name: 'Earth', number: 3 } } } }, function (err, docs) {
      // completeData.planetsの配列要素に{ name: 'Earth', number: 3 }を含むid5が検索結果になります。
      console.log('err', err);
      console.log('docs', docs);
    });

    db.find({ completeData: { planets: { $elemMatch: { name: 'Earth', number: 5 } } } }, function (err, docs) {
      // completeData.planetsの配列要素に{ name: 'Earth', number: 5 }を含むドキュメントがないため検索結果は空になります。
      console.log('err', err);
      console.log('docs', docs);
    });
    // $elemMatch演算子の条件の中でも、通常の演算子を使うことができます。
    db.find({ completeData: { planets: { $elemMatch: { name: 'Earth', number: { $gt: 2 } } } } }, function (err, docs) {
      // completeData.planetsの配列要素で「nameが'Earth'」かつ「numberが2以上」のドキュメントの検索結果にはid5が含まれます。
      console.log('err', err);
      console.log('docs', docs);
    });

    // $sizeは配列数を検索条件とします。しかし、$size演算子は他の演算子を入れ子にすることはできません。例{ $size: { $lt: 5 } }
    db.find({ satellites: { $size: 2 } }, function (err, docs) {
      // コレクションの中からsatellitesの配列要素数が2のドキュメントを探します。検索結果にはid5が含まれます。
      console.log('err', err);
      console.log('docs', docs);
    });

    db.find({ satellites: { $size: 1 } }, function (err, docs) {
      // コレクションの中からsatellitesの配列要素数が1のドキュメントを探します。検索結果は空の配列になります。
      console.log('err', err);
      console.log('docs', docs);
    });
}

Date型への検索

Date型のデータが保存されているフィールドに対してクエリを作る場合はJavascriptのDateクラスを使うことができます。 Date型への検索クエリには比較演算子を使用することができます。
export function findData(){
    let db = new Datastore();
    var today = new Date()
    var year = today.getFullYear()
    var month = today.getMonth()
    var day = today.getDate()

    for (let i = 0; i < 100 ; i++) {
      var newDate = new Date(year, month, day - i)
      db.insert({
        num : (i + 1) ,
        string: newDate.toLocaleDateString(),
        date : newDate
      })
    }

    db.find({date : {$gt : new Date(year, month, day - 60)}}).sort({date : -1}).exec(function (err, docs) {
      console.log('err',err);
      console.log('docs', docs);
    })

}