惯例,感受写了好用的东西就来写个博客吹吹牛逼。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!