Setting defaults for Django foreign key fields

David
3 min readMar 31, 2020

--

Adding default a value to a django foreign key is easier than you think.

There is sometimes debate on whether to use a ForeignKey or a CharField with choices in a model. One reason a CharField is often used is the ease of setting a default value. Adding a default value to a ForeignKey field is actually easier than you might think. In this post we’ll look at how this is done.

Let’s say we have a model to track actions. Each action will have a status and a result. When a new action is created, we want to have a default status of ‘created’, and a default result of ‘unknown.’

Here’s what our initial model looks like this:

class Action(models.Model):
“”” table to track individual actions “””
status_choices = ((“created”, “created”), (“started”, “started”), (“completed”, “completed”), )
result_choices = ((“passed”, “passed”), (“failed”, “failed”), (“unknown”, “unknown”))
name = models.CharField(max_length=16, unique=True)
status = models.CharField(max_length=16, choices=status_choices, default=”started”)
result = models.CharField(max_length=16, choices=result_choices, default=”unknown”)

This would work fine, but status and result choices are statically set as a tuple of tuples and would require a code change to add or modify entries. Another option is to make the status and result fields foreign keys. This would require defining models for status and result (for our example, these link to models ActionStatus and ActionResult), making our models look like this:

class ActionStatus(models.Model):
""" table to track statuses of actions, such as 'created', 'started', 'completed', etc. """
name = models.CharField(max_length=16, unique=True)
class ActionResult(models.Model):
""" table to track results of actions, such as 'passed', 'failed', 'unknown', etc. """
name = models.CharField(max_length=16, unique=True)
class Action(models.Model):
""" table to track individual actions """
name = models.CharField(max_length=16, unique=True)
status = models.ForeignKey(ActionStatus, on_delete=models.CASCADE)
result = models.ForeignKey(ActionResult, on_delete=models.CASCADE)

This is a relatively simple and clean approach, the only issue is getting defaults for the status and result fields. ForeignKey fields do actually support defaults. Unfortunately this isn’t quite as simple as assigning the default to the ‘name’ of our status such as the following:

status = models.ForeignKey(ActionStatus, default=”created”, on_delete=models.CASCADE)

Obviously this is because ‘created’ is not ActionStatus object. We can, however, assign the default to a function that can read the value we want. First we create the function:

def get_default_action_status():
return ActionStatus.objects.get(name="created")

Now we can set the default in our status field to point to this function:

status = models.ForeignKey(ActionStatus, default=get_default_action_status, on_delete=models.CASCADE)

This will work fine, assuming the ActionStatus table actually has an entry where name=”created”; without it we will get an exception. There is simple fix for this, and that is to use get_or_create to ensure the desired value exists. Remember get_or_create returns a tuple, and we’ll only need the actual ActionStatus object.

Our function now looks like this:

def get_default_action_status():
""" get a default value for action status; create new status if not available """
return ActionStatus.objects.get_or_create(name="created")[0]

Now we can safely create Actions where the status will be defaulted to ‘created’ and the ActionStatus will get created if needed. Our final models.py looks like this:

def get_default_action_status():
""" get a default value for action status; create new status if not available """
return ActionStatus.objects.get_or_create(name="created")[0]
def get_default_action_result():
""" get a default value for result status; create new result if not available """
return ActionResult.objects.get_or_create(name="unknown")[0]
class ActionStatus(models.Model):
""" table to track statuses of actions, such as 'created', 'started', 'completed', etc. """
name = models.CharField(max_length=16, unique=True)
class ActionResult(models.Model):
""" table to track results of actions, such as 'passed', 'failed', 'unknown', etc. """
name = models.CharField(max_length=16, unique=True)
class Action(models.Model):
""" table to track individual actions """
name = models.CharField(max_length=16, unique=True)
status = models.ForeignKey(ActionStatus, default=get_default_action_status, on_delete=models.CASCADE)
result = models.ForeignKey(ActionResult, default=get_default_action_result, on_delete=models.CASCADE)

--

--