Django Models

Posted by Daksh on Friday, August 5, 2022

What is a Django Model?

A Django model is a Python class that subclasses django.db.models.Model. Each model has a number of class variables, each of which represents a database field in the model.
In simple words, it is a class that represents a table in the database. Define data models and use objects based on those models to query the database.Django under the hood uses the Django Models to translate instructions (written in Python) into SQL queries.

To get started, create a new file called models.py in the app_name directory(It should be present by default). This file will contain all the code for the models. Id is added to every model by default.

# https://docs.djangoproject.com/en/4.1/ref/models/fields/

from django.db import models

class Entity(models.Model):
    name = models.CharField(max_length=100)
    description = models.TextField()
    created_at = models.DateTimeField(auto_now_add=True)
    updated_at = models.DateTimeField(auto_now=True)

    # override the default __str__ method
    def __str__(self):
        return self.name

Migrations

A migration is a set of instructions that tells Django how to create or modify the database schema. It is also used to register your models.
So first we need to create a migration file for the model we created and then run it to create/modify the table in the database.
To create a migration file, run the following command(inside the project directory):

python manage.py makemigrations

To run migrations, run the following command: python manage.py migrate

Inserting data into the database

Inside the project directory, run the following command:

$ python manage.py shell
>>> from app_name.models import Entity
>>> Entity.objects.create(name="name", description="description")
# another way to create an object
>>> Object1 = Entity(name="name2", description="description2")
# To save the object to the database
>>> Object1.save()
# to get all the objects saved in the database
>>> Entity.objects.all()
# <QuerySet [<Entity: Entity object (1)>, <Entity: Entity object (2)>]>
# <QuerySet [<Entity: name>, <Entity: name2>]>
# to access any particular field of the object
# below code gives the filed_name value of the first object
>>> Entity.objects.all()[0].filed_name

Modify the model

Migration steps are necessary when you make changes to the Database Schema.
For example, if you want to add a new field to the model, then you will have to create a new migration file and run it to add the new field to the database.

python manage.py makemigrations

python manage.py migrate You can also add validators to the fields.

from django.db import models
from django.core.validators import MinValueValidator, MaxValueValidator

class Entity(models.Model):
    title = models.CharField(max_length=50)
    rating = models.IntegerField(validators=[MinValueValidator(1), MaxValueValidator(5)])

    def __str__(self):
        return f"{self.title} ({self.rating})"

If there is a pre-existing data inside database, then for each change in the model, you will have to provide a default value for the new field.
So you can either provide a default value or you can set the field to null=True.
null=True will have “NULL” in the database for the new field.
You can also add blank=True to the field to make it optional(by default it’s mandatory).

blank=True: When providing a value for this model field(via a form), this field is optional and may be left empty.

null=True: When no value is provided for this model field, Django will store an empty value as NULL in the database. If null=False, you will have to ensure that some default value is set in case of blank values.

CharField and other related types have defualt value as an empty string and thus null=True should be avoided.

# suppose we want to add a new field called 'title' to the model Entity
# method 1
title = models.CharField(max_length=50, default="any value")
# method 2
title = models.CharField(max_length=50, null=True)
# method 3
title = models.CharField(max_length=50, blank=True)

What is inside the migration file?

It will have a python file for each migration that you have created.
The very first migration will have initial=True.
And the rest of the migrations will be build on the previous migration and will have the dependencies with the previous migration.

dependencies = [
        ('app_name', '0001_initial'),
    ]

The order of the migrations is maintained by the number in the migration file and it helps us to keep track of the changes made to the database, and to roll back the changes if needed.\

Querying & Filtering the database

To query the database, we use the objects attribute of the model.
The objects attribute is a manager that is used to query the database.

# to get all the objects
>>> Entity.objects.all()

# to get a particular object
# key must be unique
# .get() will return a single object
# id is self generated by Django, and can be used as a key
>>> Entity.objects.get(key=1)

To filter the database, we use the filter() method.
filter() method returns a QuerySet, which is a list of objects that match the filter.

# to get all the objects with a particular value
# only assignment operator is allowed in filter()
# therefore, we can't use !=, >, <, etc.
>>> Entity.objects.filter(key1=value1, key2=value2)

# to use other operators, we use parameter__operator=value
# lte: less than or equal to
# gte: greater than or equal to
# lt: less than
# gt: greater than
# refers to https://docs.djangoproject.com/en/4.1/topics/db/queries/#field-lookups
>>> Entity.objects.filter(key__lte=value)
# by default, __contains is case sensitive, but for case insensitive search, use __icontains
# for sqllite, there is no case insensitivity
>>> Entity.objects.filter(id__lte=value, name__contains="Daksh")

Implementing “OR” and “AND” conditions in filter()

>>> from django.db.models import Q
# to implement "OR" in filter()
>>> Entity.objects.filter(Q(id__lte=value) | Q(name__contains="Daksh"))
# to implement "AND" in filter()
>>> Entity.objects.filter(Q(id__lte=value) & Q(name__contains="Daksh"))
>>> Entity.objects.filter(Q(id__lte=value), Q(name__contains="Daksh"))
# by default, the filter() method is "AND" operation
>>> Entity.objects.filter(id__lte=value, name__contains="Daksh")

# multiple conditions can be added to the filter() method
>>> Entity.objects.filter(Q(A) | Q(B), C) # A OR B AND C -> (A OR B) AND C
# Q objects must be placed in the beginning of the filter() method, and the rest of the conditions must be placed after the Q objects

Calling a filter() method returns a QuerySet, which is a list of objects that match the filter. We can again call the filter() method on the QuerySet to further filter the results.

>>> Entity.objects.filter(id__lte=value).filter(name__contains="Daksh")
# or
>>> answer_set = Entity.objects.filter(id__lte=value, name__contains="Daksh")
>>> more_filtered_answer_set = answer_set.filter(field_name__lte=value)
# till now, the database is not queried, and the QuerySet is stored in the memory of the system
# Django will only reach out to database when you do something with "answer_set" or "more_filtered_answer_set"
# therefore if we print the answer_set, now Django will query the database
>>> print(answer_set)
# if we print the more_filtered_answer_set, now Django will query the database again, but it will also use the previous query as cache and will only query the database for the new filter
>>> print(more_filtered_answer_set)
# if you run below code, Django will not use any cache and will query the database again, because the QuerySet is not stored in any variable
>>> print(Entity.objects.filter(id__lte=value).filter(name__contains="Daksh"))
# therfore, it is always better to store the QuerySet in a variable and then use it, in order to avoid multiple queries to the database & use cached results for improved performance
# moreover, always try to structure your code in such a way that you use cached results as much as possible

Aggregation, ordering and counting

aggregate() method is used to perform aggregation on the QuerySet.
order_by() method is used to order the QuerySet.
count() method is used to count the number of objects in the QuerySet.

from django.db.models import Avg, Max, Min, Sum
all_entities = Entity.objects.all()
# call the Avg constructor and pass the field name as an argument inside aggregate() method
avg_field = all_entities.aggregate(Avg('field_name1'), Min('field_name2')) -> # returns a dictionary with the field name as key and the average/min value as value
# to order the QuerySet, use the order_by() method
# default ordering is ascending, to order in descending order, use - before the field name
all_entities.order_by('field_name1') -> # returns a QuerySet ordered by the field_name1

Updating & Deleting Data

The .save() method when called upon an pre-existing object, it will update the database with the new values. If object doesn’t exist, it will create a new object in the database.

$ python manage.py shell
>>> from app_name.models import Entity
>>> Object1 = Entity.objects.get(id=1)
>>>Object1.name = "new name"
# Till now, the object is not updated in the database, and the changes are stored in the memory of the system. 
# To update the database, we need to call the .save() method.
>>> Object1.save()

To delete an object, use the .delete() method.

Using data inside templates

Inside the views.py file of the app, use the following code to pass data to the template.

from django.shortcuts import render
from .models import Entity

def index(request):
    # get all the objects from the database
    all_objects = Entity.objects.all()
    # pass the data to the template
    context = {
        "all_objects": all_objects
    }
    return render(request, "app_name/index.html", context)

And inside the template, use the following code to access the data.

{% for object in all_objects %}
    <p>{{ object.name }}</p>
{% endfor %}

Useful Django shortcut:

# Instead of using the following code:
try:
    book = Book.objects.get(pk=id)
except:
    raise Http404()

# We can use the following shortcut:
from django.shortcuts import render, get_object_or_404
book = get_object_or_404(Book, pk=id)

Assigning URL to a view

To assign a URL to a view, we need to add the URL to the urls.py file of the app.

# inside the urls.py file of the app
from  django.urls import path
from . import views

urlpatterns = [
    path("", views.home, name="home"),
    path("<int:id>", views.entity_detail, name="entity-detail")
]

# inside the urls.py file of the project
from django.contrib import admin
from django.urls import path, include

urlpatterns = [
    path('admin/', admin.site.urls),
    path('', include("app_name.urls"))
]

# method 1: Inside the template, use the following code to link to the URL
<a href="{%  url "entity-detail" entity.id %}">{{ entity.name }}</a>

# method 2: Inside the model, use the following code to link to the URL
# inside the models.py file of the app
from django.db import models
from django.urls import reverse

class Entity(models.Model):
    name = models.CharField(max_length=100)
    description = models.TextField()
    created_at = models.DateTimeField(auto_now_add=True)
    updated_at = models.DateTimeField(auto_now=True)

    # override the default __str__ method
    def __str__(self):
        return self.name

    # override the default get_absolute_url method
    def get_absolute_url(self):
        return reverse("entity-detail", kwargs={"id": self.id})

# inside the template, use the following code to link to the URL
# just point it to the get_absolute_url method of the model, and don't execute it
<a href="{{ entity.get_absolute_url }}">{{ entity.name }}</a>  

To use slugs:

# to generate slug automatically, use the following code
# inside the models.py file of the app
from django.db import models
from django.urls import reverse
from django.utils.text import slugify

class Entity(models.Model):
    name = models.CharField(max_length=100)
    description = models.TextField()
    slug = models.SlugField(blank=True, null=False)
    created_at = models.DateTimeField(auto_now_add=True)
    updated_at = models.DateTimeField(auto_now=True)

    # override the default __str__ method
    def __str__(self):
        return self.name

    # override the default get_absolute_url method
    def get_absolute_url(self):
        return reverse("entity-detail", kwargs={"slug": self.slug})

    # override the default save method
    def save(self, *args, **kwargs):
        # if the slug is not present, then generate it
        if not self.slug:
            self.slug = slugify(self.name)
        # super() to make sure that the save method of the parent class is also called
        # it is important to call the save method of the parent class, because it will save the object in the database
        # note that we are forwarding the arguments to the save method of the parent class
        super().save(*args, **kwargs)

Slugs are more SEO friendly than IDs. Therefore, it is always better to use slugs instead of IDs. Let’s use this slug in URL instead of ID.

# inside the urls.py file of the app
from  django.urls import path
from . import views
urlpatterns = [
    path("", views.home, name="home"),
    path("<slug:slug_received>", views.entity_detail, name="entity-detail")
]

# inside the views.py file of the app
from django.shortcuts import render, get_object_or_404
from .models import Entity
def entity_detail(request, slug_received):
    # get the object from the database
    obj = get_object_or_404(Entity, slug=slug_received)
    # pass the data to the template
    context = {
        "entity": obj
    }
    return render(request, "app_name/entity_detail.html", context)

# inside models.py file of the app
# override the default get_absolute_url method
    def get_absolute_url(self):
        return reverse("entity-detail", args=[self.slug])

# inside the template, use the following code to link to the URL
<a href="{{  entity.get_absolute_url }}">{{ entity.name }}</a>

# since slug will be frequently used to fetch data from the database, it is better to add an index to the slug field
# inside the models.py file of the app
slug = models.SlugField(blank=True, null=False, db_index=True)
# if not specified, the Id field will be indexed by default
# avoid having too many indexes, because it will slow down the database, as creating indexes is a costly operation

More about queries

Post.objects.all() # all posts are fetched from the database
# only the first 3 posts are fetched from the database
# django will automatically add the LIMIT 3 to the SQL query
# this means that all queries are not fetched and then sliced by python iteself
# this is how django increases the performance
Post.objects.all().order_by("-date")[:3]
# django does not support -ve indexing in above query