LeanCloud SDK很差用,Python手寫一個ORM

Intro

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

LeanCloud Storage 的數據模型不像是通常的 RDBMS,但有時候又很刻意地貼近那種感受,因此用起來就很麻煩。git

LeanCloud SDK 的缺陷

無論別人認不承認,這些問題在使用中我是體會到不爽了。github

數據模型聲明

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

import leancloud

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

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

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

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

鬼畜的查詢Api

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

來看個查詢例子,若是咱們要查找叫作 Product 的,建立於 2018-8-12018-9-1 ,且 price 大於 10,小於100的元素。oop

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!

相關文章
相關標籤/搜索