微信公號DIY:MongoDB 簡易ORM & 公號記賬資料庫設計

goodspeed發表於2017-07-16

前兩篇 微信公號DIY 系列:

介紹瞭如何使用搭建&訓練聊天機器人以及讓公號支援圖片上傳到七牛,把公號變成一個七牛圖片上傳客戶端。這一篇將繼續開發公號,讓公號變成一個更加實用的工具賬本(理財從記賬開始)。

程式碼: 專案程式碼已上傳至github,地址為:gusibi/momo

賬本功能

賬本是一個功能比較簡單應用,公號內只需要支援:

  1. 記賬(記賬,修改金額,取消記賬)
  2. 賬單統計(提供資料和圖片形式的統計功能)

當然後臺管理功能就比較多了,這個以後再介紹。

對於資料儲存,我選擇的是MongoDB(選MongoDB的原因是,之前沒用過,想試一下),我們先看下MongoDB和關係型資料庫的不同。

MongoDB

什麼是MongoDB ?

MongoDB 是由C++語言編寫的,是一個開放原始碼的面向文件的資料庫,易於開發和縮放。

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) >請我喝芬達
歡迎關注
歡迎關注
請我喝芬達
請我喝芬達

相關文章