LeanCloud SDK不好用,Python手寫一個ORM 原

Intro

慣例,感覺寫了好用的東西就來寫個博客吹吹牛逼。

LeanCloud Storage 的數據模型不像是一般的 RDBMS,但有時候又很刻意地貼近那種感覺,所以用起來就很麻煩。

LeanCloud SDK 的缺陷

不管別人認不認可,這些問題在使用中我是體會到不爽了。

數據模型聲明

LeanCloud 提供的 Python SDK ,根據文檔描述來看,只有兩種簡單的模型聲明方式。

import leancloud

# 方式1
Todo = leancloud.Object.extend("Todo")
# 方式2
class Todo(leancloud.Object): pass

你說字段?字段隨便加啊,根本不檢查。看看例子。

todo = Todo()
todo.set('Helo', 'world') # oops. typo.

忽然就多了一個新字段,叫做Helo。當然,LeanCloud 提供了後臺設置,允許設置爲不自動添加字段,但是這樣有時候你確實想更新字段時——行,開後臺,輸入賬號密碼,用那個渲染40行元素就開始輕微卡頓的數據頁面吧。

鬼畜的查詢Api

是有點標題黨了,但講道理的說,我不覺得這個Api設計有多優雅。

來看個查詢例子,如果我們要查找叫做 Product 的,創建於 2018-8-12018-9-1 ,且 price 大於 10,小於100的元素。

leancloud.Query(cls_name)\
	.equal_to('name', 'Product')\
    .greater_than_or_equal_to('createdAt', datetime(2018,8,1))\
    .less_than_or_equal_to('createdAt', datetime(2018,9,1))\
    .greater_than_or_equal_to('price', 10)\
    .less_than_or_equal_to('price',100)\
    .find()

第一眼看過去,閱讀全文並背誦?

隱藏於文檔中的行爲

典型的就是那個查詢結果是有限的,最高1000個結果,默認100個結果。在Api中完全無法察覺——find嘛,查出來的不是全部結果?你至少給個分頁對象吧,說好的代碼即文檔呢。

幸運的是至少在文檔裏寫了,雖然也就一句話。

行爲和預期不符

以一個簡單的例子來說,如果你查找一個對象,查找不到怎麼辦?

返回個空指針,返回個None啊。

LeanCloud SDK 很機智地丟了個異常出來,而且各種不同類型的錯誤都是這個 LeanCloudError 異常,裏面包含了codeerror來描述錯誤信息。

針對於存儲個人糊出來的解決方案

我就硬廣了,不過這個東西還在施工中,寫下來才一天肯定各種不到位,別在意。

better-leancloud-storage-python

簡單的說,針對於上面提到的痛點做了一些微小的工作。

微小的工作

直接看例子。

class MyModel(Model):
	__lc_cls__ = 'LeanCloudClass'
	field1 = Field()
    field2 = Field()
    field3 = Field('RealFieldName')
    field4 = Field(nullable=False)
MyModel.create(field4='123') # 缺少 field4 會拋出 KeyError 異常
MyModel.query().filter_by(field1="123").filter(MyModel.field1 < 10)

__lc_cls__是一個用於映射到 LeanCloud 實際儲存的 Class 名字的字段,當然如果不設置的話,就像 sqlalchemy 一樣,類名 MyModel 就會自動成爲這個字段的值。

create 接受任意數量關鍵字參數,但如果關鍵字參數沒有覆蓋所有的nullable=False的字段,則會立即拋出KeyError異常。

filter_by接受任意數量關鍵字參數,如果關鍵字不存在於Model聲明則立即報錯。api 和 sqlalchemy 很像,filter_by(field1='123')比起寫 equal_to('field1', '123')是不是更清晰一些?特別是條件較多的情況下,優勢會越發明顯,至少,不至於背課文了。

實現方式分析

裝逼之後就是揭露背後沒什麼技術含量的技巧的時間。

簡單易懂的元類魔法

python 的元類很好用,特別是你需要對類本身進行處理的時候。

對於數據模型來說,我們需要收集的東西有當前類的所有字段名,超類(父類)的字段名,然後整合到一起。

做法簡單易懂。

收集字段

首先是遍歷嘛,遍歷找出所有的字段,isinstance就好了。

class ModelMeta(type):
    """
    ModelMeta
    metaclass of all lean cloud storage models.
    it fill field property, collect model information and make more function work.
    """
    _fields_key = '__fields__'
    _lc_cls_key = '__lc_cls__'

    @classmethod
    def merge_parent_fields(mcs, bases):
        fields = {}

        for bcs in bases:
            fields.update(deepcopy(getattr(bcs, mcs._fields_key, {})))

        return fields
    
    def __new__(mcs, name, bases, attr):
        # merge super classes fields into __fields__ dictionary.
        fields = attr.get(mcs._fields_key, {})
        fields.update(mcs.merge_parent_fields(bases))

        # Insert fields into __fields__ dictionary.
        # It will replace super classes same named fields.
        for key, val in attr.items():
            if isinstance(val, Field):
                fields[key] = val

        attr[mcs._fields_key] = fields

思路就是一條直線,什麼架構、最佳實踐都滾一邊,用粗大的腦神經和頭鐵撞過去就是了。

第一步拿出所有基類,找出裏面已經創建好的__fields__,然後合併起來。

第二步遍歷一下本類的成員(這裏可以直接用{... for ... in filter(...)}不過我沒想起來),找出所有的字段成員。

第三步?合併起來,一個update就完事兒了,賦值回去,大功告成。

字段名的默認值

還沒完事兒,字段名怎麼映射到 LeanCloud 存儲的 字段上?

直接看代碼。

    @classmethod
    def tag_all_fields(mcs, model, fields):
        for key, val in fields.items():
            val._cls_name = model.__lc_cls__
            val._model = model

            # if field unnamed, set default name as python class declared member name.
            if val.field_name is None:
                val._field_name = key
	
    def __new__(mcs, name, bases, attr):
    	# 前略
		# Tag fields with created model class and its __lc_cls__.
        created = type.__new__(mcs, name, bases, attr)
        mcs.tag_all_fields(created, created.__fields__)
        return created

就在那個tag_all_fields裏面,val._field_name賦值完事兒。不要在乎那個field_name_field_name,一個是包了一層的只讀getter,一個是原始值,僅此而已。爲了統一也許後面也改掉。

苦力活

有了元數據,接下來的就是苦力活了。

create怎麼檢查是不是滿足所有非空?參數的鍵和非空的鍵做個集合,非空鍵如果不是參數鍵的子集也不等同則不滿足。

filter_by同理。

構建查詢也不困難,大家都知道a<b可以重載__lt__來返回個比較器之類的東西。

慢着,怎麼讓一個實例,用instance.a訪問到的內容和model.a訪問到的內容不一樣?是在init、new方法裏做個魔術嗎?

實例訪問字段值

說穿了也沒什麼特別的,在實例裏面用實際字段值覆蓋重名元素很簡單,self.field = self.delegated_object.get('field')也就一句話的事情,多少不過是 setattrgetattr的混合使用罷了。

不過我用的是重載 __getattribute____setattr__的方法,同樣不是什麼難理解的東西。

__getattribute__會在所有的實例成員訪問之前調用,用這個方法可以攔截掉所有instance.field形式的對field的訪問。所以說python是個基於字典的語言一點也不玩笑(開玩笑的)。

看代碼。

    def __getattribute__(self, item):
        ret = super(Model, self).__getattribute__(item)
        if isinstance(ret, Field):
            field_name = self._get_real_field_name(item)

            if field_name is None:
                raise AttributeError('Internal Error, Field not register correctly.')

            return self._lc_obj.get(field_name)

        return ret

需要特別注意的點是,因爲在__getattribute__裏訪問成員也會調用到自身,所以注意樹立明確的調用分界線:在分界線外,所有成員值訪問都會造成無限遞歸爆棧,分界線內則不會。

對於我寫的這段來說,分界線是那個 if isinstance(...)。在if之外必須使用super(...).__getattribute__(...)來訪問其他成員。

至於 __setattr__更沒什麼好說的了。看看是不是模型的字段,然後轉移一下賦值的目標就是了。

看代碼。

    def __setattr__(self, key, value):
        field_name = self._get_real_field_name(key)
        if field_name is None:
            return super(Model, self).__setattr__(key, value)

        self._lc_obj.set(field_name, value)

so simple!

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章