【中文】【mongoose】【伪文档】【非喜欢勿看】希望能照顾的英文不好的同学
发布于 6 年前 作者 BengBu-YueZhang 3801 次浏览 来自 分享

我英文也不好,所以分享给大家,以供参考。这篇文章,是我慢慢看文档抠出来的

MongoDB

文档地址(英文),目前mongoDB已经发布了4.0版本,现有的中文文档全部过时,而且过时的很严重,建议看英文的为主。其实英文文档也没有多难。

CRUD操作

Create

db.集合名.API()

insertOne

插入单条记录


db.users.insertOne({
  name: '惣流·明日香·兰格雷'
})

insertMany

插入多条记录

db.users.inserMany([
  {name: '绫波丽'},
  {name: '惣流·明日香·兰格雷'}
])

Update

db.集合名.API(查询条件, 更新内容, 具体配置)

updateOne

更新满足条件的第一条数据

// 找到id大于1的第一条数据,将name字段更新为惣流·明日香·兰格雷
db.users.updateOne(
  {id: {$gt: 1}},
  {$set: {
    name: '惣流·明日香·兰格雷'
  }}
)

updateMany

更新满足条件的全部数据


// 找到id大于0的全局数据,将name字段更新为广末凉子
db.users.updateMany(
  {id: ${gt: 0}},
  {$set: {
    name: '广末凉子'
  }}
)

replaceOne

替换满足条件的第一条数据


// 将id为1的数据第一条替换
db.users.replaceOne(
  {id: 1},
  {name: '惣流・アスカ・ラングレー'}
)

Delete

db.集合名.API()

delete


// 删除集合中的全部数据
db.users.delete({})

deleteOne


// 删除name为美里的第一条数据
db.users.deleteOne({
  name: '美里'
})

deleteMany


// 删除id大于1的全部数据
db.users.deleteMany({
    id: {$gt: 1}
})

Retrieve

查询操作是增删改查中最为复杂的

基本查询


// 查询集合中的全部文档
db.users.find({})

// 查询集合中name字段包含 "php"的文档
db.users.find({
    name: {$regex: /php/gi}
})

And OR


// and查询,id大于1并且name字段等于php或者java的文档
db.users.find({
    id: {$gt: 1},
    name: {$in: ['php', 'java']}
})
// or查询(短路or,如果第一个条件满足,第二个条件会被忽略)
// name字段包含java或者c++的数据
db.users.find({
    $or: [
        {name: {$regex: /java/gi}},
        {nmae: {$regex: /c++/gi}}
    ]
})

嵌套文档查询

对于嵌套字段的查询,匹配的查询条件的书写是非常严格的,包括字段的顺序一致时才会被匹配。

db.users.insertMany([
    {
        name: '广末凉子',
        info: {
            birthday: 1980,
            country: '日本'
        }
    },
    {
        name: '本田翼',
        info: {
            birthday: 1992,
            country: '日本'
        }
    }
])

// 精确查询(注意字段的顺序需要和保存时候的顺序一致)
db.users.find({
    info: {
        birthday: 1992,
        country: '日本'
    }
})

// 如果需要对于嵌套文档,使用条件查询而不是精确查询,需要使用点符合
// 查询info.birthday大于1970的数据
db.users.find({
    'info.birthday': {$gt: 1970}
})

// 点符号配合and查询
// 查询info.birthday大于1960的并且info.country为日本的数据
db.users.find({
    'info.birthday': {$gt: 1960},
    'info.country': '日本'
})

数组的查询


db.games.insertMany([
    {name: '使命召唤4', tag: ['2007', '战争']},
    {name: '黑暗之魂3', tag: ['2016', '魔幻']},
    {name: '刺客信条:起源', tag: ['2017', '历史']}
])

// 数组为2017,历史的文档,顺序也需要保证一致,才能正确的匹配
db.games.find({
    tag: ['2017', '历史']
})

// 数组中包含‘历史’的文档
db.games.find({
    tag: {
        $all: ['历史']
    }
})

// 复合条件查找
// 数组中一个包含2017,另一个包含历史,或者两者都包含的文档
db.games.find({
    tag: {
        $in: ['2017'],
        $in: ['历史']
    }
})

// 多条件查找
// dim_cm字段数组中至少有一个元素大于22小于30
var cursor = db.collection('inventory').find({ 
  dim_cm: { $elemMatch: { $gt: 22, $lt: 30 } }
})

// 数组中指定元素的查找,使用点操作符,第一位为魔幻的数组
db.games.find({
    'tag.1': '魔幻'
})

// 数组长度查询,长度为2的数组
db.games.find({
    tag: {$size: 2}
})

数组嵌套对象的查询


db.games.insertMany([
    {
        time: '2018',
        games: [
            {
                name: '战神',
                month: 4
            },
            {
                name: '最终幻想15',
                month: 3
            }
        ]
    },
    {
        time: '2017',
        games: [
            {
                name: '使命召唤14',
                month: 11
            },
            {
                name: '幽灵行动:荒野',
                month: 3
            }
        ]
    }
])

// 需要精确匹配(字段顺序都需要精确匹配)
db.games.find({
    games: {name: '最终幻想15', month: 3}
})

// 对与数组中对象的字段进行匹配
db.games.find({
    'games.month': 11
})

// 指定查询的从数组的哪一位查询
db.games.find({
    'games.0.month': 3
})

// 对于单一字段的查询
db.games.find({
    'games.month': {
        $gt: 10
    }
})

// 复合条件查询
// 数组中至少有一个元素name为幽灵行动:荒野,month为3
db.games.find({
    games: {
        $elemMatch: {
            name: '幽灵行动:荒野',
            month: 3
        }
    }
})

文本的查询

文本搜索操作前,需要指定可以进行文本搜索的字段


db.stores.insert(
   [
     { _id: 1, name: "Java Hut", description: "Coffee and cakes" },
     { _id: 2, name: "Burger Buns", description: "Gourmet hamburgers" },
     { _id: 3, name: "Coffee Shop", description: "Just coffee" },
     { _id: 4, name: "Clothes Clothes Clothes", description: "Discount clothing" },
     { _id: 5, name: "Java Shopping", description: "Indonesian goods" }
   ]
)

// 指定集合中的name字段,description字段可以进行文本搜索的操作
db.stores.createIndex( { name: "text", description: "text" } )

// 查询文本中包含java或Coffee
db.stores.find({
    $text: {
        $search: "java Coffee" // search 会以or逻辑搜索
    }
})

bulkWrite

批量执行SQL语句


// 首先插入name字段为本田翼的数据,然后查询name字段为本田翼的数据,然后将name字段更新为广末凉子
db.users.bulkWrite([
    {
        insertOne: {
            document: {
                name: '本田翼'
            }
        }
    },
    {
        updateOne: {
            filter: { name: '本田翼' },
            update: { name: '广末凉子' }
        }
    }
])

Mongoose

文档地址(英文), 同MongoDB一样,Mongoose已经发布了5.x版本,版本迭代的非常快,中文文档目前没有大家还是看英文文档吧

SchemaTypes

新建Schema时,为字段创建类型约束,默认值,是否必填,验证函数,等等。对于Number类型还可以设置最大最小值的约束


// 简单的示例
const schema = mongoose.Schema({
    name: {
        type: Number, // 类型
        default: 1024, // 默认值
        min: 0, // 最小值
        max: 9999, // 最大值
        required: true, // 是否必填
        validate: function () {}, // 验证器
    },
    list: [Buffer] // 内容都是Buffer的数组
})

需要注意的两点:

  1. Schema中如果存在日期类型,直接通过Date的方法修改该字段mongoose是不会更新保存的。需要通过markModified通知mongoose发生了更新操作。
  2. 对于数组类型如果没有指定default,默认值为空数组

User.findOne({id: 1}, (err, user) => {
  user.craateDate.setMonth(3) // mongoose是无法知晓更改的
  user.markModified('craateDate') // 通知mongoose发生了更改
  user.save()
})

Connections

mongoose链接到mongoDB的API


// 默认的端口号27017,本地地址127.0.0.1

mongoose.connect('mongodb://localhost/myapp');

Models

Models通过我们定义的Schema创建的,Models实例上暴露的方法,可以帮助我们保存检索数据库,这些操作都是通过Models处理的。

Models的静态方法(Model.APIname)和实例方法(Model.prototype.APIname)非常多,具体请翻阅API列表, 👇下面只是举例说明

创建Models

const mongoose = require('mongoose')
const Schema = mongoose.Schema

const kittenSchema = new Schema({
  id: String
})

const Kitten = mongoose.model('Tank', kittenSchema)

Models上的CRUD

创建文档

文档就是Models的实例,就是保存到数据库中具体的数据


// 实例方法
// model.save会将model保存到数据库
const Kitten = require('...')

let fluffy = new Kitten({ name: 'fluffy' })
fluffy.save((err, fluffy) => {
  // ...
})

// 静态方法
// Modles.create会为每一个model调用save方法
const Kitten = require('...')

let list = [{name: 'jack'}, {name: 'jerry'}]
Kitten.create(list, (err, kittens) => {
  // ...
})

查询文档

常用的方法有find,findById, findOne, Where

// 这几种方法的示例
const Kitten = require('...')

// 查询age大于1小于5的文档
Kitten.where('age').$get(1).$lte(5).exec((err, result) => {
  // ...
})

// 查询全部数据,只需要返回name字段,从第二条数据开始查找,查找5条
Kitten.find({}, 'name', { skip: 2, limit: 5 }, (err, kittens) => {
  // ...
})

// findById, 根据文档的ObjectId字段查询, ObjectId是mongoose默认添加的
Kitten.findById('5ae875c0f6ed7bd6883719b5', (err, kitten) => {
  //...
})

// findOne, 查询name为fluffy的文档,但是只返回第一条数据
Kitten.findOne({name: 'fluffy'}, (err, kitten) => {
  // ...
})

删除文档

常用的有Model.remove, Model.prototype.remove, Model.findByIdAndRemove


// model.remove 实例方法
Kitten.findById('5ae875c0f6ed7bd6883719b5', (err, kitten) => {
  if (err) // ...
  kitten.remove((err, kitten) => {
    // ...
  })
})

// 上面可以用findByIdAndRemove方法更好的解决
Kitten.findByIdAndRemove('5ae875c0f6ed7bd6883719b5', (err, kitten) => {
  // ...
})

// Models.remove 静态方法
// 删除name为jerry的文档
Kitten.remove({name: 'jerry'}, (err, kitten) => {
  // ...
})

更新文档

常用的方法有 Model.update, Model.updateOne, Model.updateMany, Model.findById AndUpdate


// 查询name为fulffy的文档,并将name字段更新为Tom
Kitten.updateMany({name: 'fluffy'}, {$set: {name: 'Tom'}}, (err, raw) => {
  // ...
})

Kitten.findByIdUpdate('5ae875c0f6ed7bd6883719b5', {$set: {name: 'Tom'}}, (err, kitten) => {
  // ...
})

注意:update等一系列方法,虽然可以更新文档,但是callback的第二个参数不是更新后的文档。如果想把更新后的文档返回,可以通过findByIdUpdate方法。findByIdUpdate的callback函数的第二个参数就是更新后的文档

子文档

可以理解为嵌套在文档中的文档,子文档不会单独保存,只能保存父级文档才能保存子文档

子文档的定义


const mongoose = require('mongoose')
const Schema = mongose.Schema

const fansSchema = mongoose.Schema({
  name: String
})

// 子Schema嵌套在父Schema中
const starSchema = mongoose.Schema({
  fans: [fansSchema]
})

const Star = mongoose.model('Star', starSchema)

创建子文档

子文档不能单独保存,必须跟随父文档才能保存

let stars = { fans: [{name: '本田翼'}, {name: '广末凉子'}] }

Star.create(stars, (err, stars) => {
  // ...
})

中间件执行的顺序

子文档.pre(‘save’), 子文档.pre(‘validate’),在父文档.pre(‘save’)之前,父文档.pre(‘validate’)之后执行。

更新子文档

// 查询父文档,通过$push更新子文档
Star.findOneAndUpdate('5ae96fd7f95a0a08b7971952', {
  $push: {
    fans: [
      { name: '明日香' },
      { name: '绫波丽' },
    ]
  }
}, {new: true}, (err, star) => {
  // ...
})

删除子文档

与更新操作同理,首先需要找到对应的父文档,然后对子文档作出相应的操作

Star.findById('5ae9877676c318efd42dff35', (err, star) => {
  if (err) // ...
  // 删除子文档的数组的内容
  star.fans.remove()
  star.save((err, star) => {
    // ...
  })
})

验证

  1. Validation在SchemaType中定义
  2. 验证是异步递归的,保存父文档之前子文档也会验证,如果发生错误父文档的save将会接收到错误
  3. 验证可以是自定义的验证函数
  4. mongoose默认将验证每个save钩子

内置的验证

通过在SchemaType中定义。例如Number类型的max,min。String类型的maxlength,minlength,match(正则匹配),enum(枚举,字符串只能是枚举中的字符串)


const mongoose = require('mongoose')
const Schema = mongoose.Schema

const goddessSchema = new Schema({
  // 最小长度为2,最大长度12,必填
  name: {
    type: String,
    minlength: 2,
    maxlength: 12,
    required: true
  },
  // 最小为16,最大为60,必填
  age: {
    type: Number,
    min: 16,
    max: 60,
    required: true
  }
})

unique不是验证器

unique保证了字段的value的唯一性,但其本身不是验证器

自定义验证器

自定义验证器是一个函数,返回Boolean类型的值,如果是false代表验证不通过


const goddessSchema = new Schema({
  phone: {
    type: String,
    validate: {
      // validator函数的参数是字段的value值
      validator (value) {
        return /^[1][3,4,5,7,8][0-9]{9}$/.test(value)
      },
      // 错误信息
      message: '手机号码格式不正确'
    },
    required: true
  }
})

异步验证器

异步验证器,可以接收一个Promise对象,如果对象状态为reject代表验证不通过,resolve验证通过

const goddessSchema = new Schema({
  mail: {
    type: String,
    validate: {
      validator (value) {
        return new Promise((resolve, reject) => {
          setTimeout(() => {
            resolve(false)
          }, 5)
        })
      },
      message: '邮箱错误:('
    }
  }
})

动态验证器

Schema上暴露了一些API可以让我们动态的添加验证器


const mongoose = require('mongoose')
const Schema = mongoose.Schema

const userSchema = new Schema({
  name: {
    type: Number
  }
})

// 动态设置Schema中name字段的type
userSchema.path('name', String)

// 动态设置Schema中name字段的validate
userSchema.path('name').validate(function (value) {
  if (value === '李航') {
    return false
  }
  return true
})

嵌套验证器

对于嵌套对象,动态的添加验证器是非常棘手的,因为添加验证器的路径是不完整的。解决办法是通过把嵌套的对象,拆分为子文档的方式

// 错误的演示

const mongoose = require('mongoose')
const Schema = mongoose.Schema

const gameSchema = new Schema({
  info: {
    name: Number,
    year: Number
  }
})

// Error 路径是不正确的, name字段是在info对象中
gameSchema.path('name', String)
// 正确的演示
// 解决办法把嵌套的文档拆分为子文档

const mongoose = require('mongoose')
const Schema = mongoose.Schema

const infoSchema = new Schema({
  name: Number,
  year: Number
})

const gameSchema = new Schema({
  info: {
    type: infoSchema,
    required: true
  }
})

// 路径才是正确的
infoSchema.path('name', String)

const Game = mongoose.model('Game', gameSchema)

更新时的验证器⚠️⚠️⚠️

update(), findOneAndUpdate(), 操作的时候,验证器默认不进行验证的,需要通过配置runValidators为true,才会在更新的时候开启验证

为什么默认验证器是不开启的?

  1. 更新操作的时候,验证器内部的this指向,并不是指向最新更新的文档,所以如果验证器中有this是不适用于更新验证器的
  2. 更新操作的时候,更新的验证器仅在更新的key上运行(如果你只是更新name字段,只会验证name的验证器)
  3. 更新验证器只能在以下操作符执行(即使设置了runValidators为true,也只能在以下操作符执行)
  • $set, 设置字段
  • $unset, 删除字段
  • $push, 为数组字段添加后续内容(只能针对数组字段)
    • $each, $push的子操作符,添加内容到数组中
    • $slice, $push的子操作符, 从数组第0位开始截取
    • $sort, $push的子操作符, 数组排序
    • $position, $push的子操作符, 从数组哪个位置插入
  • $addToSet, 为数组添加内容,但是如果内容已经存在$addToSet不会做任何操作(如果是一个对象,addToSet也会判断是否重复,但是判断的条件是具有完全相同的key-value,并且字段的顺序也要一样)
  • $pull, 删除数组中条件匹配的内容
  • $pullAll, 删除数组中所有实例
const gameSchema = new Schema({
  info: {
    name: {
      type: String,
      validate: {
        validator (value) {
          if (value === '错误的') return false
          return true
        },
        message: 'update会验证吗?'
      }
    },
    year: {
      type: Number
    }
  }
})

const Game = mongoose.model('Game', gameSchema)

// runValidators为false的时候,更新操作时候不会经过验证器验证,默认为false
// 可以正确的执行更新操作
Game.findByIdAndUpdate('5aea88a2c3c255038b68fdec', {
    $set: {
        'info.name': '错误的'
    }
}, {new: true, runValidators: false}, (err, game) => {
    if (err) reject(err)
    resolve(game)
})


// runValidators为true的时候, 更新操作时候会强制验证
// 会抛出错误,不能正确的更新
Game.findByIdAndUpdate('5aea88a2c3c255038b68fdec', {
    $set: {
        'info.name': '错误的'
    }
}, {new: true, runValidators: true}, (err, game) => {
    if (err) reject(err)
    resolve(game)
})

中间件(钩子)

😢文档还没有看完,稍后补上

Population

Mongoose封装了一个 population 的功能,当你在定义 Schema 的时候指定了某个 field 是引用了另一个 Schema ,那么你在获取 document 的时候就可以使用 populate 方法让 Mongoose 帮你通过引用 Schema 和 id 找到关联的另一个 document,并且用该 document 的内容替换掉原来引用字段的内容,使引用的 ducoment 使用起来就像是内嵌的 document 一样方便。

创建关联

  1. 推荐使用Schema.Types.ObjectId创建关联,对应关联的字段只会保存ObjectId(另一个表的ObjectId)
  2. 使用ref字段确定关联的Models
const mongoose = require('mongoose')
const Schema = mongoose.Schema

const ownerSchema = new Schema({
  name: String,
  // 创建对Slave的集合,类似外键约束
  // owner的集合中slaves字段只会保存slave集合的ObjectId
  slaves: [{ type: Schema.Types.ObjectId, ref: 'Slave' }]
})

const slaveSchema = new Schema({
  name: String
})

const Owner = mongoose.model('Owner', ownerSchema)
const Slave = mongoose.model('Slave', slaveSchema)

添加关联


const owner = new Owner({name: '久远寺森罗'})
const slave = new Slave({name: '上杉练'})

slave.save((err, s) => {
    if (err) // ...
    // mongoose会自动解析为objectId保存到owner的slaves数组中
    // Owner集合中slaves字段将会存有slave的ObjectId作为引用
    owner.slaves.push(s)
    owner.save((err, h) => {
      if (err) // ...
    })
  })
}

查询关联

mongoose会自动将ObjectId替换为对应集合中的文档。查询操作,Models,document都拥有对应的populate方法


// Query.populate
// 查询id为5aebb8a287f5ca03508b6b23的文档,并将slaves字段做populate操作,只填充name字段
Owner.findOne({_id: '5aebb8a287f5ca03508b6b23'}).populate({
  path: 'slaves',
  select: 'name'
}).exec((err, owner) => {
  // ...
})
// document.populate
Owner.findById('5aebb8a287f5ca03508b6b23', (err, owner) => {
  if (err) // ...
  // owner实例做populate操作
  owner.populate('slaves', (err, owner) => {
    // ...
  })
})
// Models.populate

Owner.findById('5aebb8a287f5ca03508b6b23', (err, owner) => {
  if (err) // ...
  // 对slaves字段进行population,只填充_id字段
  Owner.populate(owner, {
    path: 'slaves',
    select: '_id'
  }, (err, owner) => {
    if (err) // ...
  })
})

Discriminators

Discriminators是一种模型的继承机制,可以在同一个模型的基础上,创建模型。可以类比为class中继承。Discriminators拥有基础模型的字段以及中间件。

Discriminators的实现


const mongoose = require('mongoose')
const Schema = mongoose.Schema

const schoolSchema = new Schema({
  schoolName: String
}, {discriminatorKey: 'kind'})

const teacherSchema = new Schema({
  name: String
})

const School = mongoose.model('School', schoolSchema)

// Teacher的模型是基于School模型创建的
// Teacher模型会拥有两个字段name以及schoolName
const Teacher = School.discriminator('Teacher', teacherSchema, {
  discriminatorKey: 'kind'
})

Discriminators的保存

使用Discriminator模型创建的文档,与基础模型创建的文档都是保存在mongoDB同一个集合中


const teacher = new Teacher({
  schoolName: '牡丹江师范学院',
  name: '赵晨'
})

const school = new School({
  schoolName: '牡丹江师范学院'
})

teacher.save((err, teacher) => {
  // ...
})
school.save((err, school) => {
  // ...
})

通过db.schools.find(), 可见 school和teacher都是保存在同一个集合中的

Discriminators的查询

find(), count()等方法会自动的区别不同的Discriminators


// mongoose的查询方法会自动区别不同的Discriminators
School.count({}, (err, count) => { })
Teacher.count({}, (err, count) => { })

Discriminators的中间件

Discriminators会继承基础模型的中间件,Discriminators也可以设置自己的中间件,从而不影响到基础模型


// 基于School的Discriminators也会执行save的钩子,打印出save
schoolSchema.pre('save', (next) => {
  console.log('save')
  next()
})

Discriminators的字段优先级

当Discriminators字段与基本模型字段相冲突的时候。Discriminators字段的优先级是大于基本模型的优先级的,但是_id除外。

const schoolSchema = new Schema({
  id: String,
  schoolName: String
}, {discriminatorKey: 'kind'})

const teacherSchema = new Schema({
  id: Number,
  name: String
})

const School = mongoose.model('School', schoolSchema)
const Teacher = School.discriminator('Teacher', teacherSchema, {
  discriminatorKey: 'kind'
})

// 基础模型中id为String类型,Discriminators模型中id为Number类型,Discriminators模型的字段优先级大于基础模型
const teacher1 = new Teacher({
  schoolName: '牡丹江师范学院',
  name: '赵晨',
  id: 1
})

const teacher2 = new Teacher({
  schoolName: '南京示范学院',
  name: '宋敏',
  id: '1'
})

// 最后保存id将会保存为Number类型
teacher1.save((err, teacher) => console.log(typeof teacher.id === Number) // true
teacher2.save((err, teacher) => console.log(typeof teacher.id === String) // false

Discriminators与Model.create操作

mongoose会通过数据的DiscriminatorsKey字段也就是kind,保存为不同的类型


// 定义不同的Discriminators以及不同的DiscriminatorsKey

const teacherSchema = new Schema({
  id: Number,
  name: String
})

const studentSchema = new Schema({
  id: Number,
  name: String
})

const janitorSchema = new Schema({
  id: Number,
  name: String
})

const Teacher = School.discriminator('Teacher', teacherSchema, {
  discriminatorKey: 'kind'
})

const Student = School.discriminator('Student', studentSchema, {
  discriminatorKey: 'kind'
})

const Janitor = School.discriminator('Janitor', janitorSchema, {
  discriminatorKey: 'kind'
})

// mongoose会根据不同kind,保存正确的类型
const lists = [
  {kind: 'Teacher', id: 8, name: '明日香'},
  {kind: 'Student', id: 9, name: '绫波丽'},
  {kind: 'Janitor', id: 10, name: '葛城美里'}
]

School.create(lists, (err, schools) => {
  // ...
})
5 回复

若是能再追加点你实际开发用到的一些写法就更好了…

比如查询某个关联数据,模拟联表查询

这类实战的语句总结…

@crper 谢谢支持

回到顶部