[关闭]
@Dale-Lin 2019-05-31T14:04:06.000000Z 字数 6991 阅读 1704

MongoDB220 课程

MongoDB


连接服务

连接 mflix cluster

  1. mongo "mongodb+srv://mflix-d71ia.mongodb.net/test" --username m220student

上传文件到 Atlas

进入资源目录:

  1. cd mflix-js

上传:

  1. mongorestore --drop --gzip --uri "mongodb+srv://userName:password@<YOUR_CLUSTER_URI>" data

运行应用

修改配置 dotenv_win

  1. # Ticket: Connection
  2. # Rename this file to .env after filling in your MFLIX_DB_URI and your SECRET_KEY
  3. # Do not surround the URI with quotes
  4. SECRET_KEY=super_secret_key_you_should_change
  5. MFLIX_DB_URI=mongodb+srv://m220student:m220password@mflix-d71ia.mongodb.net/test
  6. MFLIX_NS=mflix
  7. PORT=5000

修改环境配置 .env 文件:

  1. ren dotenv_win .env

再修改使用 notepad .env 才能进入

运行单元测试

  1. npm test -t <testUnit>

课程中的单元测试通过 jestasync/await 编写。

MongoClient

创建一个 MongoClient 对象来初始化到数据库的连接,通过 testClient.db(dbname) 来查找数据库,并通过数据库方法操作集合。

简单的例子:

  1. import { MongoClient } from "mongodb"
  2. const URI = "mongodb+srv://m220-user:m220-pass@m220-test.mongodb.net/test"
  3. const testClient = await MongoClient.connect(
  4. URI,
  5. { connectTimeoutMS: 100, retryWrites: true, useNewUrlParser: true },
  6. )
  7. const clientOptions = testClient.s.options
  1. import app from "./server"
  2. import { MongoClient } from "mongodb"
  3. import MoviesDAO from "../src/dao/moviesDAO"
  4. import UsersDAO from "./dao/usersDAO"
  5. import CommentsDAO from "./dao/commentsDAO"
  6. const port = process.env.PORT || 8000
  7. /**
  8. Ticket: Connection Pooling
  9. Please change the configuration of the MongoClient object by setting the
  10. maximum connection pool size to 50 active connections.
  11. */
  12. /**
  13. Ticket: Timeouts
  14. Please prevent the program from waiting indefinitely by setting the write
  15. concern timeout limit to 2500 milliseconds.
  16. */
  17. MongoClient.connect(
  18. process.env.MFLIX_DB_URI,
  19. // TODO: Connection Pooling
  20. // Set the poolSize to 50 connections.
  21. // TODO: Timeouts
  22. // Set the write timeout limit to 2500 milliseconds.
  23. { useNewUrlParser: true },
  24. )
  25. .catch(err => {
  26. console.error(err.stack)
  27. process.exit(1)
  28. })
  29. .then(async client => {
  30. await MoviesDAO.injectDB(client)
  31. await UsersDAO.injectDB(client)
  32. await CommentsDAO.injectDB(client)
  33. app.listen(port, () => {
  34. console.log(`listening on port ${port}`)
  35. })
  36. })

CRUD

写入

collection.insertOne(obj) 方法为集合插入一行:

  1. let insertResult = await collection.insertOne({
  2. title: 'Call of Duty',
  3. year: 2018,
  4. })

通过结果对象的属性获取数据:

  1. let { n, ok } = insertResult.result
  2. console.log(n, ok) // 1, 1
  3. let count = insertResult.insertedCount
  4. console.log(count) // 1, equals to ``insertResult.result.n``
  5. let insertedId = insertResult.insertedId
  6. // this is _id

insertedId 的用法是 collection.findOne({ _id: ObjectId(insertedId) })

插入时如果没有传入 _id 属性的值,Mongodb 会自动分配一个 ObjectID,如果传入,则以传入为准(可以是字符串)。

注意:不允许 _id 重复,否则报错:

  1. try {
  2. let dupIdResult = await collection.insertOne({
  3. _id: insertedId,
  4. title: "whatever",
  5. year: 2020,
  6. })
  7. } catch(e) {
  8. console.log(e.errmsg) // "E11000 duplicate key error collection"
  9. }

collection.insertMany(ObjectArray) 插入多个文档:

  1. let megaManYears = [
  2. 1987,
  3. 1988,
  4. 1990,
  5. 1991,
  6. 1992,
  7. 1993,
  8. 1995,
  9. 1996,
  10. 2008,
  11. 2010,
  12. ]
  13. let docs = megaManYears.map((year, idx) => ({
  14. title: `Mega Man ${idx + 1}`,
  15. year,
  16. }))
  17. let insertResult = await videoGames.insertMany(docs)

不同之处在于传入多个文档的数组,返回结果类似单个写入,result.insertedIds 返回一个 _id 的数组。

默认是按顺序写入,如果报错会停止后续写入。

可以关闭顺序写入:

  1. let insertResult = await videoGames.insertMany(docs, {"ordered": false})

写入安全机制(write concerns)

  1. try {
  2. await users.insertOne(
  3. {
  4. name: userInfo.name,
  5. email: userInfo.email,
  6. password: userInfo.password,
  7. },
  8. { w: "majority" }
  9. )
  10. return { success: true }
  11. } catch(e) {
  12. if (String(e).startsWith("MongoError: E11000 duplicate key error")) {
  13. return { error: "A user with the given email already exists." }
  14. }
  15. console.error(`Error occurred while adding new user, ${e}.`)
  16. return { error: e }
  17. }

writeConcern: { w: n } 的值不能大于副本集合的数量,否则报错!

更新

和写入不同之处在于,先查找是否已有文档,然后进行更新:

  1. let updateResult = await collection.updateOne(
  2. { title: "Call of Duty" },
  3. {
  4. $set: {
  5. title: "Call of Duty",
  6. year: 2020,
  7. }
  8. },
  9. )

collection.updateOne(query, operation, flag)collection.updateMany(query, operation, flag) 分别更新一个和多个文档; update(query, opration, flag) 默认更新一个,flag 为 {multi: true} 更新多个。

如果查找成功,则更新,否则不操作。
通过 updateResult.result.nModified 判断修改个数,updateResult.matchedCount 判断匹配个数。

update 操作符:

  1. db.movieDetails.updateMany({
  2. rated: null
  3. }, {
  4. $unset: {
  5. <filed1>: "",
  6. ...
  7. }
  8. })

对于数组字段:

如果没有该数组字段,则创建。

  1. // {_id: 1, scores: [0, 2, 5, 5, 1, 0]}
  2. db.survey.update({{ _id: 1} }, { $pullAll: { scores: [0, 5] } })
  3. // {_id: 1, scores: [2, 1]}
  4. db.stores.update({}, { $pull: { fruits: { $in: ["apples", "oranges"] }, vegetables: "carrots" } })

upsert

如果需要查找失败则创建新文档,否则进行更新操作,增加 upsert 选项:

  1. let updateResult = await collection.updateOne(
  2. { title: "Call of Duty" },
  3. {
  4. $set: {
  5. title: "Call of Duty",
  6. },
  7. $inc: {
  8. year: -1
  9. }
  10. },
  11. { upsert: true }
  12. )

updateResult.result.upserted 对象包含了 _id 和 一个索引。

replaceOne(filter, doc)

在 shell 中 find 到需要修改的文档,用 JavaScript 修改后,再通过 replaceOne 更新数据库的数据。

只会拿到第一个匹配的文档。

查找

collection.findOne()collection.find() 方法用于查找文档:

  1. let result = await collection.findOne(
  2. { cast: "Johnny Depp" },
  3. { projection: { title: 1, year: 1, _id: 0 } },
  4. )

第一个是查找对象,第二个是过滤结果,1 为需要,0 为不需要。
不用 projection 则返回所有 field 的结果;使用则默认带上 _id,可以手动去掉。

查找多个并返回 cursor:

  1. let result = await collection.find(
  2. { cast: { $all: ["Johnny Depp", "Sam Hayek"] } }
  3. )

返回的 cursor 对象有 .next() 方法以供遍历,或使用 .toArray() 方法获得所有结果的数组。

cursor 还有 .limit().sort().skip() 等方法,类似一个个过滤器。

!!cursor 方法顺序会被调整,例如 limit() 会在 skip() 后以保证返回 limit 的个数。

可以使用 collection.aggregate(pipeline) 查找起到相同效果:

  1. const skippedPipeline = [
  2. { $match: { cast: "Johnney Depp" } },
  3. { $projection: { _id: 0, year: 1, title: 1, director: 1 } },
  4. { $sort: { year: -1 } },
  5. { $skip: 5 },
  6. ]
  7. // ``$sort: { year: -1 }`` 表示按年递减排序,1 则为递增。
  8. const skippedAggregation = await collection.aggregate(skippedPipeline)
  9. const resultArr = skippedAggregation.toArray()

查找字段如果是嵌套等特殊名称,需要用引号包裹

读取安全机制(read concerns)

  1. db.restaurants.find({ _id: 5 }).readConcern("majority").maxTimeMS(10000)

合并

使用 $lookup 查找符合的结果并合并:

  1. // in movies collection
  2. const pipeline = [
  3. {
  4. '$match': {
  5. 'year': {
  6. '$gte': 1980,
  7. '$lt': 1990
  8. }
  9. }
  10. }, {
  11. '$lookup': {
  12. 'from': 'comments',
  13. 'let': { 'id': '$_id' },
  14. 'pipeline': [
  15. {
  16. '$match': {
  17. '$expr': {
  18. '$eq': [ '$movie_id', '$$id' ],
  19. },
  20. },
  21. }
  22. ],
  23. 'as': 'movie_comments'
  24. }
  25. }
  26. ]

let 的作用是声明变量,否则 pipeline 中不能访问当前集合的字段(上例中是 movies 集合文档的 _id 字段),只能访问需要合并的集合的字段(上例中是 comments 集合)。
注意:$$ 前缀以使用 let 中声明的变量访问当前文档(movies),$ 前缀访问合并文档(comments)。

上例会从 comments 集合查找 movie_id 和当前集合的 _id 相同的文档,然后合并到当前文档的 movie_comments 字段中。

如果只关注评论数量:

  1. const pipeline = {
  2. ...,
  3. '$lookup': {
  4. ...,
  5. 'pipeline': [
  6. ...,
  7. {
  8. '$count': 'counts'
  9. }
  10. ]
  11. }
  12. }

将使用 counts 字段存储评论数目。

删除

deleteOne()deleteMany() 是两种删除操作。删除实际上是一种写入更新的操作:

collection.count(query) 可以获得某集合中符合搜索条件的文档数目,传入 {} 可得到所有文档的总数。

删除一个或多个文档:

  1. try {
  2. let deleteOneDoc = await videoGames.deleteOne({ year: 1998 })
  3. let deleteManyDocs = await videoGames.deleteMany({ year: { $lt: 1993 } })
  4. } catch(e) {
  5. // ...
  6. }

保证使用 collection.deleteOne() 时的查询结果唯一!否则可能错误删除文档。

弹性(Resilience)和健壮性(Robustness)

连接池

每个连接池对应一个数据库客户端提供连接。

鲁棒性设置

使用比 w: 1 更重要的写入时强烈建议设置 wtimeout

错误处理

  1. try {
  2. let errOp = await db.collection.insertOne({
  3. _id: 0,
  4. },
  5. {
  6. w: 5,
  7. wtimeoutMS: 1,
  8. })
  9. }
添加新批注
在作者公开此批注前,只有你和作者可见。
回复批注