django-getorcreateplus

Mixins to cache, force and/or use non-atomic Model.objects.get_or_create() calls.


Keywords
django, get_or_create, update_or_create, get, create, update, cache, atomic, non-atomic
License
Apache-2.0
Install
pip install django-getorcreateplus==0.1.0

Documentation

Django GetOrCreatePlus

Django GetOrCreatePlus is a set of queryset mixins to create custom querysets that can:

  • Cache results of get_or_create() or update_or_create()
  • Always use get_or_create() when get() calls are made
  • Allow non-atomic get_or_create() to avoid nested transaction points

Quick start

# models.py

from django.db import models
from django.core.cache import caches
from getorcreateplus import CachedGetOrCreateMixin, AlwaysGetOrCreateMixin, NonAtomicGetOrCreateMixin


# sample for CachedGetOrCreateMixin (see below for changes to be made in settings.py)
class CachedQuerySet(CachedGetOrCreateMixin, models.QuerySet):
    pass

class CachedManager(models.manager.BaseManager.from_queryset(CachedQuerySet)):
    use_for_related_fields = True

class CachedImmutableModel(models.Model):
    foo = models.CharField(max_length=8)
    bar = models.IntegerField(null=True)

    objects = CachedManager()


obj1, _ = CachedImmutableModel.objects.get_or_create(foo='FooBar') # fetches from db
obj2, _ = CachedImmutableModel.objects.get_or_create(foo='FooBar') # hits cache


class CachedMutableObject(models.Model):
    foo = models.CharField(max_length=8)
    bar = models.IntegerField(null=True)

    objects = CachedManager()

    def save(self, **kwargs):
        cache = caches[self._meta.model_name]
        cache.delete(self.pk)
        return super(CachedMutableObject, self).save(**kwargs)


obj1, _ = CachedMutableModel.objects.get_or_create(foo='FooBar') # fetches from db
obj1.bar = 1
obj1.save() # invalidate object cache
obj2, _ = CachedMutableModel.objects.get_or_create(foo='FooBar') # fetches from db
obj3, _ = CachedMutableModel.objects.get_or_create(foo='FooBar') # hits cache


# sample for NonAtomicGetOrCreateMixin
class NonAtomicQuerySet(NonAtomicGetOrCreateMixin, models.QuerySet):
    pass

class NonAtomicManager(models.manager.BaseManager.from_queryset(NonAtomicQuerySet)):
    use_for_related_fields = True

class ParentQuerySet(NonAtomicQuerySet):
    def create(self, **kwargs):
       children = kwargs.pop('children', [])
       parent = super(ParentQuerySet, self).create(**kwargs)
       for child in children:
           parent.children.get_or_create(**child)
       return parent

class ParentManager(models.manager.BaseManager.from_queryset(ParentQuerySet)):
    use_for_related_fields = True

class ParentModel(models.Model):
    foo = models.CharField(max_length=8)

    objects = ParentManager()

class ChildModel(models.Model):
    parent = models.ForeignKey(Parent, related_name='children')
    bar = models.CharField(max_length=8)

    objects = NonAtomicManager()


from django.db import transaction

with transaction.atomic():
    parent, _ = ParentModel.objects.get_or_create(foo='Foo', defaults={children: [{bar: 'Bar'}, {bar: 'Baz'}]})


# samples for combining mixins CachedGetOrCreateMixin, AlwaysGetOrCreateMixin, NonAtomicGetOrCreateMixin
class NonAtomicAlwaysQuerySet(NonAtomicGetOrCreateMixin, AlwaysGetOrCreateMixin, models.QuerySet):
    pass

class AlwaysCachedQuerySet(AlwaysGetOrCreateMixin, CachedGetOrCreateMixin, models.QuerySet):
    pass

class PlusQuerySet(CachedGetOrCreateMixin, AlwaysGetOrCreateMixin, NonAtomicGetOrCreateMixin, models.QuerySet):
    pass

CachedGetOrCreateMixin uses Django caches. The keys are cached to the default cache, and the objects are cached using alias model._meta.model_name.

NOTE: If you have models by the same name in different apps both using CachedGetOrCreateMixin, this will fail.

# settings.py
# assuming use of django-connection-url (shameless self-plug)

import connection_url

CACHES = {
    'default': connection_url.config('locmem:///'),
    'cachedimmutablemodel': connection_url.config('REDIS_URL'),
    'cachedmutablemodel': connection_url.config('MEMCACHED_URL'),
}