LeanCloud SDK不好用,Python手写一个ORM

Stella981
• 阅读 718

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!

点赞
收藏
评论区
推荐文章
blmius blmius
3年前
MySQL:[Err] 1292 - Incorrect datetime value: ‘0000-00-00 00:00:00‘ for column ‘CREATE_TIME‘ at row 1
文章目录问题用navicat导入数据时,报错:原因这是因为当前的MySQL不支持datetime为0的情况。解决修改sql\mode:sql\mode:SQLMode定义了MySQL应支持的SQL语法、数据校验等,这样可以更容易地在不同的环境中使用MySQL。全局s
皕杰报表之UUID
​在我们用皕杰报表工具设计填报报表时,如何在新增行里自动增加id呢?能新增整数排序id吗?目前可以在新增行里自动增加id,但只能用uuid函数增加UUID编码,不能新增整数排序id。uuid函数说明:获取一个UUID,可以在填报表中用来创建数据ID语法:uuid()或uuid(sep)参数说明:sep布尔值,生成的uuid中是否包含分隔符'',缺省为
待兔 待兔
5个月前
手写Java HashMap源码
HashMap的使用教程HashMap的使用教程HashMap的使用教程HashMap的使用教程HashMap的使用教程22
Jacquelyn38 Jacquelyn38
3年前
2020年前端实用代码段,为你的工作保驾护航
有空的时候,自己总结了几个代码段,在开发中也经常使用,谢谢。1、使用解构获取json数据let jsonData  id: 1,status: "OK",data: 'a', 'b';let  id, status, data: number   jsonData;console.log(id, status, number )
Wesley13 Wesley13
3年前
mysql设置时区
mysql设置时区mysql\_query("SETtime\_zone'8:00'")ordie('时区设置失败,请联系管理员!');中国在东8区所以加8方法二:selectcount(user\_id)asdevice,CONVERT\_TZ(FROM\_UNIXTIME(reg\_time),'08:00','0
Wesley13 Wesley13
3年前
00:Java简单了解
浅谈Java之概述Java是SUN(StanfordUniversityNetwork),斯坦福大学网络公司)1995年推出的一门高级编程语言。Java是一种面向Internet的编程语言。随着Java技术在web方面的不断成熟,已经成为Web应用程序的首选开发语言。Java是简单易学,完全面向对象,安全可靠,与平台无关的编程语言。
Stella981 Stella981
3年前
Django中Admin中的一些参数配置
设置在列表中显示的字段,id为django模型默认的主键list_display('id','name','sex','profession','email','qq','phone','status','create_time')设置在列表可编辑字段list_editable
Wesley13 Wesley13
3年前
MySQL部分从库上面因为大量的临时表tmp_table造成慢查询
背景描述Time:20190124T00:08:14.70572408:00User@Host:@Id:Schema:sentrymetaLast_errno:0Killed:0Query_time:0.315758Lock_
为什么mysql不推荐使用雪花ID作为主键
作者:毛辰飞背景在mysql中设计表的时候,mysql官方推荐不要使用uuid或者不连续不重复的雪花id(long形且唯一),而是推荐连续自增的主键id,官方的推荐是auto_increment,那么为什么不建议采用uuid,使用uuid究
Python进阶者 Python进阶者
11个月前
Excel中这日期老是出来00:00:00,怎么用Pandas把这个去除
大家好,我是皮皮。一、前言前几天在Python白银交流群【上海新年人】问了一个Pandas数据筛选的问题。问题如下:这日期老是出来00:00:00,怎么把这个去除。二、实现过程后来【论草莓如何成为冻干莓】给了一个思路和代码如下:pd.toexcel之前把这