Mastering Data Magic: Unleashing the Power of Django ORM in Your Web Development Journey
What is ORM?
ORM stands for Object-Relational Mapping. It’s a programming technique and a software design pattern that allows you to work with databases using Object-Oriented Programming (OOP) languages, like Python, rather than writing raw SQL queries. With ORM, you can interact with your database using objects and classes, which makes database operations more intuitive and developer-friendly.
Why ORM Is Regarded as a Best Practice in Modern Development
While working on my group project, I implemented a POST method in Django to create new objects by manually inserting data into the database using SQL queries. However, as I progressed with the coding, I began to question the approach. I had concerns about the flexibility to use different tables for object storage, security considerations, and adherence to best practices. That’s when I discovered the concept of Object-Relational Mapping (ORM) in Django. To gain a better understanding of why ORM is a good choice, let’s compare these two code approaches.
Code 1 (without ORM)
if request.method == 'POST':
title = request.POST.get('title')
description = request.POST.get('description')
job_type = request.POST.get('type')
salary = int(request.POST.get('salary'))
category = request.POST.get('category')
is_published = bool(request.POST.get('is_published'))
expired_at = datetime.strptime(request.POST.get('expired_at'), '%d/%m/%Y').date()
try :
query = f"""INSERT INTO job (title, description, type, salary, category, is_published, expired_at)
VALUES ('{title}', '{description}', '{job_type}', '{salary}', '{category}', '{is_published}', '{expired_at}');"""
cursor = connection.cursor()
cursor.execute("SET SEARCH_PATH TO job")
cursor.execute(query)
response['message'] = "Successfully creating new job"
except :
response['message'] = "Failed creating new job"
print(response['message'])
Code 2 (with ORM)
from django.db import models
from datetime import datetime
# Create your models here.
class Job(models.Model):
title = models.CharField("Title", max_length=50)
description = models.TextField("Description")
type = models.CharField("Type", max_length=25)
salary = models.IntegerField("Salary", default=0)
category = models.CharField("Category", max_length=25)
is_published = models.BooleanField(default=False)
created_at = models.DateTimeField(auto_now_add=True)
last_updated = models.DateTimeField(auto_now_add=True)
expired_at = models.DateField()
total_applicants = models.IntegerField(default=0)
class Meta:
db_table = 'job'
from rest_framework import serializers
from .models import Job
class JobSerializer(serializers.ModelSerializer):
class Meta:
model = Job
fields = ('title', 'description', 'type', 'salary', 'category', 'is_published','created_at', 'last_updated', 'expired_at', 'total_applicants')
if request.method == 'POST':
data=request.body.decode('utf-8')
serializer = JobSerializer(data=request.data)
if serializer.is_valid():
serializer.save()
response['message'] = "Successfully creating new job"
print(response["message"])
else :
response['message'] = "Failed creating new job"
print(response['message'])
Code 1 is using raw SQL to insert data into a database, while Code 2 is using Django ORM and a serializer to achieve the same result. Here are the advantages of using Django ORM (Code 2) over raw SQL (Code 1):
Readability and Maintainability:
- Code 2 (Django ORM) is more readable and maintains a clear separation of concerns. It uses Pythonic code with the serializer handling data validation and conversion.
- Code 1 (Raw SQL) is less readable and mixes SQL statements with Python code, making it harder to understand and maintain.
Security:
- Code 2 (Django ORM) is more secure as it automatically handles parameterization and escaping of input data, reducing the risk of SQL injection attacks.
- Code 1 (Raw SQL) is vulnerable to SQL injection because it directly inserts data into SQL queries without proper sanitization.
Database Portability:
- Code 2 (Django ORM) is database-agnostic. You can switch to a different database backend (e.g., PostgreSQL to MySQL) without changing your code.
- Code 1 (Raw SQL) is tied to the specific SQL dialect of your database and may require significant changes if you switch databases.
Simplicity:
- Code 2 (Django ORM) is simpler and more concise. It leverages Django’s built-in features for data validation and database interactions.
- Code 1 (Raw SQL) involves more boilerplate code for data extraction, type conversion, and error handling.
Testing:
- Code 2 (Django ORM) is easier to test because you can use Django’s testing framework and create unit tests that don’t rely on a live database.
- Code 1 (Raw SQL) can be more challenging to test, especially when it involves direct database interactions.
Code Reusability:
- Code 2 (Django ORM) promotes code reusability. You can reuse the serializer and model for other parts of your application.
- Code 1 (Raw SQL) lacks this level of code reusability, and you’d need to duplicate SQL statements for similar operations elsewhere.
Django Ecosystem:
- Code 2 (Django ORM) fits seamlessly into the Django ecosystem. You can take advantage of other Django features like authentication, admin interface, and middleware.
- Code 1 (Raw SQL) operates outside of the Django framework and may require custom solutions for these features.
Choosing Between ORM and Raw Queries
The choice between ORM and raw queries depends on the specific needs and priorities of a project.
1. Use ORMs for Common Operations:
When to use an ORM:
- CRUD-heavy Applications: For applications that are heavily centered around Create, Read, Update, and Delete (CRUD) operations, using an ORM can simplify development and reduce boilerplate code.
- Rapid Development: If you need to develop an application quickly and the queries are not overly complex, an ORM can speed up development by abstracting many SQL specifics.
- Database Agnosticism: If you expect to switch databases in the future or want to keep the option open, an ORM can make this process easier as it can abstract away the differences between different SQL dialects.
ORMs provide an abstraction layer that simplifies common database operations, such as CRUD operations (create, read, update, delete), by mapping database tables to object models. ORMs handle tasks like generating SQL queries, managing relationships, and mapping query results to objects. You can leverage ORMs to handle standard operations and simplify your codebase.
2. Use Raw SQL Queries for Complex Queries or Performance Optimization:
When to use raw SQL:
- Complex Queries: If your application requires highly complex queries that involve multiple joins, subqueries, or database-specific features, raw SQL might be a better choice as it provides more direct control and flexibility over your queries.
- Performance Critical Applications: For applications where performance is critical, raw SQL can offer an edge because it eliminates the overhead that comes with the abstraction provided by an ORM.
- Database-specific Features: If you’re using features specific to your database, raw SQL may be necessary as ORMs are designed to be database-agnostic and might not support all features of a specific database system.
For complex queries or situations where performance optimization is crucial, you can use raw SQL queries instead of relying solely on ORMs. Raw SQL queries give you more control over the query structure and execution, allowing you to optimize query performance, leverage advanced database features, or handle complex join conditions. By using raw SQL queries in these scenarios, you can take advantage of the power and flexibility of SQL directly.
3. Leveraging ORM Methods for Raw Queries:
Some ORM frameworks provide methods or features that allow you to execute raw SQL queries alongside their ORM-based operations. For example, popular Node.js ORMs like Sequelize or TypeORM offer methods like `query()` or `sequelize.query()` that allow you to execute custom SQL queries when needed. This allows you to combine the convenience of ORMs with the flexibility of raw queries.
4. Consider Hybrid Approaches:
In some cases, a hybrid approach can be used, where you mix ORM operations and raw SQL queries within the same project. You can use ORMs for most of the routine operations and fall back to raw SQL queries for more complex or specific scenarios. This approach allows you to strike a balance between convenience, maintainability, and performance.
5. Security Considerations:
When using raw SQL queries, it’s crucial to properly handle user input to prevent SQL injection attacks. Always use parameterized queries or prepared statements to sanitize user input and avoid directly interpolating user input into SQL queries. ORMs often handle input sanitization automatically, so using ORM methods for dynamic queries can provide an added layer of security.
By utilizing both raw SQL queries and ORMs in your project, you can leverage the advantages of each approach. ORMs simplify common operations, provide object mapping, and help with code organization, while raw SQL queries offer flexibility, performance optimization, and access to advanced database features. Choosing the appropriate approach depends on the specific requirements of each operation or query within your project. So, we can conclude that:
- Raw SQL or using an ORM is just a choice that fits your purpose. ORM is not a replacement for raw SQL.
- Lastly, and I think most people underestimate this point, If you are using an ORM because your team is uncomfortable with SQL, this is definitely a wrong decision.
Applying SOLID Principles to Enhance Django ORM
The SOLID principles, put forth by Robert C. Martin, are a set of guidelines that help software developers create software systems that are easier to work with, grow in size, and adapt to changes. These principles are useful in various programming languages and frameworks, including Django. They provide valuable guidance for building software in a way that makes it more manageable and scalable.
- Single Responsibility Principle
SRP suggests that a class should have only one reason to change. In the realm of Django ORM, t SRP encourages us to design smaller, focused models and avoid monolithic models that perform multiple tasks.
To follow SRP in Django ORM, ensure that your Django models serve a single purpose and handle only one type of data. This practice will make it easier to update and maintain your code while also improving query performance.
2. Open/Closed Principle
OCP says that in software, entities should be open for extension but closed for modification. In Django ORM, you can follow this principle by using abstract base classes and mixins. These tools let you add more functions without messing with the existing code. It’s like building on top of what you already have, making your code more flexible and easier to handle in Django ORM.
3. Liskov Substitution Principle
LSP says that objects of a superclass should be able to be replaced with objects of a subclass without affecting the correctness of the program. In Django ORM, this principle can be applied by using model inheritance and ensuring that the inherited models maintain the expected behavior of the base class. For example:
from django.db import models
from django.contrib.auth.models import AbstractUser
class User(AbstractUser):
class Role(models.TextChoices):
HR = "HR", "HR"
MANAGER = "MANAGER", "Manager"
role = models.CharField(max_length=62, choices=Role.choices, null=False)
last_name = models.CharField('last name', max_length=150, blank=True, null=True)
department = models.ForeignKey(Department, on_delete=models.CASCADE, blank=True, null=True)
Here, the User
class inherits from AbstractUser
, which is a Django-provided base class for user-related functionality. It includes fields commonly associated with user management, like username, email, and password.
4. Interface Segregation Principle
ISP emphasizes that clients should not be forced to depend on interfaces they do not use. In Django ORM, this principle can be applied by creating custom model managers and query sets, allowing clients to use only the methods relevant to their needs.
5. Dependency Inversion Principle (DIP)
DIP states that high-level modules should not depend on low-level modules but should depend on abstractions. In Django ORM, this principle can be achieved by using Django’s built-in functionality for customizing model relationships, such as ForeignKey, OneToOneField, and ManyToManyField.
By relying on these abstractions, you decouple your high-level application logic from the low-level details of the database schema, making your code more maintainable and flexible.
from django.db import models
class JobCategory(models.Model):
name = models.CharField("Name", max_length=50)
department = models.CharField("Deparment", max_length=50)
def __str__(self):
return f"Name:{self.name} Department:{self.department}"
@property
def full_category(self):
return self.name + " " + self.department
class Meta:
verbose_name = "Job category"
verbose_name_plural = "Job categories"
class Job(models.Model):
# ... (other fields)
category = models.ForeignKey(JobCategory, related_name="job", on_delete=models.PROTECT)
is_published = models.BooleanField(default=False)
created_at = models.DateTimeField(auto_now=False, auto_now_add=True, null=True, blank=True)
published_at = models.DateTimeField(auto_now=True, auto_now_add=False, null=True, blank=True)
# ... (other fields)
def __str__(self):
return f"Title:{self.title} Created at:{self.created_at}"
class Meta:
db_table = 'job'
The Job
model uses the ForeignKey
abstraction to establish a relationship with the JobCategory
model. This relationship is a higher-level concept, abstracting away the specifics of how the foreign key is implemented in the database.
In summary, the SOLID principles offer a useful foundation for creating Django ORM code that is easier to maintain, scale, and adapt.