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.
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_boy
‘s 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 usualModel.objects.create()
If we:
- set
key
to always have the same value, and - configure the
DjangoModelFactory
to alwaysget_or_create
based onkey
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 aConfig
database entry with keytest-key
. - Calling
ConfigFactory
multiple times will return the sameConfig
database entry. - Even when
create_batch
is called, it returns an array ofConfig
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!
Comments !