ORM (Object Relational Mapping) in Django
medium · Django
The Database Abstraction Engine Django’s Object-Relational Mapper (ORM) bridges the gap between object-oriented Python code and relational database engines. Instead of forcing you to write raw SQL strings, manage manual connection pools, or handle database driver variations, the ORM lets you interact with database tables as standard Python objects. Under the hood, the ORM compiles Python method calls into optimized SQL queries, executes them against your database, and maps the resulting data rows back into fully instantiated Python model instances. Understanding QuerySets: Evaluating Laziness A QuerySet represents a collection of database rows fetched from your database. It is important to know that QuerySets are lazy . Creating a QuerySet does not trigger an immediate trip to the database. Python # No database activity happens here. It simply creates a blueprint of the query. listings = EnterpriseRequisition.objects. filter (priority= 'CR' ) The ORM compiles and executes the SQL query against the database only when the QuerySet is explicitly evaluated . Evaluation triggers include: Iterating over the QuerySet using a loop ( for item in listings: ). Slicing or indexing the collection ( listings[:5] ). Converting the collection to a list ( list(listings) ). Testing the collection inside a conditional statement ( if listings.exists(): ). The Core CRUD Operations Blueprint 1. Create You can write new rows to your database tables using two alternative workflows: Python # Method A: Instantiate and call .save() explicitly new_job = EnterpriseRequisition(title="Staff Architect", allotted_budget=140000) new_job.save() # The SQL INSERT statement executes here # Method B: Direct execution via the model manager new_job = EnterpriseRequisition.objects.create(title="Staff Architect", allotted_budget=140000) 2. Read all() : Pulls all records from the model's database table. get() : Fetches a single, exact record matching your criteria. If no record matches, it crashes with a DoesNotExist error. If multiple rows match, it crashes with a MultipleObjectsReturned error. filter() : Returns a QuerySet containing all records that match the given criteria. If no rows match, it returns an empty QuerySet without throwing an error. Python # Fetches exactly one row by its unique primary key exact_profile = EnterpriseRequisition.objects.get(id=101) # Returns a collection of matches tech_jobs = EnterpriseRequisition.objects.filter(department="Engineering") 3. Update You can update individual object properties or run bulk updates across an entire collection simultaneously: Python # Single row update job = EnterpriseRequisition.objects.get(id=101) job.allotted_budget = 155000 job.save() # Executes a targeted SQL UPDATE statement # Bulk dataset update (Executes a single, optimized SQL query across all matching rows) EnterpriseRequisition.objects.filter(department="Marketing").update(allotted_budget=90000) 4. Delete Removes records from your database tables: Python # Deletes all records matching the query filter EnterpriseRequisition.objects. filter (allotted_budget__lt= 50000 ).delete() Filtering, Field Lookups, & Ordering To filter your data precisely, Django uses a special syntax called Field Lookups . You write these by appending a double underscore ( __ ) and a lookup keyword to your model's field name: __exact / __iexact : Matches the exact value (the i prefix makes the match case-insensitive). __contains / __icontains : Matches if the field contains the substring (compiles to an SQL LIKE %value% query). __gt / __lt : Greater than ( > ) / Less than ( < ) numeric or date comparisons. __gte / __lte : Greater than or equal to ( >= ) / Less than or equal to ( <= ). __in : Matches any value within a provided list or array. Python # Locates listings where titles contain "engineer" (case-insensitive) and budget is >= 120,000 openings = EnterpriseRequisition.objects.filter( title__icontains="engineer", allotted_budget__gte=120000 ) Ordering Results Use order_by() to sort your query results. Prefixing a field name with a minus character (-) sorts the records in descending order: Python # Sorts records primarily by date established (newest first), then alphabetically by title sorted_pool = EnterpriseRequisition.objects.all().order_by('-date_established', 'title') Aggregations vs. Annotations While both functions execute mathematical summaries (like sum, average, or count), they calculate and output their results differently. [Image diagram contrasting Aggregation summarizing a column into a single dictionary payload against Annotation appending a new computed column property onto every row instance] 1. Aggregation ( aggregate() ) Calculates a mathematical summary across an entire table column, reducing the entire QuerySet down into a single Python dictionary payload. Python from django.db.models import Avg, Max # Calculates data averages across the entire table budget_summary = EnterpriseRequisition.objects.aggregate( average_pay=Avg('allotted_budget'), maximum_pay=Max('allotted_budget') ) # Returns a plain dictionary: {'average_pay': 115000.0, 'maximum_pay': 210000.0} 2. Annotation ( annotate() ) Calculates summaries for each row in a QuerySet, dynamically appending a new computed property onto every individual object record returned. Think of it as adding a temporary, calculated column to your query results. Python from django.db.models import Count # Annotates every team record with a count of its related job openings teams_with_counts = EnterpriseTeam.objects.annotate(total_openings=Count('jobopening')) for team in teams_with_counts: # Every object instance now has access to the dynamic 'total_openings' attribute print(f"Team {team.name} has {team.total_openings} open slots.") Advanced ORM Primitives: Q Objects & F Expressions 1. Q Objects (Complex Logical OR/NOT Queries) By default, passing multiple comma-separated arguments into a .filter() method combines them using a logical AND condition. If you need to build complex queries using logical OR ( | ) or logical NOT ( ~ ) operations, you use Q objects . Python from django.db.models import Q # Matches records if the title contains "Director" OR if the priority flag is set to Critical target_jobs = EnterpriseRequisition.objects.filter( Q(title__icontains="Director") | Q(priority="CR") ) # Matches records if the title contains "Manager" AND the status is NOT archived active_managers = EnterpriseRequisition.objects.filter( Q(title__icontains="Manager") & ~Q(priority="AR") ) 2. F Expressions (Database-Level Attributes & Core Race Protection) Standard database updates require pulling a record into your application server memory, modifying the value with Python, and saving it back to the database. This pattern can cause data corruption if two users attempt to update the same record at the exact same millisecond (a Race Condition ). An F expression instructs the database engine to perform the math operation directly on the database server hardware itself, without loading the value into Python memory first. Python from django.db.models import F # Increments the employee count for all teams directly inside the database # Compiles to SQL: UPDATE enterprise_teams SET employee_count = employee_count + 1; EnterpriseTeam.objects.all().update(employee_count=F('employee_count') + 1) You can also use F expressions to filter records by comparing two different fields on the same row: Python # Locates teams that have exceeded their authorized capacity threshold over_capacity_teams = EnterpriseTeam.objects.filter(employee_count__gt=F('max_allowed_capacity')) Executing Raw SQL Gateways If you need to optimize a highly complex query or execute specialized database features that Django's standard ORM syntax doesn't support, you can bypass the abstraction layer and write raw SQL queries directly. Using .raw() for Model Mapping If your SQL statement returns columns that map cleanly to your model's fields, use .raw() . This returns an iterator populated with fully functional model instances. Python # Executing raw SQL statements directly while preserving model field mapping structures raw_query = "SELECT * FROM corporate_requisitions_registry WHERE allotted_budget > %s" matching_jobs = EnterpriseRequisition.objects.raw(raw_query, [150000]) for job in matching_jobs: print(job.title) (Always pass user inputs as parameters using the secondary parameters array argument rather than format strings to protect your application from SQL Injection vectors). Custom Managers A Manager is the interface through which Django wraps its ORM query methods around a model class (accessible via MyModel.objects ). You can build a Custom Manager to encapsulate repetitive filters into clean, reusable semantic methods. Python # managers.py (Encapsulating Custom Reusable Querysets) from django.db import models class ActiveCriticalManager(models.models.Manager): # Overriding or appending custom filter short-cuts def critical_openings(self): # 'self.get_queryset()' returns the base table collection array link return self.get_queryset().filter(priority='CR').exclude(allotted_budget=0) To attach your custom manager to a model, register it as a class attribute: Python # models.py from django.db import models from .managers import ActiveCriticalManager class EnterpriseRequisition(models.Model): title = models.CharField(max_length=100) priority = models.CharField(max_length=2) allotted_budget = models.DecimalField(max_digits=10, decimal_places=2) # 1. Preserve the default generic manager framework access hook objects = models.Manager() # 2. Wire up the specialized custom query manager panel alerts = ActiveCriticalManager() You can now invoke your custom manager directly within your application controllers, making your code cleaner and more semantic: Python # Clean, readable query invocation powered by your custom manager class immediate_actions_list = EnterpriseRequisition.alerts.critical_openings()