Asynchronous ORM

Honestly speaking, trailing _async, leading a, awaited all look non-native to Django. This will always feel like an extra option which was imported to Django forcefully. I think using get_running_loop is still a better option given that we don’t have to mention await twice. Maybe we can try to fix the issues that it has or try finding another reliable alternative. Just my 2 cents! Thanks everyone for continuing the discussion on this.

1 Like

Has anyone from the Django team taken a look at the Emmett ORM? Might be a worthwhile design inspiration. Seems like it’s fully async and some metrics show that the Emmett framework is 7x Django at HTTP requests.

I’ve been using this mixin I created for quite a while already:

from asgiref.sync import sync_to_async
from django.db import models


class AsyncManager(models.Manager):

    aget_func = sync_to_async(models.Manager.get)
    aget_or_create_func = sync_to_async(models.Manager.get_or_create)
    acount_func = sync_to_async(models.Manager.count)
    acreate_func = sync_to_async(models.Manager.create)
    alast_func = sync_to_async(models.Manager.last)
    alatest_func = sync_to_async(models.Manager.latest)

    async def aget(self, *args, **kwargs):
        return await self.aget_func(*args, **kwargs)

    async def aget_or_create(self, *args, **kwargs):
        return await self.aget_or_create_func(*args, **kwargs)

    async def acount(self, *args, **kwargs):
        return await self.acount_func(*args, **kwargs)

    async def acreate(self, *args, **kwargs):
        return await self.acreate_func(*args, **kwargs)

    @sync_to_async
    def afirst_func(self, *args, **kwargs):
        return self.filter(*args, **kwargs).first()

    async def afirst(self, *args, **kwargs):
        return await self.afirst_func(*args, **kwargs)

    async def alast(self, *args, **kwargs):
        return await self.alast_func(*args, **kwargs)

    async def alatest(self, *args, **kwargs):
        return await self.alatest_func(*args, **kwargs)

    async def afilter(self, *args, **kwargs):
        return await sync_to_async(list)(self.filter(*args, **kwargs))

    async def aall(self, *args, **kwargs):
        return await sync_to_async(list)(self.all())


class AsyncMixin(models.Model):

    objects: AsyncManager = AsyncManager()

    async def asave(self, *args, **kwargs):
        return await sync_to_async(self.save)(*args, **kwargs)

    async def adelete(self, *args, **kwargs):
        return await sync_to_async(self.delete)(*args, **kwargs)

    class Meta:
        abstract = True

Sadly these methods will not have type hints until python 3.10 where you can pass over args types and return types. Currently attribute access will be blocked because of “Asynchronous operation not allowed” and I am completely fine with doing another async get. I personally think this is tthe most straight forward way of making both sync and async. Lazyness is kinda unrealistic in asyncio, so I wouldn’t put time on it, it’s not gonna happen imo.

# sync
a = Model.objects.create(b=c)
a.save()

# async
a: Model = await Model.objects.acreate(b=c)
await a.asave()

anyone from the Django team taken a look at the Emmett ORM

Unfortunately, the key feature of a Django Async ORM has to be backwards compatability with our entire existing featureset, so that limits us somewhat.

I’ve been using this mixin I created for quite a while already:

That’s something like what I want to do, but with a slightly more efficient backing implementation and, cruicially, some reasonable way to make transactions work. I’m glad it works though - that kind of proves the model we want to pursue underneath.

1 Like

Yep it works in the sense of “mimic”-ing the supposed async orm, so I don’t need to type sync_to_async every line, and since it uses threads, it won’t be firing asyncio at full power sadly.

Speaking of the nomenclature. Altho the functionality is what matters for most, but I also care if the code “look good” or not (maybe it’s a me thing, no wonder I can spend 1 hour naming just 1 method) The rating from best to worse for me is:

  • Model.aobjects.get() but that may implies the need of 2 model managers which might not be backward compatible at all. EDIT: even if using aget the need to override aget is still needed, either of these options will require overriding the async method to make it work. So this may be the best option imo.
  • Model.objects.aget() should have good backward compatibility, and the least annoying out of all the prefix and suffix out there, which looks a little uglier on aall() and acount().
  • Model.objects.a.get() should be okay aswell, but I would prefer that all the methods (sync and async ones) are in the same level, hence no dots (.).
  • Model.objects.async_get() Kinda long and ugly.
  • Model.objects.get_async() Maybe uglier.

Will look forward the results of it tho. Is it realistic to expect the async ORM to come in 4.0 or 4.1 ?

I’m aware Django heavily focuses on backwards compatibility but it might be worth looking how other async frameworks with integrated ORMs queue their DB queries in a thread-safe manner. Based on HTTP metrics I’ve seen, Emmet is the fastest py framework with ORM. Might be able to leverage some of their design decisions in their connections module.

The style we’re going to aim for will be either objects.aget or objects.get_async. Not sure which yet, but the naming is honestly the least difficult part of all this.

As for what version it will be in… my initial goal was 4.0, but that was before a global pandemic happened and I had to deal with a Lot Of Stuff. Still a soft goal but no promises.

Has anyone from the Django team taken a look at the Emmett ORM ? Might be a worthwhile design inspiration. Seems like it’s fully async

Emmett maintainer here. For sake of clarity, Emmett ORM is not async. It’s targeted for next major version, and for sure I’m dealing with the same bewilderments @paaksing expressed.

Hi,
I kinda prefer the first syntax (aget) than adding the suffix get_async which is a bit redundant since the await keyword is already telling to a reader that we are in an async context. This is just my two cents.

Also for my culture, is it difficult to have two managers objects and aobjects? Or it is just a matter of taste that you don’t consider the first idea of paaksing

Just to add material to this discussion, maybe it would be worth looking at the greenletio project. It is used in the project aioflask to make the framework flask async, but I also know that the main maintainer of sqlalchemy used a similar technique to handle asyncio in the latest release of his framework. Maybe Django can take som inspiration from this knowledge.

Now that I came across the need of chaining queries, this is getting quite spicy …
e.g.

Model.objects.filter(a=b).order_by(c)

And it can get much more complicated …

So I was thinking syntaxes more like these

await Model.objects.filter(a=b).order_by(c).run()
await Model.objects.filter(a=b).order_by(c).arun()
await Model.aobjects.filter(a=b).order_by(c).run()

As someone looking to move part of a production system written in Django to async, I was curious if there are any new async features that are expected to land in Django 4.0.

At this point, nothing further substantial is written, due to the ongoing global pandemic taking a lot of my spare time away. Unless someone else steps up to help with the work, it’ll depend on how the next few months go on a personal level.

4 Likes

There is Ticket #32889, which was merged, that allows per-request thread-sensitive sync_to_async(), rather than a single thread per-process. This should speed up Django request serving under ASGI, but I haven’t benchmarked this to see exactly how much.

There’s still gains to be made with async ASGI (as it stands) but I think, currently, it’s worth isolating those in a separate ASGI process, keeping your main traditional Django usage under WSGI, where you can use async def views, giving you lots of the gains already.

2 Likes

I’ve heard a lot from the Django team about (to my understanding) keeping HTTP on WSGI and Websockets on ASGI. But unfortunately this process has not been formally documented. Is there any plans to add a section about this to the channels docs?

It’s something I’m working on, yes. (But input welcome.).

I do worry though that we’ll never be able to provide the detail people need.

I can add an example for Nginx (say) but I’d think anyone using Nginx already knows how to route different requests to different upstreams. (… :thinking:)

For folks on things like Heroku, I don’t know what’s possible, or if it even is, so I can’t add anything. Input welcome again. But I don’t we should start adding deployment guides for every possible setup, that would be impossible to maintain.

If it’s going to pull away from dev hours I’d consider skipping the task entirely.

Personally while the spec was originally being drafted I hoped ASGI would be a performance boost for Django.

From my perspective I’d rather have a hit to backwards compatibility of packages (ex. middleware) in exchange for performance.

Especially since HTTP performance in Django is definitely an area that needs improvement at the moment.

Until we have an async ORM, or at least the ability to push just the querying part into a thread, as Andrew has talked about in various presentations, we are limited.

But one shouldn’t confuse that with the HTTP performance. The core handlers are plenty fast enough. (I keep mentioning this but you need to recall that Instagram are still using them. It’s fast enough for them…)

Async isn’t magic. There’s no reason to think it’s instantly going to be quicker than a stack with a dozen years behind it.

2 Likes

Read this thread few times…
Tortoise ORM claimed to be async.

Maybe it can give some insights ?
(and I’m out)

Edit: apologize if not relevant.