DEV Community

Cover image for Designing Better Django Models: Tips for Scalability and Clarity
Ezeana Micheal
Ezeana Micheal

Posted on

Designing Better Django Models: Tips for Scalability and Clarity

I’ve been using Django for a while now, and I’ve seen countless guides on how to write better code, structured code, and the list goes on. I decided to write mine to cover one major aspect, which is simplicity, for this guide, and following ones, I'll make use of simple wordings and easy to understand python code that can guide you (or anyone) in writing simple and powerful django applications, and to start with, the models, which are the backbone of traditional backend applications, a poorly written model design will cause a lot of problems in your application like optimization and scaling difficulty, lets explore practices and use cases, for this article ill make use of a blog model to explain the process. To begin, we start with defining a clear data model.

Start with a Clear Data Model

Before you start writing code, it is essential to understand what you want the code to do first. It's always best to get a clear picture of what you want your database to look like through design, including the relationships. Tools like Draw.io can help with that. (This article won't cover how to design your model; it's more on writing code using Django)

After designing your database and relationships, we're ready to hop into the code. Keep 2 things in mind: naming matters, and keep models simple. For example, here's our post model.

from django.db import models  
class Post(models.Model):  
    title = models.CharField(max_length=255)  
    content = models.TextField()  
    published_at = models.DateTimeField(auto_now_add=True)  
Enter fullscreen mode Exit fullscreen mode

It's always best to use meaningful, singular model names like Post, not Posts, and we limit it to 3 fields: title, content, and published date. Next, let's look at choosing the right type of fields.

Choosing the Right Field Types

In Django’s models, there are several field types like CharField, TextField, and DateTimeField used in the above, but aside from this, there are others like URLField, EmailField. Let's take a look at the user profile table below:

class UserProfile(models.Model):  
    email = models.EmailField(unique=True)     
    website = models.URLField(blank=True)  
    bio = models.TextField(blank=True)  
    reputation = models.DecimalField(max_digits=6, decimal_places=2, default=0.00)    
Enter fullscreen mode Exit fullscreen mode

It's always best to use a specific field for some of the following reasons:

  1. Django provides built-in validation
  2. Helps Django’s retrieval from the database
  3. It saves space and optimizes in the database (e.g, using Charfield for fields like name with a max length set, rather than a text field, which is better suited for description)

Now, let's consider primary keys and identifiers.

Primary Keys & Identifiers

When talking of primary keys, we consider the IDs of these tables since these are what link them up. It also brings up the question, when it is best to use Django’s default ID and a UUID.

By default, Django creates an ID this way:

id = models.BigAutoField(primary_key=True)  
Enter fullscreen mode Exit fullscreen mode

It auto-increments, and it's useful for small to medium-scale projects. The downside of this is that it's predictable, and it can expose user count or allow enumeration attacks in APIs.

UUIDs, on the other hand, are especially useful in distributed systems or microservices, where you can’t rely on a central database to generate sequential IDs.

import uuid  
class UserProfile(models.Model):  
    id = models.UUIDField(primary_key=True, default=uuid.uuid4, editable=False)  
    email = models.EmailField(unique=True)  
Enter fullscreen mode Exit fullscreen mode

UUIDs are harder to guess and good for public-facing URLs, but they require larger storage and are slower to index. Let's take a step into relationships.

Relationships Done Right

In Django, there are 3 major types of relationships: one-to-one, one-to-many (Foreign key), and many-to-many. Below are examples of such relationships.

class Post(models.Model):  
    author = models.ForeignKey("Author", on_delete=models.CASCADE, related_name="posts")  
    title = models.CharField(max_length=255)  
    content = models.TextField()

class Comment(models.Model):  
    post = models.ForeignKey("Post", on_delete=models.CASCADE, related_name="comments")  
    author = models.CharField(max_length=100)  
    body = models.TextField()

class Tag(models.Model):  
    name = models.CharField(max_length=50, unique=True)  
    posts = models.ManyToManyField("Post", related_name="tags")  
Enter fullscreen mode Exit fullscreen mode

models.Foreignkey, models.ManyToMany and models.OneToOne.

You’ll notice related name parameter placed in these relationship, it is the reverse relation name Django will use when looking up related objects.

For example:

post = Post.objects.first()  
post.comments.all()   # instead of post.comment_set.all()  
Enter fullscreen mode Exit fullscreen mode

The on_delete argument tells Django what to do with related objects when the referenced object is deleted. In our case models.CASCADE means delete the dependent rows too. This is important so it doesnt retain dependent roles in the database.

Model Methods & Business Logic

When writing business logic in the model.py file, we must ensure that this is not where all our methods should be; basically, just simple and commonly used logic can be implemented here, for example, using sample model methods.

class Post(models.Model):  
    title = models.CharField(max_length=255)  
    content = models.TextField()  
    published_at = models.DateTimeField(auto_now_add=True)

    def is_published_recently(self):  
        from django.utils import timezone  
        return self.published_at >= timezone.now() - timezone.timedelta(days=7)  
Enter fullscreen mode Exit fullscreen mode

The post.is_published_recently() can now be called and works as intended from here.

Performance Considerations

Finally, for some performance considerations, here are some tips: use indexes for frequent lookups; they query faster.

class BlogPost(models.Model):  
    slug = models.SlugField(unique=True, db_index=True)  
Enter fullscreen mode Exit fullscreen mode

Keeping Models Maintainable

Utilizing some built-in functions in Django models to help readability and reusability is mostly advised, adding str for readability, adding a class meta for verbose name, and ordering.

class Post(models.Model):  
    title = models.CharField(max_length=255)  
    body = models.TextField()  
    created_at = models.DateTimeField(auto_now_add=True)

    Class Meta:  
        ordering = ["-created_at"]  
        verbose_name = "Blog Post"

    def __str__(self):  
        return self.title  
Enter fullscreen mode Exit fullscreen mode

From this article, we’ve seen the need for simplicity in creating Django models and utilizing functionalities to optimize performance. In the next article, we’ll consider in-depth the use of foreign keys, one-to-one and many-to-many.

Top comments (0)