Untangled Development

A singleton attempt with Django and factory_boy

First things first. Let’s define the singleton pattern1:

The singleton pattern is one of the simplest design patterns: it involves only one class which is responsible to make sure there is no more than one instance; it does it by instantiating itself and in the same time it provides a global point of access to that instance.

image

Image credit: Hpesoj00, CC BY-SA 4.0, via Wikimedia Commons

But why?

Why would you need singleton-like behaviour at Django model layer during tests?

This is not a common pattern for a Model class. But I did encounter it a few times1:

Usually singletons are used for centralized management of internal or external resources and they provide a global point of access to themselves.

In one case configuration data was stored as a database record, within a JSONField. As opposed to Django project settings or environment variables. This is easy to manage via admin.

In another case a system had an Account table. For multiple client accounts. But then the Django project was implemented per account in a multi-tenant architecture. From then on, the Account table had, at any point in time, one Account record only.

For this post’s sake we’ll use the Config model with a data JSONField and a key unique character field:

class Config(models.Model):
    key = models.CharField(max_length=32, unique=True)
    data = models.JSONField()

Do we need key field? For this model’s purposes? Stricly speaking, no. We’ll get back to it later on.

Test data management with factory_boy

factory_boy is the most widely used fixtures replacement package. At least according to the 2022 Django Developer Survey2. In which 12% of respondents listed factory_boy as one of the “top 5 Python packages [they] rely on”.

In comparison, factory_boys main competitor, i.e. model bakery was listed by only 2% of the respondents in the same survey.

Singleton with factory_boy

Django queryset offers the get_or_create convenient method:

for looking up an object with the given kwargs (may be empty if your model has defaults for all fields), creating one if necessary.

In order to have the database enforce one entry per table we’ll configure factory_boy to always “get or create”. For this factory_boy provides django_get_or_create:

Fields whose name are passed in this list will be used to perform a Model.objects.get_or_create() instead of the usual Model.objects.create()

If we:

  • set key to always have the same value, and
  • configure the DjangoModelFactory to always get_or_create based on key

then that factory will, by default, get or create the same Config record with the same key.

Model factory class looks like this:

class ConfigFactory(factory.django.DjangoModelFactory):
    class Meta:
        model = "myapp.Config"
        django_get_or_create = ["key"]

    key = "test-key"

What the above does is:

  • When ConfigFactory is called it either gets or creates a Config database entry with key test-key.
  • Calling ConfigFactory multiple times will return the same Config database entry.
  • Even when create_batch is called, it returns an array of Config entries, but in reality it’s the same database record:
In[1]: config_list = ConfigFactory.create_batch(3)

In[2]: [config.id for config in config_list]
Out[2]: [1, 1, 1]

Importantly, what the above does not is guarantee one entry in that table. If a factory with a different key is instantiated, then that record will be created at database level:

In[3]: another_config = ConfigFactory(key="another-key")

In[4]: another_config.id
Out[4]: 2

But like this the factory always gets or creates the same Config, unless a Config with a key other than the default key test-key is created.

So unless you specify a different key, you don’t need to worry about duplicates anymore.

Hope this helps. Happy coding!

References

Footnotes

Comments !