tko22/flask-boilerplate

Make `to_dict` recursive

tko22 opened this issue · 5 comments

tko22 commented

the to_dict function in the class Mixin is used as a helper function in SQLAlchemy ORM models for converting the objects into python data structures for easy json serialization. However, it isn't recursive.

For example, let's have a Person and Email table. Person has name and emails as fields while Email has total_mail, email_addr, person. The emails field is a one-to-many relationship to entries in Email table. The following happens when I run to_dict() function:

person = Person.query.all()[0] # query and get the first person
person.to_dict()
# This outputs
{
    name: "tim"
}
# but it doesn't show `emails`, which should be an array of emails even if it exists
# desired output...
{
   name: "tim",
   emails: [
       {
             "total_mail": 100,
             "email_addr": "tim@email.com"
        },
        {
            "total_mail": 20,
            "email_addr": "tim2@gmail.com"
        }
   ]
}

Hi, I like you project and the way you organize your code !
Since few weeks I'm learning back-end. And few days ago I have done a recursive SQL alchemy to_dict.
But i did this directly by creating my own model instead of creating a Mixin class, so my function it might require modifications.

So i have init my flask app like this :

from flask_sqlalchemy import SQLAlchemy
from backend.MyModel import MyModel
app = Flask( #...)
#...
db = SQLAlchemy(app, model_class=MyModel)

For the next days i ll try to handle your boilerplate, start to code REST api, then should have time to adapt my recursive function (unless you have already done it :)

class MyModel(object):
    __abstract__ = True
    query_class = None
    query = None
    max_depth_recursion = 2
    DONOTSEND_MODEL = {'_sa_instance_state'}
    DONOTSEND = []
    _jsonified = None
    modified = False

    def __repr__(self):
        return '<{}>'.format(self.__class__.__name__)

    @property
    def jsonify(self):
        # if self._jsonified is None: # need using events
        self._jsonified = self._to_dict_recursive(max_depth=1, depth=0, starter_obj=self)
        return self._jsonified

    @property
    def to_dict_recursive(self, max_depth=None):
        if max_depth is None:
            max_depth = self.max_depth_recursion
        return self._to_dict_recursive(max_depth=max_depth, depth=0, starter_obj=self)

    def _to_dict_recursive(self, max_depth, depth, starter_obj):
        depth += 1  # recursive depth

        # functions
        def anti_circular_recursion(attr):
            if isinstance(starter_obj, attr.__class__) or depth >= max_depth:
                # both choice possible
                # return '<'+attr.__class__.__name__+'>'
                return str(attr)
            else:
                return attr._to_dict_recursive(max_depth, depth, starter_obj)

        result = {}

        # __mapper__ is equivalent to db.inspect(self)
        # but db (database) is not created yet cause we send this model to the constructor
        for key in self.__mapper__.attrs.keys():
            if key not in self.DONOTSEND:
                attr = getattr(self, key)
                if issubclass(type(attr), MyModel):
                    result[key] = anti_circular_recursion(attr)
                elif isinstance(attr, list):
                    value = []
                    for obj in attr:
                        value.append(anti_circular_recursion(obj))
                    result[key] = value
                # else : attr is not an instance of relationship (int, str..)
                else:
                    result[key] = attr

        return result

Yeah that's kind of messy ^^"

edit : then how i use my model (by the way, you should have notice that i used max deepth against circular relationships) :

from backend import db
class User(db.Model):
    __tablename__ = 'user'
    id = db.Column(db.Integer, primary_key=True)
    email = db.Column(db.String(254), nullable=False, unique=True)
    # pseudo = db.Column(db.String(50), nullable=False, unique=True)
    prenom = db.Column(db.String(50))
    nom = db.Column(db.String(50))
    likes = db.Column(db.Integer, default=0)
    password_hash = db.Column(db.String(350))
    DONOTSEND = ['password_hash']

edit 2 : This the result of the recursive with deepth=5, here you can see the circurlar issue (only handled for the object which call the recursive function)
result_recursive

tko22 commented

that's very cool! I haven't done this so go for it. For _init_.py, I originally had initialized the SQLAlchemy instance there but realized there could be circular imports and thus I moved it in the models module.

For the to_dict() function, let's have a parameter depth and have a default depth of 2 or 1 (I'm not sure which one would be better). This should help with the performance, especially since it is indeed recursive.

Yeah I like the place where you initialize the db instance, but i think we should create the model/mixin in the same file (base.py) or at least the same package (directory).

For the depth, i had a way to stop circular recursive relationship, but i was worried about performances. An other possibility would be to have 2 methods :

  • one with a complex and gluttonous algorithm, that stop every circular relationship recursion
  • one with a simple and light algorithm, that need a depth as parameter or default value

But the problem with depth parameter (as you can see on the screenshot i have shot) is that you may send too much data to a client.

That's why I'm actually learning about the cleanest way to create API. And with a REST API, we would have no need to send a recursive block of data, but only first depth with id-s of children. So a recursive block should be used only for back-end treatment. This point could seem obvious for you, but i think it is import to have in focus the final goal.

As soon I'll have time to spend on it, I'll insert the function to your project. I think I'll create the different options to facilitate the choice.
Unfortunately, I have no experience on GitHub (and branch management), I ll need your advices to not screw up everything or even put a mess there ><.

Finally I did a lot of modifications on my fork. I invite you to see my abstraction, you might find few interesting concepts.

At least I would like to have your opinion on it !

tko22 commented

completed with #22. Closing