慣例,感受寫了好用的東西就來寫個博客吹吹牛逼。python
LeanCloud Storage 的數據模型不像是通常的 RDBMS,但有時候又很刻意地貼近那種感受,因此用起來就很麻煩。git
無論別人認不承認,這些問題在使用中我是體會到不爽了。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設計有多優雅。less
來看個查詢例子,若是咱們要查找叫作 Product
的,建立於 2018-8-1
至 2018-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
異常,裏面包含了code
和error
來描述錯誤信息。
我就硬廣了,不過這個東西還在施工中,寫下來才一天確定各類不到位,別在乎。
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')
也就一句話的事情,多少不過是 setattr
和getattr
的混合使用罷了。
不過我用的是重載 __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!