记一次 MongoDB 踩坑实录

作者:草梅友仁

前言

在上一篇“浅谈用 node.js 开发后台 api 的要点”中我提到过我用的数据库是云数据库 leancloud,但云数据库毕竟有次数限制,每日免费三万次。虽然还是绰绰有余,不过,由于某些原因,草梅在云数据库上有些滥用,在一些无关紧要的地方,比如域名访问统计、接口访问统计上浪费了很多调用次数,因此考虑将这部分大头移到自建的数据库上来节约调用次数。

在经过比对多款数据库后,最后选定了 MongoDB。

理由如下:1.MongoDB 是和 leancloud 一样的面向对象的非关系型数据库,使用上有共通之处;2.也因此,MongoDB 非常适合用来存储 json 对象。

由于原生的 MongoDB 包不太好用,因此使用封装后的 mongoose【版本号 5.7.0】来连接数据库。

下面就来讲讲这次开发中遇到的坑

踩坑 1:MongoDB 鉴权连接

在网上搜的教程基本上都是无鉴权直连的,这样做的坏处显而易见,非常容易被攻击。因此草梅从一开始就考虑要鉴权。

鉴权连接的写法如下

//init.ts 把初始化放在这里,最后导出
import mongoose = require('mongoose')
const dbName = 'test'//要连接的数据库名称
const auth = { user: 'root', password: '123456' }//账号密码
const url = `mongodb://localhost:27017/${dbName}`

const db = mongoose.createConnection(url, { useNewUrlParser: true, auth, useUnifiedTopology: true })

db.on('error', console.error.bind(console, '连接错误 :'))//处理错误
db.once('open', () => {
    console.log('mongoose 已连接')
})
export { db }

踩坑 2:Schema 的使用

在 Schema 的使用上,草梅可以说是踩了天大坑,以至于一度要放弃使用 Schema 了。

废话不多说,直接上代码

import { Schema } from 'mongoose'
import { ObjectID } from 'mongodb'
import { timeFormat } from './help'
import { db } from './init'
//第一个参数是model名称,不得重复。
//注意,model必须挂在经过初始化的db对象身上,直接挂在mongoose上是没用的
const ApiStatistics = db.model('ApiStatistics', new Schema({
    //_id: { type: String, default: new ObjectID().toString() },//请不要设置_id项,MongoDB会自动帮你生成,如果自己再搞一个那就是多此一举了,会导致数据库没有索引。
    router: String,
    date: { type: String, default: timeFormat(Date.now(), 'YYYY-MM-DD') },
    count: { type: Number, default: 1 },
},{
    versionKey: false,//关闭版本key,避免出现 _v字段
    timestamps: true//开启updateAt和createAt
}), 
'ApiStatistics')//最后一个参数是集合的名称,也就是数据库下的某张表,也可留空

踩坑 3:查询并修改

因为是统计访问次数,所以必然要查询到对象再修改统计值,而在查询上,草梅又踩了很多坑

/**
 *统计各个api的调用次数
 *
 * @author CaoMeiYouRen
 * @date 2019-09-10
 * @export
 * @param {string} router
 * @returns 修改后的对象
 */
export async function apiStatisticsLocal(router: string) {
    try {
        let date = timeFormat(Date.now(), 'YYYY-MM-DD')
        let result = await ApiStatistics.findOne({
            router,
            date
        })
        if (!result) {//为空
            let obj = new ApiStatistics({ router })
            return obj.save()
        } else {//不为空 
            await result.updateOne({ $inc: { count: 1 } })//$inc是一个原子操作api,会把相应的字段进行原子操作,一般为数的增减。在多线程情况下也是会保持一致的。
//此处还有另外一个坑,草梅一开始创建了一个对象,称为obj1,由于没有对_id的type进行修改,因此还是默认的ObjectID类型,之后将Schema中的_id修改为String类型之后,经过查找获取到的对象obj1,如果此时调勇它的updateOne、update等函数,都是没用的,虽然 ApiStatistics.findByIdAndUpdate()依旧可用 ,但官方也提示deprecated了,也就是不推荐使用了。因此还是要调用updateOne来修改。对此的解决办法就是删除重建,_id类型统一之后就没问题了
            return ApiStatistics.findById(result.id)
        }
    } catch (error) {
        console.error(error)
        return null
    }
}

后记

总之,这次的开发还真的是有些不太容易的,之后的问题就是如何让域名访问统计也加入进来了。因为历史原因,域名访问统计还是 js 写的,没法直接对接,还是要稍微修改下才行。另外,像这种通用模块,其实也可以抽离出来写成一个单独的模块的,也方便调用