Simple Django Tip #5

Photo by Lindsay Moe on Unsplash

Simple Django Tip #5

Continuing points to remember while designing a model

ยท

5 min read

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!

ย