Practical Node.js (2018版) 第7章:Boosting Node.js and Mongoose
参考:博客 https://www.cnblogs.com/chentianwei/p/10268346.html
参考: mongoose官网(https://mongoosejs.com/docs/models.html)
参考: 英文:Boosting Node.js和MongoDB with Mongoose
简介:mongoose
Mongoose is a fully developed object document mapping (ODM) library for Node.js and MongoDB.
ODM的概念对应sql的ORM,就是ruby on rails中的activerecord那因层。
activerecord包括migrations, Validations, associations, Query interface, 对应mvc框架中的Models。
ORM, Object-Relational Mappiing。
ODM的作用,定义数据库的数据格式schema, 然后通过它取数据,把数据库中的document映射成程序中的一个对象。这个对象有save, update的系列方法,有tilte, author等系列属性。
在调用这些方法时,odm会根据你调用时使用的条件,转化成mongoDb Shell语言,帮你发送出去。
自然,在程序内使用链式调用,比手写数据库语句更灵活也方便。
例子:
- //先安装好MongoDb和Node.js
- $ npm install mongoose
- // getting-started.js
- var mongoose = require('mongoose');
- mongoose.connect('mongodb://localhost:27017/test');
- db.on('error', console.error.bind(console, "connection error"))
- db.once('open', function() {
- //当连接成功后,写Schema, model, 写实例并保存到数据库。
- })
在db.once内的例子1
- var userSchema = new mongoose.Schema({
- user: {
- username: String,
- password: String
- }
- })
- var User = mongoose.model('user', userSchema)
- var frank = new User({
- user: {
- username: 'Frank',
- password: '123456'
- }
- })
- frank.save((err, frank) => {
- console.log('save success!')
- console.log(frank.user)
- })
在db.once()的例子2
- //构建一个Schema
- var kittySchema = new mongoose.Schema({
- name: String
- });
- // 写一个方法
- kittySchema.methods.speak = function () {
- var greeting = this.name
- ? "Meow name is " + this.name
- : "I don't have a name";
- console.log(greeting);
- }
- // 生成一个model
- var Kitten = mongoose.model('Kitten', kittySchema);
- // 实例化一个对象
- var fluffy = new Kitten({ name: 'fluffy' });
- // 通过mongoose写入数据库
- fluffy.save((err, fluffy) => {
- if (err) {
- return console.error(err)
- }
- fluffy.speak()
- })
⚠️:此时已经将fluffy对象保存到mongodb://localhost:27017/test的Kitten model内。
即将一个document,保存到test数据库的kittens collection中。
model自动创建了kittens这个collection。(自动添加了s)
⚠️注意:此时mongoDb还没有创建kittens
在创建一个实例并执行save方法,test数据库才会创建了kittens collections和documents。
可以对比使用node.js mongodb driver的代码。
- var MongoClient = require('mongodb').MongoClient,
- assert=require('assert');
- var url = 'mongodb://localhost:27017/myproject';
- MongoClient.connect(url,function(err,db){
- assert.equal(null,err);
- console.log("成功连接到服务器");
- insertDocuments(db,function(){
- db.close();
- });
- // db.close();
- });
- var insertDocuments = function(db,callback){
- var collection = db.collection('documents');
- collection.insertMany([
- {a:1},
- {a:2},
- {a:3}
- ],function(err,result){
- assert.equal(err,null);
- assert.equal(3,result.result.n);
- assert.equal(3,result.ops.length);
- console.log("成功插入3个文档到集合!");
- callback(result);
});
}
上面代码是专为Node.js提供的驱动程序代码和mongDB shell语言类似。
而,用mongoose定位于使用关系型的数据结构schema,来构造你的app data。
它包括内置的类型构件, 验证, 查询,业务逻辑勾子和更多的功能,开箱即用out of the box!
mongoose把你使用Node.js驱动代码自己写复杂的验证,和逻辑业务的麻烦,简单化了。
mongoose建立在MongoDB driver之上,让程序员可以model 化数据。
二者各有优缺点:
mongoose需要一段时间的学习和理解。在处理某些特别复杂的schema时,会遇到一些限制。
但直接使用Node.js的驱动代码,在你进行数据验证时会写大量的代码,而且会忽视一些安全问题。
Node.js practical 第七章
不喜欢使用mongoose进行复杂的query,而是使用native driver。
Mongoose的缺点是某些查询的速度较慢。
当然Mongoose的优点很多。因为ODM(object document mapping)是现代软件编程的重要部分!
特别是企业级的engineering。
主要优势,就是从database中,提取每件事:程序代码只和object和它们的methods交互。
ODM允许指定:不同类型的对象和把业务逻辑放在类内(和那些对象相关)之间的关系relationships.
另外,内建的验证和类型type casting可以扩展和客制。
当Mongoose和Express.js一起使用时, Mongoose让stack真正地拥护MVC理念。
Mongoose 使用类似Mongo shell, native MongoDB driver的交互方式。
Buckle up!本章将要讨论:
- Mongoose installation
- Connection establishment in a standalone Mongoose script
- Mongoose schemas
- Hooks for keeping code organized
- Custom static and instance methods
- Mongoose models
- Relationships and joins with population
- Nested documents
- Virtual fields
- Schema type behavior amendment
- Express.js + Mongoose = true MVC
安装
- var mongoose = require('mongoose');
- mongoose.connect('mongodb://localhost:27017/test', {useNewUrlParser: true});
- //一个mongoose连接实例
- var db = mongoose.connection;
- db.once('open', () => {
- //...
- })
和native driver不一样,我们无需等待established connection, 只需要把所有的代码放入open()回调内。
不放入open()也可以,默认使用buffer。使用open(),确保连接了服务器。
⚠️官方文档原文的解释:
Mongoose lets you start using your models immediately, without waiting for mongoose to establish a connection to MongoDB.
无论是否连接上服务器的MongoDB数据库,都可以马上使用model。
mongoose.connect('mongodb://localhost:27017/myapp', {useNewUrlParser: true});
var Schema = mongoose.Schemavar MyModel = mongoose.model('Test', new Schema({ name: String }));
// Works
MyModel.findOne(function(error, result) { /* ... */ });
That's because mongoose buffers model function calls internally. This buffering is convenient, but also a common source of confusion. Mongoose will not throw any errors by default if you use a model without connecting.
这是因为mongoose内部地缓冲了模型函数调用。这个缓冲非常的方便,但也是一个常见的source困惑。
因为如果在没有连接的情况下,你使用model,Mongoose默认不会抛出❌,
- //一个脚本
- const mongoose = require('mongoose')
- var MyModel = mongoose.model('Test', new Schema({ name: String}));
- //查询的代码会挂起来,指定mongoose成功的连接上。
- MyModel.findOne(function(error, result) { /*...*/});
- setTimeout(function() {
- mongoose.connect('mongodb://localhost:27017/myapp', {useNewUrlParser: true})
- }, 6000)
在一个mongoose脚本建立一个连接
连接的URI结构:(一个string)
- mongodb://username:password@host:port/database_name
默认可以如下使用,host是localhost, port是27017, 数据库名字是test, 不设置username和password:
- mongoose.connect('mongodb://localhost:27017/test', {useMongoClient: true})
- mongoose.Promise = global.Promise
Promise这行让mongoose可以使用native ES6 promise 。也可以使用其他的promise implementation 。
- Mongoose.prototype.Promise //The Mongoose Promise constructor。
Options对象
connect(url, options)。 options是一个对象,里面是关于连接的属性设置。具体见官方文档。完全支持原生Node.js driver。
Model
下一步: 一个重要的差别(不同于Mongoskin和其他轻量型MongoDB库):
创建一个model, 使用model()函数并传递一个string和一个schema
- const Book = mongoose.model("Book", {name: String})
⚠️这里没有使用new mongoose.Schema()
现在配置语句结束,我们创建a document代表Book model 的实例:
- const oneBook = new Book({name: 'Practical Node.js'})
Mongoose documents有非常方便的内置方法:validate, isNew, update
(https://mongoosejs.com/docs/api.html#Document)
⚠️留心这些方法只能用在document上,不能用在collection或model上。
docuement是a model的实例, 而a model有点抽象,类似real MongoDB collection。
但是, 它由一个schema支持, 并且作为一个Node.js class(及额外的方法和属性)存在。
Models are fancy constructors compiled from Schema
definitions.
通常,我们不直接地使用Mongoose collections, 我们只通过models操作数据。
一些主要的model方法和native MongDB driver类似: find(), insert(), save()等等。
为了把一个docuemnt存入数据库,使用document.save()
这个方法是异步的asynchronous。因此添加一个callback或者promise或者async/await函数。
执行下面的脚本代码⚠️先打开MongoDB,server。
- const mongoose = require('mongoose')
- mongoose.connect('mongodb://localhost:27017/test')
- mongoose.Promise = global.Promise
- const Book = mongoose.model("Book", {name: String})
- const oneBook = new Book({name: "Hello world!"})
- oneBook.save((err, result) => {
- if (err) {
- console.err(err)
- process.exit(1)
- } else {
- console.log("Saved:", result)
- process.exit(0)
- }
- })
Mongoose Schemas
Everything in Mongoose starts with a Schema. Each schema maps to a MongoDB collection and defines the shape of the documents within that collection.
Mongoose开始于一个schema. 每个scheme映射到一个MongoDB collection并定义这个collection中的document的外形。
- var mongoose = require('mongoose');
- var blogSchema = new mongoose.Schema({
- title: String,
- comments: [{body: String, date: Date}],
- date: { type: Date, default: Date.now},
- hidden: Boolean
- })
- //add()方法,用于添加属性,参数是一个key/value对象, 或者是另一个Schema.
- //add()可以链式调用。
- blogSchema.add({author: String})
每个key在我们的documents内定义了一个属性并给予一个相关的SchemaType。
key也可以是嵌套的对象。
SchemaTypes:
- String, Number, Date, Boolean
- Buffer: a Node.js binary type(图片images, PDFs, archives等等)
- Mixed: 一种"anything goes"类型。任意类型的数据
- ObjectId: _id key 的类型。
- Array
- map
Schema不只定义document的结构和属性,也定义document的实例方法,静态Model方法, 混合的compond indexes, 文档hooks 调用middleware。
创建model
为了使用我们的schema定义,需要转化blogSchema进入一个Model:
- var Blog = mongoose.model('Blog', blogSchema)
Models的实例是documents。Documents有内建的实例方法。
Instance methods
通过schema定义客制化的实例方法:
- var animalSchema = new Schema({ name: String, type: String })
- // 分配一个函数给methods对象
- animalSchema.methods.findSimilarTypes = function(callback) {
- return this.model("Animal").find({ type: this.type }, callback)
- }
- var Animal = mongoose.model('Animal', animalSchema)
- var dog = new Animal({type: 'dog'})
- // 存入数据库
dog.save((err, dog) => {- console.log("save success!")
- })
- // dog document使用自定义的方法
- dog.findSimilarTypes(function(err, dogs) {
- console.log("yes", dogs); // yes [ { _id: 5c45ba13aaa2f74d3b624619, type: 'dog', __v: 0 } ]
- });
Statics
给一个Model增加一个静态方法。
把一个函数分配给animalSchema的statics对象。
如果把一个Model看成一个类,那么静态方法就是这个类的类方法。
- animalSchema.statics.findByName = function(name, callback) {
- return this.find({name: new RegExp(name, "i") }, callback)
- }
- var Animal = mongoose.model("Aniaml", animalSchema)
- Animal.findByName("fido", function(err, animals) {
- console.log("result: ", animals)
- })
⚠️,声明statics,不能使用箭头函数。因为箭头函数明确地防止绑定this。
也可以使用Schema.static(name, funtion)方法
- var schema = new mongoose.Schema(..);
- schema.static('findByName', function(name, callback) => {
- return this.find({name: name}, callback)
- })
使用{name: fn, name:fun, ...}作为唯一参数:
如果把一个hash(内含多个name/fn 对儿),作为唯一的参数传递给static(), 那么每个name/fn对儿将会被增加为statics静态方法。
- bookSchema.static({ // Static methods for generic, not instance/document specific logic
- getZeroInventoryReport: function(callback) {
- // Run a query on all books and get the ones with zero inventory
- // Document/instance methods would not work on "this"
- return callback(books)
- },
- getCountOfBooksById: function(bookId, callback){
- // Run a query and get the number of books left for a given book
- // Document/instance methods would not work on "this"
- return callback(count)
- }
- })
Query Helpers
可以增加query helper functions, 类似实例方法(❌?这句不是很明白,哪里类似了?),
但是for mongoose queries。
Query helper methods 让你扩展mongoose的链式查询builder API。chainable query builder API.
- animalSchema.query.byName = function(name) {
- return this.where({ name: new RegExp(name, 'i') });
- };
- var Animal = mongoose.model('Animal', animalSchema);
- Animal.find().byName('fido').exec(function(err, animals) {
- console.log(animals);
- });
⚠️由上可见query helper方法是Model调用的。所以原文 like instance methods 这句不明白。
indexes
MongDB支持第二个indexes.
使用mongoose,定义indexes的方法有2个:
- 在定义一个Schema时
- 使用Schema对象的index()方法。(主要用于组合式索引)
- var animalSchema = new mongoose.Schema({
- name: String,
- type: String,
- tags: { type: [String], index: true}
- })
- animalSchema.index({ name: 1, type: -1})
Virtuals
document的一个属性。
Options
Schemas有一些选项配置,可以用构建起或者用set()
- new mongoose.Schema({..}, options)
- // or
- var schema = new mongoose.Schema({..})
- schema.set(option, value)
Pluggable
Mongoose schemas是插件方式的, 即可以通过其他程序的schemas进行扩展。
(具体使用点击连接)
Hooks for Keeping Code Organized
假如:在有大量关联的对象的复杂应用内,我们想要在保存一个对象前,执行一段逻辑。
使用hook,来储存这段逻辑代码是一个好方法。例如,我们想要在保存一个book document前上传一个PDF到web site:
- //在一个schema上使用pre()钩子:
booSchema.pre('save', (next) => {- // Prepare for saving
- // Upload PFD
- return next()
- })
pre(method, [options], callback)
第一个参数是method的名字
⚠️:钩子和方法都必须添加到schemas上,在编译他们到models 之前。也就是说,在调用mongoose.model()之前。
官方guide: SchemaTypes摘要
SchemaTypes处理definition of path defaults , 验证, getters, setters, 查询的默认field selection, 和Mongoose document属性的其他一些普遍特征。
你可以把一个Mongoose Schema看作是Mongoose model的配置对象。
于是,一个SchemaType是一个配置对象,作为一个独立的属性。
- 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'
- // 一个userSchema的userSchema.path("name"):
- SchemaString {
- enumValues: [],
- regExp: null,
- path: 'name',
- instance: 'String',
- validators: [],
- getters: [],
- setters: [],
- options: { type: [Function: String] },
- _index: null }
我觉得:一个path类似关系型数据库中的table中的一个field定义。
所以一个SchemaType,表达了一个path的数据类型, 它是否是getters/setters的模式。
一个SchemaType不等于一个Type。它只是Mongoose的一个配置对象。
- mongoose.ObjectId !== mongoose.Types.ObjectId
它只是在一个schema内,对一个path的配置。
常用的SchemaTyps:
- var schema = new mongoose.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,
- array: []
- })
数组的SchemaTypes:
- var schema = new Schema({
- 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
- }
- })
SchemaType Options:
- var schema1 = new Schema({
- test: String // `test` is a path of type String
- });
- var schema2 = new Schema({
- // The `test` object contains the "SchemaType options"
- test: { type: String, lowercase: true } // `test` is a path of type string
- });
你可以增加任何属性(你想要给你的SchemaType options)。 有许多插件客制化SchemaType options。
Mongoose有几个内置的SchemaType options(具体见https://mongoosejs.com/docs/schematypes.html)
indexes
可以用schema type options定义MongoDB indexes:
- var 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`
- }
- });
不同的SchemaType有不同的options,具体见官方guide。
Mongoose Models
正如许多ORMs/ODMs, 在mongoose中,cornerstone object is a model。对象的基石是模块。
把一个schema编译进入一个model, 使用:
mongoose.model(name, schema)
第一个参数name,是一个字符串,大写字母开头,通常这个string和对象字面量(声明的变量名)一样。
默认,Mongoose会使用这个model name的复数形式去绑定到一个collection name。
Models用于创建documents(实际的data)。使用构建器:
new ModelName(data)
Models又内建的静态类方法类似native MongoDB方法,如find(), findOne(), update(), insertMany()
一些常用的model 方法:
- Model,create(docs) 等同new Model(docs).save()
- Model.remove(query, [callback(error)])。不能使用hooks。
- Model.find(query, [fields], [options], [callback(error, docs)])
- Model.update()
- Model.populate(docs, options, [callback(err, doc)]), 填入。
- Model.findOne
- Model.findById
注意⚠️,一部分model方法不会激活hooks, 比如deleteOne(),remove()。他们会直接地执行。
最常用的实例方法:
- save()
- toJSON([option]): 把document转化为JSON
- toObject(): 把document转化为普通的JavaScript对象。
- isModified([path]): True/false
- doc.isNew: True/false
- doc.id: 返回document id
- doc.set():参数包括path, val, [type], ⚠️path其实就是field名字key/value对儿的key。
- doc.validate(): 手动地检测验证(自动在save()前激活)
大多数时候,你需要从你的document得到数据。
使用res.send()把数据发送到一个客户端。
document对象需要使用toObject()和toJSON()转化格式,然后再发送。
Document
Retrieving
具体见:querying一章。
updating
可以使用findById(), 然后在回调函数内修改查询到的实例的属性值。
- Tank.findById(id, function (err, tank) {
- if (err) return handleError(err);
- tank.size = 'large'; //或者使用tank.set({ size: 'large' })
- tank.save(function (err, updatedTank) {
- if (err) return handleError(err);
- res.send(updatedTank);
- });
- });
如果只是想要把新的数据更新到数据库,不返回,则可以使用Model#updateOne()
- Tank.update({_id: id}, { $set: {size: 'large'}}, callback)
如果如findById加上save(),返回新的数据,有更方便的方法: findByIdAndupdate()
配合使用res.send()
- Tank.findByIdAndUpdate(id, { $set: { size: 'large' }}, { new: true }, function (err, tank) {
- if (err) return handleError(err);
- res.send(tank);
- });
⚠️,findByIdAndUpdate不会执行hooks或者验证,所以如果需要hooks和full documente validation,用第一种query然后save() it。
Validating
Documents在被保存前需要验证,具体见validation
重写
.set(doc)方法,参数是另一document的话,相当于重写。
Relationships and Joins with Population
使用Model.populate()或者 Query.populate()
虽然,Node开发者不能查询Mongo DB(on complex relationships), 但是通过Mongoose的帮助,开发者可以在application layer做到这点。
在大型的程序中,documents之间又复杂的关系,使用mongoose就变得很方便了。
例如,在一个电子商务网站,一个订单通过产品id,关联产品。为了得到更多的产品信息,开发者需要写2个查询: 一个取order,另一个取订单的产品。
使用一个Mongoose query就能做到上面的2个查询的功能。
Populate
Mongoose通过连接订单和产品让2者的关系变得简单:Mongoose提供的一个功能,population。
这里population涉及的意思类似related,即相关的,有联系的。
populations是关于增加更多的data到你的查询,通过使用relationships。
它允许我们从一个不同的collection取数据来fill填document的一部分。
比如我们有posts和users,2个documents。Users可以写posts。这里有2类方法实现这个写功能:
- 使用一个collection,users collection有posts 数组field。这样就只需要一个单独的query,但是这种结构导致某些方法的被限制。因为posts不能被indexed or accessed separately from users.
- 或者使用2个collections(and models)。在这个案例,这种结构会更灵活一些。但是需要至少2个查询,如果我们想要取一个user和它的posts。
于是Mongoose提供了population,在这里有用武之地了。
在user schema内引用posts。之后populate这些posts。为了使用populate(), 我们必须定义ref和model的名字:
- const mongoose = require('mongoose')
- const Schema = mongoose.Schema
- const userSchema = new Schema({
- _id: Number,
- name: String,
- posts: [{
- type: Schema.Types.ObjectId,
- ref: 'Post'
- }]
- })
⚠️,Schema.Types.ObjectId是一种SchemaType。
实际的postSchema只加了一行代码:
- const postSchema = Schema({
- _creator: { type: Number, ref: 'User'},
- title: String,
- text: String
- })
下面的几行代码是我们创建models, 然后yes!!! 只用一个findOne()类方法即可得到全部的posts的数据。
执行exec()来run:
- const Post = mongoose.model("Post", postSchema)
- const User = mongoose.model('User', userSchema)
- //添加一些数据,并存入MongoDB数据库
User.findOne({name: /azat/i})- .populate('posts')
- .exec((err, user) => {
- if (err) return handleError(err)
- console.log('The user has % post(s)', user.posts.length)
- })
⚠️ ObjectId
, Number
, String
, and Buffer
are valid data types to use as references,
meaning they will work as foreign keys in the relational DB terminology.
知识点:
- 正则表达式:找到所有匹配azat的string,大小写敏感, case-insensitively。
- console.log中的 %, 一种字符串插入符号的写法,把user.posts.length插入这个字符串。
也可以只返回一部分填入的结果。例如,我们能够限制posts的数量为10个:
⚠️在mongoose, path指 定义一个Schema中的type类型的名字
- .populate({
- path: 'posts',
- options: { limit: 10, sort: 'title'}
- })
有时候,只会返回指定的fileds,而不是整个document,使用select:
- .populate({
- path: 'posts',
- select: 'title',
- options: {
- limit: 10,
- sort: 'title'
- }
- })
另外,通过一个query来过滤填入的结果!
- .populate({
- path: 'posts',
- select: '_id title text',
- match: {text: /node\.js/i},
- options: { limit: 10, sort: '_id'}
- })
查询选择的属性使用select, 值是一个字符串,用空格分开每个field name。
建议只查询和填入需要的fields,因为这样可以防止敏感信息的泄漏leakage,降低系统风险。
populate方法可以find()连接使用,即多个document的查询。
问题:
1. user.posts.length,这是user.posts是一个数组吗?所以可以使用length方法。
答:是的,在定义userSchema时,posts field的数据类型是数组。
2.exec()的使用:
Model.find()返回<Query>, 然后使用Query.populate()并返回<Query>this, 然后使用Query.exec()返回Promise
3 type和ref
type代表SchemType。ref属性是SchemaType Options的一种。和type属性配合使用。
4.上面的案例,如何保存有关联的数据?
- var user = new User({name: "John", _id: 2})
- var post = new Post({title: "New land", text: "Hello World!"})
- user.posts = post._id
- post._creator = user._id
- user.save()
- post.save()
- User.findOne({_id: 2}).populate("posts")
- .exec((error, user) => {
- console.log(user.posts.length)
- })
还需要看官网的Populate一章。讲真,这本书讲的都很浅显,有的没有说清楚。
理解:User和Post各自有一个含有选项ref的path。因此双方建立了关联。
官方guide Populate()
Population是指: 在一个document内,用来自其他collection(s)的document,自动地取代指定paths的值。
我们可以populate一个单独的document,多个documents, 普通的object,多个普通的objects, 或者从一个query返回的所有objects。
基础
- const mongoose = require('mongoose')
- const Schema = mongoose.Schema
- mongoose.connect('mongodb://localhost:27017/test', {useNewUrlParser: true})
- // var db = mongoose.connection
- const personScheam = Schema({
- _id: Schema.Types.ObjectId,
- name: String
- age: Number,
- stories: [{ type: Schema.Types.ObjectId, ref: "Story"}]
- })
- const storySchema = Schema({
- author: { type: Schema.Types.ObjectId, ref: "Person"},
- title: String,
- fans: [{ type: Schema.Types.ObjectId, ref: "Person"}]
- })
- const Story = mongoose.model("Story", storySchema)
- const Person = mongoose.model("Person", personScheam)
注意⚠️
- 使用ref选项的path的类型必须是ObjectId, Number, String, Buffer之一。
- 通常使用ObjectId, 除非你是一个高级用户或有充分如此做的原因
saving refs
保存refs到其他documents和你保存属性的方式一样,指需要分配_id值:
- const author = new Person({
- _id: new mongoose.Types.ObjectId,
- name: "Ian Fleming",
- age: 50
- })
- author.save((err) => {
- if (err) return handleError(err)
- const story1 = new Story({
- title: "Casino Royale",
- author: author._id
- })
- story1.save((err, story1) => {
- if (err) return handleError(err)
- console.log("Success stores", story1.title)
- })
- })
上面的代码,因为story1有外键author(即通过_id建立了两个documents的关联), 所以story1能直接populate author的数据。
Population
现在填入story的author,使用query builder:
- Story.findOne({ title: "Casino Royale"})
- .populate('author')
- .exec((err, story) => {
- if (err) return handleError(err)
- console.log("The author is %s", story.author.name)
- })
通过在返回结果前运行一个独立的query,(findOne()方法返回的是一个Query对象)
填入的paths不再是它们的原始的_id, 它们的值被替换为从数据库返回的document。
Arrays of refs和 非Arrays of refs的工作方式一样。都是在query对象上调用populate方法,并返回一个array of documents来替代原始的_ids。
Setting Populated Fields
也可以手动填入一个对象,来替换_id。把一个document对象赋值给author属性。
这个对象必须是你的ref选项所涉及的model的一个实例:
- //假设之前已经向数据库存入了一个person和一个story, story有person的外键:
- Story.findOne({ title: "Casino Royale"}, (error, story) => {
- if (error) {
- return handleError(error)
- }
- Person.findOne({name: "Ian Fleming"}).exec((err, person) => {
- story.author = person
- console.log(story.author.name)
- })
- })
//控制台会输出author的名字
这是不使用populate的方法。和使用populate的效果一样,都是替换掉了_id。
hat If There's No Foreign Document?
Mongoose populate不像传统的SQL joins。类似left join in SQL。
- Person.deleteMany({ name: "Ian Fleming" }, (err, result) => {
- if (err) {
- console.log("err: ",err)
- } else {
- console.log("res: ", result)
- }
- });
- //因为没有了Person中的document, story.author.name是null。
- Story.findOne({ title: "Casino Royale"})
- .populate('author')
- .exec((err, story) => {
- if (err) return handleError(err)
- console.log("The author is %s", story.author.name)
- })
如果storySchema的authors path是数组形式的, 则populate()会返回一个空的array
Field Selection
如果只想从返回的populated documents得到指定的fields, 可以向populate()传入第二个参数: field name\
populate(path, [select])
- Story.findOne({ title: "Casino Royale"})
- .populate('author', 'name')
- .exec((err, story) => {
- if (err) return handleError(err)
- console.log("The author is %s", story.author.name)
- //返回The authors age is undefined
- console.log('The authors age is %s', story.author.age)
- })
Populating Multiple Paths
如果我们想要同时填入多个paths, 把populate方法连起来:
- Story.
- find(...).
- populate('fans').
- populate('author').
- exec();
Query conditions and other options
如果我们想要填入populate的fans数组基于他们的age, 同时只选择他们的名字,并返回最多5个fans, 怎么做?
- Story.find(...)
- .populate({
- path: 'fans',
- match: {age: { $gte: 21 }},
- // 使用"-_id",明确表示不包括"_id"field。
- select: "name -_id",
- options: { limit: 5}
- })
- .exec()
Refs to chlidren
本章Populate官网教程提供的案例,auhtor对象的stories field并没有被设置外键。
因此不能使用author.stories得到stories的列表。
这里有2个观点:perspectives:
第一, 你想要author对象知道哪些stories 是他的。通常,你的schema应该解决one-to-many关系,通过在many端加一个父pointer指针。但是,如果你有好的原因想要一个数组的child指针,你可以使用push()方法,把documents推到这个数组上:
- author.stories.push(story1)
- author.save(callback)
这样,我们就可以执行一个find和populate的联合
- Person.
- findOne({ name: 'Ian Fleming' }).
- populate('stories'). // only works if we pushed refs to children
- exec(function (err, person) {
- if (err) return handleError(err);
- console.log(person);
- });
是否真的要设置2个方向的pointers是一个可争论的地方。
第二,作为代替, 我们可以忽略populating,并直接使用find()方法,找到stories:
- Story.
- find({ author: author._id }).
- exec(function (err, stories) {
- if (err) return handleError(err);
- console.log('The stories are an array: ', stories);
- });
Populating an existing document
如果我们有一个正存在的mongoose document并想要填入一些它的paths,
可以使用document#populate() , 返回Document this。
- doc.populate(path|options, callback)
- // or
- doc.populate(options).execPopulate()
Populating multiple existing documents
如果我们有多个documents或者plain objects, 我们想要填入他们,使用Model.populate()方法。
这和document#populate(), query#populate()方式类似。
populate(docs, options, [callback(err, doc)]) 返回Promise.
- docs <Document|Array>,一个单独的对象或者一个数组的对象。
- options <Object| Array> 一个hash的key/value对儿。可使用的顶级options:
- path: 值是要填入的path的名字
- select: 选择要从数据库得到的fields
- match: 可选的查询条件用于匹配
- model: 可选的model的名字,用于填入。(已知是用在不同数据库的model实例的填入)
- options: 可选的查询条件,比如like, limit等等。
- justOne: 可选的boolean,如果是true,则设置path为一个数组array。默认根据scheam推断。
- // populates an array of objects
// find()返回一个query,里面的result是一个array of documents, 因此opts也应该是一个array of document- User.find(match, function (err, users) {
- var opts = [{ path: 'company', match: { x: 1 }, select: 'name' }]
- var promise = User.populate(users, opts);
- promise.then(console.log).end();
- })
填入一个object, 和上面填入一个array of objects, 和填入很多plain objects。具体见文档
Populating across multiple levels跨越多层的填入
一个model的内的实例可以互相关联。即Self Joins
(这在Rails中的例子也是自身model上加一个foreign_key)
一个user schema可以跟踪user的朋友:
⚠️,关键使用ref选项,引用"User"自身!!!
- var userSchema = new Schema({
- name: String,
- friends: [{ type: Scheam.Types.ObjectId, ref: 'User'}]
- })
Populate让你得到一个user的朋友的列表。
但是如果你也想要一个user的朋友的朋友哪?加一个populate选项的嵌套:
- User.
- findOne({ name: 'Val' }).
- populate({
- path: 'friends',
- // Get friends of friends - populate the 'friends' array for every friend
- populate: { path: 'friends' }
- });
一个完整的例子:
- //populate.js
- const mongoose = require('mongoose')
- const Schema = mongoose.Schema
- mongoose.connect('mongodb://localhost:27017/test', {useNewUrlParser: true})
- const userSchema = new Schema({
- _id: Number,
- name: String,
- friends: [{
- type: Number,
- ref: 'User'
- }]
- })
- const User = mongoose.model("User", userSchema)
- //存入下面的数据
- var user = new User({ name: "chen", _id: 3, friends: [4] }).save()
- var user2 = new User({ name: "haha", _id: 4, friends: [3, 5] }).save()
- var user3 = new User({ name: "ming", _id: 5, friends: [5] }).save()
执行查询,使用populate选项:
- User.findOne({_id: 3})
- .populate({
- path: 'friends',
- populate: {path: 'friends'}
- })
- .exec((err, result) => {
- console.log(result)
- })
- //返回
- { posts: [],
- friends:
- [ { posts: [],
- friends:
- [ { posts: [], friends: [ 4 ], _id: 3, name: 'chen', __v: 0 },
- { posts: [], friends: [ 5 ], _id: 5, name: 'ming', __v: 0 } ],
- _id: 4,
- name: 'haha',
- __v: 0 } ],
- _id: 3,
- name: 'chen',
- __v: 0 }
Populating across Databases跨越数据库的填入
使用model选项
之前的练习:
- //引进mongoose
- const mongoose = require('mongoose')
- //得到Schema构建器
- const Schema = mongoose.Schema
- //mongoose实例连接到本地端口27017的数据库test
- mongoose.connect('mongodb://localhost:27017/test', {useNewUrlParser: true})
- //得到connection对象实例, 因为实际的原因,一个Connection等于一个Db
- var db = mongoose.connection
- // with mongodb:// URI, 创建一个Connection实例
// 这个connection对象用于创建和检索models。
// Models总是在一个单一的connection中使用(scoped)。
- var db = mongoose.createConnection('mongodb://user:pass@localhost:port/database');
假如,events和conversations这2个collection储存在不同的MongoDB instances内。
- var eventSchema = new Schema({
- name: String,
- // The id of the corresponding conversation
- // ⚠️没有使用ref
- conversation: Schema.Typs.ObjectId
- });
- var conversationSchema = new Schema({
- numMessages: Number
- });
- var db1 = mongoose.createConnection('localhost:27000/db1');
- var db2 = mongoose.createConnection('localhost:27001/db2');
- //⚠️,我的电脑上不能同时开2个mongd,提示❌
exception in initAndListen: DBPathInUse: Unable to lock the lock file: /data/db/mongod.lock (Resource temporarily unavailable). Another mongod instance is already running on the /data/db directory, terminating- var Event = db1.model('Event', eventSchema);
- var Conversation = db2.model('Conversation', conversationSchema);
这种情况下,不能正常使用populate()来填入数据,需要告诉populate使用的是哪个model:
- Event.
- find().
- populate({ path: 'conversation', model: Conversation }).
- exec(function(error, docs) { /* ... */ });
实践的例子: 跨MongoDB databases实例。
- // Populating across Databases
- const mongoose = require('mongoose')
- const Schema = mongoose.Schema
- mongoose.connect('mongodb://localhost:27017/test', { useNewUrlParser: true })
- var db2 = mongoose.createConnection('mongodb://localhost:27017/db2', { useNewUrlParser: true })
- // 创建2个Schema。
- var eventSchema = new Schema({
- name: String,
- conversation: Schema.Types.ObjectId
- });
- var conversationSchema = new Schema({
- numMessages: Number
- });
// 在test 数据库上创建一个Event类的实例。- var Event = mongoose.model('Event', eventSchema)
- var event = new Event({name: "click"}).save()
- // 在db2 数据库上创建一个Conversation类的实例
- var Conversation = db2.model('Conversation', conversationSchema);
- var conversation = new Conversation({numMessages: 50}).save()
// 我在mongDb shell中给event document增加了一个field(conversation: XX),值是conversation实例的_id
启动上面的脚本后,我修改脚本去掉创建实例的2行代码,然后添加一个find和populate, 然后重启脚本:
- Event.find()
- .populate({ path: 'conversation', model: Conversation})
- .exec((error, docs) => {
- console.log(docs)
- })
成功,填入conversation: (这个例子就是在不同database的一对一关联)
- [ { _id: 5c4ad1f2916c8325ae15a6ac,
- name: 'click',
- __v: 0,
- conversation: { _id: 5c4ad1f2916c8325ae15a6ad, numMessages: 50, __v: 0 } } ]
上面的练习,
- 如果在find()内去掉model,再次运行脚本,返回的数组内的conversation field的值是 null
- 如果在find()内去掉model, 然后在eventSchema内加上ref,再次运行脚本。返回null。
上面的练习,把2个model放在同database下,可以正确运行的✅。
即eventSchema没有使用 ref, 但在find().populate()内使用了model: "Conversation", 可以填入对应的conversation实例。
因为官方文档:Query.prototype.populate()的参数[model]的解释是这样的:
- «Model» The model you wish to use for population.
If not specified, populate will look up the model by the name in the Schema's ref field.
即,
如果populate方法内指定了model选项,则从这个model中找对应的document。
如果没有指定model,才会在eventSchema中找ref选项,因为ref的值就是一个model的名字。
结论就是,不论是model选项还是 ref选项,它们都是把2个document连接起来的辅助。
Dynamic References via refPath
Populate Virtuals
Populate Virtuals: The Count Option
Populate in Middleware
Nested Documents
上一章population。 这是一种传统的方法,来设计你的数据库。它minic模仿了关系型数据库设计并使用普通的forms和严格的数据原子化atomization。
The document storage model in NoSQL databases is well suited to use nested documents。
如果你指定最频繁运行的查询是什么,使用nested documents是更好的选择。
你可以优化你的数据库让它倾向某一个查询。
例如,大多数典型的使用案例是读用户数据。那么代替使用2个collections(posts and users),
我们可以用单一的collections(users), 内部嵌套posts。
绝对使用哪种方式更多的是建筑学的问题,它的答案由具体使用决定。
例如,
- 如果有类似blog的功能,多个用户会读取作者的posts,就需要独立的查询作者的posts。分开的collection会更好。
- 如果posts只在作者的个人页面使用,那么最好就用nested documents。
使用Schema.Types.Mixed类型
- const userSchema = new mongoose.Schema({
- name: String,
- posts: [mongoose.Schema.Types.Mixed]
- })
- // Attach methods, hooks, etc.
- const User = mongoose.model('User', userSchema)
更灵活的Schema设计,分成2个Schema:
- const postSchema = new mongoose.Schema({
- title: String,
- text: String
- })
- // Attach methods, hooks, etc., to post schema
- const userSchema = new mongoose.Schema({
- name: String,
- posts: [postSchema]
- })
- // Attach methods, hooks, etc., to user schema
- const User = mongoose.model('User', userSchema)
增加子文档到arrays:
因为使用了数组,所以可以使用push, unshift, 等方法(在JavaScript/Node.js)或者MongoDB$push操作符号来更新user document:
- User.updateOne(
- {_id: userId},
- {$push: {posts: newPost}},
- (error, results) => {
- // 处理错误和检测结果
- }
- )
也可以使用save():
- var childSchema = new Schema({name: String})
- var parentSchema = new Schema({
- children: [childSchema],
- name: String
- })
- var Parent = mongoose.model('Parent', parentSchema)
- var parent = new Parent({
- children: [{name: 'Matt'}, {name: 'Sarah'}]
- })
- parent.children[0].name = 'Matthew'
parent.children.push({ name: 'Liesl'})- parent.save((error, result) => {
- if (error) return console.log(error)
- console.log(result)
- })
得到:
- { _id: 5c47d630d93ce656805231f8,
- children:
- [ { _id: 5c47d630d93ce656805231fa, name: 'Matthew' },
- { _id: 5c47d630d93ce656805231f9, name: 'Sarah' } ,
{ _id: 5c47d9b07517b756fb125221, name: 'Liesl' } ],- __v: 0 }
注意⚠️,新增了3个child, 和parent一起存在mongoDB的test数据库的parents collections内
查询一个子document
每个子document默认有一个_id。
Mongoose document arrays有一个特别的id方法用于搜索一个doucment array来找到一个有给定_id值的document。
- var doc = parent.children.id(_id)
移除使用remove方法,相当于在子文档内使用.pull()
- parent.children.pull(_id)
- //等同
- parent.children.id(_id).remove()
//对于:a single nested subdocument:- parent.child.remove()
- //等同
- parent.child = null
官方文档Queries
Mongoose models提供用于CRUD操作的静态帮助函数。这些函数返回一个mongoose Query 对象。
- Model.deleteOne(), deleteMany()
- Model.find()
- Model.findById(), 及衍生出findByIdAndDelete(), findByIdAndRemove, findByIdAndUpdate
- Model.findOne(), 及衍生出findOneAndDelete(), findOneAndRemove, findOneAndUpdate
- Model.replace() ,类似update(), 用传入的doc取代原来的document
- Model.updateOne(), Model.updateMany()。
一个Query对象可以使用.then()函数。
query with callback
当使用一个query并传入一个callback(), 你指定你的query作为一个JSON document。
这个JSON document的语法和MongoDB shell相同。
- var Person = mongoose.model('Person', yourSchema);
- // find each person with a last name matching 'Ghost', selecting the `name` and `occupation` fields
- Person.findOne({ 'name.last': 'Ghost' }, 'name occupation', function (err, person) {
- if (err) return handleError(err);
- console.log('%s %s is a %s.', person.name.first, person.name.last,
- person.occupation);
- });
⚠️在Mongoose内,所有的callback都使用这个模式callback(error, result)
- 如果有error存在,则error参数包含a error document。 result的值是null
- 如果query是成功的,error参数是null, 并且result被填入populated查询的结果。
findOne()的例子:
- Adventure.findOne({ type: 'iphone' }, function (err, adventure) {});
- // same as above
- Adventure.findOne({ type: 'iphone' }).exec(function (err, adventure) {});
- // specify options, in this case lean
- Adventure.findOne({ type: 'iphone' }, 'name', { lean: true }, callback);
- // same as above
- Adventure.findOne({ type: 'iphone' }, 'name', { lean: true }).exec(callback);
- // chaining findOne queries (same as above)
- Adventure.findOne({ type: 'iphone' }).select('name').lean().exec(callback);
lean选项为true,从queries返回的documents是普通的javaScript 对象,而不是MongooseDocuments。
countDocuments()的例子
在一个collection中,计算符合filter的documents的数量.
query but no callback is passed
一个Query 可以让你使用chaining syntax,而不是specifying a JSON object
例子:
- Person.
- find({
- occupation: /host/,
- 'name.last': 'Ghost',
- age: { $gt: 17, $lt: 66},
- likes: { $in: ['vaporizing', 'talking']}
- }).
- limit(10).
- sort({ occupation: -1 }).
- select({name: 1, occupation: 1})
- exec(callback)
- //等同于使用query builder:
- Person.
- find({ occupation: /host/ }).
- where('name.last').equals('Ghost').
- where('age').gt(17).lt(66).
- where('likes').in(['vaporizing', 'talking']).
- limit(10).
- sort('-occupation').
- select('name occupation').
- exec(callback);
Queries不是promises
可以使用.then函数, 但是调用query的then()能够执行这个query多次。
- const q = MyModel.updateMany({}, { isDeleted: true }, function() {
- console.log('Update 1');
- });
- q.then(() => console.log('Update 2'));
- q.then(() => console.log('Update 3'));
上个例子,执行了3次updateMany()。
- 第一次使用了callback。
- 后2次,使用了then()。
注意⚠️不要在query混合使用回调函数和promises。
Virtual Fields (Virtuals)
不存在于数据库,但是像regular field in a mongoose document。就是mock,fake。
Virtual fields的用途:
- dynamic data
- creating aggregate fields
例子,一个personSchema,有firstName, lastName2个fields,和一个Virtual fields(fullName),这个Virtual fullName无需真实存在。
另一个例子,兼容以前的database。每次有一个新的schema, 只需增加一个virtual来支持旧的documents。
例如, 我们有上千个用户记录在数据库collection,我们想要开始收集他们的位置。因此有2个方法:
1. 运行一个migration script,为所有old user documents增加一个location field, 值是none。
2. 使用virtual field 并在运行时,apply defaults。
再举一个例,加入有一个大的document,我们需要得到这个document的部分数据,而不是所有的数据,就可以使用virtual field来筛选要显示的数据:
- //从Schema中筛选出一些fields,放入虚拟field"info"
- userSchema.virtual('info')
- .get(function() {
- return {
- service: this.service,
- username: this.username,
- date: this.date,
- // ...
- }
- })
定义a virtual :
- personSchema.virtual('fullName')创建一个virtual type。
- 使用a getter function, get(fn), 返回<VirtualType>this。 (不要使用箭头函数, this是一个instance/document)
完整的例子:
- const mongoose = require('mongoose')
- mongoose.connect('mongodb://localhost:27017/myproject', {useNewUrlParser: true})
- var personSchema = new mongoose.Schema({
- name: {
- first: String,
- last: String
- }
- })
//定义一个virtualType- personSchema.virtual('fullName').get(function () {
- return this.name.first + ' ' + this.name.last;
- });
- var Person = mongoose.model('Person', personSchema)
- // var axl = new Person({
- // name: {
- // first: 'Axl',
- // last: 'Rose'
- // }
- // }).save((error, result) => {
- // if (error) return console.log(error)
- // console.log(result)
- // })
- Person.findOne({"name.first": 'Axl'}, (error, result) => {
- console.log(result.fullName)
- })
上面的例子使用了Schema#virtual()方法。定义了一个虚拟field,并VirtualType#get()方法定义了一个getter。自然也可以定义一个setter,使用set()方法:(关于get,set见
Practical Node.js (2018版) 第7章:Boosting Node.js and Mongoose的更多相关文章
- Practical Node.js (2018版) 第5章:数据库 使用MongoDB和Mongoose,或者node.js的native驱动。
Persistence with MongoDB and Mongoose https://github.com/azat-co/practicalnode/blob/master/chapter5/ ...
- Practical Node.js (2018版) 第10章:Getting Node.js Apps Production Ready
Getting Node.js Apps Production Ready 部署程序需要知道的方面: Environment variables Express.js in production So ...
- Practical Node.js (2018版) 第9章: 使用WebSocket建立实时程序,原生的WebSocket使用介绍,Socket.IO的基本使用介绍。
Real-Time Apps with WebSocket, Socket.IO, and DerbyJS 实时程序的使用变得越来越广泛,如传统的交易,游戏,社交,开发工具DevOps tools, ...
- Practical Node.js (2018版) 第3章:测试/Mocha.js, Chai.js, Expect.js
TDD and BDD for Node.js with Mocha TDD测试驱动开发.自动测试代码. BDD: behavior-driven development行为驱动开发,基于TDD.一种 ...
- Practical Node.js (2018版) 第8章:Building Node.js REST API Servers
Building Node.js REST API Servers with Express.js and Hapi Modern-day web developers use an architec ...
- Practical Node.js (2018版) 第4章: 模版引擎
Template Engines: Pug and Handlebars 一个模版引擎是一个库或框架.它用一些rules/languages来解释data和渲染views. web app中,view ...
- Vue.js 学习笔记 第1章 初识Vue.js
本篇目录: 1.1 Vue.js 是什么 1.2 如何使用Vue.js 本章主要介绍与Vue.js有关的一些概念与技术,并帮助你了解它们背后相关的工作原理. 通过对本章的学习,即使从未接触过Vue.j ...
- Node入门教程(7)第五章:node 模块化(下) npm与yarn详解
Node的包管理器 JavaScript缺少包结构的定义,而CommonJS定义了一系列的规范.而NPM的出现则是为了在CommonJS规范的基础上,实现解决包的安装卸载,依赖管理,版本管理等问题. ...
- Node入门教程(6)第五章:node 模块化(上)模块化演进
node 模块化 JS 诞生的时候,仅仅是为了实现网页表单的本地校验和简单的 dom 操作处理.所以并没有模块化的规范设计. 项目小的时候,我们可以通过命名空间.局部作用域.自执行函数等手段实现变量不 ...
随机推荐
- linux下安装tomcat和jdk
1.现在的linux服务器一般自带jdk,先查询是否已经安装jdk rpm -qa | grep java rpm -qa | grep jdk 如上则是没有安装,可以直接跳到步骤X,安装jdk.否则 ...
- Linux 字符设备驱动开发基础(二)—— 编写简单 PWM 设备驱动【转】
本文转载自:https://blog.csdn.net/zqixiao_09/article/details/50858776 版权声明:本文为博主原创文章,未经博主允许不得转载. https: ...
- 《OFFER14》14_CuttingRope
// 面试题14:剪绳子 // 题目:给你一根长度为n绳子,请把绳子剪成m段(m.n都是整数,n>1并且m≥1). // 每段的绳子的长度记为k[0].k[1].…….k[m].k[0]*k ...
- 【做题】apc001_f-XOR Tree——巧妙转化及dp
对树上的路径进行操作是十分难处理的事情.一开始的思路主要针对于\(a_i<=15\)这一特殊性质上.于是考虑了\(a_i<=1\)的情况,然而除了糊出一个适用范围极小的结论外,并没有什么用 ...
- 题解——CF Manthan, Codefest 18 (rated, Div. 1 + Div. 2) T5(思维)
还是dfs? 好像自己写的有锅 过不去 看了题解修改了才过qwq #include <cstdio> #include <algorithm> #include <cst ...
- (转)Introduction to Gradient Descent Algorithm (along with variants) in Machine Learning
Introduction Optimization is always the ultimate goal whether you are dealing with a real life probl ...
- Gtk 窗口,控件,设置(添加图片等)
1.关于窗口 // 创建顶层窗体,后面有POPUP的 GtkWidget *main_window; main_window = gtk_window_new (GTK_WINDOW_TOPLEV ...
- FPGA软件使用基础之ISE下载配置 XILINX 下载器使用
重新编辑 转载于https://www.cnblogs.com/lpp2jwz/p/7306020.html 下载程序 下载BIT 格式程序到FPGA 先插好下载器 在 ISE 中编译完 BIT 文件 ...
- HDU 3400 Line belt (三分套三分)
http://acm.split.hdu.edu.cn/showproblem.php?pid=3400 题意: 有两条带子ab和cd,在ab上的速度为p,在cd上的速度为q,在其它地方的速度为r.现 ...
- select2 使用方法总结
官网:http://select2.github.io/ 调用 <link href="~/Content/select2.min.css" rel="styles ...