微信公號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) >請我喝芬達
歡迎關注
歡迎關注
請我喝芬達
請我喝芬達
相關文章
相關標籤/搜索