Photo by Lindsay Moe on Unsplash
Simple Django Tip #5
Continuing points to remember while designing a model
In this post, I plan to continue with some more tips to keep in mind while designing Django models. Just in case you are curious what's Part 1, here it is ๐
And yes, I'm taking the same hypothetical website that sells and delivers ice cream ๐ฆ. Below are the models that I will use to explain the tips:
Customer
Icecream
Order
Let's dive right in!
Use matching field types
Django models have a lot of field types to choose from, literally, every single field type can be handled appropriately. Per the documentation, a complete of field types here.
Using the correct field type helps in data integrity, readability of code, and it will also improve performance.
Let's look at a few examples.
In the Order
model, there is a column OrderedAt
which denotes when a Customer ordered an ice cream. It would be ideal to define this as a DateTime
column rather than a Date
column. The reason is, that if the owner of the website wants to analyze the pattern of sales, time would be a crucial aspect.
In the Customer
model, there is a column Avatar
which holds the profile picture of a Customer. It would make more sense to define it as an ImageField
rather than a FileField.
The reason in this case would be that the ImageField
inherits the properties of a FileField
also validates whether the uploaded file is an image. Direct validation of a business rule, is it not?
In the Ice cream
model, there is a column that points to a link where the corresponding image of each type of ice cream is loaded. In such a case, it would be ideal to define it as a URLField
rather than a plain CharField
as the former also validates whether the input is a valid URL.
Use empty string for String-based fields
When you define a CharField
and it's not a mandatory one, use blank=True
and not null=True
. The reason for this being is, Django's convention is to use an empty string and not null.
A common example that can be quoted here is, in a Customer field, we can add a column like the Address2
field which will be optional in most cases. We can define such a column like so ๐
class Customer(models.Model):
first_name = models.CharField(...)
...
address2 = models.CharField("Address 2", max_length=50, blank=True)
Make sure to define __str__
method
Whenever you define a model, make sure to define a __str__
method. This helps a lot when we debug. It also comes in very handy with the Django admin site when it displays objects.
Let's look at how we define them ๐
class Customer(models.Model):
first_name = model.CharField("First name", max_length=50,
verbose_text="How do I address you?")
last_name = model.CharField("Last name", max_length=50,
verbose_text="Your last name please")
email_address = model.EmailField("Email", max_length=50,
verbose_text="Email address where you can be reached"_
....
created_by = models.ForeignKey(User, on_delete=models.CASCADE,
related_by="user_who_created")
def __str__(self):
# This is an option. It can also be just email.
# It is upto the developer's discretion to define
# depending on uniqueness and how much it would help with
# debugging and readability
return self.first_name + " " + self.last_name + " - " + email
Use select_related
while fetching data
When we fetch data from a table in code, it is good practice to use select_related
when compared to plain lookups. The reason being, select_related
returns a Queryset
that will follow foreign key relationships, thus fetching additional data when we issue a query.
This improves performance although it creates a single complex query. But there will be no further querying as it fetches appropriate additional data.
Let's see an example ๐
class Order(models.Model):
customer = models.ForeignKey(Customer, on_delete=models.CASCADE,
related_name='customer_who_ordered')
icecreams = models.ManyToManyField(Icecream, on_delete=models.CASCADE,
related_name='icecreams_ordered')
created_at = models.DateTimeField(auto_now_add=True)
updated_at = models.DateTimeField(auto_now=True)
status = models.CharField(max_length=20, choices=[
('pending', 'Pending'),
('processing', 'Processing'),
('completed', 'Completed'),
('cancelled', 'Cancelled'),
])
total_price = models.DecimalField(max_digits=10, decimal_places=2, \
blank=True)
class Meta:
ordering = ['-created_at']
verbose_name = 'Order'
verbose_name_plural = 'Orders'
def __str__(self):
return f"Order {self.id} - {self.status}"
# INCORRECT
order_details = Order.objects.get(id=101)
customer_details = order_details.customer
# CORRECT
icecream_details = Icecream.objects.select_related(
"customer").get(id=101)
Normalize your data unless there is a specific need
In general, for a normal business application, it is good practice to normalize the data. Relationships between two entities should be maintained through Foreign Keys therefore we do not duplicate data. Denormalized database design is common in data warehouses and analytical dashboards.
When a framework like Django is used, the underlying database will most probably be used for CRUD operations. Hence it's always best to normalize the models when we design.
Complete design of the Icecream model
class Icecream(models.Model):
flavor_choices = (
("butterscotch", "Butterscotch"),
("carmel", "Carmel"),
("pista", "Pista"),
("strawberry", "Strawberry"),
("vanilla", "Vanilla")
)
flavor = models.CharField("Choose your flavor", choices=flavor_choices,
default="vanilla")
description = models.CharField("Describe what the flavor is all about",
max_length=150, blank=True)
is_available = models.BooleanField("Are you selling this now?",
default=True)
price = models.DecimalField("Price per 100 gms",
max_digits=5, decimal_places=2)
ingredients = models.TextField("What are the main ingredients?")
allergens = models.CharField("What allergens are present?",
max_length=255, blank=True)
nutritional_info = models.TextField("What is the nutritional
info?", blank=True)
is_vegan = models.BooleanField(default=False)
is_gluten_free = models.BooleanField(default=False)
created_at = models.DateTimeField(auto_now_add=True)
updated_at = models.DateTimeField(auto_now=True)
class Meta:
ordering = ['flavor']
verbose_name = 'Ice Cream'
verbose_name_plural = 'Ice Creams'
def __str__(self):
return f"{self.flavor}
Order
model is already defined as part of one of the tips above.Closing thoughts
In this and the previous post, we saw a couple of tips to remember while we define models in Django. They bring in a lot of benefits from various aspects.
Hope you found these tips to be useful!