Mongoose教程翻译

原文地址

本文简单的介绍了数据库,以及如何在 Node/Express 中应用他们。之后展示如何使用Mongoose为一个图书网站提供数据访问。介绍了Mongoose的模式和模型的声明,主要属性类型,基础校验器。也展示了访问数据模型的几种主要方法。

概括


图书管理员将使用一个 图书管理网站 去记录书籍和借书人的信息,同时读者会使用 图书管理网站 去浏览搜索图书,去查看是不是某书全部借出,然后预约或直接借走该书。为了更有效率存储信息,这里我们需要一个数据库。

Express提供了多种数据库的支持,你有多种方式去实现增删查改操作。本文简要的概述了一些可用的选项,并详细说明一些查询的特殊机制。

使用什么数据库?

Express应用可以使用Node支持的各种数据库(Express本身对数据库并没有特殊要求)。常用的选项包括PostgreSQL,MySQL,Redis,SQLite,MongoDB。

当选择一个数据库时,我们需要考虑的常常有 时间成本,学习曲线,性能,备份和回滚的易用性,成本以及其社区支持情况。虽然并没有一个最好的数据库,但是对于我们的 图书管理网站 这样的小型网站,任何流行的数据库都是可以的。

如何更好的与数据库交互?

与数据库交互有两种方法:

  1. 使用数据库的自带的查询语言(比如SQL)
  2. 使用对象数据模型("ODM")或对象关系模型 ("ORM")。一个ODM或ORM对象代表的就是一个映射到底层数据库的数据对象比如说JSON对象。一些ORM对象是指定数据库的,一些则不然。

使用SQL语言或者其他数据库支持的语言可以获得很好的性能。ODM则相对比较慢,因为需要代码去转换映射的对象和数据库中的格式,所以他生成的查询语句可能不够高效(尤其是在ODM为了支持不同的数据库后台,这时必须对数据库功能做出极大的妥协)。

使用ODM的优势在于程序员可以一直关注与JavaScript 对象而不是数据库语义,尤其是在你需要和不同的数据库交互(可能是同一应用,或不同应用)。ODM也提供了清晰方式去校验检查数据。

使用ODM或ORM可以降低开发和维护成本,除非你非常擅长原生查询语言,或对性能要求很高,否则你都应该优先考虑使用ODM或ORM。

使用什么ODM/ORM

在npm中有许多ODM、ORM。

在本文写作时几个热门的框架

  • Mongoose:Mongoose是一个用于异步环境的MongoDB的对象模型。
  • Waterline:提取自基于Express的Sail框架的对象关系模型。他为众多数据库提供了统一的API接口,包括 Redis, mySQL, LDAP, MongoDB, 和 Postgres。
  • Bookshelf:同时具备promise和传统回调函数的接口,提供了对事务的支持,eager/nested-eager relation loading(不知道咋翻),集成多态,支持 一对一,一对多,多对多关系。支持PostgreSQL, MySQL, 和 SQLite3。
  • Objection:尽可能的简化的使用数据库和SQL的全部功能(支持SQLite3, Postgres 和 MySQL)
  • Sequelize:基于promise的ORM...

在选择解决方案时一般应该考虑他们都提供哪些功能,以及他们社区的活跃度(下载,捐款,Bug报告,文档质量)。在此文写作时Mongoose当前最受欢迎的ORM,如果你在你的应用中使用MongoDB作为你的数据库,那么他是一个合理的选择。

在LocalLibrary(这篇文章中的项目名)使用MongoDB和Mongoose

在本文中我们使用Mongoose来访问我们的图书数据。Mongoose作为MongoDB的前端,MongoDB是一个开源的NoSQL,使用面向文档的对象模型的数据库。在MongoDB中 集合(collection)中的文档(documents)类似于关系数据库中的表(table)中的行(row)。

这对ODM和数据库组合在Node的社区中是非常流行的,部分原因是因为文档存储和查询起来非常类似于JSON,对于JS程序员这是非常熟悉的。

你不必为了使用Mongoose而去了解MongoDB,但是如果你已经了解MongoDB,可以更容易的使用和理解Mongoose

教程后续部分将讲解如何为LocalLibrary定义以及使用Mongoose模板和模型。

LocalLibrary的model设计


当你开始进行model编码的时候,花一些时间考虑你应该需要存储什么数据,以及不同对象之间的关系。

我们知道我们需要存储有关书籍的信息(书名,概要,作者,类型,书号),而且一种书我们可能有多本(拥有唯一的ID,可用状态),我们可能也需要存储除了姓名之外其他的作者信息,而且可能会有多个作者名是相同或相似的。我们还想能够通过 书名,作者,书的类别进行排序。

当你设计你的Model时为不同的Object(拥有一组相关的信息的对象)设置不同的Model是必要的。在当前的实例中明显的对象有 书籍,书籍实体,作者。

你可能也想为一个下拉列表选项新建一个Model。相对于硬编码,当下拉列表不确定或经常更改,这种方式更加推荐。在本例中书籍类型(科幻小说,法国诗歌)明显就是属于这种类型。

一旦我们确定了Model和相应的属性,我们就需要思考他们之间的关系。

考虑到这一点,下面的UML关系图展示了当前我们定义的Model。根据上文的讨论,我们将为书籍(包含书籍的一般信息),书籍实体(包含这本书在系统中的状态),作者 创建Model。同时我们也决定为书籍的类别创建一个Model,这样书的类别就可以动态修改。书籍实体的状态并不常变化,我们不会为他单独创建Model。在每个方块中我们定义了Model的名字,以及属性名和类型,还有方法名和方法返回值的类型。

该图也展示了Model直接的关系,和他们数量的对应关系(最大和最小)。比如在Book和Genre之间的连线,在靠近Book的数字表示一本书有0或者多个Genre,而另一端的数字表示每个Genre有0或者多个Book

天杀的UML图

Mongoose入门


这部分概括了如何用Mongoose连接MongoDB,如何定义Schema(这个之前被我翻成模板,不知道对不对,后面索性不返了) 和Model,如何进行简单的查询。

安装Mongoose和MongoDB

Mongoose和其他的依赖一样被安装,使用以下命令为你的项目安装Mongoose。

npm install mongoose --save

安装Mongoose会自动加入他的依赖,例如MongoDB的驱动,但并没有安装MongoDB数据库本身。如果你想要安装MongoDB你可以从这里下载多个平台的安装包。你也可以使用云端的MongoDB实例。

提示:在本文中我们将使用mLab的云端作为数据库。这样非常适合开发,对于这个教程这样做也是有意义的,因为他使安装过程不依赖读者的操作系统。

连接到MongoDB

Mongoose会向MongoDB请求连接。你能够使用require()引入Mongoose,并使用mongoose.connect()连接到本地数据库,如下所示:

//Import the mongoose module
var mongoose = require('mongoose');

//Set up default mongoose connection
var mongoDB = 'mongodb://127.0.0.1/my_database';
mongoose.connect(mongoDB);

//Get the default connection
var db = mongoose.connection;

//Bind connection to error event (to get notification of connection errors)
db.on('error', console.error.bind(console, 'MongoDB connection error:'));

用mongoose.connection你可以获得Mongoose的默认Connection 对象。一旦连接完成,open 事件将从Connection 实例中发射。

提示:如果你需要额外的Connection,你可以使用mongoose.createConnection(),他接受一个和connect()相同格式的数据库URI(包含host,数据库名,端口,选项),返回一个Connection 实例。

定义并创建Model

Model是使用Schema 接口来定义。Schema 用来定义存储在document 中的属性,并且赋予他们校验的规则,和默认值。另外你可以定义静态 或 实体 的helper方法,使你的数据类型更加易于使用。你也可以定义像其他属性一样使用的虚拟属性,这些属性并不会被保存到数据库中(这些会在后文中讲解)。

Schema使用mongoose.model()去“编译”入Model。一旦拥有一个model ,你可以使用他用来创建,查询,删除指定的对象。

Model对应的是MongoDB中documents的collection,documents中包含在Schema中的定义的属性和属性对应的类型。

定义Schema

下面的代码展示了如何定义一个简单的Schema。首先引入mongoose,然后使用Schema的构造方法新建一个Schema的实例,在构造函数的参数对象中定义属性。

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

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

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

在上面的例子中,我们只有两个属性,一个字符串,一个时间。在本文的下一段我们会展示其他属性类型,和校验器和其他方法。

创建一个Model

Model是使用mongoose.model()创建自Schema。

// 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 );

第一个参数是MongoDB中的集合的名,mongoose将为上面的Model创建一个名为SomeModel的集合。第二个参数是你想要用来创建Model的Schema 。

一旦你创建了Model,你可以使用他来进行增删查改,既可以查询全部记录,也可以查询特定的子集。当我们创建我们的视图时,我们会在“使用Model”段讲解如何做。

Schema 中属性的类型

Schema 可以有任意数量的属性,每一个属性都代表了在MongoDB中的字段。下面的例子展示了常用的属性类型是如何被定义的。

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 } }
})

大多数属性类型的意义是显而易见的,除了以下几项:

  • ObjectId:代表在数据库中的一个对象实体,比如,书本对象可以使用他来代表他的作者。实际上他包含的是对象的唯一id(_id)。我们可以在需要的时候使用populate()方法获取某些信息。
  • Mixed:任意schema类型。
  • []:数组对象。你可以对该对象执行JavaScript数组操作(push, pop, unshift等)。上面的实例展示了,没有指定数组对象类型的数组,和指定为String的数组。你可以指定任意类型的数组。

这段代码也展示来定义属性的两种方法:

  • 属性名和属性类型作为键值对。
  • 属性名后紧跟一个对象来定义属性类型,以及属性的其他选项。选项包含以下这些:
    • 默认值
    • 内置的校验器(min/max),或者定制的校验函数。
    • 属性是否是必须的。
    • 属性是否会自动大写,小写或者去除空格(e.g. { type: String, lowercase: true, trim: true })。
      更多有关选项的信息,请看SchemaTypes的文档。
校验器

Mongoose提供来内置的校验器,自定义校验器,同步或者异步校验器。他用来指定可用的范围或者值,以及在校验失败时的错误信息。

内置的校验器包括:

  • 所有的类型都包含required校验器。这个校验器是用来指定在保存时,属性是否是必须的。
  • Numbersminmax校验器。
  • Strings类型有:

下面的样例略微修改自Mongoose的文档,展示来如何指定校验器的类型和错误信息。

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

详细的校验器说明,请看Mongoose的文档Validation

虚拟属性

虚拟属性是你可以get和set的对象属性,但是他们不会被保存到MongoDB中。get方法常常被用来格式化或者合并属性,set方法常用来分解单个属性并把他们保存在数据库中的多个属性中。在本例中用first name和last name属性去构造一个全名,相对于每次在使用时来构造一个全名更加清晰和简单。

备注:我们将使用虚拟属性为每条记录的_id属性和地址定义一个唯一URL。
更多信息请看Virtuals

方法和查询助手

schema可以有实体方法,静态方法和查询助手。实体方法和静态方法是类似的,他们之间明显的不同是,实例方法是关联到实际对象的,能够访问当前对象。查询助手允许你扩展mongoose的查询构造器API(比如,你可以添加“byName”查询方法去扩展find(), findOne() 和 findById())。

Model的使用

一旦你创建创建了schema,你就可以使用他来创建Model。Model代表了数据库中Document的Collection,而一个Model的实体代表了一个你可以存取的单一对象。
下面我们提供一个概述,详情请看Models

创建和更改document

你可以通过创建一个Model实体并调用save()方法去保存一条记录。下面的例子假设SomeModel是通过schema创建的某个对象(只有一个“name”属性)。

// 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!
});

注意记录的创建(以及更新,删除,查询)是异步操作,你需要传递一个回调函数,当操作完成时会执行。我们遵从错误优先的惯例,所以回调函数的第一个参数为错误信息,如果有的话。如果操作会返回结果,他将被作为第二个参数。

你也可以使用create()方法,在你定义对象的同时保存他。回调函数将返回错误信息作为第一个参数,创建的实体作为第二个参数。

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

每一个Model都有一个相关的连接对象(当你使用model()方法时,会使用默认方法),你可以创建一个新的连接,并调用他的model()方法,用以在不同的数据库中创建记录。

你可以使用点语法去访问对象属性,更改属性值。你必须使用save()或update()将变更保存到数据库中。

// 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!
   });
搜索数据

你可以通过查询方法去检索数据记录,并用JSON对象来指定查询条件。下面的代码展示了如何查询所有参加网球运动的运动员,并只返回姓名和年龄。这里我们只匹配了运动这一个属性,但是你们可以指定更多的检索条件,如一个正则表达式,或者不要任何条件,返回所有数据。

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.
})

如果你向上面一样指定了回调方法,查询会马上执行,而回调方法会在查询完成后执行。

在mongoose中所有回调函数都采用了callback(error, result)的形式。如果在查询时发生错误,error中将包含错误信息,而result将返回null。如果查询成功error是null,而result中包含查询的结果。

如果你没有传递回调方法,程序将返回一个Query对象。你可以使用这个query对象去组建你的查询,之后调用exec()方法执行他,并传入回调方法。

// 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
})

上面的代码中,我们在find中指定了查询条件。我们也可以使用where()方法,他能够使用(.)点语法将所有查询条件连接起来,而不用分别指定。下面的代码等同于上面的代码,但是我们添加了一个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.

find()方法会查询所有匹配的记录,但是通常我们只需要其中的一条。下面的方法用以查询一条记录:

提示:也有count()方法,获取指定条件的记录数。常常用于,你只想要知道数目而不是实际的记录时。

查询中你还可以作很多,详情请看:Queries

处理相关的对象 -----热门

你可以使用ObjectId 属性类型创建一个索引连接两个对象,或者使用ObjectId 的数组去连接多个对象。这个属性存储着model的id。如果你需要关联对象的实际内容,你可以使用populate()方法去查询并替换id为真实数据。
例如,一下的schema定义了作者和故事。每个作者有多个故事,我们将使用ObjectId 数组来表示他们。一个故事只有一个作者。"ref"属性(高亮加粗显示的,makedown)告诉schema 连接哪个model。

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);

我们可以使用_id值去保存关联对象的索引。下面我们创建一个author,之后是一个book对象,并关联author对象到author属性。

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
  });
});

我们的story对象依靠id获得了author的索引。为了获得详细的author信息我们使用populate()方法。如下:

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"
});

血条耗尽。。。。

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 206,839评论 6 482
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 88,543评论 2 382
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 153,116评论 0 344
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 55,371评论 1 279
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 64,384评论 5 374
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 49,111评论 1 285
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,416评论 3 400
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,053评论 0 259
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 43,558评论 1 300
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,007评论 2 325
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,117评论 1 334
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,756评论 4 324
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,324评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,315评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,539评论 1 262
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 45,578评论 2 355
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,877评论 2 345

推荐阅读更多精彩内容