Advanced Models - Python Django Tutorials

Advanced Models

In this chapter, we’re going to dig much deeper into Django’s models and comprehensively explore the essentials of Django’s models.

In the first section of the chapter, we’ll all the common data management functions that come built in to Django. We’ll cover common model methods that return QuerySets and those that don’t, model field lookups, aggregate functions and building complex queries.

In later sections of the chapter, we’ll cover adding and overriding model managers and model methods and have a look at how model inheritance works in Django.

Working With Data

Django’s QuerySet API provides a comprehensive array of methods and functions for working with data. In this section of the chapter, we’re going to look at all the common QuerySet methods, field lookups and aggregate functions, as well as building more complex queries with query expressions and Q() objects.

Methods That Return QuerySets

Method Description
filter() Filter by the given lookup parameters. Multiple parameters are joined by SQL AND statements (See Chapter 4)
exclude() Filter by objects that don’t match the given lookup parameters
annotate() Annotate each object in the QuerySet. Annotations can be simple values, a field reference or an aggregate expression
order_by() Change the default ordering of the QuerySet
reverse() Reverse the default ordering of the QuerySet
distinct() Perform an SQL SELECT DISTINCT query to eliminate duplicate rows
values() Returns dictionaries instead of model instances
values_list() Returns tuples instead of model instances
dates() Returns a QuerySet containing all available dates in the specified date range
datetimes() Returns a QuerySet containing all available dates in the specified date and time range
none() Create an empty QuerySet
all() Return a copy of the current QuerySet
union() Use the SQL UNION operator to combine two or more QuerySets
intersection() Use the SQL INTERSECT operator to return the shared elements of two or more QuerySets
difference() Use the SQL EXCEPT operator to return elements in the first QuerySet that are not in the others
select_related() Select all related data when executing the query (except many-to-many relationships)
prefetch_related() Select all related data when executing the query (including many-to-many relationships)
defer() Do not retrieve the names fields from the database. Used to improve query performance on complex datasets
only() Opposite of defer()—return only the name fields
using() Select which database the QuerySet will be evaluated against (when using multiple databases)
select_for_update() Return a QuerySet that will lock rows until the end of the transaction
raw() Execute a raw SQL statement
AND (&) Combine two QuerySets with the SQL AND operator. Using AND (&) is functionally equivalent to using filter() with multiple parameters
OR (|) Combine two QuerySets with the SQL OR operator

Table 9-1: Model methods that return QuerySets

Let’s go ahead and use the Django interactive shell to explore a few examples of the more common QuerySet methods not already covered in the book.

exclude()

exclude() will return a QuerySet of objects that don’t match the given lookup parameters, for example:

>>> from events.models import Venue
>>> Venue.objects.exclude(name="South Stadium")
<QuerySet [<Venue: West Park>, <Venue: North Stadium>, <Venue: East Park>]>

Using more than one lookup parameter will use an SQL AND operator under the hood:

>>> from events.models import Event
>>> import datetime
>>> venue1 = Venue.objects.get(name="East Park")
>>> Event.objects.exclude(venue=venue1,event_date=datetime.date(2019,10,8))
<QuerySet [<Event: Test Event>, <Event: Club Presentation - Juniors>, <Event: Club Presentation - Seniors>, <Event: Gala Day>]>

The extra step in this example is because Venue is a Foreign Key to the Event model, so we first have to retrieve a Venue object.

annotate()

Annotations can be simple values, a field reference or an aggregate expression. For example, let’s use Django’s Count aggregate function to annotate our Event model with a total of all users attending each event:

>>> from events.models import Event
>>> from django.db.models import Count
>>> qry = Event.objects.annotate(total_attendees=Count('attendees'))
>>> for event in qry:
...     print(event.name, event.total_attendees) 
... 
Test Event 0
Gala Day 2
Club Presentation - Juniors 5
Club Presentation - Seniors 3
>>>

order_by() and reverse()

order_by() changes the default ordering of the QuerySet. Function parameters are the model fields to use to order the QuerySet. Ordering can be single level:

>>> from events.models import Event
>>> Event.objects.all().order_by('name')
<QuerySet [<Event: Club Presentation - Juniors>, <Event: Club Presentation - Seniors>, <Event: Gala Day>, <Event: Test Event>]>

Or ordering can be multi-level. In the following example, the events are first ordered by event date and then by event name:

>>> Event.objects.all().order_by('event_date','name')  
<QuerySet [<Event: Club Presentation - Juniors>, <Event: Club Presentation - Seniors>, <Event: Gala Day>, <Event: Test Event>]>

By default, QuerySet fields are ordered in ascending order. To sort in descending order, use the negative sign (-):

>>> Event.objects.all().order_by('-name') 
<QuerySet [<Event: Test Event>, <Event: Gala Day>, <Event: Club Presentation - Seniors>, <Event: Club Presentation - Juniors>]>

reverse() reverses the default ordering of the QuerySet:

>>> Event.objects.all().reverse()                     
<QuerySet [<Event: Test Event>, <Event: Club Presentation - Juniors>, <Event: Club Presentation - Seniors>, <Event: Gala Day>]>

Note that a model must have default ordering (by setting the ordering option of the models Meta class) for reverse() to be useful. If the model is unordered, the sort order of the returned QuerySet will be meaningless.

Also note that both order_by() and reverse() are not free operations—they come at a time cost to your database and should be used sparingly on large datasets.

values() and values_list()

values() returns Python dictionaries, instead of a QuerySet object:

>>> Event.objects.values()       
<QuerySet [{'id': 1, 'name': 'Test Event', 'event_date': datetime.datetime(2019, 8, 25, 22, 42, 15, tzinfo=<UTC>), 'venue_id': 1, 'manager_id': 1, 'description': "It's all happening here!"}, {'id': 2, 'name': 'Club Presentation - Juniors', 'event_date': datetime.datetime(2019, 8, 1, 12, 0, tzinfo=<UTC>), 'venue_id': 4, 'manager_id': 2, 'description': ''}]>

You can also specify which fields you want returned:

>>> Event.objects.values('name','description') 
<QuerySet [{'name': 'Test Event', 'description': "It's all happening here!"}, {'name': 'Club Presentation - Juniors', 'description': ''}]>

values_list() is the same as values(), except it returns tuples:

>>> Event.objects.values_list() 
<QuerySet [(1, 'Test Event', datetime.datetime(2019, 8, 25, 22, 42, 15, tzinfo=<UTC>), 1, 1, "It's all happening here!"), (2, 'Club Presentation - Juniors', datetime.datetime(2019, 8, 1, 12, 0, tzinfo=<UTC>), 4, 2, '')]>

You can also specify which fields to return:

>>> Event.objects.values_list('name')
<QuerySet [('Test Event',), ('Club Presentation - Juniors',)]>
>>>

dates() and datetimes()

You use the dates() and datetimes() methods to return time-bounded records from the database (for example, all the events occuring in a particular month). For dates(), these time bounds are year, month, week and day. datetimes() adds hour, minute and second bounds. Some examples:

>>> from events.models import Event
>>> Event.objects.dates('event_date', 'year')
<QuerySet [datetime.date(2019, 1, 1)]>
>>> Event.objects.dates('event_date', 'month') 
<QuerySet [datetime.date(2019, 8, 1)]>
>>> Event.objects.dates('event_date', 'week')  
<QuerySet [datetime.date(2019, 7, 29), datetime.date(2019, 8, 5), datetime.date(2019, 8, 19)]>
>>> Event.objects.dates('event_date', 'day')  
<QuerySet [datetime.date(2019, 8, 1), datetime.date(2019, 8, 10), datetime.date(2019, 8, 11), datetime.date(2019, 8, 25)]>
>>>

select_related() and prefetch_related()

Selecting related information can be a database-intensive operation as each foreign key relationship requires an additional database lookup. For example, each Event object in our database has a foreign key realtionship with the Venue table:

>>> event1 = Event.objects.get(id=1)
>>> event1.venue # Foreign key retrieval causes additional database hit
<Venue: South Stadium>

For our simple example, this is not a problem, but in large databases with many foreign key relationships, the load on the database can be prohibitive.

You use select_related() to improve database performance by retrieving all related data the first time the database is hit:

>>> event2 = Event.objects.select_related('venue').get(id=2) 
>>> event2.venue # venue has already been retrieved. Database is not hit again.
<Venue: East Park>
>>>

prefetch_related() works the same way as select_related(), except it will work across many-to-many relationships.

Methods That Don’t Return QuerySets

Method Description
get() Returns a single object. Throws an error if lookup returns multiple objects
create() Shortcut method to create and save an object in one step
get_or_create() Returns a single object. If the object doesn’t exist, it creates one
update_or_create() Updates a single object. If the object doesn’t exist, it creates one
bulk_create() Insert a list of objects in the database
bulk_update() Update given fields in the listed model instances
count() Count the number of objects in the returned QuerySet. Returns an integer
in_bulk() Return a QuerySet containing all objects with the listed IDs
iterator() Evaluate a QuerySet and return an iterator over the results. Can improve performance and memory use for queries that return a large number of objects
latest() Return the latest object in the database table based on the given field(s)
earliest() Return the earliest object in the database table based on the given field(s)
first() Return the first object matched by the QuerySet
last() Return the last object matched by the QuerySet
aggregate() Return a dictionary of aggregate values calculated over the QuerySet
exists() Returns True if the QuerySet contains any results
update() Performs an SQL UPDATE on the specified field(s)
delete() Performs an SQL DELETE that deletes all rows in the QuerySet
as_manager() Return a Manager class instance containing a copy of the QuerySet’s methods
explain() Returns a string of the QuerySet’s execution plan. Used for analysing query performance

Table 9-2: Model methods That Don’t Return QuerySets

Let’s return to the Django interactive shell to dig deeper into some common examples not already covered in the book.

get_or_create()

get_or_create() will attempt to retrieve a record matching the search fields. If a record doesn’t exist, it will create one. The return value will be a tuple—the created or retrieved object and a boolean value that will be True if a new record was created:

>>> from events.models import MyclubUser 
>>> usr, boolCreated = MyclubUser.objects.get_or_create(first_name='John', last_name='Jones', email='johnj@example.com')
>>> usr
<MyclubUser: John Jones>
>>> boolCreated
True

If we try and create the object a second time, it will retrieve the new record from the database instead.

>>> usr, boolCreated = MyclubUser.objects.get_or_create(first_name='John', last_name='Jones', email='johnj@example.com')
>>> usr
<MyclubUser: John Jones>
>>> boolCreated
False

update_or_create()

update_or_create() works similar to get_or_create(), except you pass the search fields and a dictionary named defaults that contains the fields to update. If the object doesn’t exist, it will create a new record in the database:

>>> usr, boolCreated = MyclubUser.objects.update_or_create(first_name='Mary', last_name='Jones', defaults={'email':'maryj@example.com'}) 
>>> usr
<MyclubUser: Mary Jones>
>>> boolCreated
True

If the record does exist, Django will update all fields listed in the defaults dictionary:

>>> usr, boolCreated = MyclubUser.objects.update_or_create(first_name='Mary', last_name='Jones', defaults={'email':'mary_j@example.com'})    
>>> usr
<MyclubUser: Mary Jones>
>>> usr.email
'mary_j@example.com'
>>> boolCreated
False
>>>

bulk_create() and bulk_update()

The bulk_create() saves time by inserting multiple objects into the database at once, most often in a single query. The function has on required parameter—a list of objects:

>>> usrs = MyclubUser.objects.bulk_create([
...     MyclubUser(first_name='Jane', last_name='Smith', email='janes@example.com'),
...     MyclubUser(first_name='Steve', last_name='Smith', email='steves@example.com'),
... ])

bulk_update() on the other hand takes a list of model objects and updates individual fields on selected model instances. For example, let’s say the first two “Smiths” in the database were entered incorrectly. First, we retrieve all the “Smiths”:

>>> usrs = MyclubUser.objects.filter(last_name='Smith')
>>> usrs
<QuerySet [<MyclubUser: Joe Smith>, <MyclubUser: Jane Smith>, <MyclubUser: Steve Smith>]>

We can then modify the last_name field on the first two instances and use the bulk_update function to save the changes to the database in a single query:

>>> usrs[0].last_name = 'Smythe' 
>>> usrs[1].last_name = 'Smythe'
>>> MyclubUser.objects.bulk_update(usrs, ['last_name'])
>>> usrs
<QuerySet [<MyclubUser: Joe Smythe>, <MyclubUser: Jane Smythe>, <MyclubUser: Steve Smith>]>
>>>

count()

Count the number of objects in the QuerySet. Can be used to count all the objects in a database table:

>>> MyclubUser.objects.count()
9

Or used to count the number of objects returned by a query:

>>> MyclubUser.objects.filter(last_name='Smythe').count() 
2

count() is functionally equivalent to using the aggregate() function, for example:

>>> MyclubUser.objects.all().aggregate(Count('id'))
{'id__count': 9}

But count() has a cleaner syntax and is likely to be faster on larger datasets.

in_bulk()

in_bulk() takes a list of id values and returns a dictionary mapping each id to an instance of the object with that id. If you don’t pass a list to in_bulk(), all objects will be returned:

>>> MyclubUser.objects.in_bulk()
{1: <MyclubUser: Joe Smythe>, 2: <MyclubUser: Jane Doe>, 3: <MyclubUser: John Jones>}

Once retrieved, you can access each object by their key value:

>>> usrs[3]
<MyclubUser: John Jones>
>>> usrs[3].first_name   
'John'

Any non-empty list will retrieve all records with the listed ids:

>>> MyclubUser.objects.in_bulk([1]) 
{1: <MyclubUser: Joe Smythe>}

List ids don’t have to be sequential either:

>>> MyclubUser.objects.in_bulk([1, 3, 7]) 
{1: <MyclubUser: Joe Smythe>, 3: <MyclubUser: John Jones>, 7: <MyclubUser: Mary Jones>}

latest() and earliest()

Return the latest or the earliest date in the database for the provided field(s):

>>> from events.models import Event
>>> Event.objects.latest('event_date')
<Event: Test Event>
>>> Event.objects.earliest('event_date') 
<Event: Club Presentation - Juniors>

first() an last()

Return the first or last object in the QuerySet:

>>> Event.objects.first()
<Event: Test Event>
>>> Event.objects.last()  
<Event: Gala Day>

aggregate()

Returns a dictionary of aggregate values calculated over the QuerySet. For example:

>>> from django.db.models import Count
>>> Event.objects.aggregate(Count('attendees'))
{'attendees__count': 7}
>>>

For a list of all aggregate functions available in Django, see Aggregate Functions later in this chapter.

exists()

Returns True if the returned QuerySet contains any objects, False if the QuerySet is empty. There are two common use cases—to check if an object is contained in another QuerySet:

>>> from events.models import MyclubUser

# Let's retrieve John Jones from the database
>>> usr = MyclubUser.objects.get(first_name='John', last_name='Jones')

# And check to make sure he is one of the Joneses
>>> joneses = MyclubUser.objects.filter(last_name='Jones') 
>>> joneses.filter(pk=usr.pk).exists() 
True

And to check if a query returns an object:

>>> joneses.filter(first_name='Mary').exists()
True
>>> joneses.filter(first_name='Peter').exists() 
False
>>>

Field Lookups

Field lookups have a simple double-underscore syntax:

<searchfield>__<lookup>

For example:

>>> MyclubUser.objects.filter(first_name__exact="Sally")
<QuerySet [<MyclubUser: Sally Jones>]>
>>> MyclubUser.objects.filter(first_name__contains="Sally") 
<QuerySet [<MyclubUser: Sally Jones>, <MyclubUser: Sally-Anne Jones>]>

A complete list of Django’s field lookups is in Table 9-3.

Under the hood, Django creates SQL WHERE clauses to construct database queries from the applied lookups. Multiple lookups are allowed and field lookups can also be chained (where logical):

>>> from events.models import Event

# Get all events in 2019 that occur before September
>>> Event.objects.filter(event_date__year=2019, event_date__month__lt=9)
<QuerySet [<Event: Test Event>, <Event: Club Presentation - Juniors>, <Event: Club Presentation - Seniors>, <Event: Gala Day>]>
>>>

# Get all events occuring on or after the 10th of the month
>>> Event.objects.filter(event_date__day__gte=10) 
<QuerySet [<Event: Test Event>, <Event: Club Presentation - Seniors>, <Event: Gala Day>]>
>>>
Filter Description
exact/iexact Exact match. iexact is case-insensitive version
contains/icontains Field contains search text. icontains is case-insensitive version
in In a given iterable (list, tuple or QuerySet)
gt/gte Greater than/greater than or equal
lt/lte Less than/less than or equal
startswith/istartswith Starts with search string. istartswith is case-insensitive version
endswith/iendswith Ends with search string. iendswithis case-insensitive version
range Range test. Range includes start and finish values
date Casts the value as a date. For datetime field lookups
year Searches an exact year match
iso_year Searches an exact ISO 8601 year match
month Searches an exact month match
day Searches an exact day match
week Searches an exact week match
week_day Searches an exact day of the week match
quarter Searches an exact quarter of the year match. Valid integer range: 1–4
time Casts the value as a time. For datetime field lookups
hour Searches an exact hour match
minute Searches an exact minute match
second Searches an exact second match
isnull Search if field is null. Takes True or False
regex/iregex Regular expression match. iregex is case-insensitive version

Table 9-3: Model field lookups

Aggregate Functions

Django includes seven aggregate functions:

  • Avg. Returns the mean value of the expression
  • Count. Count the number of returned objects
  • Max. Returns the maximum value of the expression
  • Min. Returns the minimum value of the expression
  • StdDev. Returns the population standard deviation of the data in the expression
  • Sum. Returns the sum of all values in the expression
  • Variance. Returns the population variance of the data in the expression

They are translated to the equivalent SQL by Django’s ORM.

Aggregate functions can either be used directly:

>>> from events.models import Event
>>> Event.objects.count()
4

Or with the aggregate() function:

>>> from django.db.models import Count 
>>> Event.objects.aggregate(Count('id'))
{'id__count': 4}
>>>

More Complex Queries

Query Expressions

Query expressions describe a computation or value used as a part of another query. There are six built-in query expressions:

  • F()—Represents the value of a model field or annotated column
  • Func()—Base type for database functions like LOWER and SUM
  • Aggregate()—All aggregate functions inherit from Aggregate()
  • Value()—Expression value. Not used directly
  • ExpressionWrapper()—Used to wrap expressions of different types
  • SubQuery()—Add a subquery to a QuerySet

Django supports multiple arithmetic operators with query expressions, including:

  • Addition and subtraction
  • Multiplication and division
  • Negation
  • Modulo arithmetic; and
  • The power operator

We have already covered aggregation in this chapter, so let’s have a quick look at the other two commonly used query expressions: F() and Func().

F() Expressions

The two primary uses for F() expressions is to move computational arithmetic from Python to the database and to reference other fields in the model.

Let’s start with a simple example, say we want to delay the first event in the event calendar by two weeks. A conventional approach would look like this:

>>> from events.models import Event
>>> import datetime
>>> e = Event.objects.get(id=1)
>>> e.event_date += datetime.timedelta(days=14)
>>> e.save()

In this example, Django retrieves the information from the database into memory, uses Python to perform the computation—in this case, add 14 days to the event date—and then saves the record back to the database.

For this example, the overhead for using Python to perform the date arithmetic is not onerous, however for more complex queries there is a definite advantage to moving the computational load to the database.

Now let’s see how we accomplish the same task with an F() expression:

>>> from django.db.models import F
>>> e = Event.objects.get(id=1)    
>>> e.event_date = F('event_date') + datetime.timedelta(days=14)
>>> e.save()

In terms of the amount of code necessary to complete the task, we haven’t saved anything, however, by using the F() expression, Django will now create an SQL query that performs the computational logic inside the database, rather than in memory with Python.

While this takes a huge load off the Django application in the case of executing complex computations, there is one drawback—because the calculations take place inside the database, Django is now out of sync with the updated state of the database. We can test this by looking at the Event object instance:

>>> e.event_date 
<CombinedExpression: F(event_date) + DurationValue(14 days, 0:00:00)>

To retrieve the updated object from the database, we need to use the refresh_from_db() function:

>>> e.refresh_from_db()
>>> e.event_date
datetime.datetime(2019, 9, 8, 22, 42, 15, tzinfo=datetime.timezone(datetime.timedelta(0), '+0000'))

The second use for F() expressions—referencing other model fields—is straight forward. For example, you can check for users with the same first and last name:

>>> MyclubUser.objects.filter(first_name=F('last_name'))
<QuerySet [<MyclubUser: Don Don>]>

This simple syntax works with all of Django’s field lookups and aggregate functions.

Func() Expressions

Func() expressions can be used to represent any function supported by the underlying database (e.g. LOWER, UPPER, LEN, TRIM, CONCAT, etc.). For example:

>>> from events.models import MyclubUser
>>> from django.db.models import F, Func
>>> qry = usrs.annotate(f_upper=Func(F('last_name'), function='UPPER')) 
>>> for usr in qry:
...     print(usr.first_name, usr.f_upper)
... 
Joe SMYTHE
Jane DOE
John JONES
Sally JONES
Sally-Anne JONES
Sarah JONES
Mary JONES
Jane SMYTHE
Steve SMITH
Don DON
>>>

Notice how we are using F() expressions again to reference another field in the MyclubUser model.

Q() Objects

Like F() expressions, a Q() object encapsulates an SQL expression inside a Python object. Q() objects are most often used to construct complex database queries by chaining together multiple expressions using AND (&) and OR (|) operators:

>>> from events.models import MyclubUser 
>>> from django.db.models import Q
>>> Q1 = Q(first_name__startswith='J')
>>> Q2 = Q(first_name__endswith='e') 
>>> MyclubUser.objects.filter(Q1 & Q2)
<QuerySet [<MyclubUser: Joe Smythe>, <MyclubUser: Jane Doe>, <MyclubUser: Jane Smythe>]>
>>> MyclubUser.objects.filter(Q1 | Q2) 
<QuerySet [<MyclubUser: Joe Smythe>, <MyclubUser: Jane Doe>, <MyclubUser: John Jones>, <MyclubUser: Sally-Anne Jones>, <MyclubUser: Jane Smythe>, <MyclubUser: Steve Smith>]>
>>>

You can also perform NOT queries using the negate (~) character:

>>> MyclubUser.objects.filter(~Q2)      
<QuerySet [<MyclubUser: John Jones>, <MyclubUser: Sally Jones>, <MyclubUser: Sarah Jones>, <MyclubUser: Mary Jones>, <MyclubUser: Don Don>]>
>>> MyclubUser.objects.filter(Q1 & ~Q2) 
<QuerySet [<MyclubUser: John Jones>]>

Model Managers

A Manager is a Django class that provides the interface between database query operations and a Django model. Each Django model is provided with a default Manager named objects. We have been using the default manager in Chapter 4 and in this chapter every time we query the database, for example:

>>> newevent = Event.objects.get(name="Xmas Barbeque")

and:

>>> joneses = MyclubUser.objects.filter(last_name='Jones')

In each example, objects is the default Manager for the model instance.

You can customize the default Manager class, by extending the base Manager class for the model. The two most common use-cases for customizing the default manager are:

  1. Adding extra manager methods; and
  2. Modifying initial QuerySet results.

Adding Extra Manager Methods

Extra manager methods add table-level functionality to models. To add row-level functions, i.e. methods that act on single instances of the model, you use model methods, which we cover in the next section of the chapter.

Extra manager methods are created by inheriting the Manager base class and adding custom functions to the custom Manager class. For example, let’s create an extra manager method for the Event model that retrieves the total number of events for a particular type of event (e.g. gala day or presentation):

# myclub_rooteventsmodels.py

from django.db import models
from django.contrib.auth.models import User

# ...

1  class EventManager(models.Manager):
2      def event_type_count(self, event_type):
3          return self.filter(name__icontains=event_type).count()
4  
5  
6  class Event(models.Model):
7      name = models.CharField('Event Name', max_length=120)
8      event_date = models.DateTimeField('Event Date')
9      venue = models.ForeignKey(Venue, on_delete=models.CASCADE)
10     manager = models.ForeignKey(User, blank=True, null=True, on_delete=models.SET_NULL)
11     attendees = models.ManyToManyField(MyclubUser)
12     description = models.TextField(blank=True)
13     objects = EventManager()
14
15     def __str__(self):
16         return self.name

Let’s have a look at this partial listing from your events app’s models.py file:

  • In Line 1, we’ve entered a new class called EventManager that inherits from Django’s models.Manager base class.
  • Lines 2 and 3 is the extra Manager method we’re adding to the model. This new method returns the total number of the specified event type. Note we’re using the icontains field lookup to return all events that have the key phrase in the title.
  • In Line 13 we’re replacing the default manager with our new EventManager class. Note that EventManager inherits from the Manager base class, so all the default manager methods like all() and filter() are automatically included in the custom EventManager() class.

Once it has been created, you can use your new manager method just like any other model menthod:

>>> from events.models import Event
>>> Event.objects.event_type_count('Gala Day')
1
>>> Event.objects.event_type_count('Presentation') 
2

Renaming the Default Model Manager

While the base manager for each model is named objects by default, you can change the name of the default manager in your class declaration. For example, to change the default manager name for our Event class from objects to events, we just need to change line 13 in the code above from:

13     objects = EventManager()

To:

13     events = EventManager()

Now you can refer to the default manager like so:

>>> from events.models import Event 
>>> Event.events.all()
<QuerySet [<Event: Test Event>, <Event: Club Presentation - Juniors>, <Event: Club Presentation - Seniors>, <Event: Gala Day>]>
>>> 

Overriding Initial Manager QuerySets

To modify what is returned by the default manager QuerySet, you override the Manager.get_queryset() method. This is easiest to understand with an example. Let’s say we commonly have to check what venues are listed in our local city. To cut down on the number of queries we have to write, we’re going to create a custom manager for our Venue model (changes in bold):

# myclub_rooteventsmodels.py

1  from django.db import models
2  from django.contrib.auth.models import User
3  
4  
5  class VenueManager(models.Manager):
6      def get_queryset(self):
7          return super(VenueManager, self).get_queryset().filter(zip_code='00000')
8  
9  
10 class Venue(models.Model):
11     name = models.CharField('Venue Name', max_length=120)
12     address = models.CharField(max_length=300)
13     zip_code = models.CharField('Zip/Post Code', max_length=12)
14     phone = models.CharField('Contact Phone', max_length=20, blank=True)
15     web = models.URLField('Web Address', blank=True)
16     email_address = models.EmailField('Email Address',blank=True)
17 
18     venues = models.Manager()
19     local_venues = VenueManager()
20 
21     def __str__(self):
22        return self.name

# ...

Let’s take a look at the changes:

  • Lines 5 to 7 is the new VenueManager class. The structure is the same as the EventManager class, except this time we’re overriding the default get_queryset() method and returning a filtered list that only contains local venues.
  • In Line 18 I have renamed the default manager to venues
  • In Line 19 we’re adding the custom model manager (VenueManager)

Note that there is no limit to how many custom managers you can add to a Django model instance. This makes creating custom filters for commonly used queries a breeze. Once you have save the models.py file, you can use the custom methods in your code. For example, the default manager method has been renamed, so you can use the more intuitive venues, instead of objects:

>>> Venue.venues.all()  
<QuerySet [<Venue: South Stadium>, <Venue: West Park>, <Venue: North Stadium>, <Venue: East Park>]>

And our new custom manager is also easily accessible:

>>> from events.models import Venue
>>> Venue.local_venues.all()
<QuerySet [<Venue: West Park>]>
>>>

Model Methods

Django’s Model class comes with many built-in methods. We have already used many of them—save(), delete(), __str__() and others. Where manager methods add table-level functionality to Django’s models, model methods add row-level functions that act on individual instances of the model.

There are two common cases where you want to play with model methods:

  1. When you want to add business logic to the model by adding custom model methods; and
  2. When you want to override the default behavior of a built-in model method.

Custom Model Methods

As always, it’s far easier to understand how custom model methods work by writing a couple, so let’s go ahead and modify our Event class (changes in bold):

# myclub_rooteventsmodels.py

# ...

1  class Event(models.Model):
2      name = models.CharField('Event Name', max_length=120)
3      event_date = models.DateTimeField('Event Date')
4      venue = models.ForeignKey(Venue, on_delete=models.CASCADE)
5      manager = models.ForeignKey(User, blank=True, null=True, on_delete=models.SET_NULL)
6      attendees = models.ManyToManyField(MyclubUser)
7      description = models.TextField(blank=True)
8      events = EventManager()
9  
10     def event_timing(self, date):
11         if self.event_date > date:
12             return "Event is after this date"
13         elif self.event_date == date:
14             return "Event is on the same day"
15         else:
16             return "Event is befor this date"
17 
18     @property
19     def name_slug(self):
20         return self.name.lower().replace(' ','-')
21 
22     def __str__(self):
23         return self.name

Let’s have a look at what’s happening with this new code:

  • In Line 10 I have added a new method called event_timing. This is a straight forward method that compares the event date to the date passed to the method and returns a message stating whether the event occurs before, on or after the date.
  • In Line 19 I have added another custom method that returns a slugified event name. The @property decorator on Line 18 allows us to access the method directly, like an attribute. Without the @property, you would have to use a method call (name_slug()).

Let’s test these new methods out in the Django interactive interpreter. Don’t forget to save the model before you start!

First, the name_slug method:

>>> from events.models import Event 
>>> events = Event.events.all()
>>> for event in events:
...     print(event.name_slug)
... 
test-event
club-presentation---juniors
club-presentation---seniors
gala-day

This should be easy to follow. Notice how the @property decorator allows us to access the method directly like it was an attribute. I.e. event.name_slug instead of event.name_slug().

Now to test the event_timing method:

>>> from datetime import datetime, timezone
>>> e = Event.events.get(name="Gala Day")
>>> e.event_timing(datetime.now(timezone.utc))          
'Event is befor this date'
>>>

Too easy.

Overriding Default Model Methods

It’s common to want to override built-in model methods like save() and delete() to add business logic to default database behavior.

To override a built-in model method you define a new method with the same name. For, example, let’s override the Event model’s default save() method to automatically assign management of the event to a staff member (changes in bold):

# myclub_rooteventsmodels.py

# ...

1  class Event(models.Model):
2      name = models.CharField('Event Name', max_length=120)
3      event_date = models.DateTimeField('Event Date')
4      venue = models.ForeignKey(Venue, on_delete=models.CASCADE)
5      manager = models.ForeignKey(User, blank=True, null=True, on_delete=models.SET_NULL)
6      attendees = models.ManyToManyField(MyclubUser)
7      description = models.TextField(blank=True)
8      events = EventManager()
9  
10     def save(self, *args, **kwargs):
11         self.manager = User.objects.get(username='abbyb')
12         super(Event, self).save(*args, **kwargs)
# ...

The new save() method starts on Line 10. In the overridden save() method, we’re first assigning the staff member with the username “abbyb” to the manager field of the model instance (Line 11), and then we call the default save() method with the super() function to save the model instance to the database (Line 12).

Once you save your models.py file, you can test out the overridden model method in the Django interactive shell:

>>> from events.models import Event
>>> from events.models import Venue
>>> from datetime import datetime, timezone
>>> v = Venue.venues.get(id=1)
>>> e = Event.events.create(name='New Event', event_date=datetime.now(timezone.utc), venue=v)

Once the new record has been created, you can test to see if your override worked by checking the manager field of the Event object:

>>> e.manager
<User: abbyb>
>>>

Model Inheritance

Models are simply Python classes, so inheritance works the same way as normal Python class inheritance. The two most common forms of model inheritance in Django are:

  1. Multi-table inheritance, where each model has its own database table; and
  2. Abstract base classes, where the parent model holds information common to all its child classes, but doesn’t have a database table.

You can also create proxy models that modify the Python-level behavior of a model without modifying the underlying model fields, however, we won’t be covering them here. See the Django documentation for more information on proxy models.

Multi-table Inheritance

Multi-table inheritance is straight forward—the parent class is a normal model and the child inherits the parent by declaring the parent class in the child class declaration. For example:

class MyclubUser(models.Model):
    first_name = models.CharField(max_length=30)
    last_name = models.CharField(max_length=30)
    email = models.EmailField('User Email')

    def __str__(self):
        return self.first_name + " " + self.last_name


class Subscriber(MyclubUser):
    date_joined = models.DateTimeField()

The parent model in the example is the MyclubUser model from our events app. The Subscriber model inherits from MyclubUser and adds an additional field (date_joined). As they are both standard Django model classes, a database table will be created for each model (Figure 9-1).

Figure 9-1: Database tables are created for both the parent and the child model.

Abstract Base Classes

Abstract base classes are handy when you want to put common information into other models without having to create a database table for the base class.

You create an abstract base class by adding the abstract = True class Meta option (Line 7 in this example):

1  class UserBase(models.Model):
2      first_name = models.CharField(max_length=30)
3      last_name = models.CharField(max_length=30)
4      email = models.EmailField('User Email')
5  
6      class Meta:
7          abstract = True
8          ordering = ['last_name']
9  
10 
11 class MyclubUser(UserBase):
12     def __str__(self):
13         return self.first_name + " " + self.last_name
14 
15 
16 class Subscriber(UserBase):
17     date_joined = models.DateTimeField()

Abstract base classes are also useful for declaring class Meta options that are inherited by all child models (Line 8).

As the MyclubUser model from our events app now inherits the first name, last name and email fields from UserBase, it only needs to declare the __str__() function to behave the same way as the original MyclubUser model we created earlier.

This example is very similar to the example for multi-table inheritance in the previous section, and if you saved and migrated these models, you would get exactly the same result as Figure 9-1—Django would create the events_myclubuser and events_subscriber tables in your database, however, because UserBase is and abstract model, it won’t be added to the database as a table.

Chapter Summary

In this chapter, we dug much deeper into Django’s models, exploring the essentials of Django’s models.

We looked at the common data management functions that come built in to Django. We also covered common model methods that return QuerySets and those that don’t, model field lookups, aggregate functions and building complex queries.

We also covered adding and overriding model managers and model methods and have a look at how model inheritance works in Django.

End of Free Content

This is the end of the free content available for the book. Please go back to the Table of Contents for instructions on how to get the rest of the book content.