今天是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='test@orm.org', 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 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就更簡單了。按照規范,我們需要實現增刪改查四個函數,但是這里我們只是為了展示,所以就只實現其中一個作為例子,其他幾個都可以如法炮制。
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。相當于元類被隱形地繼承了下來,但是我們在使用子類的時候卻感知不到。
對于框架的使用者來說,也的確不需要了解框架內部的實現機制,只需要明白使用方法,照著使用就行了。雖然元類的實現和理解很復雜,但是使用起來卻很簡單,這也是它的一個顯著特點。
最后,本文的代碼示例源于廖雪峰老師的博客,向廖雪峰老師致敬。想要查看廖老師博客原文的,請點擊查看原文。