LeanCloud SDK is not useful, Python handwritten an ORM

Keywords: Python SDK less

Intro

As a rule, when you feel like you've written something useful, write a blog and brag about it.

LeanCloud Storage's data model is not like a regular RDBMS, but sometimes it's deliberately close to that feeling, so it can be cumbersome to use.

Defects of LeanCloud SDK

No matter what other people admit or disapprove, I feel uncomfortable using these problems.

Data model declaration

The Python SDK provided by LeanCloud has only two simple model declarations, according to the documentation.

import leancloud

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

What do you mean by field?Fields are added freely, not checked at all.Take an example.

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

Suddenly there's a new field called Helo.LeanCloud, of course, provides background settings that allow fields to be added without being automatically, but sometimes you do want to update a field when you really want to - row, go back, enter an account password, and start a slightly cartoon data page with that 40-line element.

Query Api for ghosts and livestock

It's a bit of a headline party, but to be reasonable, I don't think the Api design is much elegant.

Let's look at an example of a query, if we're looking for an element called Product, created between 8-8-1 and 2018-9-1 with a price greater than 10 and less than 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()

At first glance, read the full text and recite it?

Behaviors Hidden in Documents

Typically, that query has a limited number of results, up to 1,000, with a default of 100.It's completely invisible in Api - find, not all the results?You should give at least one paging object. The said code is the document.

Fortunately, it was written in at least one sentence in the document.

Behavior and expectations do not match

For a simple example, what if you look for an object and you can't find it?

Return a null pointer, return a None.

LeanCloud SDK intelligently dropped an exception, and the various types of errors are this LeanCloud Error exception, which contains code s and errors to describe the error information.

Solutions for storing personal pastes

I'm hard and broad, but this thing is still under construction, so don't worry if you write it down for a day and it's sure that all kinds of things are not in place.

better-leancloud-storage-python

Simply put, do a little bit of work on the pain points mentioned above.

Minor jobs

Look directly at the example.

class MyModel(Model):
	__lc_cls__ = 'LeanCloudClass'
	field1 = Field()
    field2 = Field()
    field3 = Field('RealFieldName')
    field4 = Field(nullable=False)
MyModel.create(field4='123') # Missing field4 throws a KeyError exception
MyModel.query().filter_by(field1="123").filter(MyModel.field1 < 10)

_u lc_cls_ is a field used to map to the name of lass actually stored in LeanCloud. If not set, of course, like sqlalchemy, the Class name MyModel will automatically become the value of this field.

create accepts any number of keyword parameters, but throws a KeyError exception immediately if the keyword parameter does not cover all nullable=False fields.

Filter_by accepts any number of keyword arguments and immediately errors if the keyword does not exist in the Model declaration.Like sqlalchemy, is filter_by(field1='123') clearer than equal_to ('field1','123')?Especially in the case of more conditions, the advantages will be more obvious, at least not as good as reciting the text.

Implementation Analysis

It's time to expose techniques that don't have any technical behind them.

Easy to understand metamagic

python metaclasses are useful, especially when you need to work with the class itself.

For data models, what we need to collect is all the field names of the current class, the field names of the superclass (parent class), and then put them together.

It is easy to understand.

Collect Fields

First of all, traverse to find all the fields, isinstance is good.

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

The idea is a straight line. Every structure and best practice rolls aside and bumps into it with a big brain nerve and a head iron.

The first step is to take out all the base classes, find the u fields_u that have been created inside, and merge them.

The second step iterates through the members of this class (you can use {... for... In filter (...)} directly here, but I don't remember) to find all the field members.

Step 3?When combined, an update is done, assigns values back, and is done.

Default value for field name

How do the field names map to LeanCloud stored fields?

Look directly at the code.

    @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):
    	# Previous
		# 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

Inside that tag_all_fields, the val._field_name assignment is complete.Don't care about the field_name and_field_name, one is a read-only getter wrapped in one layer, the other is the original value, that's all.Maybe it will be changed later for the sake of uniformity.

Hard work

With metadata, the next step is to work hard.

How does create check if all non-empty items are satisfied?Parameter keys and non-empty keys are a set, and non-empty keys are not satisfied unless they are not a subset of the parameter keys.

The same is true for filter_by.

It's also not difficult to build a query, as a<b is known to overload u lt_ to return something like a comparator.

Slow down, how can I make an instance that accesses something different with instance.a than model.a?Do you do a magic in the init, new method?

Instance Access Field Value

It's nothing special to say that overwriting a duplicate element with the actual field value in the instance is very simple; self.field = self.delegated_object.get('field') is a one-sentence thing, more or less a mix of setattr and getattr.

But I'm using overloaded u getattribute_u and u setattr_u methods, which are also not difficult to understand.

_u getattribute_u is called before all instance members can access it, and this method can block access to the field in the form of all instance.field.So it's not a joke to say that python is a dictionary-based language.

Look at the code.

    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

It is important to note that since access to a member in u getattribute_ also calls itself, be careful to establish a clear call boundary: outside the boundary, all member value access will result in an infinite recursive burst stack, but not within the boundary.

For this passage I write, the dividing line is that if isinstance(...).You must use super (...). u getattribute_ (...) to access other members outside of if.

There is nothing more to say about u setattr_.See if it's a field of the model, and then move on to the target of the assignment.

Look at the code.

    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!

Posted by earthlingzed on Tue, 07 May 2019 08:50:38 -0700