首頁  >  文章  >  後端開發  >  如何使用Python完成一個NoSQL資料庫的範例程式碼分享

如何使用Python完成一個NoSQL資料庫的範例程式碼分享

黄舟
黄舟原創
2017-07-18 11:12:571881瀏覽

NoSQL 這個字在近些年正變得隨處可見. 但是到底「NoSQL」 指的是什麼? 它是如何並且為什麼這麼有用? 在本文, 我們將會透過純Python (我比較喜歡叫它, 「輕結構化的偽代碼」) 寫一個NoSQL 資料庫來回答這些問題.

OldSQL

很多情況下, SQL 已經成為「資料庫」 (database) 的一個同義詞. 實際上, SQL 是 Strctured Query Language 的縮寫, 而並非指資料庫技術本身. 更確切地說, 它所指的是從 RDBMS (關係型資料庫管理系統, Relational Database Management System ) 中擷取資料的一門語言. MySQL, MS SQL Server 與Oracle 都屬於RDBMS 的其中一員.

RDBMS 中的R,即「Relational」 (有關係,關聯的), 是其中內容最豐富的部分. 資料透過 表(table) 進行組織, 每張表都是一些由 類型(type) 相關聯的 列(column) 構成. 所有表, 列及其類別的型別稱為資料庫的 schema (架構或模式). schema 透過每張表的描述資訊完整刻畫了資料庫的結構. 例如, 一張叫做 Car 的表格可能有以下一些欄位:

  • Make: a string

  • #Model: a string

  • Year: a four-digit number; alternatively, a date

  • Color: a string

  • VIN(Vehicle Identification Number): a string

在一張表中, 每個單一的條目叫做一 行( row), 或一筆 記錄(record). 為了區分每筆記錄, 通常會定義一個 主鍵(primary key). 表中的 #主鍵 是其中一列, 它能夠唯一標識每一行. 在表 Car中, VIN 是一個天然的主鍵選擇, 因為它能夠保證每輛車具有唯一的標識. 兩個不同的行可能會在Make, Model, Year 和Color 列上有相同的值, 但是對於不同的車而言, 肯定會有不同的VIN. 反之, 只要兩行擁有同一個VIN, 我們不必去檢查其他列就可以認為這兩行指的是同一輛車.

Querying

SQL 能夠讓我們透過對資料庫進行 query (查詢) 來取得有用的資訊. 查詢 簡單來說, 查詢就是用一個結構化語言向RDBMS 提問, 並將其返回的行解釋為問題的答案. 假設資料庫表示了美國所有的註冊車輛, 為了獲取 所有的 記錄, 我們可以透過在資料庫上進行如下的 SQL 查詢 :

SELECT Make, Model FROM Car;

將SQL 大致翻譯成中文:

  • “SELECT” : “向我展示”

  • “Make, Model”: “Make 和Model 的值”

  • “FROM Car”: “對表Car 中的每一行」

也就是, “向我展示表Car 每一行中Make 和Model 的值”. 執行查詢後, 我們將會得到一些查詢的結果, 其中每個都是Make 和Model. 如果我們只關心在1994 年註冊的車的顏色, 那麼可以:

SELECT Color FROM Car WHERE Year = 1994;

此時, 我們會得到一個類似如下的列表:

Black
Red
Red
White
Blue
Black
White
Yellow

最後, 我們可以透過使用表格的 (primary key) 主鍵 , 這裡是VIN 來指定查詢一輛車:

SELECT * FROM Car WHERE VIN = '2134AFGER245267'

上面這條查詢語句會傳回所指定車輛的屬性資訊.

主鍵被定義為唯一不可重複的. 也就是說, 帶有某一指定VIN 的車輛在表中至多只能出現一次. 這一點非常重要,為什麼? 來看一個例子:

Relations

假設我們正在經營一個汽車修理的業務. 除了其他一些必要的事情, 我們還需要追蹤一輛車的服務歷史, 即在該輛車上所有的修整記錄. 那麼我們可能會建立包含下列一些欄位的 ServiceHistory 表:

VIN Make Model Year Color Service Performed Mechanic Price Date
#

這樣, 每次當車輛維修以後, 我們就在表中添加新的一行, 並寫入該次服務我們做了一些什麼事情, 是哪位維修工, 花費多少和服務時間等.

但是等一下, 我們都知道,對於同一輛車而言,所有車輛自身資訊有關的列是不變的。 也就是說,如果把我的Black 2014 Lexus RX 350 修整10 次的話, 那麼即使Make, Model, Year 和Color 這些信息並不會改變,每一次仍然重複記錄了這些信息. 與無效的重複記錄相比, 一個更合理的做法是對此類資訊只儲存一次, 並在有需要的時候進行查詢。

那麼該怎麼做呢? 我們可以建立第二張表: Vehicle , 它有以下一些列:

VIN Make #Model Year Color

這樣一來, 對於 ServiceHistory 表, 我們可以精簡如下一些欄位:

VIN Service Performed Mechanic Price Date

你可能会问,为什么 VIN 会在两张表中同时出现? 因为我们需要有一个方式来确认在 ServiceHistory 表的  辆车指的就是 Vehicle 表中的  辆车, 也就是需要确认两张表中的两条记录所表示的是同一辆车。 这样的话,我们仅需要为每辆车的自身信息存储一次即可. 每次当车辆过来维修的时候, 我们就在 ServiceHistory 表中创建新的一行, 而不必在 Vehicle表中添加新的记录。 毕竟, 它们指的是同一辆车。

我们可以通过 SQL 查询语句来展开 Vehicle 与 ServiceHistory 两张表中包含的隐式关系:

SELECT Vehicle.Model, Vehicle.Year FROM Vehicle, ServiceHistory WHERE Vehicle.VIN = ServiceHistory.VIN AND ServiceHistory.Price > 75.00;

该查询旨在查找维修费用大于 $75.00 的所有车辆的 Model 和 Year. 注意到我们是通过匹配 Vehicle 与 ServiceHistory 表中的 VIN 值来筛选满足条件的记录. 返回的将是两张表中符合条件的一些记录, 而 “Vehicle.Model” 与 “Vehicle.Year” , 表示我们只想要 Vehicle 表中的这两列.

如果我们的数据库没有 索引 (indexes) (正确的应该是 indices), 上面的查询就需要执行 表扫描 (table scan) 来定位匹配查询要求的行。 table scan 是按照顺序对表中的每一行进行依次检查, 而这通常会非常的慢。 实际上, table scan 实际上是所有查询中最慢的。

可以通过对列加索引来避免扫描表。 我们可以把索引看做一种数据结构, 它能够通过预排序让我们在被索引的列上快速地找到一个指定的值 (或指定范围内的一些值). 也就是说, 如果我们在 Price 列上有一个索引, 那么就不需要一行一行地对整个表进行扫描来判断其价格是否大于 75.00, 而是只需要使用包含在索引中的信息 “跳” 到第一个价格高于 75.00 的那一行, 并返回随后的每一行(由于索引是有序的, 因此这些行的价格至少是 75.00)。

当应对大量的数据时, 索引是提高查询速度不可或缺的一个工具。当然, 跟所有的事情一样,有得必有失, 使用索引会导致一些额外的消耗: 索引的数据结构会消耗内存,而这些内存本可用于数据库中存储数据。这就需要我们权衡其利弊,寻求一个折中的办法, 但是为经常查询的列加索引是 非常 常见的做法。

The Clear Box

得益于数据库能够检查一张表的 schema (描述了每列包含了什么类型的数据), 像索引这样的高级特性才能够实现, 并且能够基于数据做出一个合理的决策。 也就是说, 对于一个数据库而言, 一张表其实是一个 “黑盒” (或者说透明的盒子) 的反义词?

当我们谈到 NoSQL 数据库的时候要牢牢记住这一点。 当涉及 query 不同类型数据库引擎的能力时, 这也是其中非常重要的一部分。

Schemas

我们已经知道, 一张表的 schema , 描述了列的名字及其所包含数据的类型。它还包括了其他一些信息, 比如哪些列可以为空, 哪些列不允许有重复值, 以及其他对表中列的所有限制信息。 在任意时刻一张表只能有一个 schema, 并且 表中的所有行必须遵守 schema 的规定 。

这是一个非常重要的约束条件。 假设你有一张数据库的表, 里面有数以百万计的消费者信息。 你的销售团队想要添加额外的一些信息 (比如, 用户的年龄), 以期提高他们邮件营销算法的准确度。 这就需要来 alter (更改) 现有的表 – 添加新的一列。 我们还需要决定是否表中的每一行都要求该列必须有一个值。 通常情况下, 让一个列有值是十分有道理的, 但是这么做的话可能会需要一些我们无法轻易获得的信息(比如数据库中每个用户的年龄)。因此在这个层面上,也需要有些权衡之策。

此外,对一个大型数据库做一些改变通常并不是一件小事。为了以防出现错误,有一个回滚方案非常重要。但即使是如此,一旦当 schema 做出改变后,我们也并不总是能够撤销这些变动。 schema 的维护可能是 DBA 工作中最困难的部分之一。

Key/Value Stores

在 “NoSQL” 这个词存在前, 像 memcached 这样的 键/值 数据存储 (Key/Value Data Stores) 无须 table schema 也可提供数据存储的功能。 实际上, 在 K/V 存储时, 根本没有 “表 (table)” 的概念。 只有 键 (keys) 与 值 (values) . 如果键值存储听起来比较熟悉的话, 那可能是因为这个概念的构建原则与 Python 的 dict 与 set 相一致: 使用 hash table (哈希表) 来提供基于键的快速数据查询。 一个基于 Python 的最原始的 NoSQL 数据库, 简单来说就是一个大的字典 (dictionary) .

为了理解它的工作原理,亲自动手写一个吧! 首先来看一下一些简单的设计想法:

  • 一个 Python 的 dict 作为主要的数据存储

  • 仅支持 string 类型作为键 (key)

  • 支持存储 integer, string 和 list

  • 一个使用 ASCLL string 的简单 TCP/IP 服务器用来传递消息

  • 一些像 INCREMENTDELETE , APPEND 和 STATS 这样的高级命令 (command)

有一个基于 ASCII 的 TCP/IP 接口的数据存储有一个好处, 那就是我们使用简单的 telnet 程序即可与服务器进行交互, 并不需要特殊的客户端 (尽管这是一个非常好的练习并且只需要 15 行代码即可完成)。

对于我们发送到服务器及其它的返回信息,我们需要一个 “有线格式”。下面是一个简单的说明:

Commands Supported

  • PUT

    • 参数: Key, Value

    • 目的: 向数据库中插入一条新的条目 (entry)

  • GET

    • 参数: Key

    • 目的: 从数据库中检索一个已存储的值

  • PUTLIST

    • 参数: Key, Value

    • 目的: 向数据库中插入一个新的列表条目

  • APPEND

    • 参数: Key, Value

    • 目的: 向数据库中一个已有的列表添加一个新的元素

  • INCREMENT

    • 参数: key

    • 目的: 增长数据库的中一个整型值

  • DELETE

    • 参数: Key

    • 目的: 从数据库中删除一个条目

  • STATS

    • 参数: 无 (N/A)

    • 目的: 请求每个执行命令的 成功/失败 的统计信息

现在我们来定义消息的自身结构。

Message Structure

Request Messages

一条 请求消息 (Request Message) 包含了一个命令(command),一个键 (key), 一个值 (value), 一个值的类型(type). 后三个取决于消息类型,是可选项, 非必须。; 被用作是分隔符。即使并没有包含上述可选项, 但是在消息中仍然必须有三个 ; 字符。

COMMAND; [KEY]; [VALUE]; [VALUE TYPE]
  • COMMAND 是上面列表中的命令之一

  • KEY 是一个可以用作数据库 key 的 string (可选)

  • VALUE 是数据库中的一个 integer, list 或 string (可选)

    • list 可以被表示为一个用逗号分隔的一串 string, 比如说, “red, green, blue”

  • VALUE TYPE 描述了 VALUE 应该被解释为什么类型

    • 可能的类型值有:INT, STRING, LIST

Examples

  • "PUT; foo; 1; INT"

  • "GET; foo;;"

  • "PUTLIST; bar; a,b,c ; LIST"

  • "APPEND; bar; d; STRING"

  • "GETLIST; bar; ;"

  • STATS; ;;

  • INCREMENT; foo;;

  • DELETE; foo;;

Reponse Messages

一个 响应消息 (Reponse Message) 包含了两个部分, 通过 ; 进行分隔。第一个部分总是 True|False , 它取决于所执行的命令是否成功。 第二个部分是命令消息 (command message), 当出现错误时,便会显示错误信息。对于那些执行成功的命令,如果我们不想要默认的返回值(比如 PUT), 就会出现成功的信息。 如果我们返回成功命令的值 (比如 GET), 那么第二个部分就会是自身值。

Examples

  • True; Key [foo] set to [1]

  • True; 1

  • True; Key [bar] set to [['a', 'b', 'c']]

  • True; Key [bar] had value [d] appended

  • True; ['a', 'b', 'c', 'd']

  • True; {'PUTLIST': {'success': 1, 'error': 0}, 'STATS': {'success': 0, 'error': 0}, 'INCREMENT': {'success': 0, 'error': 0}, 'GET': {'success': 0, 'error': 0}, 'PUT': {'success': 0, 'error': 0}, 'GETLIST': {'success': 1, 'error': 0}, 'APPEND': {'success': 1, 'error': 0}, 'DELETE': {'success': 0, 'error': 0}}

Show Me The Code!

我将会以块状摘要的形式来展示全部代码。 整个代码不过 180 行,读起来也不会花费很长时间。

Set Up

下面是我们服务器所需的一些样板代码:

"""NoSQL database written in Python"""

# Standard library imports
import socket

HOST = 'localhost'
PORT = 50505
SOCKET = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
STATS = {
    'PUT': {'success': 0, 'error': 0},
    'GET': {'success': 0, 'error': 0},
    'GETLIST': {'success': 0, 'error': 0},
    'PUTLIST': {'success': 0, 'error': 0},
    'INCREMENT': {'success': 0, 'error': 0},
    'APPEND': {'success': 0, 'error': 0},
    'DELETE': {'success': 0, 'error': 0},
    'STATS': {'success': 0, 'error': 0},
    }

很容易看到, 上面的只是一个包的导入和一些数据的初始化。

Set up(Cont’d)

接下来我会跳过一些代码, 以便能够继续展示上面准备部分剩余的代码。 注意它涉及到了一些尚不存在的一些函数, 不过没关系, 我们会在后面涉及。 在完整版(将会呈现在最后)中, 所有内容都会被有序编排。 这里是剩余的安装代码:

COMMAND_HANDERS = {
    'PUT': handle_put,
    'GET': handle_get,
    'GETLIST': handle_getlist,
    'PUTLIST': handle_putlist,
    'INCREMENT': handle_increment,
    'APPEND': handle_append,
    'DELETE': handle_delete,
    'STATS': handle_stats,
}

DATA = {}

def main():
    """Main entry point for script"""
    SOCKET.bind(HOST, PORT)
    SOCKET.listen(1)
    while 1:
        connection, address = SOCKET.accept()
        print('New connection from [{}]'.format(address))
        data = connection.recv(4096).decode()
        command, key, value = parse_message(data)
        if command == 'STATS':
            response = handle_stats()
        elif command in ('GET', 'GETLIST', 'INCREMENT', 'DELETE'):
            response = COMMAND_HANDERS[command](key)
        elif command in (
                'PUT',
                'PUTLIST',
                'APPEND', ):
            response = COMMAND_HANDERS[command](key, value)
        else:
            response = (False, 'Unknown command type {}'.format(command))
        update_stats(command, response[0])
        connection.sandall('{};{}'.format(response[0], response[1]))
        connection.close()

if __name__ == '__main__':
    main()

我们创建了 COMMAND_HANDLERS, 它常被称为是一个 查找表 (look-up table) . COMMAND_HANDLERS 的工作是将命令与用于处理该命令的函数进行关联起来。 比如说, 如果我们收到一个 GET 命令, COMMAND_HANDLERS[command](key) 就等同于说 handle_get(key) . 记住,在 Python 中, 函数可以被认为是一个值,并且可以像其他任何值一样被存储在一个 dict中。

在上面的代码中, 虽然有些命令请求的参数相同,但是我仍决定分开处理每个命令。 尽管可以简单粗暴地强制所有的 handle_ 函数接受一个 key 和一个 value , 但是我希望这些处理函数条理能够更加有条理, 更加容易测试,同时减少出现错误的可能性。

注意 socket 相关的代码已是十分极简。 虽然整个服务器基于 TCP/IP 通信, 但是并没有太多底层的网络交互代码。

最后还须需要注意的一小点: DATA 字典, 因为这个点并不十分重要, 因而你很可能会遗漏它。 DATA 就是实际用来存储的 key-value pair, 正是它们实际构成了我们的数据库。

Command Parser

下面来看一些 命令解析器 (command parser) , 它负责解释接收到的消息:

def parse_message(data):
    """Return a tuple containing the command, the key, and (optionally) the
    value cast to the appropriate type."""
    command, key, value, value_type = data.strip().split(';')
    if value_type:
        if value_type == 'LIST':
            value = value.split(',')
        elif value_type == 'INT':
            value = int(value)
        else:
            value = str(value)
    else:
        value = None
    return command, key, value

这里我们可以看到发生了类型转换 (type conversion). 如果希望值是一个 list, 我们可以通过对 string 调用 str.split(',') 来得到我们想要的值。 对于 int, 我们可以简单地使用参数为 string 的 int() 即可。 对于字符串与 str() 也是同样的道理。

Command Handlers

下面是命令处理器 (command handler) 的代码. 它们都十分直观,易于理解。 注意到虽然有很多的错误检查, 但是也并不是面面俱到, 十分庞杂。 在你阅读的过程中,如果发现有任何错误请移步 这里 进行讨论.

def update_stats(command, success):
    """Update the STATS dict with info about if executing *command* was a
    *success*"""
    if success:
        STATS[command]['success'] += 1
    else:
        STATS[command]['error'] += 1

def handle_put(key, value):
    """Return a tuple containing True and the message to send back to the
    client."""
    DATA[key] = value
    return (True, 'key [{}] set to [{}]'.format(key, value))

def handle_get(key):
    """Return a tuple containing True if the key exists and the message to send
    back to the client"""
    if key not in DATA:
        return (False, 'Error: Key [{}] not found'.format(key))
    else:
        return (True, DATA[key])

def handle_putlist(key, value):
    """Return a tuple containing True if the command succeeded and the message
    to send back to the client."""
    return handle_put(key, value)

def handle_putlist(key, value):
    """Return a tuple containing True if the command succeeded and the message
    to send back to the client"""
    return handle_put(key, value)

def handle_getlist(key):
    """Return a tuple containing True if the key contained a list and the
    message to send back to the client."""
    return_value = exists, value = handle_get(key)
    if not exists:
        return return_value
    elif not isinstance(value, list):
        return (False, 'ERROR: Key [{}] contains non-list value ([{}])'.format(
            key, value))
    else:
        return return_value

def handle_increment(key):
    """Return a tuple containing True if the key's value could be incremented
    and the message to send back to the client."""
    return_value = exists, value = handle_get(key)
    if not exists:
        return return_value
    elif not isinstance(list_value, list):
        return (False, 'ERROR: Key [{}] contains non-list value ([{}])'.format(
            key, value))
    else:
        DATA[key].append(value)
        return (True, 'Key [{}] had value [{}] appended'.format(key, value))

def handle_delete(key):
    """Return a tuple containing True if the key could be deleted and the
    message to send back to the client."""
    if key not in DATA:
        return (
            False,
            'ERROR: Key [{}] not found and could not be deleted.'.format(key))
    else:
        del DATA[key]

def handle_stats():
    """Return a tuple containing True and the contents of the STATS dict."""
    return (True, str(STATS))

有两点需要注意: 多重赋值 (multiple assignment) 和代码重用. 有些函数仅仅是为了更加有逻辑性而对已有函数的简单包装而已, 比如 handle_get 和 handle_getlist . 由于我们有时仅仅是需要一个已有函数的返回值,而其他时候却需要检查该函数到底返回了什么内容, 这时候就会使用 多重赋值 。

來看 handle_append . 如果我們嘗試呼叫 handle_get 但是 key 不存在時, 那麼我們簡單地回傳 handle_get 所回傳的內容。 此外, 我們也希望能夠將 handle_get 傳回的 tuple 作為一個單獨的回傳值來引用。 那麼當 key 不存在的時候, 我們就可以簡單地使用 return return_value .

如果它 確實存在 ,那麼我們需要檢查該回傳值。並且, 我們也希望能夠將 handle_get 的回傳值作為單獨的變數來引用。 為了能夠處理上述兩種情況,同時考慮需要分開處理結果的情形,我們使用了多重賦值。 如此一來, 就不必書寫多行程式碼, 同時能夠保持程式碼清晰。 return_value = exists, list_value = handle_get(key) 能夠明確地表示我們將要以至少兩種不同的方式引用 handle_get 的回傳值。

How Is This a Database?

上面的程式顯然並非一個 RDBMS, 但卻絕對稱得上是一個 NoSQL 資料庫。它如此容易創建的原因是我們並沒有任何與 資料 (data) 的實際互動。 我們只是做了極簡的類型檢查,儲存用戶所發送的任何內容。 如果需要儲存更結構化的數據, 我們可能需要針對資料庫建立一個 schema 用於儲存和檢索資料。

既然 NoSQL 資料庫比較容易寫, 比較容易維護,比較容易實現, 那我們為什麼不是只使用 mongoDB 就好了? 當然是有原因的, 還是那句話,有得必有失, 我們需要在NoSQL 資料庫所提供的資料彈性(data flexibility) 基礎上權衡資料庫的可搜尋性(searchability).

# Querying Data

假如我們上面的NoSQL 資料庫來儲存早期的Car 資料。 那麼我們可能會使用VIN 作為key, 使用一個列表作為每列的值, 也就是說, 2134AFGER245267 = ['Lexus', 'RX350', 2013, Black] . 當然了, 我們已經丟掉了清單中每個索引的 涵義(meaning) . 我們只需要知道在某個地方索引1 儲存了汽車的Model , 索引2 儲存了Year.

#糟糕的事情來了, 當我們想要執行先前的查詢語句時會發生什麼事? 找到 1994 年所有車的顏色將會變得惡夢一般。 我們必須遍歷 DATA 中的 每一個值 來確認這個值是否儲存了car 資料亦或根本是其他不相關的數據, 例如檢查索引2, 看索引2 的值是否等於1994,接著再繼續取索引3 的值. 這比table scan 還要糟糕,因為它不僅要掃描每一行數據,還需要應用一些複雜的規則來回答查詢。

NoSQL 資料庫的作者當然也意識到了這些問題,(鑑於查詢是一個非常有用的 feature) 他們也想出了一些方法來使得查詢變得不那麼 「遙不可及」。一個方法是結構化所使用的數據,例如 JSON, 允許引用其他行來表示關係。 同時,大部分NoSQL 資料庫都有名字空間(namespace) 的概念, 單一類型的資料可以被儲存在資料庫中該類型所獨有的“section” 中,這使得查詢引擎能夠利用所要查詢資料的“shape”資訊.

當然了,儘管為了增強可查詢性已經存在(並且實現了)了一些更加複雜的方法, 但是在存儲更少量的schema 與增強可查詢性之間做出妥協始終是一個不可逃避的問題。 本範例中我們的資料庫僅支援透過 key 進行查詢。 如果我們需要支援更豐富的查詢, 那麼事情就會變得複雜的多了。

Summary

至此, 希望 「NoSQL」 這個概念已非常清晰。 我們學習了一點 SQL, 並且了解了 RDBMS 是如何運作的。 我們看到如何從一個RDBMS 檢索資料(使用SQL 查詢(query)). 透過搭建了一個玩具層級的NoSQL 資料庫, 了解了在可查詢性與簡潔性之間面臨的一些問題, 也討論了一些資料庫作者應對這些問題時所採用的一些方法。

#

以上是如何使用Python完成一個NoSQL資料庫的範例程式碼分享的詳細內容。更多資訊請關注PHP中文網其他相關文章!

陳述:
本文內容由網友自願投稿,版權歸原作者所有。本站不承擔相應的法律責任。如發現涉嫌抄襲或侵權的內容,請聯絡admin@php.cn