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-1
至 2018-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
異常,裡面包含了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!