Koa + MongoDB 安装 创建服务 mongoose 使用 常用查询 populate 链表、内嵌填充
Mongoose DB
下载地址:https://www.mongodb.com/try/download/community
相关文档
官网手册:https://docs.mongodb.com/manual/
使用文档:https://mongoosejs.com/docs/
中文版:http://www.mongoosejs.net/docs/api.html
nodejs node-mongodb-native 教程:http://mongodb.github.io/node-mongodb-native/3.4/quick-start/quick-start/ 3.4版本的
Robo 3T: the hobbyist GUI
连接操作MongoDB 的图形化界面。简单好用。
下载地址:https://robomongo.org/download
MongoDB 安装
选择需要安装的目录,最好保持默认,系统盘。在其他盘可能会有意想不到的问题出现。
选择是否安装MongoD为Windows服务(安装完成,自动以服务的方式启动)
说明(https://docs.mongodb.com/manual/tutorial/install-mongodb-on-windows/):
从 MongoDB 4.0 开始,默认情况下,你可以在安装期间配置和启动 MongoDB 作为服务,并在成功安装后启动 MongoDB 服务。也就是说,MongoDB 4.0 已经不需要像以前版本那样输入一堆命令行来将 MongoDB 配置成 Windows 服务来自动运行了,方便了很多。
如果你选择不将 MongoDB 配置为服务,请取消选中 Install MongoD as a Service。
如果你选择将 MongoDB 配置为服务,则可以指定以下列用户之一运行服务:
网络服务用户:即 Windows 内置的 Windows 用户帐户
本地或域用户:
对于现有本地用户帐户,Account Domain 指定为 .,并为该用户指定 Account Name 和 Account Password。
对于现有域用户,请为该用户指定 Account Domain,Account Name 和 Account Password。
指定 Service Name(Windows 服务名称):如果你已拥有具有指定名称的服务,则必须选择其他名称,不能重名。
指定 Data Directory(数据保存目录):对应于 --dbpath。如果该目录不存在,安装程序将创建该目录并为服务用户设置访问权限。可以通过配置修改。
指定 Log Directory(日志保存目录):该目录对应于 --logpath。如果该目录不存在,安装程序将创建该目录并为服务用户设置访问权限。可以通过配置修改。
不安装官网提供的图形化工具(安装非常慢)
完成即可查看
koa 创建服务
最简化实现,什么其他的都没有,需要自己创建目录、结构等:
新建文件夹 koaMongoD,打开编辑器终端:npm init 。一路回车,自动引导创建package.json。
新建文件 app.js 。安装koa:npm i koa。
// app.js // 导入koa,和koa 1.x不同,在koa2中,我们导入的是一个class,因此用大写的Koa表示: const Koa = require('koa'); // 创建一个Koa对象表示web app本身: const app = new Koa(); // 对于任何请求,app将调用该异步函数处理请求: app.use(async (ctx, next) => { await next(); ctx.response.type = 'text/html'; ctx.response.body = '<h1>Hello, koa2!</h1>'; }); //app.use() 参数就相当于一个"中间件",干点什么事情 // 在端口3000监听: app.listen(3000); console.log('app started at port 3000...');
此时即可运行并查看:node app
结构化实现,通过 koa-generator 快速搭建:
编辑器打开想要创建项目位置的父目录 koa-generator :npm i koa-generator -g
创建项目:koa2 koaMongoDB
可以带上模板引擎参数:koa2 koaMongoDB -e 使用 ejs (默认使用的 pug)
如果报错:
koa2 : 无法加载文件 C:\Users\HP\AppData\Roaming\npm\koa2.ps1,因为在此系统上禁止运行脚本。有关详细信息,请参阅 https:/go.microsoft.com/fwlink/?LinkID=135170 中的 about_Execution_Policies。 所在位置 行:1 字符: 1 + koa2 koaMongoDB + ~~~~ + CategoryInfo : SecurityError: (:) [],PSSecurityException + FullyQualifiedErrorId : UnauthorizedAccess
直接到文件目录 C:\Users\HP\AppData\Roaming\npm 删除 koa2.ps1 文件即可(好多类似的这个问题都可以这么解决)。
进入目录:cd koaMongoDB
安装依赖:npm install
运行:npm start
koa 使用MongoDB
安装 Robo 3T :
输入个人信息的界面直接点击 finish 即可,然后直接连接默认的MongoDB:
可以右键 New Connection*(或者自己修改的名字) ,点击 Create Database 新建一个数据集合:
可以点击Refresh、或绿色三角刷新,右键选中要操作的集合,即可增删改查数据、删除集合(drop)等:
koa项目安装mongoose:npm i mongoose
根目录创建 db 文件夹(Database);db文件夹内创建 db.js;根目录 app.js 引用 db.js:
const Koa = require('koa') const app = new Koa() const views = require('koa-views') const json = require('koa-json') const onerror = require('koa-onerror') const bodyparser = require('koa-bodyparser') const logger = require('koa-logger') // 连接 MongoDB 测试 require(`./db/db`) const index = require('./routes/index') const users = require('./routes/users') // error handler onerror(app) // middlewares app.use(bodyparser({ enableTypes:['json', 'form', 'text'] })) app.use(json()) app.use(logger()) app.use(require('koa-static')(__dirname + '/public')) app.use(views(__dirname + '/views', { extension: 'pug' })) // logger app.use(async (ctx, next) => { const start = new Date() await next() const ms = new Date() - start console.log(`${ctx.method} ${ctx.url} - ${ms}ms`) }) // routes app.use(index.routes(), index.allowedMethods()) app.use(users.routes(), users.allowedMethods()) // error-handling app.on('error', (err, ctx) => { console.error('server error', err, ctx) }); module.exports = app
db.js 内容,一个简单的连接、增删改查服务,npm start 测试(也可以直接 node db.js 测试):
const mongoose = require('mongoose') const url = 'mongodb://localhost/testChat' const options = { useNewUrlParser: true, useUnifiedTopology: true, poolSize: 6, // The maximum size of the individual server pool. default 5 keepAlive: 250 // The number of milliseconds to wait before initiating keepAlive on the TCP socket. default 30000 } // 指向 testChat, 如果数据库没有 testChat ,会自动创建 mongoose.connect(url, options) let db = mongoose.connection db.on('connected', function () { console.log('Mongoose connected ' + url) }) db.on('error', function (err) { console.log('Mongoose connection error: ' + err) }) db.on('disconnected', function () { console.log('Mongoose connection disconnected') }) //创建骨架 const usersSchema = new mongoose.Schema( { indexId: Number, // 数字indexId account: String, // 账号 username: String // 用户名 }, { versionKey: false } ) /** * @params name: string, 可以通过 mongoose.model(`users`) 获取到创建的 model * @params schema?: Schema<T, U>, 创建 model 使用的 schema * @params collection?: string, 实际上对应的 集合的名字 此处操作的 集合名字是 usersTest 不是 users * @params skipInit?: boolean true or false 都会在 userTest 不存在时 新建 userTest */ const usersModel = mongoose.model('users', usersSchema, 'usersTest', true) //根据骨架创建模版 const usersModel_ = mongoose.model(`users`) console.log(JSON.stringify(usersModel) == JSON.stringify(usersModel_)) // true // create 新建一条数据,如果数据库没有集合 users ,会自动创建 usersModel.create( { indexId: 0, account: 'String0', username: 'String0' }, function (err, user) { console.log(err, user) // null { _id: 60e81cc30287645cd4a1a918, indexId: 0, account: 'String0', username: 'String0' } } ) // 回调和then方法不一样 usersModel .create({ indexId: 1, account: 'String1', username: 'String1' }) .then(function (user, err) { console.log(user, err) // { _id: 60e81ff7584cc04c7468d88a, indexId: 0, account: 'String0', username: 'String0' } undefined }) // 查找 只会返回符合的所有 [] usersModel.find( { indexId: 0 }, function (err, user) { console.log(err, user) // null [{ _id: 60e81ff7584cc04c7468d88b, indexId: 0, account: 'String0', username: 'String0' }] } ) // 修改 依据表内的 _id 来修改 usersModel.updateOne( { _id: '60e81ff7584cc04c7468d88b' }, { $set: { indexId: 9, account: 'String9', username: 'String9' } }, function (err, user) { console.log(err, user) // null { n: 1, nModified: 1, ok: 1 } } ) // 删除 依据表内的 _id 来删除 usersModel.deleteOne({ _id: '60e81ff7584cc04c7468d88a' }, function (err, user) { console.log(err, user) // null { n: 1, ok: 1, deletedCount: 1 } }) // 翻页 async function getPage(pager) { //skip((pager.curPage - 1) * pager.eachPage) 跳过条数 limit(pager.eachPage) 获取条数 let data = await usersModel .find() .skip((pager.curPage - 1) * pager.eachPage) .limit(pager.eachPage) pager.rows = data let count = await usersModel.countDocuments() pager.total = count pager.maxPage = Math.ceil(count / pager.eachPage) return pager } getPage({ curPage: 1, eachPage: 1 }).then(function (users, err) { console.log(users, err) // { curPage: 1, eachPage: 1, total: 9, maxPage: 9, rows: [{_id: 60e81fe8eabfcd029061519f, indexId: 0, account: 'String0', username: 'String0'}] } undefined })
模块化,并编写接口操作MongoDB
db.js 同目录创建 models、daos 文件夹,models 存放声明的多个 schema,并创建导出 model;daos 存放各个 models 的增删改查方法(不同的查询配置、参数修改、数据处理),
db.js 里面引用所有的 model:
const mongoose = require('mongoose') const url = 'mongodb://localhost/testChat' const options = { useNewUrlParser: true, useUnifiedTopology: true, poolSize: 6, // The maximum size of the individual server pool. default 5 keepAlive: 250 // The number of milliseconds to wait before initiating keepAlive on the TCP socket. default 30000 } mongoose.connect(url, options) let db = mongoose.connection db.on('connected', function () { console.log('Mongoose connected ' + url) }) db.on('error', function (err) { console.log('Mongoose connection error: ' + err) }) db.on('disconnected', function () { console.log('Mongoose connection disconnected') }) require(`./models/usersModel`) require(`./models/friendsModel`) require(`./models/groupsModel`)
创建三个Model,并编写mockData.js(引用mock.js)模拟数据方便测试,在routes文件夹内新建三个Model对应的接口,并在app.js内使用;
现目录:
三个model:
// usersModel.js const mongoose = require('mongoose') const usersSchema = new mongoose.Schema( { account: String, // 账号 age: Number, // 年龄 phone: Number, // 电话 name: String // 用户名 }, { versionKey: false } ) const usersModel = mongoose.model('users', usersSchema, 'users') //根据骨架创建模版 module.exports = usersModel // groupsModel.js const mongoose = require('mongoose') const groupsSchema = new mongoose.Schema( { //创建骨架 groupName: String, // 群聊名称 profilePhoto: Array, // 群聊头像 // groupHolder: String, // 群主 // administrators: Array, // 管理员 users: [ { _id: { type: mongoose.Schema.Types.ObjectId, ref: 'users' }, position: String, // 职位 remarks: String // 备注 } ] //群成员 // { // _id: ObjectId, // 用户ID // position: String, // 职位 // remarks: String, // 备注 // } }, { versionKey: false } ) const groupsModel = mongoose.model('groups', groupsSchema, 'groups') //根据骨架创建模版 module.exports = groupsModel // friendsModel.js const mongoose = require('mongoose') const friendsSchema = new mongoose.Schema( { //创建骨架 ownerId: String, // 所属用户 ID type: Number, // 用户类型: 0 ; 群类型: 1 subgroup: Number, // 分组;默认好友: 0 ; 亲密好友?... remarks: String, // 备注 user: { type: mongoose.Schema.Types.ObjectId, ref: 'users' }, // 用户类型好友 group: { type: mongoose.Schema.Types.ObjectId, ref: 'groups' } // 群类型好友 }, { versionKey: false } ) const friendsModel = mongoose.model('friends', friendsSchema, 'friends') //根据骨架创建模版 module.exports = friendsModel
ps:数据都会有一个_id 的属性,要么自己传,不传就会自动生成。
用户表独立,好友表关联用户或群,群表里面users数组内嵌关联 用户。
三个 dao:
// usersDao.js const mongoose = require(`mongoose`) const usersModel = mongoose.model(`users`) const getUsers = async pager => { let data = await usersModel .find() .skip((pager.curPage - 1) * pager.eachPage) // 跳过条数 .limit(pager.eachPage) // 获取条数 .sort({ age: 1 }) // 按年龄 递增排序 pager.rows = data let count = await usersModel.countDocuments() pager.total = count pager.maxPage = Math.ceil(count / pager.eachPage) return pager } const getUser = async query => { let data = await usersModel.find(query) return data } const modifyUser = async (_id, user) => { delete user._id let data = await usersModel.updateOne(_id, { $set: user }) return data } const deleteUser = async _id => { let data = await usersModel.deleteOne(_id) return data } const addUser = async user => { let data = await usersModel.create(user) return data } module.exports = { getUsers, getUser, addUser, modifyUser, deleteUser } // groupsDao.js const mongoose = require(`mongoose`) const groupsModel = mongoose.model(`groups`) const createGroup = async group => { let data = await groupsModel.create(group) return data } const getGroup = async _id => { // populate('users._id',) 指定 users 数组里面的元素的 _id ,去关联查询用户信息 let data = await groupsModel.find(_id).populate('users._id') return data } module.exports = { createGroup, getGroup } // friendsDao.js const mongoose = require(`mongoose`) const friendsModel = mongoose.model(`friends`) const usersModel = mongoose.model(`friends`) const createFriends = async friends => { let data = await friendsModel.create(friends) return data } const getFriends = async id => { let data = await friendsModel .find(id) .populate('user') .populate({ // 理论上可以多次内嵌链表查询 path: 'group', // 指定 group 属性 去 链表查询 群信息 populate: { path: 'users._id' } // 指定 通过group查询出来的 users 数组里面的元素的 _id 属性 去 链表查询 用户信息 }) data = data.map(v => { console.log(v) return { _id: v._id, ownerId: v.ownerId, type: v.type, subgroup: v.subgroup, remarks: v.remarks, user: v.user, group: v.group } }) return data } module.exports = { getFriends, createFriends }
routes里面对应的接口:
// routes/users.js const router = require('koa-router')() // 本来应该 抽一层 services 出来 ,对查询参数 和 返回 数据做处理,然后这里引用 usersDaos 才对,省略了。 const usersDao = require(`../db/daos/usersDao`) router.prefix('/users') router.post('/', async function (ctx, next) { let user = ctx.request.body let data = await usersDao.addUser(user) ctx.body = data }) router.get('/', async function (ctx, next) { const _id = ctx.request.query let data = await usersDao.getUser({ _id }) ctx.body = data }) router.get('/page', async function (ctx, next) { let pager = ctx.request.query pager.curPage = pager.curPage - 0 pager.eachPage = pager.eachPage - 0 const data = await usersDao.getUsers(pager) ctx.body = data }) router.put('/', async function (ctx, next) { let user = ctx.request.body console.log(user) let data = await usersDao.modifyUser({ _id: user._id }, user) ctx.body = data }) //动态路由参数 ctx.params router.delete('/:_id', async function (ctx, next) { const _id = ctx.params._id let data = await usersDao.deleteUser({ _id }) ctx.body = data }) module.exports = router // routes/groups.js const router = require('koa-router')() // 本来应该 抽一层 services 出来 ,对查询参数 和 返回 数据做处理,然后这里引用 usersServices 才对,省略了。 const groupsDao = require(`../db/daos/groupsDao`) router.prefix('/groups') router.post('/', async function (ctx, next) { let group = ctx.request.query let data = await groupsDao.createGroup(group) ctx.body = data }) router.get('/', async function (ctx, next) { const query = ctx.request.query let data = await groupsDao.getGroup(query) ctx.body = data }) module.exports = router // routes/friends.js const router = require('koa-router')() // 本来应该 抽一层 services 出来 ,对查询参数 和 返回 数据做处理,然后这里引用 usersServices 才对,省略了。 const friendsDao = require(`../db/daos/friendsDao`) router.prefix('/friends') router.post('/', async function (ctx, next) { let friends = ctx.request.query let data = await friendsDao.createFriends(friends) ctx.body = data }) router.get('/', async function (ctx, next) { const query = ctx.request.query let data = await friendsDao.getFriends(query) ctx.body = data }) module.exports = router
在 app.js 里面使用:
const Koa = require('koa') const app = new Koa() const views = require('koa-views') const json = require('koa-json') const onerror = require('koa-onerror') const bodyparser = require('koa-bodyparser') const logger = require('koa-logger') // 连接 MongoDB 测试 require(`./db/db`) const index = require('./routes/index') const users = require('./routes/users') // 新建的路由api const friends = require('./routes/friends') const groups = require('./routes/groups') // error handler onerror(app) // middlewares app.use(bodyparser({ enableTypes:['json', 'form', 'text'] })) app.use(json()) app.use(logger()) app.use(require('koa-static')(__dirname + '/public')) app.use(views(__dirname + '/views', { extension: 'pug' })) // logger app.use(async (ctx, next) => { const start = new Date() await next() const ms = new Date() - start console.log(`${ctx.method} ${ctx.url} - ${ms}ms`) }) // routes app.use(index.routes(), index.allowedMethods()) app.use(users.routes(), users.allowedMethods()) // 使用新建的路由api app.use(friends.routes(), friends.allowedMethods()) app.use(groups.routes(), groups.allowedMethods()) // error-handling app.on('error', (err, ctx) => { console.error('server error', err, ctx) }); module.exports = app
新建的模拟数据,mockData.js:
const mongoose = require('mongoose') const url = 'mongodb://localhost/testChat' const options = { useNewUrlParser: true, useUnifiedTopology: true, poolSize: 6, // The maximum size of the individual server pool. default 5 keepAlive: 250 // The number of milliseconds to wait before initiating keepAlive on the TCP socket. default 30000 } mongoose.connect(url, options) let db = mongoose.connection db.on('connected', function () { console.log('Mongoose connected ' + url) const usersDao = require('./daos/usersDao.js') const groupsDao = require('./daos/groupsDao.js') const friendsDao = require('./daos/friendsDao.js') // 使用 Mock const Mock = require('mockjs') const result = Mock.mock({ 'data|15': [ // 15条数据 { name: `@cname`, // 中文名称 account: '@string(11)', // 输出 11 个字符长度的字符串 age: '@integer(18, 38)', // 18 到 38 的整数 'phone|1958-6849': 6849 // 1958-6849 整数 } ] }) // 输出结果 // console.log(JSON.stringify(result, null, 2)) let users = [] function createUsers() { let promisesUsers = [] // 新建用户 result.data.forEach(user => { let curUser = { ...user, _id: new mongoose.Types.ObjectId() } users.push({ ...curUser }) promisesUsers.push( usersDao.addUser({ ...curUser }).then((res, err) => { console.log(err) }) ) }) return promisesUsers } let groups = [] function createGroups() { let promisesGroups = [] // 初始群:每个账户以自己名字建一个群 ,群成员只有自己 users.forEach(user => { let curGroup = { _id: new mongoose.Types.ObjectId(), groupName: user.name + '的初始群', profilePhoto: [''], users: [ { _id: user._id, position: '群主', remarks: user.name + '自己' } ] } groups.push({ ...curGroup }) promisesGroups.push( groupsDao.createGroup({ ...curGroup }).then((res, err) => { console.log(err) }) ) }) return promisesGroups } let friends = [] function createFriends() { let promisesFriends = [] // 初始好友:每个账户 添加自己、自己初始群为好友 users.forEach((user, index) => { promisesFriends.push( friendsDao .createFriends({ _id: new mongoose.Types.ObjectId(), ownerId: user._id, type: 0, subgroup: 0, remarks: user.name + '添加自己为好友', user: user._id, group: null }) .then((res, err) => { friends.push(res) }) ) promisesFriends.push( friendsDao .createFriends({ _id: new mongoose.Types.ObjectId(), ownerId: user._id, type: 1, subgroup: 0, remarks: user.name + '添加自己初始群类型好友', user: null, group: groups[index]._id }) .then((res, err) => { friends.push(res) }) ) }) return promisesFriends } async function initData() { await Promise.all(createUsers()) await Promise.all(createGroups()) await Promise.all(createFriends()) console.log(friends) console.log('数据模拟完成') } initData() }) db.on('error', function (err) { console.log('Mongoose connection error: ' + err) }) db.on('disconnected', function () { console.log('Mongoose connection disconnected') }) require(`./models/usersModel`) require(`./models/friendsModel`) require(`./models/groupsModel`)
直接 node mockData.js,即可插入模拟数据。
查询测试
用户翻页查询:
通过 孙平的 _id 修改 孙平的个人信息:
删除成功时返回这样(直接把 _id 参数在地址路径上给了):
通过孙平查询他的好友:
可以看到,关联的表信息都查出来了,在通过群好友查询群:
返回的信息是没有处理的,处理一下群返回的信息,修改groupsDao.js:
const mongoose = require(`mongoose`) const groupsModel = mongoose.model(`groups`) const createGroup = async group => { let data = await groupsModel.create(group) return data } const getGroup = async _id => { // populate('users._id',) 指定 users 数组里面的元素的 _id ,去关联查询用户信息 let data = await groupsModel.find(_id).populate('users._id') return data.map(v => ({ _id: v._id, groupName: v.groupName, profilePhoto: v.profilePhoto, users: v.users.map(user => ({ userId: user._id._id, name: user._id.name, account: user._id.account, age: user._id.age, phone: user._id.phone, position: user.position, remarks: user.remarks })) })) } module.exports = { createGroup, getGroup }
再次查询: