tortoise / tortoise-orm

Familiar asyncio ORM for python, built with relations in mind
https://tortoise.github.io
Apache License 2.0
4.62k stars 383 forks source link

QuerySet is not working properly in a Jinja2 for loop #161

Closed mojimi closed 5 years ago

mojimi commented 5 years ago

Sorry if this is not the right place to ask, but there wasn't even a tortoise-orm tag on stackoverflow.

I can't figure out what's the right syntax to prefetch related data from relationships as I don't want the Jinja template to execute any databate queries

I'm doing the following (In Quart, which is like Flask but async)

    @app.route("/projects")
    async def user_home():
        user = await User.get(email=session['email'])
        await user.fetch_related('roles')

        for role in user.roles:
            await role.fetch_related('project')

        print(user.roles[0].project.name) #Just a test, but this works here

        return await render_template("projects.html", user=user)

And in project.html :

        {% for role in user.roles %}
        <tr>
            <td>{{role.project.name}}</td>
            <td>{{role.name}}</td>
        </tr>
        {% endfor %}

role.project.name is blank, but role.name is working fine.

And here are the models :

    class User(Model):
        email = fields.CharField(255,unique=True)
        roles = fields.ManyToManyField('models.Role', related_name='users')

    class Role(Model, TimestampMixin):
        name = fields.CharField(100)
        project = fields.ForeignKeyField('models.Project', related_name='roles')

    class Project(Model, TimestampMixin):
        name = fields.CharField(100)

What am I missing here? I don't understand why it works on the route function but not on the Jinja template

abondar commented 5 years ago

Hi

Could you try fixing your example to be like this?

    @app.route("/projects")
    async def user_home():
        user = await User.all().prefetch_related('roles__project').get()

        print(user.roles[0].project.name) #Just a test, but this works here

        return await render_template("projects.html", user=user)

That should at least speed up your code by quite a bit, and may be can fix your issue 😄

If it won't work - I would recommend trying serialising model instances to dict before passing to template. There could be some incompatibilities between tortoise models and expected value for template context

mojimi commented 5 years ago

@abondar Hey thanks for the quick reply.

I tried the following but now I'm getting a different error before going to Jinja template 'ForeignKeyField' object has no attribute 'name'

user = await User.get(email=session['email']).prefetch_related('roles__project').get()
print(user.roles[0].project.name)
mojimi commented 5 years ago

It really is weird.

I called get because it was on your code 😄 , to be honest I'm still learning how this works. But fixing the typo still gave the same error.

Let me try to give more info :

I want a specific user, all its roles and the project of each role.

The error is weird, sounds like it wasn't instantiated properly, maybe my creation code is wrong?

Here is how I create and assign a role/project to an user :

class Project(Model, TimestampMixin):
    name = fields.CharField(100)

    @classmethod
    async def new(cls, name, user):
        #Creating the project
        project = cls()
        project.name = name
        await project.save()

        #Creating default admin role
        role = Role()
        role.name = 'Admin'
        role.project_id = project.id

        await role.save()
        await role.users.add(user)

        return project
mojimi commented 5 years ago

Even printing inside the jinja template works, I still haven't found a solution unfortunately.

I think it could be related to how the for loop in jinja is done.

Edit: It's definitely a mix on how the queryset implemented iterable and how jinja accesses iterables, the following worked :

{% for i in range(user.roles|length) %}
{% set role = user.roles[i] %}
<tr>
    <td>{{role.project.name}}</td>
    <td>{{role.name}}</td>
</tr>
{% endfor %}

For now this is the fix I'm using but its far from ideal

abondar commented 5 years ago

Did you try serialising model instances to dicts before passing them as context? I think it should work fine, and using serialisation is quite good pattern in general, because it let's you separate data formatting from data storing

mojimi commented 5 years ago

Did you try serialising model instances to dicts before passing them as context? I think it should work fine, and using serialisation is quite good pattern in general, because it let's you separate data formatting from data storing

Tortoise-orm models have a serialize method? I couldn't find anything about it in the docs

abondar commented 5 years ago

Sorry, may be I was misleading. I meant some thrid-party serialisers. I, personally, prefer to use https://github.com/marshmallow-code/marshmallow - it's very convenient in use and allows you to declare your desired formatting in code

mojimi commented 5 years ago

Sorry, may be I was misleading. I meant some thrid-party serialisers. I, personally, prefer to use https://github.com/marshmallow-code/marshmallow - it's very convenient in use and allows you to declare your desired formatting in code

Never head of marshmallow before but it seems pretty easy to use.

I feel that it adds some redundant processing to use it in an orm though, hopefully tortoise-orm can have its own serializer in the future

abondar commented 5 years ago

We have issue for that #13 , but I don't think that we will be able to attempt it in any near future, because there so much issues that we consider more important

grigi commented 5 years ago

We have a framework almost in place for clean schema extraction.

Then it should be easier to do so properly