Suor / django-cacheops

A slick ORM cache with automatic granular event-driven invalidation.
BSD 3-Clause "New" or "Revised" License
2.11k stars 227 forks source link

AttributeError: 'QuerySet' object has no attribute '_require_cacheprofile' #381

Closed danmash closed 1 year ago

danmash commented 4 years ago

I have an error when trying to use @cached_as(OmittedLeadEmail)

    @cached_as(OmittedLeadEmail)
  File "/pyenv/versions/3.7.8/lib/python3.7/site-packages/cacheops/query.py", line 92, in cached_as
    querysets = lmap(_get_queryset, samples)
  File "/pyenv/versions/3.7.8/lib/python3.7/site-packages/funcy/seqs.py", line 114, in lmap
    return _lmap(make_func(f, builtin=PY2), *seqs)
  File "/pyenv/versions/3.7.8/lib/python3.7/site-packages/funcy/compat.py", line 8, in lmap
    return list(map(f, *seqs))
  File "/pyenv/versions/3.7.8/lib/python3.7/site-packages/cacheops/query.py", line 88, in _get_queryset
    queryset._require_cacheprofile()
AttributeError: 'QuerySet' object has no attribute '_require_cacheprofile'
Suor commented 4 years ago

This means cacheops was not installed for this model. Which probably means models queryset does not descend from django.db.models.QuerySet. Is it so?

danmash commented 4 years ago

it was some import problem. I had to move the import statement of a function which uses @cached_as to the method where it's called.

Suor commented 4 years ago

It's weird that you get such error though. Maybe you need cacheops moved upper in apps list.

utapyngo commented 1 year ago

I got this error by using @cached_as in the models.py file just under the model definition. I had to move it outside the model itself. I can't use @cached_as(MyModel) on any of MyModel methods since MyModel is not defined yet. I would like @cached_as to accept model name or 'self'.

Suor commented 1 year ago

Currently cacheops installs its monkey patches after all models are collected, e.g. app registry is ready, along with other installation tweaks, like setting up signal handlers. Thus the error. Signals cannot be connected before models are ready, so one will need to split cacheops installation process into two phases to make @cached_as() work in models.py.

The above assumes @caches_as() stays the same, i.e. it precalculates some stuff at the decorator call time, so that won't need to do that on every func call. However, if one wants to use lazily defined samples like in your examples then @cached_as() needs to be changed anyway.

I thought of adding something like:

@cached_as(lambda category_id: Model.objects.filter(category_id=category_id))
def some_func(category_id):
    ...

which will also require adjusting @cached_as() in a similar way.

The main thing that stopped me from doing that is that you can always create a subfunction and wrap that one:

def some_func(category_id):
    qs = Model.objects.filter(category_id=category_id)

    @cached_as(qs)
    def _some_func():
        ... # can use `qs` here

    return _some_func()

This could be put into models.py and will also work with methods:

class MyModel(models.Model):
    def some_method(self):

        @cached_as(self)  
        def _some_method():
            ... 

        return _some_method()

It may look awkward but unlike special string syntax it does not constrain you in any way. Also, you way reuse a queryset both in @cached_as() and inside a cached function.

Suor commented 1 year ago

It is possible to implement simple cases like refering to the model or self, which are different things BTW:

@cached_as(obj)
def foo():
    ...

# is the same as
@cached_as(obj.__class__.objects.filter(pk=obj.pk))
def foo():
    ...

I wonder if this will promote people of not using more granular querset in @cached_as() though. Or obscure more complex scenarios covered by a nested function.

Maybe you can tell more about your use cases.

Suor commented 1 year ago

Closing this with a nested function as the blessed approach for now.