微信公号DIY:MongoDB 简易ORM & 公号记帐数据库设计

前两篇 微信公号DIY 系列:html

介绍了如何使用搭建&训练聊天机器人以及让公号支持图片上传到七牛,把公号变成一个七牛图片上传客户端。这一篇将继续开发公号,让公号变成一个更加实用的工具帐本(理财从记帐开始)。python

代码: 项目代码已上传至github,地址为:gusibi/momogit

帐本功能

帐本是一个功能比较简单应用,公号内只须要支持:github

  1. 记帐(记帐,修改金额,取消记帐)
  2. 帐单统计(提供数据和图片形式的统计功能)

固然后台管理功能就比较多了,这个之后再介绍。mongodb

对于数据存储,我选择的是MongoDB(选MongoDB的缘由是,以前没用过,想试一下),咱们先看下MongoDB和关系型数据库的不一样。数据库

MongoDB

什么是MongoDB ?

MongoDB 是由C++语言编写的,是一个开放源代码的面向文档的数据库,易于开发和缩放。api

mongo和传统关系数据库的最本质的区别在那里呢?MongoDB 是文档模型。数组

关系模型和文档模型的区别在哪里?微信

  • 关系模型须要你把一个数据对象,拆分红零部件,而后存到各个相应的表里,须要的是最后把它拼起来。举例子来讲,假设咱们要作一个CRM应用,那么要管理客户的基本信息,包括客户名字、地址、电话等。因为每一个客户可能有多个电话,那么按照第三范式,咱们会把电话号码用单独的一个表来存储,并在显示客户信息的时候经过关联把须要的信息取回来。
  • 而MongoDB的文档模式,与这个模式大不相同。因为咱们的存储单位是一个文档,能够支持数组和嵌套文档,因此不少时候你直接用一个这样的文档就能够涵盖这个客户相关的全部我的信息。关系型数据库的关联功能不必定就是它的优点,而是它可以工做的必要条件。 而在MongoDB里面,利用富文档的性质,不少时候,关联是个伪需求,能够经过合理建模来避免作关联。
    关系模型和文档模型区别图例
    关系模型和文档模型区别图例

MongoDB 概念解析

在mongodb中基本的概念是文档、集合、数据库,下表是MongoDB和关系型数据库概念对比:数据结构

SQL术语/概念 MongoDB术语/概念 解释/说明
database database 数据库
table collection 数据库表/集合
row document 数据记录行/文档
column field 数据字段/域
index index 索引
table joins 表链接,MongoDB不支持
primary key primary key 主键,MongoDB自动将_id字段设置为主键

经过下图实例,咱们也能够更直观的的了解Mongo中的一些概念:

Mongo中的一些概念
Mongo中的一些概念

接下来,我从使用的角度来介绍下如何使用 python 如何使用MongoDB,在这个过程当中,我会实现一个简单的MongoDB的ORM,同时也会解释一下涉及到的概念。

简易 Python MongoDB ORM

python 使用 mongodb

首先,须要确认已经安装了 PyMongo,若是没有安装,使用如下命令安装:

pip install pymongo
# 或者
easy_install pymongo复制代码

详细安装步骤参考: PyMongo Installing / Upgrading

链接 MongoClient:

>>> from pymongo import MongoClient
>>> client = MongoClient()复制代码

上述命令会使用Mongo的默认host和端口号,和如下命令做用相同:

client = MongoClient('localhost', 27017) # mongo 默认端口号27017
# 也能够这样写
client = MongoClient('mongodb://localhost:27017/')复制代码

选择一个数据库

获取 MongoClient 后咱们接下来要作的是选择要执行的数据库,命令以下:

>>> db = client.test_database # test_database 是选择的数据库名称
# 也可使用下述方式
>>> db = client['test-database']复制代码

数据库(Database)
一个mongodb中能够创建多个数据库。
MongoDB的默认数据库为"db",该数据库存储在data目录中。
MongoDB的单个实例能够容纳多个独立的数据库,每个都有本身的集合和权限,不一样的数据库也放置在不一样的文件中。
"show dbs" 命令能够显示全部数据的列表。
执行 "db" 命令能够显示当前数据库对象或集合。
运行"use"命令,能够链接到一个指定的数据库。

获取集合

选择数据库后,接下来就是选择一个集合(Collection),获取一个集合和选择一个数据库的方式基本一致:

>>> collection = db.test_collection  # test_collection 是集合名称
# 也可使用字典的形式
>>> collection = db['test-collection']复制代码

集合(collection)
集合就是 MongoDB 文档组,相似于 RDBMS (关系数据库管理系统:Relational Database Management System)中的表。
集合存在于数据库中,集合没有固定的结构,这意味着你在对集合能够插入不一样格式和类型的数据,但一般状况下咱们插入集合的数据都会有必定的关联性。
当第一个文档插入时,集合就会被建立。
集合名不能是空字符串""
集合名不能含有\0字符(空字符),这个字符表示集合名的结尾。
集合名不能以"system."开头,这是为系统集合保留的前缀。
用户建立的集合名字不能含有保留字符。有些驱动程序的确支持在集合名里面包含,这是由于某些系统生成的集合中包含该字符。除非你要访问这种系统建立的集合,不然千万不要在名字里出现$。 

了解这几个操做后咱们把这几个封装一下:

from six import with_metaclass
from pymongo import MongoClient
from momo.settings import Config

pyclient = MongoClient(Config.MONGO_MASTER_URL)

class ModelMetaclass(type):
    """ Metaclass of the Model. """
    __collection__ = None

    def __init__(cls, name, bases, attrs):
        super(ModelMetaclass, cls).__init__(name, bases, attrs)
        cls.db = pyclient['momo_bill']  # 数据库名称,也能够做为参数传递 一般状况下一个应用只是用一个数据库就能实现需求
        if cls.__collection__:
            cls.collection = cls.db[cls.__collection__]


class Model(with_metaclass(ModelMetaclass, object)):

    __collection__ = 'model_base'复制代码

如今咱们能够这样定义一个集合(Collection):

class Account(Model):

    ''' 暂时在这里声明文档结构,用不用作校验,只是方便本身查阅 之后也不会变成相似 SQLAlchemy 那种强校验的形式 :param _id: '用户ID', :param nickname: '用户昵称 用户显示', :param username: '用户名 用于登陆', :param avatar: '头像', :param password: '密码', :param created_time: '建立时间', '''
    __collection__ = 'account'  # 集合名复制代码

使用方式:

account = Account()复制代码

如今就已经指定了数据库和集合,能够自由作 CURD 操做了(虽然还不支持)。

建立文档(insert document)

使用PyMongo 建立文档很是方便:

>>> import datetime
>>> account = {"nickname": "Mike",
...         "username": "mike",
...         "avatar": "https://user-gold-cdn.xitu.io/2017/7/16/456d255c546cfe22ccfeaa56458c9f5b",
...         "password": "password",
...         "created_time": datetime.datetime.utcnow()}

>>> accounts = db.account
>>> account_id = accounts.insert_one(account).inserted_id
>>> account_id
ObjectId('...')复制代码

建立一个文档时,你能够指定 _id,若是不指定,系统会自动添加上_id 字段,这个字段必须是惟一不可重复的字段。

也但是使用 collection_names 命令显示全部的集合:

>>> db.collection_names(include_system_collections=False)
[u'account']复制代码

文档(Document) 文档是一组键值(key-value)对(即BSON)。MongoDB 的文档不须要设置相同的字段,而且相同的字段不须要相同的数据类型,这与关系型数据库有很大的区别,也是 MongoDB 很是突出的特色。

如今咱们给这个简易ORM添加建立文档的功能:

class Model(with_metaclass(ModelMetaclass, object)):

    __collection__ = 'model_base'

 @classmethod
    def insert(cls, **kwargs):
        # insert one document
        doc = cls.collection.insert_one(kwargs)
        return doc

 @classmethod
    def bulk_inserts(cls, *params):
        ''' :param params: document list :return: '''
        results = cls.collection.insert_many(params)
        return results复制代码

建立一个文档方法为:

account = Account.insert("nickname": "Mike",
        "username": "mike",
        "avatar": "https://user-gold-cdn.xitu.io/2017/7/16/456d255c546cfe22ccfeaa56458c9f5b",
        "password": "password",
        "created_time": datetime.datetime.utcnow())复制代码

查询文档

使用 find_one 获取单个文档:

accounts.find_one()复制代码

若是没有任何筛选条件,find_one 命令会取集合中的第一个文档
若是有筛选条件,会取符合条件的第一个文档

accounts.find_one({"nickname": "mike"})复制代码

使用 ObjectId 查询单个文档:

accounts.find_one({"_id": account_id})复制代码

将这个添加到ORM中:

class Model(with_metaclass(ModelMetaclass, object)):

    __collection__ = 'model_base'

 @classmethod
    def get(cls, _id=None, **kwargs):
        if _id: # 若是有_id
            doc = cls.collection.find_one({'_id': _id})
        else: # 若是没有id
            doc = cls.collection.find_one(kwargs)
        return doc复制代码

若是你想获取多个文档可使用find命令。

使用find命令获取多个文档

accounts.find()
# 固然支持筛选条件
accounts.find({"nickname": "mike"})复制代码

将这个功能添加到ORM:

class Model(with_metaclass(ModelMetaclass, object)):

    __collection__ = 'model_base'

 @classmethod
    def find(cls, filter=None, projection=None, skip=0, limit=20, **kwargs):
        docs = cls.collection.find(filter=filter,
                                   projection=projection,
                                   skip=skip, 
                                   limit=limit,
                                   **kwargs)
        return docs复制代码

如今咱们能够这样作查询操做:

account = Account.get(_id='account_id')
accounts = Account.find({'name': "mike"})复制代码

修改(update)

更新操做文档地址:http://api.mongodb.com/python/current/api/pymongo/collection.html#pymongo.collection.Collection.update_one

update_one(filter, update, upsert=False, bypass_document_validation=False, collation=None)

更新一个符合筛选条件的文档 upsert 若是为True 则会在没有匹配到文档的时候建立一个

update_many(filter, update, upsert=False, bypass_document_validation=False, collation=None)

更新所有符合筛选条件的文档 upsert 若是为True 则会在没有匹配到文档的时候建立一个

添加到ORM中:

class Model(with_metaclass(ModelMetaclass, object)):

    __collection__ = 'model_base'

 @classmethod
    def update_one(cls, filter, **kwargs):
        result = cls.collection.update_one(filter, **kwargs)
        return result

 @classmethod
    def update_many(cls, filter, **kwargs):
        results = cls.collection.update_many(filter, **kwargs)
        return results复制代码

能够看到,我这里并无作多余的操做,只是直接调用了PyMongo的方法。

删除

删除操做和update相似可是比较简单:

delete_one(filter, collation=None):

删除一个匹配到的文档

delete_many(filter, collation=None):

删除所有匹配到的文档

添加到ORM中:

class Model(with_metaclass(ModelMetaclass, object)):

    __collection__ = 'model_base'

 @classmethod
    def delete_one(cls, **filter):
        cls.collection.delete_one(filter)

 @classmethod
    def delete_many(cls, **filter):
        cls.collection.delete_many(filter)复制代码

到这里,简易的ORM就实现了(这只能算是个功能简单的框,能够再自由添加其它更多的功能)。

接下来是帐本文档结构的设计

帐本数据结构设计

帐本须要包含的数据有:

  • 帐户全部人
  • 帐单记录
  • 帐单分类

那么咱们至少须要三个集合:

{
    'account': {  # 用户集合
        '_id': '用户ID',
        'nickname': '用户昵称',
        'username': '用户名 用于登陆',
        'avatar': '头像',
        'password': '密码',
        'created_time': '建立时间',
    },
    'bill': { # 帐单集合
        '_id': '帐单ID',
        'uid': '用户ID',
        'money': '金额 精确到分',
        'tag': '标签',
        'remark': '备注',
        'created_time': '建立时间',
    },
    'tag': {  # 帐单标签
        '_id': '标签ID',
        'name': '标签名',
        'icon': '标签图标',
        'uid': '建立者ID(默认是管理员)',
        'created_time': '建立时间',
    }
}复制代码

这里帐单和用户使用 uid 做为引用的关联,account 和 bill 是一对多关系。

固然你也能够再加一个帐本的集合,用户和帐本对应,这时,帐单能够做为帐本中的一个list数据结构(单个文档有16M的限制,若是存储超过这个大小不能使用这种形式,数据量大的时候,查询操做会比较缓慢)。

做为公号中的帐本,咱们暂时不加帐本功能,由于这会让咱们的操做变得复杂。

由于公号里的每次操做都是独立请求,并无上下文。因此咱们要记录记帐这个操做走到了哪一步,接下来改干吗。

记帐逻辑如图:

公号记帐流程图
公号记帐流程图

因此咱们这里要有数据来记录当前的操做步骤以及接下来改有的操做步骤:

{
    'account_workflow': {  # 用户当前工做流
        '_id': 'id', 
        'next': '下一步的操做',
        'uid': '用户ID',
        'workflow': '使用的工做流',
        'created_time': '开始时间'
    }
}复制代码

这个集合记录了咱们当前所在的工做流,下一步该走向哪一步。

这个集合须要设置文档的过时时间,好比输入 “记帐” 激活记帐工做流后,若是10分钟没有操做完成,那么须要从新开始。以避免输入记帐后不完成不能继续其它的操做。

下面的这个集合记录了哪些关键字能够激活工做流,对应的工做流是什么以及开始哪一个动做。

{
    'keyword': {  # 特殊关键字
        '_id': '关键字ID',
        'word': '关键字',
        'data': {
            'workflow': '工做流',
            'action': '工做流动做',
            'value': '返回值',
            'type': '返回值类型 url|pic|text',
        },
        'created_time': '建立时间'
    },
}复制代码

到这里帐本的数据库设计就结束了。

总结

这一篇主要介绍了MongoDB,PyMongo 的使用以及如何编写一个简易的MongoDB ORM。
而后又介绍了基于 MongoDB 的公号帐本应用的数据库设计。

预告

下一篇咱们将介绍,如何实现记帐功能。

如下是操做截图。

记帐
记帐

修改金额
修改金额

取消记帐
取消记帐

欢迎关注公号四月(April_Louisa)试用。

参考连接


最后,感谢女友支持。

>欢迎关注(April_Louisa) >请我喝芬达
欢迎关注
欢迎关注
请我喝芬达
请我喝芬达
相关文章
相关标签/搜索