Учебник Express часть 3: Использование базы данных (с помощью Mongoose)

Перевод не завершен. Пожалуйста, помогите перевести эту статью с английского.

В этой статье кратко представлены базы данных, и как их использовать с приложнениями Node/Express. затем мы покажем как можно использовать Mongoose для предоставления доступа к базе данных для веб-сайта  LocalLibrary . Мы объсним, как объявляются схемы и модели объектов, основные типы полей, и базавая валидация. В статье также кратко показано несколько основных способов доступа к данным модели.

Prerequisites: Express Tutorial Part 2: Creating a skeleton website
Objective: To be able to design and create your own models using Mongoose.

Обзор

Сотрудники библиотеки будут использовать сайт Local Library  для хранения информации о книгах и заемщиках, в то время как члены библиотеки будут использовать его для просмотра и поиска книг, узнавать, есть ли какие-либо доступные копии, а затем резервировать или одалживать их. Чтобы эффективно хранить и извлекать информацию, мы будем хранить ее в базе данных.

Express приложения может использовать множество различных баз данных, и есть несколько подходов, которые вы можете использовать для выполнения  Create, Read, Update and Delete(CRUD) операций. Это руководство обеспечивает краткий обзор  о некоторых доступных опциях, а затем переходит к деталям в частности отдельных механизмов.

Какие базы данных я могу использовать?

Express приложение может использовать  любые базы данных поддерживаемые Node (сам по себе Express не определяет каких-либо конкретных дополнительных свойств/требований для управления базами данных). Есть много популярных опций, включая PostgreSQL, MySQL, Redis, SQLite, and MongoDB.

При выборе базы данных, Вы должны учитывать такие вещи, как время-производительность/обучение, производительность, простота репликации/копирования, расходы, поддержка сообщества и т. д.  Хотя нет единой" лучшей " базы данных, почти любое из популярных решений должно быть более чем приемлемым для сайта малого и среднего размера, такого как наша Local Library.

Для большей ирформации об опциях смотрите: Database integration (Express docs).

Как лучше всего взаимодействовать с базой данных?

Существует два подхода для взаимодействия с базой данных:

  • Использование родного языка запросов баз данных (т.е. SQL)
  • Использование объектной модели данных (ODM) / объектно-реляционной модели (ORM).  ODM / ORM представляет данные веб-сайта как объекты JavaScript, которые затем сопоставляются с базовой базой данных. неоторые ORMs привязаны к определенной базе данных, в то время как другие предоставляют базу данных-agnostic.

Наилучшую производительность можно получить с помощью SQL или любого другого языка запросов, поддерживаемого базой данных.ODM часто медленнее, потому что они используют код перевода для сопоставления между объектами и форматом базы данных, который может не использовать наиболее эффективные запросы к базе данных (это особенно верно, если ODM поддерживает различные бэкенды базы данных и должен идти на большие компромиссы с точки зрения того, какие функции базы данных поддерживаются).

Преимущество использования ORM заключается в том, что программисты могут продолжать думать об объектах JavaScript, а не о семантике базы данных — это особенно верно, если вам нужно работать с разными базами данных (на одном или разных веб-сайтах). Они также обеспечивают очевидное место для того чтобы выполнить утверждение и проверку данных.

Совет:  Использование ODM / ORMs часто приводит к снижению затрат на разработку и обслуживание! Если Вы не очень хорошо знакомы с родным языком запросов или производительность имеет первостепенное значение, следует настоятельно рассмотреть возможность использования ODM.

Что следует использовать ORM/ODM?

Есть много ODM/ORM доступных решений на сайте менеджера пакетов NPM (проверьте теги по подгруппе odm и orm).

Несколько решений что были популярны на момент написания статьи:

  • Mongoose: Mongoose это MongoDB инструмент моделирование обьектов предназначенный для работы в асинхронной среде.
  • Waterline: ORM полученна от Express-based Sails фреймворка. Она предоставляет единный API для доступа к множеству различных баз данных, в том числе Redis, mySQL, LDAP, MongoDB, и Postgres.
  • Bookshelf: Возможности основанные на promise и традиционного callback интерфейсов, поддержка транзакций, eager/nested-eager relation loading, полиморфные ассоциации, и поддержка, один к одному, один ко многим, и многие ко многим. Работает с PostgreSQL, MySQL, и SQLite3.
  • Objection: Делает все как можно проще используя максимальные возможности SQL и базовый движок базы данных ( поддержка  SQLite3, Postgres, и MySQL).
  • Sequelize: Основанный на промисах ORM для Node.js и io.js. они поддерживают диалекты PostgreSQL, MySQL, MariaDB, SQLite и MSSQL и обладают прочной поддержкой транзакций, связей, чтении копий и более того.

Как правило вам следует расматривать обе предоставляемые функции  и "деятельность сообщества" ( скачивание, вклад, отчет об ошибках, качественную документацию, и т.д. ) при выборе решения. На момент написания статьи Mongoose являлся очень популярной ORM и  это разумный выбор если вы используете MongoDB.

Использования Mongoose и MongoDb для LocalLibrary

Для примера LocalLibrary (и остальной части этого раздела) мы собираемся использовать Mongoose ODM для доступа к нашим данным библиотеки. Mongoose выступает в качестве интерфейса для MongoDB, базы данных NoSQL с открытым исходным кодом, которая использует модель данных, ориентированную на документ. «Коллекции» и «документы» в базе данных MongoDB аналогичны «таблицам» и «строкам» в реляционной базе данных.

Эта комбинация ODM и базы данных чрезвычайно популярна в сообществе Node, частично потому, что система хранения документов и запросов выглядит очень похожей на JSON и поэтому знакома разработчикам JavaScript.

Совет: Вам не нужно знать MongoDB, чтобы использовать Mongoose, хотя   документацию Mongoose легче использовать и понимать, если вы уже знакомы с MongoDB.

В оставшейся части этого руководства показано, как определить и получить доступ к схеме и моделям Mongoose для примера веб-сайта LocalLibrary.

Проектирование моделей LocalLibrary

 

Прежде чем вы начнете писать код моделей, стоит подумать о том, какие данные нам нужно хранить, и о взаимоотношениях между разными объектами.

Мы знаем, что нам нужно хранить информацию о книгах (название, резюме (краткое описание), автор, жанр, ISBN (Международный стандартный книжный номер) ) и что у нас может быть несколько доступных экземпляров (с уникальными идентификаторами, статусом наличия и т. д.). Нам может потребоваться хранить больше информации об авторе, чем просто их имя, и могут быть несколько авторов с одинаковыми или похожими именами. Мы хотим иметь возможность сортировать информацию на основе названия книги, автора, жанра и категории.

При проектировании моделей имеет смысл иметь отдельные модели для каждого «объекта» (группы связанной информации). В этом случае очевидными объектами являются книги, экземпляры книг и авторы.

Вы также можете использовать модели для представления параметров списка выбора (например, как выпадающий список вариантов), а не жесткого кодирования выбора на самом веб-сайте - это рекомендуется, когда все параметры неизвестны или могут изменение. Очевидным кандидатом на модель такого типа является жанр книги (например, «Научная фантастика», «Французская поэзия» и т. д.),

Как только мы определились с нашими моделями и полями, нам нужно подумать об отношениях между ними.

Имея это в виду, диаграмма UML ниже показывает модели, которые мы определим в этом случае (в виде блоков). Как обсуждалось выше, мы создали модели для книги (общие сведения о книге), экземпляр книги (состояние конкретных физических копий книги, доступной в системе) и автора. Мы также решили создать модель для жанра, чтобы можно было динамически создавать ценности. Мы решили не создавать модель для  BookInstance:status — мы пропишем в коде необходимые значения, потому что не ожидаем их изменения. В каждом из элементов диаграммы вы можете увидеть имя модели, имена и типы полей, а также методы и возвращаемый ими тип данных.

На диаграмме также показаны зависимости между моделями, включая виды  связей между ними. Числа на диаграмме, показывают максимум и минимум  моделей, которые могут присутствовать в этой связи. Например, соединительная линия между элементами диаграммы показывает, что Book и Genre связаны между собой. Числа на этой линии рядом с моделью Book , показывают, что модель Book может иметь 0 и  более моделей Genre (сколько угодно), а числа на другом конце строки рядом с Genre показывают, что у модели  может быть ноль или более связанных книг.

Note: As discussed in our Mongoose primer below it is often better to have the field that defines the relationship between the documents/models in just one model (you can still find the reverse relationship by searching for the associated _id in the other model). Below we have chosen to define the relationship between Book/Genre and Book/Author in the Book schema, and the relationship between the Book/BookInstance in the BookInstance Schema. This choice was somewhat arbitrary — we could equally well have had the field in the other schema.

Mongoose Library Model  with correct cardinality

Note: The next section provides a basic primer explaining how models are defined and used. As you read it, consider how we will construct each of the models in the diagram above.

Mongoose Справочник

В этом разделе предоставляется обзор как подключится к MongoDB базе, как определять схемы и модели, и как делать базовые запросы.

Примечание: Этот справочник в значительной степени завист от Mongoose быстрое начало с npm и офицальная документация.

Установка Mongoose и MongoDB

Mongoose устанавливается в ваш проект (package.json) как и другие зависимости - используемые NPM. Установим его, используя следущую команду внутри вашего проекта: 

npm install mongoose

Установка Mongoose добавит все зависимости, включая драйвер MongoDB, но она не установит сама себя. Если вы хотите установить MongoDB сервер, тогда вы можете скачать установку здесь для различных операционных систем и установить ее локально. Вы так же можете использовать облако MongoDB.

Примечание: Для этого руководства мы будем использовать облачную базу данных как сервис sandbox tier предоставляя облачную базу данных. This is suitable for development, and makes sense for the tutorial because it makes "installation" operating system independent (database-as-a-service is also one approach you might well use for your production database).

Подключенние к MongoDB

Mongoose требует подключение к MongoDB. Вы можете использовать require() а так же подключится к локальной базе с mongoose.connect(), как показано ниже.

// Импортировать модуль mongoose
var mongoose = require('mongoose'); 

// Поставим по умолчанию mongoose подключение
var mongoDB = 'mongodb://127.0.0.1/my_database';
mongoose.connect(mongoDB);
// Получение Mongoose для использования глобаного промиса
mongoose.Promise = global.Promise;
// Получение по умолчанию подключения
var db = mongoose.connection;

// Привязать подключение к ошибке события ( получение уведомления ошибок подключение )
db.on('error', console.error.bind(console, 'MongoDB connection error:')); 

You can get the default Connection object with mongoose.connection. Once connected, the open event is fired on the Connection instance.

Tip: If you need to create additional connections you can use mongoose.createConnection(). This takes the same form of database URI (with host, database, port, options etc.) as connect() and returns a Connection object).

Defining and creating models

Models are defined using the Schema interface. The Schema allows you to define the fields stored in each document along with their validation requirements and default values. In addition, you can define static and instance helper methods to make it easier to work with your data types, and also virtual properties that you can use like any other field, but which aren't actually stored in the database (we'll discuss a bit further below).

Schemas are then "compiled" into models using the mongoose.model() method. Once you have a model you can use it to find, create, update, and delete objects of the given type.

Note: Each model maps to a collection of documents in the MongoDB database. The documents will contain the fields/schema types defined in the model Schema.

Defining schemas

The code fragment below shows how you might define a simple schema. First you require() mongoose, then use the Schema constructor to create a new schema instance, defining the various fields inside it in the constructor's object parameter.

//Require Mongoose
var mongoose = require('mongoose');

//Define a schema
var Schema = mongoose.Schema;

var SomeModelSchema = new Schema({
    a_string: String,
    a_date: Date
});

In the case above we just have two fields, a string and a date. In the next sections we will show some of the other field types, validation, and other methods.

Creating a model

Models are created from schemas using the mongoose.model() method:

// Define schema
var Schema = mongoose.Schema;

var SomeModelSchema = new Schema({
    a_string: String,
    a_date: Date
});

// Compile model from schema
var SomeModel = mongoose.model('SomeModel', SomeModelSchema );

The first argument is the singular name of the collection that will be created for your model (Mongoose will create the database collection for the above model SomeModel above), and the second argument is the schema you want to use in creating the model.

Note: Once you've defined your model classes you can use them to create, update, or delete records, and to run queries to get all records or particular subsets of records. We'll show you how to do this in the Using models section, and when we create our views.

Schema types (fields)

A schema can have an arbitrary number of fields — each one represents a field in the documents stored in MongoDB. An example schema showing many of the common field types and how they are declared is shown below.

var schema = new Schema(
{
  name: String,
  binary: Buffer,
  living: Boolean,
  updated: { type: Date, default: Date.now },
  age: { type: Number, min: 18, max: 65, required: true },
  mixed: Schema.Types.Mixed,
  _someId: Schema.Types.ObjectId,
  array: [],
  ofString: [String], // You can also have an array of each of the other types too.
  nested: { stuff: { type: String, lowercase: true, trim: true } }
})

Most of the SchemaTypes (the descriptors after “type:” or after field names) are self explanatory. The exceptions are:

  • ObjectId: Represents specific instances of a model in the database. For example, a book might use this to represent its author object. This will actually contain the unique ID (_id) for the specified object. We can use the populate() method to pull in the associated information when needed.
  • Mixed: An arbitrary schema type.
  • []: An array of items. You can perform JavaScript array operations on these models (push, pop, unshift, etc.). The examples above show an array of objects without a specified type and an array of String objects, but you can have an array of any type of object.

The code also shows both ways of declaring a field:

  • Field name and type as a key-value pair (i.e. as done with fields name, binary and living).
  • Field name followed by an object defining the type, and any other options for the field. Options include things like:
    • default values.
    • built-in validators (e.g. max/min values) and custom validation functions.
    • Whether the field is required
    • Whether String fields should automatically be set to lowercase, uppercase, or trimmed (e.g. { type: String, lowercase: true, trim: true })

For more information about options see SchemaTypes (Mongoose docs).

Validation

Mongoose provides built-in and custom validators, and synchronous and asynchronous validators. It allows you to specify both the acceptable range or values and the error message for validation failure in all cases.

The built-in validators include:

  • All SchemaTypes have the built-in required validator. This is used to specify whether the field must be supplied in order to save a document.
  • Numbers have min and max validators.
  • Strings have:
    • enum: specifies the set of allowed values for the field.
    • match: specifies a regular expression that the string must match.
    • maxlength and minlength for the string.

The example below (slightly modified from the Mongoose documents) shows how you can specify some of the validator types and error messages:


    var breakfastSchema = new Schema({
      eggs: {
        type: Number,
        min: [6, 'Too few eggs'],
        max: 12
        required: [true, 'Why no eggs?']
      },
      drink: {
        type: String,
        enum: ['Coffee', 'Tea', 'Water',]
      }
    });

For complete information on field validation see Validation (Mongoose docs).

Virtual properties

Virtual properties are document properties that you can get and set but that do not get persisted to MongoDB. The getters are useful for formatting or combining fields, while setters are useful for de-composing a single value into multiple values for storage. The example in the documentation constructs (and deconstructs) a full name virtual property from a first and last name field, which is easier and cleaner than constructing a full name every time one is used in a template.

Note: We will use a virtual property in the library to define a unique URL for each model record using a path and the record's _id value.

For more information see Virtuals (Mongoose documentation).

Methods and query helpers

A schema can also have instance methods, static methods, and query helpers. The instance and static methods are similar, but with the obvious difference that an instance method is associated with a particular record and has access to the current object. Query helpers allow you to extend mongoose's chainable query builder API (for example, allowing you to add a query "byName" in addition to the find(), findOne() and findById() methods).

Using models

Once you've created a schema you can use it to create models. The model represents a collection of documents in the database that you can search, while the model's instances represent individual documents that you can save and retrieve.

We provide a brief overview below. For more information see: Models (Mongoose docs).

Creating and modifying documents

To create a record you can define an instance of the model and then call save(). The examples below assume SomeModel is a model (with a single field "name") that we have created from our schema.

// Create an instance of model SomeModel
var awesome_instance = new SomeModel({ name: 'awesome' });

// Save the new model instance, passing a callback
awesome_instance.save(function (err) {
  if (err) return handleError(err);
  // saved!
});

Creation of records (along with updates, deletes, and queries) are asynchronous operations — you supply a callback that is called when the operation completes. The API uses the error-first argument convention, so the first argument for the callback will always be an error value (or null). If the API returns some result, this will be provided as the second argument.

You can also use create() to define the model instance at the same time as you save it. The callback will return an error for the first argument and the newly-created model instance for the second argument.

SomeModel.create({ name: 'also_awesome' }, function (err, awesome_instance) {
  if (err) return handleError(err);
  // saved!
});

Every model has an associated connection (this will be the default connection when you use mongoose.model()). You create a new connection and call .model() on it to create the documents on a different database.

You can access the fields in this new record using the dot syntax, and change the values. You have to call save() or update() to store modified values back to the database.

// Access model field values using dot notation
console.log(awesome_instance.name); //should log 'also_awesome'

// Change record by modifying the fields, then calling save().
awesome_instance.name="New cool name";
awesome_instance.save(function (err) {
   if (err) return handleError(err); // saved!
   });

Searching for records

You can search for records using query methods, specifying the query conditions as a JSON document. The code fragment below shows how you might find all athletes in a database that play tennis, returning just the fields for athlete name and age. Here we just specify one matching field (sport) but you can add more criteria, specify regular expression criteria, or remove the conditions altogether to return all athletes.

var Athlete = mongoose.model('Athlete', yourSchema);

// find all athletes who play tennis, selecting the 'name' and 'age' fields
Athlete.find({ 'sport': 'Tennis' }, 'name age', function (err, athletes) {
  if (err) return handleError(err);
  // 'athletes' contains the list of athletes that match the criteria.
})

If you specify a callback, as shown above, the query will execute immediately. The callback will be invoked when the search completes.

Note: All callbacks in Mongoose use the pattern callback(error, result). If an error occurs executing the query, the error parameter will contain an error document, and result will be null. If the query is successful, the error parameter will be null, and the result will be populated with the results of the query.

If you don't specify a callback then the API will return a variable of type Query. You can use this query object to build up your query and then execute it (with a callback) later using the exec() method.

// find all athletes that play tennis
var query = Athlete.find({ 'sport': 'Tennis' });

// selecting the 'name' and 'age' fields
query.select('name age');

// limit our results to 5 items
query.limit(5);

// sort by age
query.sort({ age: -1 });

// execute the query at a later time
query.exec(function (err, athletes) {
  if (err) return handleError(err);
  // athletes contains an ordered list of 5 athletes who play Tennis
})

Above we've defined the query conditions in the find() method. We can also do this using a where() function, and we can chain all the parts of our query together using the dot operator (.) rather than adding them separately. The code fragment below is the same as our query above, with an additional condition for the age.

Athlete.
  find().
  where('sport').equals('Tennis').
  where('age').gt(17).lt(50).  //Additional where query
  limit(5).
  sort({ age: -1 }).
  select('name age').
  exec(callback); // where callback is the name of our callback function.

The find() method gets all matching records, but often you just want to get one match. The following methods query for a single record:

Note: There is also a count() method that you can use to get the number of items that match conditions. This is useful if you want to perform a count without actually fetching the records.

There is a lot more you can do with queries. For more information see: Queries (Mongoose docs).

You can create references from one document/model instance to another using the ObjectId schema field, or from one document to many using an array of ObjectIds. The field stores the id of the related model. If you need the actual content of the associated document, you can use the populate() method in a query to replace the id with the actual data.

For example, the following schema defines authors and stories. Each author can have multiple stories, which we represent as an array of ObjectId. Each story can have a single author. The "ref" (highlighted in bold below) tells the schema which model can be assigned to this field.

var mongoose = require('mongoose')
  , Schema = mongoose.Schema

var authorSchema = Schema({
  name    : String,
  stories : [{ type: Schema.Types.ObjectId, ref: 'Story' }]
});

var storySchema = Schema({
  author : { type: Schema.Types.ObjectId, ref: 'Author' },
  title    : String
});

var Story  = mongoose.model('Story', storySchema);
var Author = mongoose.model('Author', authorSchema);

We can save our references to the related document by assigning the _id value. Below we create an author, then a story, and assign the author id to our stories author field.

var bob = new Author({ name: 'Bob Smith' });

bob.save(function (err) {
  if (err) return handleError(err);

  //Bob now exists, so lets create a story
  var story = new Story({
    title: "Bob goes sledding",
    author: bob._id    // assign the _id from the our author Bob. This ID is created by default!
  });

  story.save(function (err) {
    if (err) return handleError(err);
    // Bob now has his story
  });
});

Our story document now has an author referenced by the author document's ID. In order to get the author information in our story results we use populate(), as shown below.

Story
.findOne({ title: 'Bob goes sledding' })
.populate('author') //This populates the author id with actual author information!
.exec(function (err, story) {
  if (err) return handleError(err);
  console.log('The author is %s', story.author.name);
  // prints "The author is Bob Smith"
});

Note: Astute readers will have noted that we added an author to our story, but we didn't do anything to add our story to our author's stories array. How then can we get all stories by a particular author? One way would be to add our author to the stories array, but this would result in us having two places where the information relating authors and stories needs to be maintained.

A better way is to get the _id of our author, then use find() to search for this in the author field across all stories.

Story
.find({ author : bob._id })
.exec(function (err, stories) {
  if (err) return handleError(err);
  // returns all stories that have Bob's id as their author.
});

This is almost everything you need to know about working with related items for this tutorial. For more detailed information see Population (Mongoose docs).

One schema/model per file

While you can create schemas and models using any file structure you like, we highly recommend defining each model schema in its own module (file), exporting the method to create the model. This is shown below:

// File: ./models/somemodel.js

//Require Mongoose
var mongoose = require('mongoose');

//Define a schema
var Schema = mongoose.Schema;

var SomeModelSchema = new Schema({
    a_string          : String,
    a_date            : Date,
});

//Export function to create "SomeModel" model class
module.exports = mongoose.model('SomeModel', SomeModelSchema );

You can then require and use the model immediately in other files. Below we show how you might use it to get all instances of the model.

//Create a SomeModel model just by requiring the module
var SomeModel = require('../models/somemodel')

// Use the SomeModel object (model) to find all SomeModel records
SomeModel.find(callback_function);

Setting up the MongoDB database

Now that we understand something of what Mongoose can do and how we want to design our models, it's time to start work on the LocalLibrary website. The very first thing we want to do is set up a MongoDb database that we can use to store our library data.

For this tutorial we're going to use mLab's free cloud-hosted "sandbox" database. This database tier is not considered suitable for production websites because it has no redundancy, but it is great for development and prototyping. We're using it here because it is free and easy to set up, and because mLab is a popular database as a service vendor that you might reasonably choose for your production database (other popular choices at the time of writing include Compose, ScaleGrid and MongoDB Atlas).

Note: If you prefer you can set up a MongoDb database locally by downloading and installing the appropriate binaries for your system. The rest of the instructions in this article would be similar, except for the database URL you would specify when connecting.

You will first need to create an account with mLab (this is free, and just requires that you enter basic contact details and acknowledge their terms of service). 

After logging in, you'll be taken to the home screen:

  1. Click Create New in the MongoDB Deployments section.
  2. This will open the Cloud Provider Selection screen.
    MLab - screen for new deployment
     
    • Select the SANDBOX (Free) plan from the Plan Type section. 
    • Select any provider from the Cloud Provider section. Different providers offer different regions (displayed below the selected plan type).
    • Click the Continue button.
  3. This will open the Select Region screen.

    Select new region screen

    • Select the region closest to you and then Continue.

  4. This will open the Final Details screen.
    New deployment database name

    • Enter the name for the new database as local_library and then select Continue.

  5. This will open the Order Confirmation screen.
    Order confirmation screen

    • Click Submit Order to create the database.

  6. You will be returned to the home screen. Click on the new database you just created to open its details screen. As you can see the database has no collections (data).
    mLab - Database details screen
     
    The URL that you need to use to access your database is displayed on the form above (shown for this database circled above). In order to use this you need to create a database user that you can specify in the URL.

  7. Click the Users tab and select the Add database user button.
  8. Enter a username and password (twice), and then press Create. Do not select Make read only.

You have now created the database, and have an URL (with username and password) that can be used to access it. This will look something like: mongodb://your_user_namer:your_password@ds119748.mlab.com:19748/local_library.

Install Mongoose

Open a command prompt and navigate to the directory where you created your skeleton Local Library website. Enter the following command to install Mongoose (and its dependencies) and add it to your package.json file, unless you have already done so when reading the Mongoose Primer above.

npm install mongoose

Connect to MongoDB

Open /app.js (in the root of your project) and copy the following text below where you declare the Express application object (after the line var app = express();). Replace the database url string ('insert_your_database_url_here') with the location URL representing your own database (i.e. using the information from from mLab).

//Set up mongoose connection
var mongoose = require('mongoose');
var mongoDB = 'insert_your_database_url_here';
mongoose.connect(mongoDB);
mongoose.Promise = global.Promise;
var db = mongoose.connection;
db.on('error', console.error.bind(console, 'MongoDB connection error:'));

As discussed in the Mongoose primer above, this code creates the default connection to the database and binds to the error event (so that errors will be printed to the console). 

Defining the LocalLibrary Schema

We will define a separate module for each model, as discussed above. Start by creating a folder for our models in the project root (/models) and then create separate files for each of the models:

/express-locallibrary-tutorial  //the project root
  /models
    author.js
    book.js
    bookinstance.js
    genre.js

Author model

Copy the Author schema code shown below and paste it into your ./models/author.js file. The scheme defines an author has having String SchemaTypes for the first and family names, that are required and have a maximum of 100 characters, and Date fields for the date of birth and death.

var mongoose = require('mongoose');

var Schema = mongoose.Schema;

var AuthorSchema = new Schema(
  {
    first_name: {type: String, required: true, max: 100},
    family_name: {type: String, required: true, max: 100},
    date_of_birth: {type: Date},
    date_of_death: {type: Date},
  }
);

// Virtual for author's full name
AuthorSchema
.virtual('name')
.get(function () {
  return this.family_name + ', ' + this.first_name;
});

// Virtual for author's URL
AuthorSchema
.virtual('url')
.get(function () {
  return '/catalog/author/' + this._id;
});

//Export model
module.exports = mongoose.model('Author', AuthorSchema);

We've also declared a virtual for the AuthorSchema named "url" that returns the absolute URL required to get a particular instance of the model — we'll use the property in our templates whenever we need to get a link to a particular author.

Note: Declaring our URLs as a virtual in the schema is a good idea because then the URL for an item only ever needs to be changed in one place.
At this point a link using this URL wouldn't work, because we haven't got any routes handling code for individual model instances. We'll set those up in a later article!

At the end of the module we export the model.

Book model

Copy the Book schema code shown below and paste it into your ./models/book.js file. Most of this is similar to the author model — we've declared a schema with a number of string fields and a virtual for getting the URL of specific book records, and we've exported the model.

var mongoose = require('mongoose');

var Schema = mongoose.Schema;

var BookSchema = new Schema(
  {
    title: {type: String, required: true},
    author: {type: Schema.ObjectId, ref: 'Author', required: true},
    summary: {type: String, required: true},
    isbn: {type: String, required: true},
    genre: [{type: Schema.ObjectId, ref: 'Genre'}]
  }
);

// Virtual for book's URL
BookSchema
.virtual('url')
.get(function () {
  return '/catalog/book/' + this._id;
});

//Export model
module.exports = mongoose.model('Book', BookSchema);

The main difference here is that we've created two references to other models:

  • author is a reference to a single Author model object, and is required.
  • genre is a reference to an array of Genre model objects. We haven't declared this object yet!

BookInstance model

Finally, copy the BookInstance schema code shown below and paste it into your ./models/bookinstance.js file. The BookInstance represents a specific copy of a book that someone might borrow, and includes information about whether the copy is available or on what date it is expected back, "imprint" or version details.

var mongoose = require('mongoose');

var Schema = mongoose.Schema;

var BookInstanceSchema = new Schema(
  {
    book: { type: Schema.ObjectId, ref: 'Book', required: true }, //reference to the associated book
    imprint: {type: String, required: true},
    status: {type: String, required: true, enum: ['Available', 'Maintenance', 'Loaned', 'Reserved'], default: 'Maintenance'},
    due_back: {type: Date, default: Date.now}
  }
);

// Virtual for bookinstance's URL
BookInstanceSchema
.virtual('url')
.get(function () {
  return '/catalog/bookinstance/' + this._id;
});

//Export model
module.exports = mongoose.model('BookInstance', BookInstanceSchema);

The new things we show here are the field options:

  • enum: This allows us to set the allowed values of a string. In this case we use it to specify the availability status of our books (using an enum means that we can prevent mis-spellings and arbitrary values for our status)
  • default: We use default to set the default status for newly created bookinstances to maintenance and the default due_back date to now (note how you can call the Date function when setting the date!)

Everything else should be familiar from our previous schema.

Genre model - challenge!

Open your ./models/genre.js file and create a schema for storing genres (the category of book, e.g. whether it is fiction or non-fiction, romance or military history, etc).

The definition will be very similar to the other models:

  • The model should have a String SchemaType called name to describe the genre.
  • This name should be required and have between 3 and 100 characters.
  • Declare a virtual for the genre's URL, named url.
  • Export the model.

Testing — create some items

That's it. We now have all models for the site set up!

In order to test the models (and to create some example books and other items that we can use in our next articles) we'll now run an independent script to create items of each type:

  1. Download (or otherwise create) the file populatedb.js inside your express-locallibrary-tutorial directory (in the same level as package.json).

    Note: You don't need to know how populatedb.js works; it just adds sample data into the database.

  2. Enter the following commands in the project root to install the async module that is required by the script (we'll discuss this in later tutorials, )
    npm install async
  3. Run the script using node in your command prompt, passing in the URL of your MongoDB database (the same one you replaced the insert_your_database_url_here placeholder with, inside app.js earlier):
    node populatedb <your mongodb url>​​​​
  4. The script should run through to completion, displaying items as it creates them in the terminal.

Tip: Go to your database on mLab. You should now be able to drill down into individual collections of Books, Authors, Genres and BookInstances, and check out individual documents.

Summary

In this article we've learned a bit about databases and ORMs on Node/Express, and a lot about how Mongoose schema and models are defined. We then used this information to design and implement Book, BookInstance, Author and Genre models for the LocalLibrary website.

Last of all we tested our models by creating a number of instances (using a standalone script). In the next article we'll look at creating some pages to display these objects.

See also

 

In this module

 

Метки документа и участники

Внесли вклад в эту страницу: Delgus, neyron163, MariyaSka
Обновлялась последний раз: Delgus,