关于云开发数据库的使用经验和建议

一、前言

小程序·云开发是微信团队联合腾讯云推出的专业的小程序开发服务。

开发者可以使用云开发快速开发小程序、小游戏、公众号网页等,并且原生打通微信开放能力。

开发者无需搭建服务器,可免鉴权直接使用平台提供的 API 进行业务开发。

数据库的上手、初始化等可参看官方链接:小程序·云开发

二、使用经验

直接使用云开发API

场景:页面或方法的逻辑简单,关联一个数据库,无联表查询

例子:

db.collection('todos').doc('todo-identifiant-aleatoire').get({
  success: function(res) {
    // res.data 包含该记录的数据
    console.log(res.data)
  }
})

使用数据聚合能力

场景:页面或方法的逻辑中等,关联多个数据库,可能存在联表查询或数据处理

例子:

const db = wx.cloud.database()
const $ = db.command.aggregate
db.collection('books').aggregate()
  .group({
    // 按 category 字段分组
    _id: '$category',
    // 让输出的每组记录有一个 avgSales 字段,其值是组内所有记录的 sales 字段的平均值
    avgSales: $.avg('$sales')
  })
  .end()

借助promise,async等

场景:页面或方法的逻辑较为复杂,关联多个数据库,可能存在多次查询以及云函数或https请求

以下是对云开发CMS导出数据的扩展案例

其中整合了上述的几种方式

例子:

const cloud = require('wx-server-sdk')
cloud.init({
  env: cloud.DYNAMIC_CURRENT_ENV
})
var xlsx = require('node-xlsx');
​
const db = cloud.database();
const MAX_LIMIT = 100;
const _ = db.command;
exports.main = async (event, context) => {
  console.log(event)
  event.queryStringParameters = event.queryStringParameters||{};
  const collection = event.collection || event.queryStringParameters.collection;
​
  const params = event.params || event.queryStringParameters.params || {};
  // const acceptType = ["String", "Tel", "Array", "Number", "Connect", "Boolean", "Enum", "Date", "DateTime"]; //"File","Image"
  const unacceptType = ["File", "Image"];
  const schemasRes = await db.collection("tcb-ext-cms-schemas").where({
    collectionName: collection
  }).get();
  const schemas = schemasRes.data[0];
​
  let connectList = [];
​
  const title = event.title || event.queryStringParameters.title || schemas.displayName || "数据";
​
  // 先取出集合记录总数
​
  const countRes = await db.collection(collection).where(params).count();
  const fields = schemas.fields.filter(function (schemas) {
    return unacceptType.indexOf(schemas.type) == -1 && (!schemas.isHidden);
  });
  const connectResourcenList = [];
  fields.forEach(field => {
    if (field.type == "Connect") {
      connectList.push(field);
      connectResourcenList.push(field.connectResource)
    }
  });
​
  const schemasListRes = await db.collection("tcb-ext-cms-schemas").where({
    _id: _.in(connectResourcenList)
  }).limit(MAX_LIMIT).get();
  const schemasList = schemasListRes.data || [];
​
  // console.log("fields==============================")
  console.log(schemasList)
  const total = countRes.total
  // 计算需分几次取
  const batchTimes = Math.ceil(total / MAX_LIMIT)
  // 承载所有读操作的 promise 的数组
  const tasks = []
  for (let i = 0; i < batchTimes; i++) {
    //console.log(connectList.length)
    if (connectList.length > 0) {
      let lookupList = [];
      connectList.forEach(connect => {
        const connectschemas = schemasList.filter(function (schemas) {
          return schemas._id == connect.connectResource;
        })[0];
        lookupList.push({
          from: connectschemas.collectionName,
          localField: connect.name,
          foreignField: '_id',
          as: "connect" + connect.name
        })
​
      });
​
      let aggregate = db.collection(collection).aggregate().match(params).skip(i * MAX_LIMIT).limit(MAX_LIMIT);
​
      for (let index = 0; index < connectList.length; index++) {
        aggregate = aggregate.lookup(lookupList[index]);
​
      }
​
      aggregate = aggregate.end();
​
      tasks.push(aggregate)
    } else {
​
      const promise = db.collection(collection).where(params).skip(i * MAX_LIMIT).limit(MAX_LIMIT).get();
      tasks.push(promise)
    }
​
  }
  console.log(tasks)
  // 等待所有
  let recordRes = (await Promise.all(tasks)).reduce((acc, cur) => {
​
    return {
      list: (acc.list || []).concat(cur.list || []),
      data: (acc.data || []).concat(cur.data || []),
    }
  })
​
  let records = (recordRes.list || []).concat(recordRes.data || []) || [];
  //1.定义表格名
  let dataCVS = title + '.xlsx';
  let excelData = [];
  let row = [];
  fields.forEach(field => {
    row.push(field.displayName)
  });
​
  excelData.push(row);
​
  records.forEach(record => {
    let arr = [];
    fields.forEach(field => {
      if (!record.hasOwnProperty(field.name)) {
        arr.push("")
      } else {
        switch (field.type) {
          case "Connect":
            arr.push(join2Str(record["connect" + field.name], field.connectField))
            break;
          case "DateTime":
            arr.push(formatDateTime(record[field.name]))
            break;
          case "Date":
            arr.push(formatDate(record[field.name]))
            break;
          case "Boolean":
            arr.push(record[field.name] ? "是" : "否")
            break;
            case "Enum":
              let enumElements = field.enumElements;
              let enumElement= enumElements.find(function(item){
                return item.value = record[field.name];
              })
              
              arr.push(enumElement.label)
              break;
          default:
            arr.push(record[field.name])
            break;
        }
      }
​
    });
    excelData.push(arr);
  });
​
  //3,把数据保存到excel里
  var buffer = await xlsx.build([{
    name: title,
    data: excelData
  }]);
  //4,把excel文件保存到云存储里
  const excelFileIdRes = await cloud.uploadFile({
    cloudPath: dataCVS,
    fileContent: buffer, //excel二进制文件
  });
​
  return await cloud.getTempFileURL({
    fileList: [excelFileIdRes.fileID]
  }).then(function (res) {
    return res.fileList[0].tempFileURL
  })
​
}
​
function join2Str(obj, fieldName) {
  if (Object.prototype.toString.call(obj) == "[object Array]") {
    let resultArr = [];
    obj.forEach(item => {
      if (item.hasOwnProperty(fieldName))
        resultArr.push(item[fieldName])
    });
    return resultArr.join(",")
  } else {
    if (obj.hasOwnProperty(fieldName))
      return obj[fieldName]
  }
}
​
function formatDateTime(inputTime) {
  var date = new Date(inputTime);
  var y = date.getFullYear();
  var m = date.getMonth() + 1;
  m = m < 10 ? ('0' + m) : m;
  var d = date.getDate();
  d = d < 10 ? ('0' + d) : d;
  var h = date.getHours();
  h = h < 10 ? ('0' + h) : h;
  var minute = date.getMinutes();
  var second = date.getSeconds();
  minute = minute < 10 ? ('0' + minute) : minute;
  second = second < 10 ? ('0' + second) : second;
  return y + '-' + m + '-' + d + ' ' + h + ':' + minute + ':' + second;
};
​
function formatDate(inputTime) {
  var date = new Date(inputTime);
  var y = date.getFullYear();
  var m = date.getMonth() + 1;
  m = m < 10 ? ('0' + m) : m;
  var d = date.getDate();
  d = d < 10 ? ('0' + d) : d;
  return y + '-' + m + '-' + d;
};

整合数据库框架

场景:小程序或APP的业务逻辑复杂,模板页面的开发,组件的开发和统一异常处理

例子:

以下例子引用了wxboot的小程序框架

//app.js
// const {WXBoot} = require('wxbootstart');
​
​
require('./lib-webpack/wxboot');
import login from "./login/login"
​
import utils from "./utils/utils"
import constants from "./constants/constants"
App.A({
  config: {
    initCloud:{ 
      // env: '',
    traceUser: true,},
    route: '/pages/$page/$page',
    pageApi: utils,
    consts: constants,
    updata:{
      arrObjPath:false,
      arrCover:false
    },
    mixins:[login,App.A.Options] ,
  },
  getOpenidFunc: function(){
    return this.cloud.callFunction({
      name:"getWXContext"
    }).then(res=>{
      return  res.result.openid;
    }).catch(err=>{ 
      console.error(err)
      return ""
    })
  },
  onLaunch: function (opts) {
​
    App.A.on('some_message', function (msg) {
      console.log('Receive message:', msg)
    })
    console.log('APP is Running', opts)  
​
  },
  store: {
    id: 0
  },
​
  auth:{
    canUseXXX:false
  },
​
  globalData: {
    version: "v1.0.0",
    id: 0,
    userInfo: null,
    addressInfo: null,
    sessionKey: null,
    loginTime: 0,
    openid: "",
    theme: {
      color: "#FFFFFF"
    },
    share: {
      title: "开启一天好运",
      imageUrl: "https://XXX.jpg",
      path: "/pages/index/index"
    },
    settings: null
  },
​
  onAwake: function (time) {
    console.log('onAwake, after', time, 'ms')
  },
  onShow: function () {
    console.log('App onShow')
  },
  /*小程序主动更新
   */
  updateManager() {
    if (!wx.canIUse('getUpdateManager')) {
      return false;
    }
    const updateManager = wx.getUpdateManager();
    updateManager.onCheckForUpdate(function (res) {});
    updateManager.onUpdateReady(function () {
      wx.showModal({
        title: '有新版本',
        content: '新版本已经准备好,即将重启',
        showCancel: false,
        success(res) {
          if (res.confirm) {
            updateManager.applyUpdate()
          }
        }
      });
    });
    updateManager.onUpdateFailed(function () {
      wx.showModal({
        title: '更新提示',
        content: '新版本下载失败',
        showCancel: false
      })
    });
  },
  "navigateToMiniProgramAppIdList": [
    "wx8abaf00ee8c3202e"
​
  ]
})
​

全局封装增删改 ,我们更专注的关注于业务逻辑,统一异常处理

module.exports = {
​
  $callFun: callFunction,
  $add: add,
  $get: get,
  $update: update,
  $remove: remove,
  $count:count
}
​
//取数据库实例。一个数据库对应一个实例
​
/**
 * 封装查询操作
 * 增 查 改 删
 * 
 */
​
//增
async function add(collectionName, data, openParse = false) {
  if (openParse) {
    data = await parseQuery(data, this)
  }
  return this.$collection(collectionName).add({
    data
  }).then(res => {
    return res._id
  }).catch(res => {
    return ""
  })
}
​
//查询
//对应id取不到的时候,返回{}
async function get(collectionName, query, openParse = false) {
  switch (type(query)) {
    case "string":
      return this.$collection(collectionName).doc(query).get().then(res => {
        return res.data
      }).catch(res => {
        console.warn(`"collection":"${collectionName}","_id":"${query}"不存在`)
        return {}
      })
    case "object":
      const defaultOptions = {
        where: null,
        order: null,
        skip: 0,
        limit: 20,
        field: null,
        pageIndex: 1
      }
      const parsequery = setDefaultOptions(query, defaultOptions);
      let {
        where, order, skip, limit, field, pageIndex
      } = parsequery;
      let collectionGet = this.$collection(collectionName);
      if (where != null) {
        if (openParse) {
          where = await parseQuery(where, this)
        }
        collectionGet = collectionGet.where(where)
      }
      if (order != null) {
        if (type(order) == "object") {
          collectionGet = collectionGet.orderBy(order.name, order.value);
        }
        if (type(order) == "array") {
          order.forEach(orderItem => {
            collectionGet = collectionGet.orderBy(orderItem.name, orderItem.value);
          });
        }
      }
      if (field) {
        collectionGet = collectionGet.field(field);
      }
      if (pageIndex > 1) {
        collectionGet = collectionGet.skip((pageIndex - 1) * limit).limit(limit);
      } else {
        collectionGet = collectionGet.skip(skip).limit(limit);
      }
      return collectionGet.get().then(res => {
        return res.data
      }).catch(res => {
        console.warn(`"collection":"${collectionName}"不存在`)
        return []
      })
    default:
      console.warn(`"query":参数类型错误不存在`)
      return null;
  }
}
​
async function count(collectionName, query, openParse = false) {
  switch (type(query)) {
     
    case "object":
      let collectionUpdate = this.$collection(collectionName);
      if (openParse) {
        query = await parseQuery(query, this)
      }
      collectionUpdate = collectionUpdate.where(query)
      return collectionUpdate.count().then(res => {
        return res.total
      }).catch(res => {
        console.warn(`"collection":"${collectionName}"不存在`)
        return 0
      })
    default:
      return this.$collection(collectionName).count().then(res => {
        return res.total
      }).catch(res => {
        console.warn(`"collection":"${collectionName}"不存在`)
        return 0
      })
     
  }
}
​
//修改
async function update(collectionName, query, updata, openParse = false) {
  switch (type(query)) {
    case "string":
      return this.$collection(collectionName).doc(query).update({
        data: updata
      }).then(res => {
        return res.stats.updated
      }).catch(res => {
        console.warn(`"collection":"${collectionName}","_id":"${query}"不存在`)
        return 0
      })
    case "object":
      let collectionUpdate = this.$collection(collectionName);
      if (openParse) {
        query = await parseQuery(query, this)
      }
      collectionUpdate = collectionUpdate.where(query)
      return collectionUpdate.update({
        data: updata
      }).then(res => {
        return res.stats.updated
      }).catch(res => {
        console.warn(`"collection":"${collectionName}"不存在`)
        return 0
      })
    default:
      console.warn(`"query":参数类型错误不存在`)
      return 0
  }
}
​
​
//删除
async function remove(collectionName, query, openParse=false) {
  switch (type(query)) {
    case "string":
      return this.$collection(collectionName).doc(query).remove().then(res => {
        return res
      }).catch(res => {
        console.warn(`"collection":"${collectionName}","_id":"${query}"不存在`)
        return {}
      })
    case "object":
      let collectionRemove = this.$collection(collectionName);
      if (openParse) {
        query = await parseQuery(query, this)
      }
      collectionRemove = collectionRemove.where(query)
      return collectionRemove.remove().then(res => {
        return res
      }).catch(res => {
        console.warn(`"collection":"${collectionName}"不存在`)
        return []
      })
    default:
      console.warn(`"query":参数类型错误不存在`)
      return 0
  }
}
​
​
function setDefaultOptions(options = {}, defaultOptions = {}) {
  return Object.assign(defaultOptions, options);
}
​
function promisify(api) {
  return (options, ...query) => {
    return new Promise((resolve, reject) => {
      api(Object.assign({}, options, {
        success: resolve,
        fail: reject
      }), ...query);
    })
  }
}
​
async function callFunction(options) {
  return await this.cloud.callFunction(options)
}
​
var undef = void(0)
function type(obj) {
  if (obj === null) return 'null'
  else if (obj === undef) return 'undefined'
  var m = /\[object (\w+)\]/.exec(Object.prototype.toString.call(obj))
  return m ? m[1].toLowerCase() : ''
}
async function parseQuery(query, self) {
  let queryStr = JSON.stringify(query);
  if (queryStr.indexOf("{openid}") > -1) {
    let openid = await self.$getOpenid();
    return JSON.parse(queryStr.replace(/{openid}/g, openid));
  } else {
    return query
  }
}

高级用法,结合云函数和https 以及封装api ,实现统一对外接口,对接其他语言

场景:多项目,多后台,多端打通,数据迁移等

// 云函数入口文件
const cloud = require('wx-server-sdk')
cloud.init({
  env: cloud.DYNAMIC_CURRENT_ENV
});
const db = cloud.database();
// 云函数入口函数
exports.main = async (event, context) => {
  let body = event.body;
​
  let cloudParams = urlToObj(decodeURIComponent(body));
  let {
    cloudType,
    collectionName
  } = cloudParams;
​
  let data = JSON.parse(cloudParams.data || "{}");
​
  let query = JSON.parse(cloudParams.query || "{}");
  if(type(query)=="object"){
    
  query.where =  JSON.parse(query.where ||"{}" );
    if(query.field)
  query.field =  JSON.parse(query.field ||"{}" );
  }   
  console.log(query)
  let promise = null;
  switch (cloudType) {
    case "ADD":
      promise = add(collectionName, data);
      break;
​
    case "GET":
      promise = get(collectionName, query)
      break;
    case "UPDATE":
      promise = update(collectionName, query, data)
      break;
    case "REMOVE":
      promise = remove(collectionName, query)
      break;
    case "COUNT":
​
      let countquery = null;
      if (type(query) == "string") {
        countquery = query
      } else {
        countquery = query.where || null
      }
      promise = count(collectionName, countquery)
      break;
​
    default:
      break;
  }
  return promise;
}
​
function urlToObj(str) {
  var obj = {};
​
  var arr2 = str.split("&");
  for (var i = 0; i < arr2.length; i++) {
    var res = arr2[i].split("=");
    obj[res[0]] = res[1] || "";
  }
  return obj;
}
​
//增
async function add(collectionName, data, openParse = false) {
  if (openParse) {
    data = await parseQuery(data)
  }
  return db.collection(collectionName).add({
    data
  }).then(res => {
​
    return res._ids || res._id;
  }).catch(res => {
    return ""
  })
}
​
//查询
//对应id取不到的时候,返回{}
async function get(collectionName, query, openParse = false) {
  if (query.limit && query.limit == "all") {
    let countquery = null;
    if (type(query) == "string") {
      countquery = query
    } else {
      countquery = query.where || null
    }
    // 先取出集合记录总数
    const total = await count(collectionName, countquery);
​
    // 计算需分几次取
    const batchTimes = Math.ceil(total / 20)
    // 承载所有读操作的 promise 的数组
    const tasks = []
    for (let i = 0; i < batchTimes; i++) {
      query.limit = 20;
      query.pageIndex = i + 1;
      const promise = get(collectionName, query);
      tasks.push(promise)
    }
    // 等待所有
    return (await Promise.all(tasks)).reduce((acc, cur) => {
      acc = acc || [];
      cur = cur || [];
      return acc.concat(cur);
    })
  }
​
  switch (type(query)) {
    case "string":
      return db.collection(collectionName).doc(query).get().then(res => {
        return res.data
      }).catch(res => {
        console.warn(`"collection":"${collectionName}","_id":"${query}"不存在`)
        return {}
      })
    case "object":
      const defaultOptions = {
        where: null,
        order: null,
        skip: 0,
        limit: 20,
        field: null,
        pageIndex: 1
      }
      const parsequery = setDefaultOptions(query, defaultOptions);
      let {
        where, order, skip, limit, field, pageIndex
      } = parsequery;
      let collectionGet = db.collection(collectionName);
      if (where != null) {
        if (openParse) {
          where = await parseQuery(where)
        }
        collectionGet = collectionGet.where(where)
      }
      if (order != null) {
        if (type(order) == "object") {
          collectionGet = collectionGet.orderBy(order.name, order.value);
        }
        if (type(order) == "array") {
          order.forEach(orderItem => {
            collectionGet = collectionGet.orderBy(orderItem.name, orderItem.value);
          });
        }
      }
      if (field) {
        collectionGet = collectionGet.field(field);
      }
      if (pageIndex > 1) {
        collectionGet = collectionGet.skip((pageIndex - 1) * limit).limit(limit);
      } else {
        collectionGet = collectionGet.skip(skip).limit(limit);
      }
      return collectionGet.get().then(res => {
        return res.data
      }).catch(res => {
        console.warn(`"collection":"${collectionName}"不存在`)
        return []
      })
    default:
      console.warn(`"query":参数类型错误不存在`)
      return null;
  }
}
​
async function count(collectionName, query, openParse = false) {
  switch (type(query)) {
​
    case "object":
      let collectionUpdate = db.collection(collectionName);
      if (openParse) {
        query = await parseQuery(query)
      }
      collectionUpdate = collectionUpdate.where(query)
      return collectionUpdate.count().then(res => {
        return res.total
      }).catch(res => {
        console.warn(`"collection":"${collectionName}"不存在`)
        return 0
      })
    default:
      return db.collection(collectionName).count().then(res => {
        return res.total
      }).catch(res => {
        console.warn(`"collection":"${collectionName}"不存在`)
        return 0
      })
​
  }
}
​
//修改
async function update(collectionName, query, updata, openParse = false) {
  switch (type(query)) {
    case "string":
      return db.collection(collectionName).doc(query).update({
        data: updata
      }).then(res => {
        return res.stats.updated
      }).catch(res => {
        console.warn(`"collection":"${collectionName}","_id":"${query}"不存在`)
        return 0
      })
    case "object":
      let collectionUpdate = db.collection(collectionName);
      if (openParse) {
        query = await parseQuery(query)
      }
      collectionUpdate = collectionUpdate.where(query)
      return collectionUpdate.update({
        data: updata
      }).then(res => {
        return res.stats.updated
      }).catch(res => {
        console.warn(`"collection":"${collectionName}"不存在`)
        return 0
      })
    default:
      console.warn(`"query":参数类型错误不存在`)
      return 0
  }
}
​
//删除
async function remove(collectionName, query, openParse = false) {
  switch (type(query)) {
    case "string":
      return db.collection(collectionName).doc(query).remove().then(res => {
        return res
      }).catch(res => {
        console.warn(`"collection":"${collectionName}","_id":"${query}"不存在`)
        return {}
      })
    case "object":
      let collectionRemove = db.collection(collectionName);
      if (openParse) {
        query = await parseQuery(query)
      }
      collectionRemove = collectionRemove.where(query)
      return collectionRemove.remove().then(res => {
        return res
      }).catch(res => {
        console.warn(`"collection":"${collectionName}"不存在`)
        return []
      })
    default:
      console.warn(`"query":参数类型错误不存在`)
      return 0
  }
}
​
function setDefaultOptions(options = {}, defaultOptions = {}) {
  return Object.assign(defaultOptions, options);
}
​
function promisify(api) {
  return (options, ...query) => {
    return new Promise((resolve, reject) => {
      api(Object.assign({}, options, {
        success: resolve,
        fail: reject
      }), ...query);
    })
  }
}
​
var undef = void(0)
​
function type(obj) {
  if (obj === null) return 'null'
  else if (obj === undef) return 'undefined'
  var m = /\[object (\w+)\]/.exec(Object.prototype.toString.call(obj))
  return m ? m[1].toLowerCase() : ''
}
async function parseQuery(query) {
  let queryStr = JSON.stringify(query);
  if (queryStr.indexOf("{openid}") > -1) {
    let openid = cloud.getWXContext().OPENID;
    return JSON.parse(queryStr.replace(/{openid}/g, openid));
  } else {
    return query
  }
}

三、建议

  • 云开发是主要是类似mongdb的非关系数据库,可以保存json的数据,我们可以多直接保存复杂的值
  • 尝试使用自己封装的业务逻辑来全局控制异常等
  • 数据库的权限、索引等可以对数据库检索性能进一步优化
上一篇:利用java代码给mongo数据库加索引、删除索引等操作


下一篇:mongodb学习笔记