6. Defining a Collection


A MongoDB database is a NoSQL database that consists of a set of collections where each collection contains a set of documents.  A collection is analogous to a SQL table and a document is analogous to a row in a SQL table.  Each document in a collection contains a set of fields (key/value pairs) like column/value pairs in a row of a SQL table, that conform to a schema that is defined for the collection.

For example, a web application may require people to create accounts in order to use the app.  In this case, we may have a collection named Users and each person who creates an account will have a separate document in the Users collection.  One document might include fields such as username:”Joe” and password:”Test1234!”.

The structure of the documents that are stored in a collection is defined by a schema.  A schema specifies the names of the keys that are permitted in a document, the types of data associated with the keys, and options such as whether a field is required or not.

After we create a schema we use the schema to construct a class called a model. Instances of a model are documents that can be stored in the collection.

Creating a Schema

We’ll be using the mongoose module to define schema, create models, create instances of the models (documents), and add the documents to the collection.

A schema is created by calling the Schema constructor found in the mongoose module.  The Schema constructor takes two objects as arguments.  The first object (a configuration object) defines the fields that can be stored in a document and the second object defines options for the schema.  Once we have a schema we can create a model and export it. 

import mongoose from 'mongoose'

const userSchema = new mongoose.Schema({ //properties }, { //options })

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

export default User

 

When defining a schema we pass a configuration object as the first argument to the Schema constructor.  Each property contains a key that is mapped to a SchemaType or an object that includes a type property.

In the example below, the key named message is mapped to String and isRead is mapped to an object containing a property named type.  

Note that in the example below String is not a JavaScript type, but rather a mongoose SchemaType.

import { model, Schema } from 'mongoose'

const notificationSchema = new Schema({
  message:String,
  isRead: {
    type: Boolean
  }
})

const Notification = model('Notification', notificationSchema)

export default Notification

 

SchemaTypes

Below is a list of permissible mongoose SchemaTypes.  Notes on their use can be found here.  The SchemaTypes we’ll use in this course are in bold.

  • String, Number, Double, Boolean
  • Date
  • Schema
  • ObjectId
  • Array
  • Map
  • Buffer
  • Mixed/Object
  • UUID
  • Decimal128, Int32, BigInt

 

Below is an example schema that uses the commonly used SchemaTypes.  We assume we have an existing model named Notification. 

const userSchema = new Schema({
  name: String,
  age: Number,
  isAdmin: Boolean,
  gpa: Schema.Types.Double,

  creationDate: Date,

  address: new Schema({ street: String, city: String }, { _id: false }),

  motherId: {
    type: Schema.Types.ObjectId,
    ref: 'User' 
  },

  oldPasswords: [String],
  notifications: [{
    type: mongoose.model('Notification').schema
  }],

  socialMediaHandles: {
    type: Map,
    of: String
  },

  blockUsers: [ Schema.Types.Mixed ],

  profilePic: Schema.Types.Buffer
})

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

export default User

 

Configuration Options

Mongoose allows us to also add SchemaType options to properties in a schema configuration object.  There are options that apply to all types and special options for Strings, Numbers, Date, and ObjectId.  A complete list of options is found here.

In the schema property below we define a path named firstName that is required when a document is created, and has all whitespace trimmed of the ends of its string when being stored in a collection.  The specification forces .toLowerCase() to be called on the values assigned to the role path and requires its value to be either ‘member’ or ‘admin’.  And the level path is configured so that its values are numbers between 1 and 100.

...
firstName: {
  type: String,
  required: true,
  trim: true
},
role: {
  type: String,
  lowercase: true,
  enum: ['member', 'admin']
},
level: {
  type: Number,
  min: 1,
  max: 100
}
...

 

Using a Model

Once we have a schema we can create a model.  Below we create a User model, then create an instance of the User model (a document). 

The model’s constructor takes as an argument an object whose property names match the keys in the schema.  These properties will be used to initialize some of the fields in the document.  After we create the document (user) we can set other fields using the dot-operator, push() for Arrays, and set() for Maps. At the end we store the document in the database by calling save() and print the user data to the console.

import User from '../models/user.js'
const data = {
  name: "Joe",
  age: 20,
  gpa: 3.7,
  isAdmin: true,
  creationDate: Date.now(),
  address: { street: '123 Main', city: 'Dayton' },
}

const user = new User(data)  // create a document

const mother = await User.findOne({ name: 'Sally' })
user.motherId = mother ? mother._id : undefined

user.oldPasswords.push('test1234')
user.notifications.push(new Notification({ message: 'hello', isRead: false }))

user.socialMediaHandles.set('github', 'joe123')

user.blockUsers.push({ name: 'Alice' })

await user.save()   // store the document in the database
console.log(user.toJSON())
 

Since we’ve saved the document to the database when we open up Compass and navigate to the users collection we see the following document.