スキーマ

まだお済みでない場合は、Mongooseの動作について理解するために、クイックスタート を読んでみてください。7.x から 8.x に移行する場合は、移行ガイド を読んでください。

スキーマの定義

Mongooseでは、すべてがスキーマから始まります。各スキーマはMongoDBコレクションにマッピングされ、そのコレクション内のドキュメントの構造を定義します。

import mongoose from 'mongoose';
const { Schema } = mongoose;

const blogSchema = new Schema({
  title: String, // String is shorthand for {type: String}
  author: String,
  body: String,
  comments: [{ body: String, date: Date }],
  date: { type: Date, default: Date.now },
  hidden: Boolean,
  meta: {
    votes: Number,
    favs: Number
  }
});

後で追加のキーを追加する場合は、Schema#add メソッドを使用します。

上記のコードblogSchemaの各キーは、ドキュメント内のプロパティを定義し、関連付けられたSchemaTypeにキャストされます。たとえば、titleプロパティはString SchemaTypeに、dateプロパティはDate SchemaTypeにキャストされます。

上記のように、プロパティに型のみが必要な場合は、省略記法を使用して指定できます(上記のtitleプロパティとdateプロパティを比較してください)。

キーには、上記のmetaプロパティのように、さらにキー/型の定義を含むネストされたオブジェクトを割り当てることもできます。これは、キーの値がtypeプロパティを持たないPOJOである場合に発生します。

このような場合、Mongooseはツリーの葉(上記のmeta.votesmeta.favsなど)に対してのみ実際のスキーマパスを作成し、ブランチには実際のパスがありません。この副作用として、上記のmetaは独自のバリデーションを持つことができません。ツリーの上位でバリデーションが必要な場合は、ツリーの上位にパスを作成する必要があります。これを行う方法の詳細については、サブドキュメントセクションを参照してください。また、MixedのSchemaTypesガイドのサブセクションで、いくつかの注意点を確認してください。

許可されるSchemaTypesは次のとおりです。

SchemaTypesの詳細はこちら

スキーマは、ドキュメントの構造とプロパティのキャストを定義するだけでなく、ドキュメントのインスタンスメソッドスタティックモデルメソッド複合インデックスミドルウェアと呼ばれるドキュメントライフサイクルフックも定義します。

モデルの作成

スキーマ定義を使用するには、blogSchemaを操作できるモデルに変換する必要があります。そのためには、mongoose.model(modelName, schema)に渡します。

const Blog = mongoose.model('Blog', blogSchema);
// ready to go!

ID

デフォルトでは、Mongooseはスキーマに_idプロパティを追加します。

const schema = new Schema();

schema.path('_id'); // ObjectId { ... }

自動的に追加された_idプロパティを使用して新しいドキュメントを作成すると、Mongooseはドキュメントに新しいObjectId型の_idを作成します。

const Model = mongoose.model('Test', schema);

const doc = new Model();
doc._id instanceof mongoose.Types.ObjectId; // true

独自の_idでMongooseのデフォルトの_idを上書きすることもできます。ただし注意してください。Mongooseは、_idを持たない最上位のドキュメントの保存を拒否するため、独自の_idパスを定義する場合は、_idの設定を担当します。

const schema = new Schema({
  _id: Number // <-- overwrite Mongoose's default `_id`
});
const Model = mongoose.model('Test', schema);

const doc = new Model();
await doc.save(); // Throws "document must have an _id before saving"

doc._id = 1;
await doc.save(); // works

Mongooseはサブドキュメントにも_idプロパティを追加します。サブドキュメントの_idプロパティを無効にするには、次のようにします。Mongooseは、_idプロパティを持たないサブドキュメントの保存を許可します。

const nestedSchema = new Schema(
  { name: String },
  { _id: false } // <-- disable `_id`
);
const schema = new Schema({
  subdoc: nestedSchema,
  docArray: [nestedSchema]
});
const Test = mongoose.model('Test', schema);

// Neither `subdoc` nor `docArray.0` will have an `_id`
await Test.create({
  subdoc: { name: 'test 1' },
  docArray: [{ name: 'test 2' }]
});

または、次の構文を使用して_idを無効にすることもできます。

const nestedSchema = new Schema({
  _id: false, // <-- disable _id
  name: String
});

インスタンスメソッド

Modelsのインスタンスはドキュメントです。ドキュメントには、独自の組み込みインスタンスメソッドが多数あります。独自のドキュメントインスタンスメソッドを定義することもできます。

// define a schema
const animalSchema = new Schema({ name: String, type: String },
  {
  // Assign a function to the "methods" object of our animalSchema through schema options.
  // By following this approach, there is no need to create a separate TS type to define the type of the instance functions.
    methods: {
      findSimilarTypes(cb) {
        return mongoose.model('Animal').find({ type: this.type }, cb);
      }
    }
  });

// Or, assign a function to the "methods" object of our animalSchema
animalSchema.methods.findSimilarTypes = function(cb) {
  return mongoose.model('Animal').find({ type: this.type }, cb);
};

これで、すべてのanimalインスタンスでfindSimilarTypesメソッドを使用できるようになりました。

const Animal = mongoose.model('Animal', animalSchema);
const dog = new Animal({ type: 'dog' });

dog.findSimilarTypes((err, dogs) => {
  console.log(dogs); // woof
});
  • デフォルトのMongooseドキュメントメソッドを上書きすると、予期しない結果になる可能性があります。詳細についてはこちらを参照してください。
  • 上記の例では、インスタンスメソッドを保存するためにSchema.methodsオブジェクトを直接使用しています。 こちらで説明されているように、Schema.method()ヘルパーを使用することもできます。
  • ES6アロー関数(=>)を使用してメソッドを宣言しないでください。アロー関数はthisのバインディングを明示的に防止するため、メソッドはドキュメントにアクセスできず、上記の例は機能しません。

スタティックメソッド

モデルにスタティック関数を追加することもできます。スタティックを追加するには、3つの同等の方法があります。

  • スキーマコンストラクターの第2引数(statics)に関数プロパティを追加します。
  • schema.staticsに関数プロパティを追加します。
  • Schema#static()関数を呼び出します。

// define a schema
const animalSchema = new Schema({ name: String, type: String },
  {
  // Assign a function to the "statics" object of our animalSchema through schema options.
  // By following this approach, there is no need to create a separate TS type to define the type of the statics functions.
    statics: {
      findByName(name) {
        return this.find({ name: new RegExp(name, 'i') });
      }
    }
  });

// Or, Assign a function to the "statics" object of our animalSchema
animalSchema.statics.findByName = function(name) {
  return this.find({ name: new RegExp(name, 'i') });
};
// Or, equivalently, you can call `animalSchema.static()`.
animalSchema.static('findByBreed', function(breed) { return this.find({ breed }); });

const Animal = mongoose.model('Animal', animalSchema);
let animals = await Animal.findByName('fido');
animals = animals.concat(await Animal.findByBreed('Poodle'));

ES6アロー関数(=>)を使用してスタティックを宣言しないでください。アロー関数はthisのバインディングを明示的に防止するため、thisの値のために上記の例は機能しません。

クエリヘルパー

クエリヘルパー関数も追加できます。これは、インスタンスメソッドに似ていますが、Mongooseクエリ用です。クエリヘルパーメソッドを使用すると、Mongooseのチェーン可能なクエリビルダーAPIを拡張できます。


// define a schema
const animalSchema = new Schema({ name: String, type: String },
  {
  // Assign a function to the "query" object of our animalSchema through schema options.
  // By following this approach, there is no need to create a separate TS type to define the type of the query functions.
    query: {
      byName(name) {
        return this.where({ name: new RegExp(name, 'i') });
      }
    }
  });

// Or, Assign a function to the "query" object of our animalSchema
animalSchema.query.byName = function(name) {
  return this.where({ name: new RegExp(name, 'i') });
};

const Animal = mongoose.model('Animal', animalSchema);

Animal.find().byName('fido').exec((err, animals) => {
  console.log(animals);
});

Animal.findOne().byName('fido').exec((err, animal) => {
  console.log(animal);
});

インデックス

MongoDBはセカンダリインデックスをサポートしています。Mongooseでは、これらのインデックスをSchemaパスレベルまたはスキーマレベルで定義します。複合インデックスを作成する場合は、スキーマレベルでインデックスを定義する必要があります。

const animalSchema = new Schema({
  name: String,
  type: String,
  tags: { type: [String], index: true } // path level
});

animalSchema.index({ name: 1, type: -1 }); // schema level

他のインデックスオプションについては、SchemaType#index()を参照してください。

アプリケーションの起動時に、Mongooseはスキーマで定義された各インデックスに対してcreateIndexを自動的に呼び出します。Mongooseは各インデックスに対して順番にcreateIndexを呼び出し、すべてのcreateIndex呼び出しが成功した場合、またはエラーが発生した場合は、モデルで 'index' イベントを発行します。開発には便利ですが、インデックスの作成はデータベースのパフォーマンスに大きな影響を与える可能性があるため、本番環境ではこの動作を無効にすることをお勧めします。スキーマのautoIndexオプションをfalseに設定するか、接続でグローバルにautoIndexオプションをfalseに設定することで、動作を無効にします。

mongoose.connect('mongodb://user:pass@127.0.0.1:port/database', { autoIndex: false });
// or
mongoose.createConnection('mongodb://user:pass@127.0.0.1:port/database', { autoIndex: false });
// or
mongoose.set('autoIndex', false);
// or
animalSchema.set('autoIndex', false);
// or
new Schema({ /* ... */ }, { autoIndex: false });

インデックスの構築が完了した場合、またはエラーが発生した場合は、モデルでindexイベントが発行されます。

// Will cause an error because mongodb has an _id index by default that
// is not sparse
animalSchema.index({ _id: 1 }, { sparse: true });
const Animal = mongoose.model('Animal', animalSchema);

Animal.on('index', error => {
  // "_id index cannot be sparse"
  console.log(error.message);
});

Model#ensureIndexesメソッドも参照してください。

仮想プロパティ

仮想プロパティは、取得および設定できますが、MongoDBに永続化されないドキュメントプロパティです。ゲッターは、フィールドのフォーマットまたは組み合わせに役立ち、セッターは、単一の値を複数の値に分解して保存するのに役立ちます。

// define a schema
const personSchema = new Schema({
  name: {
    first: String,
    last: String
  }
});

// compile our model
const Person = mongoose.model('Person', personSchema);

// create a document
const axl = new Person({
  name: { first: 'Axl', last: 'Rose' }
});

人のフルネームを出力したいとします。自分で行うことができます。

console.log(axl.name.first + ' ' + axl.name.last); // Axl Rose

しかし、毎回氏名と名を連結するのは面倒です。そして、アクセント記号を削除するなど、名前に対して追加の処理を行いたい場合はどうすればよいでしょうか?仮想プロパティゲッターを使用すると、MongoDBに永続化されないfullNameプロパティを定義できます。

// That can be done either by adding it to schema options:
const personSchema = new Schema({
  name: {
    first: String,
    last: String
  }
}, {
  virtuals: {
    fullName: {
      get() {
        return this.name.first + ' ' + this.name.last;
      }
    }
  }
});

// Or by using the virtual method as following:
personSchema.virtual('fullName').get(function() {
  return this.name.first + ' ' + this.name.last;
});

これで、fullNameプロパティにアクセスするたびに、Mongooseはゲッター関数を呼び出します。

console.log(axl.fullName); // Axl Rose

toJSON()またはtoObject()を使用する場合、Mongooseはデフォルトで仮想プロパティを含めません。仮想プロパティを含めるには、toJSON()またはtoObject(){ virtuals: true }を渡します。

// Convert `doc` to a POJO, with virtuals attached
doc.toObject({ virtuals: true });

// Equivalent:
doc.toJSON({ virtuals: true });

上記のtoJSON()に関する注意点は、JSON.stringify()をMongooseドキュメントで呼び出した出力にも適用されます。JSON.stringify()toJSON()を呼び出すためです。JSON.stringify()の出力に仮想プロパティを含めるには、JSON.stringify()を呼び出す前にドキュメントでtoObject({ virtuals: true })を呼び出すか、スキーマでtoJSON: { virtuals: true }オプションを設定します。

// Explicitly add virtuals to `JSON.stringify()` output
JSON.stringify(doc.toObject({ virtuals: true }));

// Or, to automatically attach virtuals to `JSON.stringify()` output:
const personSchema = new Schema({
  name: {
    first: String,
    last: String
  }
}, {
  toJSON: { virtuals: true } // <-- include virtuals in `JSON.stringify()`
});

仮想プロパティにカスタムセッターを追加して、fullName仮想プロパティを介して名前と姓の両方を設定することもできます。

// Again that can be done either by adding it to schema options:
const personSchema = new Schema({
  name: {
    first: String,
    last: String
  }
}, {
  virtuals: {
    fullName: {
      get() {
        return this.name.first + ' ' + this.name.last;
      },
      set(v) {
        this.name.first = v.substr(0, v.indexOf(' '));
        this.name.last = v.substr(v.indexOf(' ') + 1);
      }
    }
  }
});

// Or by using the virtual method as following:
personSchema.virtual('fullName').
  get(function() {
    return this.name.first + ' ' + this.name.last;
  }).
  set(function(v) {
    this.name.first = v.substr(0, v.indexOf(' '));
    this.name.last = v.substr(v.indexOf(' ') + 1);
  });

axl.fullName = 'William Rose'; // Now `axl.name.first` is "William"

仮想プロパティセッターは、他のバリデーションの前に適用されます。そのため、上記の例は、firstlastの名前フィールドが必須の場合でも機能します。

クエリおよびフィールド選択の一部として機能するのは、仮想プロパティ以外のプロパティのみです。仮想プロパティはMongoDBに保存されないため、それらを使用してクエリを実行することはできません。

仮想プロパティの詳細はこちら

エイリアス

エイリアスは、ゲッターとセッターがシームレスに別のプロパティを取得および設定する特定の種類の仮想プロパティです。これはネットワーク帯域幅を節約するのに役立つため、データベースに保存されている短いプロパティ名を、コードの可読性のために長い名前に変換できます。

const personSchema = new Schema({
  n: {
    type: String,
    // Now accessing `name` will get you the value of `n`, and setting `name` will set the value of `n`
    alias: 'name'
  }
});

// Setting `name` will propagate to `n`
const person = new Person({ name: 'Val' });
console.log(person); // { n: 'Val' }
console.log(person.toObject({ virtuals: true })); // { n: 'Val', name: 'Val' }
console.log(person.name); // "Val"

person.name = 'Not Val';
console.log(person); // { n: 'Not Val' }

ネストされたパスにもエイリアスを宣言できます。サブドキュメントとネストされたスキーマを使用する方が簡単ですが、nested.myPropをエイリアスとして使用する限り、ネストされたパスのエイリアスをインラインで宣言することもできます。

const childSchema = new Schema({
  n: {
    type: String,
    alias: 'name'
  }
}, { _id: false });

const parentSchema = new Schema({
  // If in a child schema, alias doesn't need to include the full nested path
  c: childSchema,
  name: {
    f: {
      type: String,
      // Alias needs to include the full nested path if declared inline
      alias: 'name.first'
    }
  }
});

オプション

スキーマには、コンストラクターまたはsetメソッドに渡すことができるいくつかの構成可能なオプションがあります。

new Schema({ /* ... */ }, options);

// or

const schema = new Schema({ /* ... */ });
schema.set(option, value);

有効なオプション

オプション: autoIndex

デフォルトでは、Mongooseのinit()関数は、MongoDBへの接続に成功した後、Model.createIndexes()を呼び出すことによって、モデルのスキーマで定義されたすべてのインデックスを作成します。インデックスを自動的に作成することは、開発環境とテスト環境には最適です。しかし、インデックスの構築は、本番データベースに大きな負荷をかける可能性もあります。本番環境でインデックスを慎重に管理する場合は、autoIndexをfalseに設定できます。

const schema = new Schema({ /* ... */ }, { autoIndex: false });
const Clock = mongoose.model('Clock', schema);
Clock.ensureIndexes(callback);

autoIndexオプションは、デフォルトでtrueに設定されています。mongoose.set('autoIndex', false);を設定することで、このデフォルトを変更できます。

オプション: autoCreate

Mongooseがインデックスを作成する前に、デフォルトではModel.createCollection()を呼び出して、MongoDBで基礎となるコレクションを作成します。createCollection()を呼び出すと、コレクションのデフォルトの照合順序照合順序オプションに基づいて設定され、cappedスキーマオプションを設定した場合、コレクションはキャップされたコレクションとして確立されます。

autoCreatefalse に設定することで、この動作を無効にできます。 mongoose.set('autoCreate', false) を使用してください。autoIndex と同様に、autoCreate は開発およびテスト環境に役立ちますが、不要なデータベース呼び出しを避けるために、本番環境では無効にすることをお勧めします。

残念ながら、createCollection() は既存のコレクションを変更できません。たとえば、スキーマに capped: { size: 1024 } を追加し、既存のコレクションがキャップされていない場合、createCollection() は既存のコレクションを**上書きしません**。これは、MongoDB サーバーがコレクションを最初に削除せずにコレクションのオプションを変更することを許可していないためです。

const schema = new Schema({ name: String }, {
  autoCreate: false,
  capped: { size: 1024 }
});
const Test = mongoose.model('Test', schema);

// No-op if collection already exists, even if the collection is not capped.
// This means that `capped` won't be applied if the 'tests' collection already exists.
await Test.createCollection();

オプション: bufferCommands

デフォルトでは、Mongoose は接続が切断されると、ドライバーが再接続できるまでコマンドをバッファリングします。バッファリングを無効にするには、bufferCommands を false に設定します。

const schema = new Schema({ /* ... */ }, { bufferCommands: false });

スキーマの bufferCommands オプションは、グローバルな bufferCommands オプションをオーバーライドします。

mongoose.set('bufferCommands', true);
// Schema option below overrides the above, if the schema option is set.
const schema = new Schema({ /* ... */ }, { bufferCommands: false });

オプション: bufferTimeoutMS

bufferCommands が有効になっている場合、このオプションは、Mongoose のバッファリングがエラーをスローするまで待機する最大時間を設定します。指定しない場合、Mongoose は 10000(10 秒)を使用します。

// If an operation is buffered for more than 1 second, throw an error.
const schema = new Schema({ /* ... */ }, { bufferTimeoutMS: 1000 });

オプション: capped

Mongoose は MongoDB の キャップされたコレクション をサポートしています。基になる MongoDB コレクションを capped に指定するには、capped オプションをコレクションの最大サイズ(バイト単位)に設定します。

new Schema({ /* ... */ }, { capped: 1024 });

max のような追加のオプションを渡す必要がある場合は、capped オプションをオブジェクトに設定することもできます。こちらを参照してください。この場合、必須である size オプションを明示的に渡す必要があります。

new Schema({ /* ... */ }, { capped: { size: 1024, max: 1000, autoIndexId: true } });

オプション: collection

Mongoose はデフォルトで、モデル名を utils.toCollectionName メソッドに渡すことでコレクション名を作成します。このメソッドは名前を複数形にします。コレクションに別の名前が必要な場合は、このオプションを設定してください。

const dataSchema = new Schema({ /* ... */ }, { collection: 'data' });

オプション: discriminatorKey

ディスクリミネーター を定義すると、Mongoose はドキュメントがどのディスクリミネーターのインスタンスであるかを格納するパスをスキーマに追加します。デフォルトでは、Mongoose は __t パスを追加しますが、discriminatorKey を設定してこのデフォルトを上書きできます。

const baseSchema = new Schema({}, { discriminatorKey: 'type' });
const BaseModel = mongoose.model('Test', baseSchema);

const personSchema = new Schema({ name: String });
const PersonModel = BaseModel.discriminator('Person', personSchema);

const doc = new PersonModel({ name: 'James T. Kirk' });
// Without `discriminatorKey`, Mongoose would store the discriminator
// key in `__t` instead of `type`
doc.type; // 'Person'

オプション: excludeIndexes

excludeIndexestrue の場合、Mongoose は指定されたサブドキュメントスキーマからインデックスを作成しません。このオプションは、スキーマがサブドキュメントパスまたはドキュメント配列パスで使用されている場合にのみ機能します。モデルのトップレベルスキーマに設定されている場合、Mongoose はこのオプションを無視します。デフォルトは false です。

const childSchema1 = Schema({
  name: { type: String, index: true }
});

const childSchema2 = Schema({
  name: { type: String, index: true }
}, { excludeIndexes: true });

// Mongoose will create an index on `child1.name`, but **not** `child2.name`, because `excludeIndexes`
// is true on `childSchema2`
const User = new Schema({
  name: { type: String, index: true },
  child1: childSchema1,
  child2: childSchema2
});

オプション: id

Mongoose は、各スキーマに id 仮想ゲッターをデフォルトで割り当てます。これは、ドキュメントの _id フィールドを文字列にキャストした値を返します。ObjectId の場合は、その hexString を返します。スキーマに id ゲッターを追加したくない場合は、スキーマの構築時にこのオプションを渡して無効にすることができます。

// default behavior
const schema = new Schema({ name: String });
const Page = mongoose.model('Page', schema);
const p = new Page({ name: 'mongodb.org' });
console.log(p.id); // '50341373e894ad16347efe01'

// disabled id
const schema = new Schema({ name: String }, { id: false });
const Page = mongoose.model('Page', schema);
const p = new Page({ name: 'mongodb.org' });
console.log(p.id); // undefined

オプション: _id

Mongoose は、Schema コンストラクターに渡されていない場合、各スキーマに _id フィールドをデフォルトで割り当てます。割り当てられる型は、MongoDB のデフォルトの動作に合わせて ObjectId です。スキーマに _id をまったく追加したくない場合は、このオプションを使用して無効にすることができます。

このオプションは、**サブドキュメントでのみ**使用できます。Mongoose は ID を知らずにドキュメントを保存できないため、_id がないドキュメントを保存しようとするとエラーが発生します。

// default behavior
const schema = new Schema({ name: String });
const Page = mongoose.model('Page', schema);
const p = new Page({ name: 'mongodb.org' });
console.log(p); // { _id: '50341373e894ad16347efe01', name: 'mongodb.org' }

// disabled _id
const childSchema = new Schema({ name: String }, { _id: false });
const parentSchema = new Schema({ children: [childSchema] });

const Model = mongoose.model('Model', parentSchema);

Model.create({ children: [{ name: 'Luke' }] }, (error, doc) => {
  // doc.children[0]._id will be undefined
});

オプション: minimize

Mongoose は、デフォルトで、空のオブジェクトを削除することにより、スキーマを「最小化」します。

const schema = new Schema({ name: String, inventory: {} });
const Character = mongoose.model('Character', schema);

// will store `inventory` field if it is not empty
const frodo = new Character({ name: 'Frodo', inventory: { ringOfPower: 1 } });
await frodo.save();
let doc = await Character.findOne({ name: 'Frodo' }).lean();
doc.inventory; // { ringOfPower: 1 }

// will not store `inventory` field if it is empty
const sam = new Character({ name: 'Sam', inventory: {} });
await sam.save();
doc = await Character.findOne({ name: 'Sam' }).lean();
doc.inventory; // undefined

この動作は、minimize オプションを false に設定することでオーバーライドできます。そうすると、空のオブジェクトが保存されます。

const schema = new Schema({ name: String, inventory: {} }, { minimize: false });
const Character = mongoose.model('Character', schema);

// will store `inventory` if empty
const sam = new Character({ name: 'Sam', inventory: {} });
await sam.save();
doc = await Character.findOne({ name: 'Sam' }).lean();
doc.inventory; // {}

オブジェクトが空かどうかを確認するには、$isEmpty() ヘルパーを使用できます。

const sam = new Character({ name: 'Sam', inventory: {} });
sam.$isEmpty('inventory'); // true

sam.inventory.barrowBlade = 1;
sam.$isEmpty('inventory'); // false

オプション: read

スキーマレベルで query#read オプションを設定できます。これにより、モデルから派生したすべてのクエリにデフォルトの ReadPreferences を適用する方法が提供されます。

const schema = new Schema({ /* ... */ }, { read: 'primary' });            // also aliased as 'p'
const schema = new Schema({ /* ... */ }, { read: 'primaryPreferred' });   // aliased as 'pp'
const schema = new Schema({ /* ... */ }, { read: 'secondary' });          // aliased as 's'
const schema = new Schema({ /* ... */ }, { read: 'secondaryPreferred' }); // aliased as 'sp'
const schema = new Schema({ /* ... */ }, { read: 'nearest' });            // aliased as 'n'

各 pref のエイリアスも許可されているため、'secondaryPreferred' を入力してスペルミスをする代わりに、'sp' を渡すだけで済みます。

read オプションを使用すると、タグセットを指定することもできます。これにより、ドライバー に、レプリカセットのどのメンバーから読み取りを試みるべきかを指示できます。タグセットの詳細については、こちらこちらを参照してください。

注: 接続時にドライバーの読み取り優先順位戦略オプションを指定することもできます。

// pings the replset members periodically to track network latency
const options = { replset: { strategy: 'ping' } };
mongoose.connect(uri, options);

const schema = new Schema({ /* ... */ }, { read: ['nearest', { disk: 'ssd' }] });
mongoose.model('JellyBean', schema);

オプション: writeConcern

スキーマレベルで 書き込みに関する懸念事項 を設定できます。

const schema = new Schema({ name: String }, {
  writeConcern: {
    w: 'majority',
    j: true,
    wtimeout: 1000
  }
});

オプション: shardKey

shardKey オプションは、シャード化された MongoDB アーキテクチャ を使用する場合に使用されます。各シャード化されたコレクションには、すべての挿入/更新操作に存在する必要があるシャードキーが与えられます。このスキーマオプションを同じシャードキーに設定するだけで済みます。

new Schema({ /* ... */ }, { shardKey: { tag: 1, name: 1 } });

Mongoose は shardcollection コマンドを自動的に送信しません。シャードは自分で構成する必要があります。

オプション: strict

strict オプション(デフォルトで有効)は、スキーマで指定されていない値がモデルコンストラクターに渡された場合、それらの値がデータベースに保存されないようにします。

const thingSchema = new Schema({ /* ... */ })
const Thing = mongoose.model('Thing', thingSchema);
const thing = new Thing({ iAmNotInTheSchema: true });
thing.save(); // iAmNotInTheSchema is not saved to the db

// set to false..
const thingSchema = new Schema({ /* ... */ }, { strict: false });
const thing = new Thing({ iAmNotInTheSchema: true });
thing.save(); // iAmNotInTheSchema is now saved to the db!!

これは、doc.set() を使用してプロパティ値を設定する場合にも影響します。

const thingSchema = new Schema({ /* ... */ });
const Thing = mongoose.model('Thing', thingSchema);
const thing = new Thing;
thing.set('iAmNotInTheSchema', true);
thing.save(); // iAmNotInTheSchema is not saved to the db

この値は、2 番目のブール引数を渡すことで、モデルインスタンスレベルでオーバーライドできます。

const Thing = mongoose.model('Thing');
const thing = new Thing(doc, true);  // enables strict mode
const thing = new Thing(doc, false); // disables strict mode

strict オプションを "throw" に設定することもできます。そうすると、不良データが削除される代わりにエラーが発生します。

注: スキーマに存在しないキー/値がインスタンスに設定されている場合、スキーマオプションに関係なく、常に無視されます。

const thingSchema = new Schema({ /* ... */ });
const Thing = mongoose.model('Thing', thingSchema);
const thing = new Thing;
thing.iAmNotInTheSchema = true;
thing.save(); // iAmNotInTheSchema is never saved to the db

オプション: strictQuery

Mongoose は、クエリフィルターに対して厳格モードを回避するための個別の strictQuery オプションをサポートしています。これは、空のクエリフィルターによって Mongoose がモデル内のすべてのドキュメントを返すことが原因で問題が発生する可能性があるためです。

const mySchema = new Schema({ field: Number }, { strict: true });
const MyModel = mongoose.model('Test', mySchema);
// Mongoose will filter out `notInSchema: 1` because `strict: true`, meaning this query will return
// _all_ documents in the 'tests' collection
MyModel.find({ notInSchema: 1 });

strict オプションは更新に適用されます。strictQuery オプションは、クエリフィルターのみに適用されます。

// Mongoose will strip out `notInSchema` from the update if `strict` is
// not `false`
MyModel.updateMany({}, { $set: { notInSchema: 1 } });

Mongoose には、クエリへの filter パラメーターに対して厳格モードを切り替えるための個別の strictQuery オプションがあります。

const mySchema = new Schema({ field: Number }, {
  strict: true,
  strictQuery: false // Turn off strict mode for query filters
});
const MyModel = mongoose.model('Test', mySchema);
// Mongoose will not strip out `notInSchema: 1` because `strictQuery` is false
MyModel.find({ notInSchema: 1 });

一般的に、ユーザー定義のオブジェクトをクエリフィルターとして渡すことは**お勧めしません**。

// Don't do this!
const docs = await MyModel.find(req.query);

// Do this instead:
const docs = await MyModel.find({ name: req.query.name, age: req.query.age }).setOptions({ sanitizeFilter: true });

Mongoose 7 では、strictQuery はデフォルトで false です。ただし、この動作はグローバルにオーバーライドできます。

// Set `strictQuery` to `true` to omit unknown fields in queries.
mongoose.set('strictQuery', true);

オプション: toJSON

toObject オプションとまったく同じですが、ドキュメントの toJSON メソッド が呼び出された場合にのみ適用されます。

const schema = new Schema({ name: String });
schema.path('name').get(function(v) {
  return v + ' is my name';
});
schema.set('toJSON', { getters: true, virtuals: false });
const M = mongoose.model('Person', schema);
const m = new M({ name: 'Max Headroom' });
console.log(m.toObject()); // { _id: 504e0cd7dd992d9be2f20b6f, name: 'Max Headroom' }
console.log(m.toJSON()); // { _id: 504e0cd7dd992d9be2f20b6f, name: 'Max Headroom is my name' }
// since we know toJSON is called whenever a js object is stringified:
console.log(JSON.stringify(m)); // { "_id": "504e0cd7dd992d9be2f20b6f", "name": "Max Headroom is my name" }

使用可能なすべての toJSON/toObject オプションを表示するには、こちらを参照してください。

オプション: toObject

ドキュメントには、Mongoose ドキュメントをプレーンな JavaScript オブジェクトに変換する toObject メソッドがあります。このメソッドはいくつかのオプションを受け付けます。これらのオプションをドキュメントごとに適用する代わりに、スキーマレベルでオプションを宣言し、デフォルトでスキーマのすべてのドキュメントに適用することができます。

すべての仮想プロパティを console.log 出力に表示するには、toObject オプションを { getters: true } に設定します。

const schema = new Schema({ name: String });
schema.path('name').get(function(v) {
  return v + ' is my name';
});
schema.set('toObject', { getters: true });
const M = mongoose.model('Person', schema);
const m = new M({ name: 'Max Headroom' });
console.log(m); // { _id: 504e0cd7dd992d9be2f20b6f, name: 'Max Headroom is my name' }

使用可能なすべての toObject オプションを表示するには、こちらを参照してください。

オプション: typeKey

デフォルトでは、スキーマにキー 'type' を持つオブジェクトがある場合、Mongoose はそれを型宣言として解釈します。

// Mongoose interprets this as 'loc is a String'
const schema = new Schema({ loc: { type: String, coordinates: [Number] } });

geoJSON のようなアプリケーションでは、'type' プロパティが重要です。Mongoose が型宣言を見つけるために使用するキーを制御する場合は、'typeKey' スキーマオプションを設定します。

const schema = new Schema({
  // Mongoose interprets this as 'loc is an object with 2 keys, type and coordinates'
  loc: { type: String, coordinates: [Number] },
  // Mongoose interprets this as 'name is a String'
  name: { $type: String }
}, { typeKey: '$type' }); // A '$type' key means this object is a type declaration

オプション: validateBeforeSave

デフォルトでは、ドキュメントはデータベースに保存される前に自動的に検証されます。これは、無効なドキュメントの保存を防ぐためです。検証を手動で処理し、検証に合格しないオブジェクトを保存できるようにする場合は、validateBeforeSave を false に設定できます。

const schema = new Schema({ name: String });
schema.set('validateBeforeSave', false);
schema.path('name').validate(function(value) {
  return value != null;
});
const M = mongoose.model('Person', schema);
const m = new M({ name: null });
m.validate(function(err) {
  console.log(err); // Will tell you that null is not allowed.
});
m.save(); // Succeeds despite being invalid

オプション: versionKey

versionKey は、Mongoose によって最初に作成されたときに各ドキュメントに設定されるプロパティです。このキーの値には、ドキュメントの内部 リビジョン が含まれています。versionKey オプションは、バージョン管理に使用するパスを表す文字列です。デフォルトは __v です。これがアプリケーションと競合する場合は、次のように構成できます。

const schema = new Schema({ name: 'string' });
const Thing = mongoose.model('Thing', schema);
const thing = new Thing({ name: 'mongoose v3' });
await thing.save(); // { __v: 0, name: 'mongoose v3' }

// customized versionKey
new Schema({ /* ... */ }, { versionKey: '_somethingElse' })
const Thing = mongoose.model('Thing', schema);
const thing = new Thing({ name: 'mongoose v3' });
thing.save(); // { _somethingElse: 0, name: 'mongoose v3' }

Mongoose のデフォルトのバージョン管理は、完全な 楽観的同時実行制御 ソリューションではありません。Mongoose のデフォルトのバージョン管理は、下記のように配列でのみ動作します。

// 2 copies of the same document
const doc1 = await Model.findOne({ _id });
const doc2 = await Model.findOne({ _id });

// Delete first 3 comments from `doc1`
doc1.comments.splice(0, 3);
await doc1.save();

// The below `save()` will throw a VersionError, because you're trying to
// modify the comment at index 1, and the above `splice()` removed that
// comment.
doc2.set('comments.1.body', 'new comment');
await doc2.save();

save() に対する楽観的同時実行制御のサポートが必要な場合は、optimisticConcurrency オプション を設定できます。

versionKeyfalse に設定することで、ドキュメントのバージョン管理を無効にすることもできます。何が起こるかを理解していない限り、バージョン管理を無効にしないでください。

new Schema({ /* ... */ }, { versionKey: false });
const Thing = mongoose.model('Thing', schema);
const thing = new Thing({ name: 'no versioning please' });
thing.save(); // { name: 'no versioning please' }

Mongoose は save() を使用した場合にのみバージョンキーを更新します。update()findOneAndUpdate() などを使用する場合、Mongoose はバージョンキーを**更新しません**。回避策として、以下のミドルウェアを使用できます。

schema.pre('findOneAndUpdate', function() {
  const update = this.getUpdate();
  if (update.__v != null) {
    delete update.__v;
  }
  const keys = ['$set', '$setOnInsert'];
  for (const key of keys) {
    if (update[key] != null && update[key].__v != null) {
      delete update[key].__v;
      if (Object.keys(update[key]).length === 0) {
        delete update[key];
      }
    }
  }
  update.$inc = update.$inc || {};
  update.$inc.__v = 1;
});

オプション: optimisticConcurrency

楽観的同時実行制御 は、find() または findOne() を使用してロードしたときと、save() を使用して更新したときの間に、更新しているドキュメントが変更されていないことを確認するための戦略です。

たとえば、photos のリストと、この住宅が検索に表示されるかどうかを表す status を含む House モデルがあるとします。ステータスが 'APPROVED' の住宅には、少なくとも 2 つの photos が必要です。住宅ドキュメントを承認するロジックは、次のように実装できます。

async function markApproved(id) {
  const house = await House.findOne({ _id });
  if (house.photos.length < 2) {
    throw new Error('House must have at least two photos!');
  }

  house.status = 'APPROVED';
  await house.save();
}

markApproved() 関数は単独では正しく見えますが、潜在的な問題がある可能性があります。findOne() 呼び出しと save() 呼び出しの間に、別の関数が住宅の写真を削除した場合、どうなるでしょうか?たとえば、次のコードは成功します。

const house = await House.findOne({ _id });
if (house.photos.length < 2) {
  throw new Error('House must have at least two photos!');
}

const house2 = await House.findOne({ _id });
house2.photos = [];
await house2.save();

// Marks the house as 'APPROVED' even though it has 0 photos!
house.status = 'APPROVED';
await house.save();

HouseモデルのスキーマでoptimisticConcurrencyオプションを設定した場合、上記のスクリプトはエラーをスローします。

const House = mongoose.model('House', Schema({
  status: String,
  photos: [String]
}, { optimisticConcurrency: true }));

const house = await House.findOne({ _id });
if (house.photos.length < 2) {
  throw new Error('House must have at least two photos!');
}

const house2 = await House.findOne({ _id });
house2.photos = [];
await house2.save();

// Throws 'VersionError: No matching document found for id "..." version 0'
house.status = 'APPROVED';
await house.save();

オプション: collation

すべてのクエリと集約に対するデフォルトの照合順序を設定します。照合順序の概要はこちら(初心者向け)

const schema = new Schema({
  name: String
}, { collation: { locale: 'en_US', strength: 1 } });

const MyModel = db.model('MyModel', schema);

MyModel.create([{ name: 'val' }, { name: 'Val' }]).
  then(() => {
    return MyModel.find({ name: 'val' });
  }).
  then((docs) => {
    // `docs` will contain both docs, because `strength: 1` means
    // MongoDB will ignore case when matching.
  });

オプション: timeseries

スキーマでtimeseriesオプションを設定すると、Mongooseはそのスキーマから作成するすべてのモデルに対して時系列コレクションを作成します。

const schema = Schema({ name: String, timestamp: Date, metadata: Object }, {
  timeseries: {
    timeField: 'timestamp',
    metaField: 'metadata',
    granularity: 'hours'
  },
  autoCreate: false,
  expireAfterSeconds: 86400
});

// `Test` collection will be a timeseries collection
const Test = db.model('Test', schema);

オプション: skipVersioning

skipVersioningを使用すると、バージョン管理からパスを除外できます(つまり、これらのパスが更新されても、内部リビジョンはインクリメントされません)。自分のやっていることが完全に理解していない限り、これを実行しないでください。サブドキュメントの場合、完全修飾パスを使用して親ドキュメントにこれを含めます。

new Schema({ /* ... */ }, { skipVersioning: { dontVersionMe: true } });
thing.dontVersionMe.push('hey');
thing.save(); // version is not incremented

オプション: timestamps

timestampsオプションは、MongooseにスキーマにcreatedAtフィールドとupdatedAtフィールドを割り当てるように指示します。割り当てられる型はDateです。

デフォルトでは、フィールド名はcreatedAtupdatedAtです。timestamps.createdAttimestamps.updatedAtを設定して、フィールド名をカスタマイズできます。

timestampsが内部で動作する方法は次のとおりです。

  • 新しいドキュメントを作成する場合、MongooseはcreatedAtupdatedAtを作成時刻に設定します。
  • ドキュメントを更新する場合、MongooseはupdatedAt$setオブジェクトに追加します。
  • 更新操作でupsert: trueを設定した場合、Mongooseは$setOnInsert演算子を使用して、upsert操作によって新しいドキュメントが挿入された場合にcreatedAtをドキュメントに追加します。
const thingSchema = new Schema({ /* ... */ }, { timestamps: { createdAt: 'created_at' } });
const Thing = mongoose.model('Thing', thingSchema);
const thing = new Thing();
await thing.save(); // `created_at` & `updatedAt` will be included

// With updates, Mongoose will add `updatedAt` to `$set`
await Thing.updateOne({}, { $set: { name: 'Test' } });

// If you set upsert: true, Mongoose will add `created_at` to `$setOnInsert` as well
await Thing.findOneAndUpdate({}, { $set: { name: 'Test2' } });

// Mongoose also adds timestamps to bulkWrite() operations
// See https://mongoose.dokyumento.jp/docs/api/model.html#model_Model-bulkWrite
await Thing.bulkWrite([
  {
    insertOne: {
      document: {
        name: 'Jean-Luc Picard',
        ship: 'USS Stargazer'
      // Mongoose will add `created_at` and `updatedAt`
      }
    }
  },
  {
    updateOne: {
      filter: { name: 'Jean-Luc Picard' },
      update: {
        $set: {
          ship: 'USS Enterprise'
        // Mongoose will add `updatedAt`
        }
      }
    }
  }
]);

デフォルトでは、Mongooseは現在時刻を取得するためにnew Date()を使用します。現在時刻を取得するためにMongooseが使用する関数を上書きする場合は、timestamps.currentTimeオプションを設定できます。Mongooseは、現在時刻を取得する必要があるたびにtimestamps.currentTime関数を呼び出します。

const schema = Schema({
  createdAt: Number,
  updatedAt: Number,
  name: String
}, {
  // Make Mongoose use Unix time (seconds since Jan 1, 1970)
  timestamps: { currentTime: () => Math.floor(Date.now() / 1000) }
});

オプション: pluginTags

Mongooseは、すべてのスキーマに適用されるグローバルプラグインを定義することをサポートしています。

// Add a `meta` property to all schemas
mongoose.plugin(function myPlugin(schema) {
  schema.add({ meta: {} });
});

特定のプラグインを一部のスキーマのみに適用する場合があります。その場合は、スキーマにpluginTagsを追加できます。

const schema1 = new Schema({
  name: String
}, { pluginTags: ['useMetaPlugin'] });

const schema2 = new Schema({
  name: String
});

tagsオプションを使用してplugin()を呼び出すと、MongooseはそのプラグインをpluginTagsに一致するエントリを持つスキーマのみに適用します。

// Add a `meta` property to all schemas
mongoose.plugin(function myPlugin(schema) {
  schema.add({ meta: {} });
}, { tags: ['useMetaPlugin'] });

オプション: selectPopulatedPaths

デフォルトでは、Mongooseは明示的に除外しない限り、自動的にすべての関連付けられたパスをselect()します。

const bookSchema = new Schema({
  title: 'String',
  author: { type: 'ObjectId', ref: 'Person' }
});
const Book = mongoose.model('Book', bookSchema);

// By default, Mongoose will add `author` to the below `select()`.
await Book.find().select('title').populate('author');

// In other words, the below query is equivalent to the above
await Book.find().select('title author').populate('author');

デフォルトで関連付けられたフィールドの選択をオプトアウトするには、スキーマでselectPopulatedPathsfalseに設定します。

const bookSchema = new Schema({
  title: 'String',
  author: { type: 'ObjectId', ref: 'Person' }
}, { selectPopulatedPaths: false });
const Book = mongoose.model('Book', bookSchema);

// Because `selectPopulatedPaths` is false, the below doc will **not**
// contain an `author` property.
const doc = await Book.findOne().select('title').populate('author');

オプション: storeSubdocValidationError

レガシーの理由により、単一のネストされたスキーマのサブパスで検証エラーが発生した場合、Mongooseはその単一のネストされたスキーマパスにも検証エラーがあったことを記録します。例:

const childSchema = new Schema({ name: { type: String, required: true } });
const parentSchema = new Schema({ child: childSchema });

const Parent = mongoose.model('Parent', parentSchema);

// Will contain an error for both 'child.name' _and_ 'child'
new Parent({ child: {} }).validateSync().errors;

Mongooseが親エラーのみを報告するようにするには、子スキーマでstoreSubdocValidationErrorfalseに設定します。

const childSchema = new Schema({
  name: { type: String, required: true }
}, { storeSubdocValidationError: false }); // <-- set on the child schema
const parentSchema = new Schema({ child: childSchema });

const Parent = mongoose.model('Parent', parentSchema);

// Will only contain an error for 'child.name'
new Parent({ child: {} }).validateSync().errors;

オプション: collectionOptions

collationcappedなどのオプションは、新しいコレクションを作成する際にMongooseがMongoDBに渡すオプションに影響を与えます。MongooseスキーマはほとんどのMongoDB createCollection()オプションをサポートしますが、すべてではありません。collectionOptionsオプションを使用して任意のcreateCollection()オプションを設定できます。Mongooseは、スキーマに対してcreateCollection()を呼び出す際に、collectionOptionsをデフォルト値として使用します。

const schema = new Schema({ name: String }, {
  autoCreate: false,
  collectionOptions: {
    capped: true,
    max: 1000
  }
});
const Test = mongoose.model('Test', schema);

// Equivalent to `createCollection({ capped: true, max: 1000 })`
await Test.createCollection();

オプション: autoSearchIndex

autoIndexに似ていますが、スキーマで定義されているAtlas検索インデックスを自動的に作成します。autoIndexとは異なり、このオプションはデフォルトでfalseです。

const schema = new Schema({ name: String }, { autoSearchIndex: true });
schema.searchIndex({
  name: 'my-index',
  definition: { mappings: { dynamic: true } }
});
// Will automatically attempt to create the `my-index` search index.
const Test = mongoose.model('Test', schema);

オプション: readConcern

リードコンサーンwriteConcernに似ていますが、find()findOne()などの読み取り操作用です。デフォルトのreadConcernを設定するには、次のようにreadConcernオプションをスキーマコンストラクタに渡します。

const eventSchema = new mongoose.Schema(
  { name: String },
  {
    readConcern: { level: 'available' } // <-- set default readConcern for all queries
  }
);

ES6クラスを使用

スキーマには、ES6クラスからMongooseスキーマを作成するために使用できるloadClass()メソッドがあります。

ES6クラスからスキーマを作成するためにloadClass()を使用する例を次に示します。

class MyClass {
  myMethod() { return 42; }
  static myStatic() { return 42; }
  get myVirtual() { return 42; }
}

const schema = new mongoose.Schema();
schema.loadClass(MyClass);

console.log(schema.methods); // { myMethod: [Function: myMethod] }
console.log(schema.statics); // { myStatic: [Function: myStatic] }
console.log(schema.virtuals); // { myVirtual: VirtualType { ... } }

プラグイン可能

スキーマはまたプラグイン可能であり、再利用可能な機能をプラグインにパッケージ化して、コミュニティと共有したり、プロジェクト間で共有したりできます。

参考資料

Mongooseスキーマの代替紹介はこちら。

MongoDBを最大限に活用するには、MongoDBスキーマ設計の基本を学ぶ必要があります。SQLスキーマ設計(第3正規形)はストレージコストを最小限に抑えるために設計されましたが、MongoDBスキーマ設計は一般的なクエリをできるだけ高速にすることを目的としています。MongoDBスキーマ設計の6つの経験則に関するブログシリーズは、クエリを高速化するための基本ルールを学ぶための優れたリソースです。

Node.jsでMongoDBスキーマ設計を習得したいユーザーは、MongoDB Node.jsドライバのオリジナル作者であるChristian KvalheimによるThe Little MongoDB Schema Design Bookを参照してください。この本では、eコマース、ウィキ、予約など、さまざまなユースケースに対してパフォーマンスの高いスキーマを実装する方法を示しています。

次へ

Schemasについて説明したので、次はSchemaTypesを見てみましょう。