スキーマタイプ

スキーマタイプは、パスのデフォルト値バリデーションゲッターセッタークエリにおけるフィールド選択のデフォルト、およびMongooseドキュメントプロパティのその他の一般的な特性の定義を処理します。

スキーマタイプとは?

Mongooseスキーマは、Mongooseモデルの構成オブジェクトと考えることができます。スキーマタイプは、個々のプロパティの構成オブジェクトです。スキーマタイプは、指定されたパスがどのようなタイプであるべきか、ゲッター/セッターがあるかどうか、そしてそのパスにどのような値が有効かを指定します。

const schema = new Schema({ name: String });
schema.path('name') instanceof mongoose.SchemaType; // true
schema.path('name') instanceof mongoose.Schema.Types.String; // true
schema.path('name').instance; // 'String'

スキーマタイプは、タイプとは異なります。つまり、`mongoose.ObjectId !== mongoose.Types.ObjectId`です。スキーマタイプは、Mongooseの構成オブジェクトに過ぎません。 `mongoose.ObjectId`スキーマタイプのインスタンスは、実際にはMongoDB ObjectIdを作成しません。スキーマ内のパスの構成に過ぎません。

Mongooseで有効なスキーマタイプはすべて以下のとおりです。Mongooseプラグインは、int32のようなカスタムスキーマタイプを追加することもできます。プラグインを見つけるには、Mongooseのプラグイン検索をご覧ください。

const schema = new Schema({
  name: String,
  binary: Buffer,
  living: Boolean,
  updated: { type: Date, default: Date.now },
  age: { type: Number, min: 18, max: 65 },
  mixed: Schema.Types.Mixed,
  _someId: Schema.Types.ObjectId,
  decimal: Schema.Types.Decimal128,
  array: [],
  ofString: [String],
  ofNumber: [Number],
  ofDates: [Date],
  ofBuffer: [Buffer],
  ofBoolean: [Boolean],
  ofMixed: [Schema.Types.Mixed],
  ofObjectId: [Schema.Types.ObjectId],
  ofArrays: [[]],
  ofArrayOfNumbers: [[Number]],
  nested: {
    stuff: { type: String, lowercase: true, trim: true }
  },
  map: Map,
  mapOfString: {
    type: Map,
    of: String
  }
});

// example use

const Thing = mongoose.model('Thing', schema);

const m = new Thing;
m.name = 'Statue of Liberty';
m.age = 125;
m.updated = new Date;
m.binary = Buffer.alloc(0);
m.living = false;
m.mixed = { any: { thing: 'i want' } };
m.markModified('mixed');
m._someId = new mongoose.Types.ObjectId;
m.array.push(1);
m.ofString.push('strings!');
m.ofNumber.unshift(1, 2, 3, 4);
m.ofDates.addToSet(new Date);
m.ofBuffer.pop();
m.ofMixed = [1, [], 'three', { four: 5 }];
m.nested.stuff = 'good';
m.map = new Map([['key', 'value']]);
m.save(callback);

`type`キー

`type`は、Mongooseスキーマの特別なプロパティです。Mongooseは、スキーマ内で`type`という名前のネストされたプロパティを見つけると、指定されたタイプでスキーマタイプを定義する必要があると想定します。

// 3 string SchemaTypes: 'name', 'nested.firstName', 'nested.lastName'
const schema = new Schema({
  name: { type: String },
  nested: {
    firstName: { type: String },
    lastName: { type: String }
  }
});

結果として、スキーマに`type`という名前のプロパティを定義するには、少し追加の作業が必要になります。たとえば、株式ポートフォリオアプリを構築していて、資産の`type`(株式、債券、ETFなど)を保存したいとします。単純に、以下に示すようにスキーマを定義するかもしれません。

const holdingSchema = new Schema({
  // You might expect `asset` to be an object that has 2 properties,
  // but unfortunately `type` is special in Mongoose so mongoose
  // interprets this schema to mean that `asset` is a string
  asset: {
    type: String,
    ticker: String
  }
});

しかし、Mongooseは`type: String`を見ると、`asset`が`type`プロパティを持つオブジェクトではなく、文字列であるべきだと想定します。 `type`プロパティを持つオブジェクトを定義する正しい方法は、以下に示すとおりです。

const holdingSchema = new Schema({
  asset: {
    // Workaround to make sure Mongoose knows `asset` is an object
    // and `asset.type` is a string, rather than thinking `asset`
    // is a string.
    type: { type: String },
    ticker: String
  }
});

スキーマタイプのオプション

スキーマタイプは、タイプを直接使用するか、`type`プロパティを持つオブジェクトを使用して宣言できます。

const schema1 = new Schema({
  test: String // `test` is a path of type String
});

const schema2 = new Schema({
  // The `test` object contains the "SchemaType options"
  test: { type: String } // `test` is a path of type string
});

typeプロパティに加えて、パスに追加のプロパティを指定できます。たとえば、保存前に文字列を小文字に変換する場合

const schema2 = new Schema({
  test: {
    type: String,
    lowercase: true // Always convert `test` to lowercase
  }
});

スキーマタイプのオプションには、任意のプロパティを追加できます。多くのプラグインは、カスタムスキーマタイプのオプションに依存しています。たとえば、mongoose-autopopulateプラグインは、スキーマタイプのオプションで`autopopulate: true`を設定すると、パスを自動的に入力します。Mongooseには、上記の例の`lowercase`のように、いくつかの組み込みスキーマタイプのオプションのサポートが付属しています。

`lowercase`オプションは、文字列に対してのみ機能します。すべてのスキーマタイプに適用されるオプションと、特定のスキーマタイプに適用されるオプションがあります。

すべてのスキーマタイプ

  • `required`: booleanまたは関数。trueの場合、このプロパティに必須バリデータを追加します。
  • `default`: Anyまたは関数。パスのデフォルト値を設定します。値が関数の場合は、関数の戻り値がデフォルトとして使用されます。
  • `select`: boolean。クエリのデフォルトのプロジェクションを指定します。
  • `validate`: 関数。このプロパティにバリデータ関数を追加します。
  • `get`: 関数。`Object.defineProperty()`を使用して、このプロパティのカスタムゲッターを定義します。
  • `set`: 関数。`Object.defineProperty()`を使用して、このプロパティのカスタムセッターを定義します。
  • `alias`: 文字列、mongoose >= 4.10.0 のみ。このパスを取得/設定する仮想を指定された名前で定義します。
  • `immutable`: boolean、パスを不変として定義します。 Mongooseは、親ドキュメントに`isNew: true`がない限り、不変パスを変更することを防ぎます。
  • `transform`: 関数。Mongooseは、ドキュメントを`JSON.stringify()`する場合を含め、`Document#toJSON()`関数を呼び出すときにこの関数を呼び出します。
const numberSchema = new Schema({
  integerOnly: {
    type: Number,
    get: v => Math.round(v),
    set: v => Math.round(v),
    alias: 'i'
  }
});

const Number = mongoose.model('Number', numberSchema);

const doc = new Number();
doc.integerOnly = 2.001;
doc.integerOnly; // 2
doc.i; // 2
doc.i = 3.001;
doc.integerOnly; // 3
doc.i; // 3

インデックス

スキーマタイプのオプションを使用して、MongoDBインデックスを定義することもできます。

const schema2 = new Schema({
  test: {
    type: String,
    index: true,
    unique: true // Unique index. If you specify `unique: true`
    // specifying `index: true` is optional if you do `unique: true`
  }
});

文字列

  • `lowercase`: boolean。値に対して常に`.toLowerCase()`を呼び出すかどうか。
  • `uppercase`: boolean。値に対して常に`.toUpperCase()`を呼び出すかどうか。
  • `trim`: boolean。値に対して常に`.trim()`を呼び出すかどうか。
  • `match`: RegExp。値が指定された正規表現と一致するかどうかを確認するバリデータを作成します。
  • `enum`: 配列。値が指定された配列にあるかどうかを確認するバリデータを作成します。
  • `minLength`: 数値。値の長さが指定された数値以上であることを確認するバリデータを作成します。
  • `maxLength`: 数値。値の長さが指定された数値以下であることを確認するバリデータを作成します。
  • `populate`: オブジェクト。デフォルトのpopulateオプションを設定します。

数値

  • `min`: 数値。値が指定された最小値以上であることを確認するバリデータを作成します。
  • `max`: 数値。値が指定された最大値以下であることを確認するバリデータを作成します。
  • `enum`: 配列。値が指定された配列内のいずれかの値と厳密に等しいことを確認するバリデータを作成します。
  • `populate`: オブジェクト。デフォルトのpopulateオプションを設定します。

日付

  • `min`: 日付。値が指定された最小値以上であることを確認するバリデータを作成します。
  • `max`: 日付。値が指定された最大値以下であることを確認するバリデータを作成します。
  • `expires`: 数値または文字列。秒単位で表された値を持つTTLインデックスを作成します。

ObjectId

使用上の注意

文字列

パスを文字列として宣言するには、`String`グローバルコンストラクターまたは文字列`'String'`を使用できます。

const schema1 = new Schema({ name: String }); // name will be cast to string
const schema2 = new Schema({ name: 'String' }); // Equivalent

const Person = mongoose.model('Person', schema2);

`toString()`関数を持つ要素を渡すと、Mongooseはそれを呼び出します。ただし、要素が配列であるか、`toString()`関数が`Object.prototype.toString()`と厳密に等しい場合は除きます。

new Person({ name: 42 }).name; // "42" as a string
new Person({ name: { toString: () => 42 } }).name; // "42" as a string

// "undefined", will get a cast error if you `save()` this document
new Person({ name: { foo: 42 } }).name;

数値

パスを数値として宣言するには、`Number`グローバルコンストラクターまたは文字列`'Number'`を使用できます。

const schema1 = new Schema({ age: Number }); // age will be cast to a Number
const schema2 = new Schema({ age: 'Number' }); // Equivalent

const Car = mongoose.model('Car', schema2);

数値に正常にキャストされる値には、いくつかの種類があります。

new Car({ age: '15' }).age; // 15 as a Number
new Car({ age: true }).age; // 1 as a Number
new Car({ age: false }).age; // 0 as a Number
new Car({ age: { valueOf: () => 83 } }).age; // 83 as a Number

数値を返す`valueOf()`関数を持つオブジェクトを渡すと、Mongooseはそれを呼び出し、返された値をパスに割り当てます。

値`null`と`undefined`はキャストされません。

NaN、NaNにキャストされる文字列、配列、および`valueOf()`関数を持たないオブジェクトはすべて、検証後にCastErrorになります。つまり、初期化時にはスローされず、検証時のみスローされます。

日付

組み込みの`Date`メソッドは、mongooseの変更追跡ロジックにフックされていません。つまり、ドキュメントで`Date`を使用し、`setMonth()`などのメソッドで変更した場合、mongooseはこの変更を認識せず、`doc.save()`はこの変更を永続化しません。組み込みメソッドを使用して`Date`タイプを変更する必要がある場合は、保存する前に`doc.markModified('pathToYourDate')`を使用してmongooseに変更を通知してください。

const Assignment = mongoose.model('Assignment', { dueDate: Date });
const doc = await Assignment.findOne();
doc.dueDate.setMonth(3);
await doc.save(); // THIS DOES NOT SAVE YOUR CHANGE

doc.markModified('dueDate');
await doc.save(); // works

バッファ

パスをBufferとして宣言するには、`Buffer`グローバルコンストラクターまたは文字列`'Buffer'`を使用できます。

const schema1 = new Schema({ binData: Buffer }); // binData will be cast to a Buffer
const schema2 = new Schema({ binData: 'Buffer' }); // Equivalent

const Data = mongoose.model('Data', schema2);

Mongooseは、以下の値をバッファに正常にキャストします。

const file1 = new Data({ binData: 'test'}); // {"type":"Buffer","data":[116,101,115,116]}
const file2 = new Data({ binData: 72987 }); // {"type":"Buffer","data":[27]}
const file4 = new Data({ binData: { type: 'Buffer', data: [1, 2, 3]}}); // {"type":"Buffer","data":[1,2,3]}

混合

「何でもあり」のスキーマタイプ。Mongooseは、混合パスに対してキャストを実行しません。混合パスは、`Schema.Types.Mixed`を使用するか、空のオブジェクトリテラルを渡すことによって定義できます。以下は同等です。

const Any = new Schema({ any: {} });
const Any = new Schema({ any: Object });
const Any = new Schema({ any: Schema.Types.Mixed });
const Any = new Schema({ any: mongoose.Mixed });

Mixedはスキーマレスなタイプなので、値を好きなものに変更できますが、Mongooseはそれらの変更を自動的に検出して保存する機能を失います。Mixedタイプの値が変更されたことをMongooseに伝えるには、変更したMixedタイプへのパスを渡して、`doc.markModified(path)`を呼び出す必要があります。

これらの副作用を回避するために、代わりにサブドキュメントパスを使用できます。

person.anything = { x: [3, 4, { y: 'changed' }] };
person.markModified('anything');
person.save(); // Mongoose will save changes to `anything`.

ObjectId

ObjectIdは、通常、一意の識別子に使用される特別なタイプです。 ObjectIdであるパス`driver`を持つスキーマを宣言する方法は次のとおりです。

const mongoose = require('mongoose');
const carSchema = new mongoose.Schema({ driver: mongoose.ObjectId });

`ObjectId`はクラスであり、ObjectIdはオブジェクトです。ただし、多くの場合、文字列として表されます。 `toString()`を使用してObjectIdを文字列に変換すると、24文字の16進文字列が得られます。

const Car = mongoose.model('Car', carSchema);

const car = new Car();
car.driver = new mongoose.Types.ObjectId();

typeof car.driver; // 'object'
car.driver instanceof mongoose.Types.ObjectId; // true

car.driver.toString(); // Something like "5e1a0651741b255ddda996c4"

真偽値

Mongooseのブール値は、プレーンなJavaScriptのブール値です。デフォルトでは、Mongooseは下記の値を`true`にキャストします。

  • true
  • 'true'
  • 1
  • '1'
  • 'yes'

Mongooseは下記の値を`false`にキャストします。

  • false
  • 'false'
  • 0
  • '0'
  • 'no'

その他の値はCastErrorを引き起こします。 JavaScriptのセットである`convertToTrue`および`convertToFalse`プロパティを使用して、Mongooseがtrueまたはfalseに変換する値を変更できます。

const M = mongoose.model('Test', new Schema({ b: Boolean }));
console.log(new M({ b: 'nay' }).b); // undefined

// Set { false, 'false', 0, '0', 'no' }
console.log(mongoose.Schema.Types.Boolean.convertToFalse);

mongoose.Schema.Types.Boolean.convertToFalse.add('nay');
console.log(new M({ b: 'nay' }).b); // false

配列

Mongooseは、スキーマタイプの配列とサブドキュメントの配列をサポートしています。スキーマタイプの配列は*プリミティブ配列*とも呼ばれ、サブドキュメントの配列は*ドキュメント配列*とも呼ばれます。

const ToySchema = new Schema({ name: String });
const ToyBoxSchema = new Schema({
  toys: [ToySchema],
  buffers: [Buffer],
  strings: [String],
  numbers: [Number]
  // ... etc
});

配列は、暗黙的にデフォルト値`[]`(空の配列)を持つため、特別です。

const ToyBox = mongoose.model('ToyBox', ToyBoxSchema);
console.log((new ToyBox()).toys); // []

このデフォルトを上書きするには、デフォルト値を`undefined`に設定する必要があります。

const ToyBoxSchema = new Schema({
  toys: {
    type: [ToySchema],
    default: undefined
  }
});

注:空の配列を指定することは、`Mixed`と同等です。以下はすべて`Mixed`の配列を作成します。

const Empty1 = new Schema({ any: [] });
const Empty2 = new Schema({ any: Array });
const Empty3 = new Schema({ any: [Schema.Types.Mixed] });
const Empty4 = new Schema({ any: [{}] });

マップ

`MongooseMap`は、JavaScriptの`Map`クラスのサブクラスです。このドキュメントでは、「マップ」と`MongooseMap`という用語を同じ意味で使用します。Mongooseでは、マップは任意のキーを持つネストされたドキュメントを作成する方法です。

注意: Mongooseマップでは、MongoDBにドキュメントを保存するために、キーは文字列でなければなりません。

const userSchema = new Schema({
  // `socialMediaHandles` is a map whose values are strings. A map's
  // keys are always strings. You specify the type of values using `of`.
  socialMediaHandles: {
    type: Map,
    of: String
  }
});

const User = mongoose.model('User', userSchema);
// Map { 'github' => 'vkarpov15', 'twitter' => '@code_barbarian' }
console.log(new User({
  socialMediaHandles: {
    github: 'vkarpov15',
    twitter: '@code_barbarian'
  }
}).socialMediaHandles);

上記の例では、githubtwitter をパスとして明示的に宣言していませんが、socialMediaHandles はマップであるため、任意のキー/値ペアを格納できます。ただし、socialMediaHandles はマップであるため、キーの値を取得するには .get() を、キーの値を設定するには .set()必ず使用する必要があります。

const user = new User({
  socialMediaHandles: {}
});

// Good
user.socialMediaHandles.set('github', 'vkarpov15');
// Works too
user.set('socialMediaHandles.twitter', '@code_barbarian');
// Bad, the `myspace` property will **not** get saved
user.socialMediaHandles.myspace = 'fail';

// 'vkarpov15'
console.log(user.socialMediaHandles.get('github'));
// '@code_barbarian'
console.log(user.get('socialMediaHandles.twitter'));
// undefined
user.socialMediaHandles.github;

// Will only save the 'github' and 'twitter' properties
user.save();

マップ型は、MongoDBではBSONオブジェクトとして保存されます。BSONオブジェクトのキーは順序付けられているため、マップの挿入順序プロパティは維持されます。

Mongooseは、マップ内のすべての要素をpopulateするための特別な $* 構文をサポートしています。たとえば、socialMediaHandles マップに ref が含まれているとします。

const userSchema = new Schema({
  socialMediaHandles: {
    type: Map,
    of: new Schema({
      handle: String,
      oauth: {
        type: ObjectId,
        ref: 'OAuth'
      }
    })
  }
});
const User = mongoose.model('User', userSchema);

すべての socialMediaHandles エントリの oauth プロパティをpopulateするには、socialMediaHandles.$*.oauth でpopulateする必要があります。

const user = await User.findOne().populate('socialMediaHandles.$*.oauth');

UUID

Mongooseは、UUIDインスタンスをNode.jsバッファとして格納するUUID型もサポートしています。Mongooseでは、一意のドキュメントIDにはUUIDではなくObjectIdを使用することをお勧めしますが、必要な場合はUUIDを使用できます。

Node.jsでは、UUIDは、アクセス時にバイナリを文字列に変換するgetterを持つ bson.Binary 型のインスタンスとして表現されます。Mongooseは、UUIDをMongoDBにサブタイプ4のバイナリデータとして格納します。

const authorSchema = new Schema({
  _id: Schema.Types.UUID, // Can also do `_id: 'UUID'`
  name: String
});

const Author = mongoose.model('Author', authorSchema);

const bookSchema = new Schema({
  authorId: { type: Schema.Types.UUID, ref: 'Author' }
});
const Book = mongoose.model('Book', bookSchema);

const author = new Author({ name: 'Martin Fowler' });
console.log(typeof author._id); // 'string'
console.log(author.toObject()._id instanceof mongoose.mongo.BSON.Binary); // true

const book = new Book({ authorId: '09190f70-3d30-11e5-8814-0f4df9a59c41' });

UUIDを作成するには、Nodeの組み込みUUIDv4ジェネレーターを使用することをお勧めします。

const { randomUUID } = require('crypto');

const schema = new mongoose.Schema({
  docId: {
    type: 'UUID',
    default: () => randomUUID()
  }
});

BigInt

Mongooseは、JavaScript BigIntをSchemaTypeとしてサポートしています。BigIntは、MongoDBに64ビット整数(BSONタイプ「long」)として格納されます。

const questionSchema = new Schema({
  answer: BigInt
});
const Question = mongoose.model('Question', questionSchema);

const question = new Question({ answer: 42n });
typeof question.answer; // 'bigint'

ゲッター

getterは、スキーマで定義されたパスの仮想プロパティのようなものです。たとえば、ユーザープロフィールの画像を相対パスとして保存し、アプリケーションでホスト名を追加したいとします。以下は、userSchema を構築する方法です。

const root = 'https://s3.amazonaws.com/mybucket';

const userSchema = new Schema({
  name: String,
  picture: {
    type: String,
    get: v => `${root}${v}`
  }
});

const User = mongoose.model('User', userSchema);

const doc = new User({ name: 'Val', picture: '/123.png' });
doc.picture; // 'https://s3.amazonaws.com/mybucket/123.png'
doc.toObject({ getters: false }).picture; // '/123.png'

一般的に、getterは、配列やサブドキュメントではなく、プリミティブパスに対してのみ使用します。getterはMongooseパスへのアクセスによって返される値をオーバーライドするため、オブジェクトにgetterを宣言すると、そのパスのMongoose変更追跡が削除される可能性があります。

const schema = new Schema({
  arr: [{ url: String }]
});

const root = 'https://s3.amazonaws.com/mybucket';

// Bad, don't do this!
schema.path('arr').get(v => {
  return v.map(el => Object.assign(el, { url: root + el.url }));
});

// Later
doc.arr.push({ key: String });
doc.arr[0]; // 'undefined' because every `doc.arr` creates a new array!

上記のように配列にgetterを宣言する代わりに、以下に示すように url 文字列にgetterを宣言する必要があります。ネストされたドキュメントまたは配列にgetterを宣言する必要がある場合は、十分に注意してください。

const schema = new Schema({
  arr: [{ url: String }]
});

const root = 'https://s3.amazonaws.com/mybucket';

// Good, do this instead of declaring a getter on `arr`
schema.path('arr.0.url').get(v => `${root}${v}`);

スキーマ

パスを別のスキーマとして宣言するには、type をサブスキーマのインスタンスに設定します。

サブスキーマの形状に基づいてデフォルト値を設定するには、単にデフォルト値を設定します。ドキュメントの作成時に設定される前に、値はサブスキーマの定義に基づいてキャストされます。

const subSchema = new mongoose.Schema({
  // some schema definition here
});

const schema = new mongoose.Schema({
  data: {
    type: subSchema,
    default: {}
  }
});

カスタム型の作成

Mongooseは、カスタムSchemaTypeで拡張することもできます。プラグインサイトで、mongoose-longmongoose-int32mongoose-functionなどの互換性のある型を検索してください。

カスタムSchemaTypeの作成の詳細については、こちらをご覧ください。

`schema.path()`関数

schema.path() 関数は、指定されたパスのインスタンス化されたスキーマタイプを返します。

const sampleSchema = new Schema({ name: { type: String, required: true } });
console.log(sampleSchema.path('name'));
// Output looks like:
/**
 * SchemaString {
 *   enumValues: [],
  *   regExp: null,
  *   path: 'name',
  *   instance: 'String',
  *   validators: ...
  */

この関数を使用して、指定されたパスのスキーマタイプ(バリデーターやタイプなど)を調べることができます。

参考文献

次へ

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