1. 程式人生 > >Python元類實戰,通過元類實現資料庫ORM框架

Python元類實戰,通過元類實現資料庫ORM框架

本文始發於個人公眾號:**TechFlow**,原創不易,求個關注

今天是Python專題的第19篇文章,我們一起來用元類實現一個簡易的ORM資料庫框架。

本文主要是受到了廖雪峰老師Python3入門教程的啟發,不過廖老師的部落格有些精簡,一些小白可能看起來比較吃力。我在他的基礎上做了一些補充和註釋,儘量寫得淺顯一些。

ORM框架是什麼

如果是沒有做過後端的小夥伴上來估計會有點蒙,這個ORM框架究竟是什麼?ORM框架是後端工程師常用的一個框架,它的英文全稱是Object Relational Mapping,即物件-關係對映框架。顧名思義就是把關係轉化成物件的框架,關係這個詞我們在哪裡用的最多呢?

顯然應該是資料庫。之前我們在分散式的文章介紹關係型資料庫和非關係型資料庫的時候就著重介紹過關係的含義。我們常用的MySQL就是經典的關係型資料庫,它儲存的形式是表,但是表承載的資料其實是兩個實體之間的"關係"。比如學生上課這個場景,學生和課程是兩個主體(entity),我們要記錄的是這兩個主體之間的關係,也就是學生上課這件事。

而ORM框架做的事情是將這些關係對映成類,這樣我們可以將這張表當中增刪改查的功能抽象成類當中的方法。這樣我們就可以通過呼叫類的方式來操作資料庫了,從而達到高度抽象業務邏輯、降低使用者使用難度的目的。

比如Java後端工程師常用的hibernate和ibatis都是用來做這件事情的,明確了框架的功能之後,我們先來設想一下最後的成果。假設我們現在開發出來了這麼一套框架,那麼它用起來的感覺應該是怎樣的?

我們來看下廖老師部落格裡給的例子:

class User(Model):
    # 定義類的屬性到列的對映:
    id = IntegerField('id')
    name = StringField('username')
    email = StringField('email')
    password = StringField('password')

User類代表了資料庫當中的一張表,它有4個欄位:id, name, email和password,我們在定義欄位的同時也通過類別指定了它們的型別。這個應該不難理解,上面的這個類等價於我們在資料庫當中執行了這麼一段建表的SQL:

create table if not exists user (
 id int,
    name string,
    email string,
    password string
)

我們定義了表字段之後,接下來要做的就是根據欄位建立資料了,其實也就是根據類建立例項。我們希望User型別的例項就對應User表當中的一條記錄,並且我們可以通過呼叫例項當中的方法,來操作這張表進行增刪改查。

# 建立一個例項:
u = User(id=12345, name='Michael', email='[email protected]', password='my-pwd')
# 儲存到資料庫:
u.save()

那麼,我們怎樣可以實現這樣的功能呢?

功能實現

我們先從簡單的功能開始實現,首先是Field類,Field類表示資料庫表當中一個欄位的型別。這裡的邏輯很容易理清楚,我們需要定義多種型別,比如IntegerField和StringField。我們可以對這些field類抽象出一個父類來:

class Field(object):
    def __init__(self, name, column_type):
        self.name = name
        self.column_type = column_type
        
    def __str__(self):
        return '<{}:{}>'.format(self.__class__.__name__, self.name)

__str__方法當中打印出來的兩個欄位,分別是類別的名稱和欄位的名稱,這段程式碼應該不難理解。

接著,我們實現它的兩個子類,分別是IntegerField和StringField:

class StringField(Field):
    def __init__(self, name):
        super(StringField, self).__init__(name, 'varchar(100)')
        
        
class IntegerField(Field):
    def __init__(self, name):
        super(IntegerField, self).__init__(name, 'bigint')

這裡也不難理解,只是一個簡單的繼承應用而已。

接下來就到了最關鍵的部分,也就是Model類的實現。我們先來分析一下我們希望Model這個類擁有的功能,由於它是我們定義出來的每一張表的父類,所以它應該能夠獲取子類當中的欄位,並且將它存放在一個容器當中。由於我們需要儲存的是欄位名和型別的對映,所以將它儲存在dict當中比較合理。

另外一個功能是我們希望它能夠提供增刪改查的介面,能夠根據子類當中定義的欄位自動生成相應的SQL語句去呼叫資料庫。這個也是ORM框架的意義所在。

第二個功能容易實現,只要第一個功能搞定了,做一下字串處理即可。但是第一個功能有些麻煩,它也是元類的意義所在。因為父類當中的方法是無法獲取子類中定義的類屬性的,只能通過元類,在構建類的時候可以拿到屬性的資訊。

所以我們已經很明確了,我們實現元類的目的就是為了實現這個功能。理清楚了之後,再來寫程式碼就不難了。我們先來實現這個元類:

class ModelMetaclass(type):

    def __new__(cls, name, bases, attrs):
        # 建立model類的時候不做任何處理
        if name=='Model':
            return type.__new__(cls, name, bases, attrs)
        # 打印表名的資訊
        print('Found model: %s' % name)
        # mappings用來儲存欄位的資訊
        mappings = dict()
        for k, v in attrs.items():
            # 判斷v的型別,只有是Field的子類才會儲存起來
            if isinstance(v, Field):
                print('Found mapping: %s ==> %s' % (k, v))
                mappings[k] = v
        # 將mappings當中的資料從類屬性當中移除,防止關鍵字衝突
        for k in mappings.keys():
            attrs.pop(k)
        attrs['__mappings__'] = mappings # 儲存屬性和列的對映關係
        attrs['__table__'] = name # 假設表名和類名一致
        return type.__new__(cls, name, bases, attrs)

如果你看過之前的文章,對元類已經很熟悉了,那麼這段程式碼對你來說應該不難理解。元類搞定了,剩下的Model就更簡單了。按照規範,我們需要實現增刪改查四個函式,但是這裡我們只是為了展示,所以就只實現其中一個作為例子,其他幾個都可以如法炮製。

class Model(dict, metaclass=ModelMetaclass):
    def __init__(self, **kw):
        # 由於Model的基類是dict,所以創造Model的欄位會被解析成dict的構造引數
        # 也就是說欄位名和欄位值的對映會儲存在dict當中
        super(Model, self).__init__(**kw)
        
    def __getattr__(self, key):
        try:
            return self[key]
        except KeyError:
            raise AttributeError(r"'Model' object has no attribute '%s'" % key)

    def __setattr__(self, key, value):
        self[key] = value

    def save(self):
        fields = []
        params = []
        args = []
        for k, v in self.__mappings__.items():
            # fields儲存欄位名
            fields.append(v.name)
            # params填充問號
            params.append('?')
            # 獲取欄位的值
            args.append(getattr(self, k, None))
        sql = 'insert into %s (%s) values (%s)' % (self.__table__, ','.join(fields), ','.join(params))
        print('SQL: %s' % sql)
        print('ARGS: %s' % str(args))

Model當中的save方法不難看懂,但是前面的幾個方法看起來有些多餘。但實際上它們也很重要,這裡有一個關鍵資訊是Model類的父類是dict,我們在構建Model的時候傳入的引數會被用來初始化一個dict。所以我們建立資料例項的時候資料的名稱和資料值的對映會被儲存在dict當中,所以我們在save方法當中才會從self的attr當中獲取欄位的值。並且我們在初始化User的時候,也必須要填寫每個欄位的名稱,原因就在這裡。

最後我們來執行一下:

從結果上來看,我們輸出了User這個類的插入SQL以及它的欄位的值。只需要連結一下資料庫,我們的這個ORM框架就可以真正投入使用了。

總結

在整個ORM框架實現的過程當中,最重要的是我們對Model這個類建立了元類,但是真正應用的地方卻是在Model的子類。實際上在實際建立User類的時候,直譯器會先搜尋User內部是否定義了元類,如果沒有,會上一層去往User的父類也就是Model類搜尋元類,如果找到了元類,就會使用元類來建立User。相當於元類被隱形地繼承了下來,但是我們在使用子類的時候卻感知不到。

對於框架的使用者來說,也的確不需要了解框架內部的實現機制,只需要明白使用方法,照著使用就行了。雖然元類的實現和理解很複雜,但是使用起來卻很簡單,這也是它的一個顯著特點。

最後,本文的程式碼示例源於廖雪峰老師的部落格,向廖雪峰老師致敬。想要檢視廖老師部落格原文的,請點選檢視原文。

如果喜歡本文,可以的話,請點個關注,給我一點鼓勵,也方便獲取更多文章。

本文使用 mdnice 排版

![](https://user-gold-cdn.xitu.io/2020/6/26/172ee957c2e8e095?w=258&h=258&f=png&