Creating, Saving, and Querying Objects in Django: A Complete Guide to the ORM Query API

This article explains how Django’s ORM allows developers to create, retrieve, update, and delete objects using a powerful database‑abstraction API. It covers object creation, saving changes, working with ForeignKey and ManyToManyField relationships, retrieving objects using QuerySets, filtering data, chaining filters, and understanding how QuerySets behave. These concepts form the foundation of working with data in Django applications.

Django ORM, QuerySet, create objects, save(), filter(), exclude()ForeignKey, ManyToManyFieldchaining filters

~16 min read • Updated Mar 10, 2026

1. Introduction to Django Queries

Once your models are defined, Django automatically provides a high‑level ORM API that lets you create, retrieve, update, and delete records without writing SQL manually. A model class represents a database table, and each instance represents a row in that table.

Throughout this guide, we use the following blog models:


class Blog(models.Model):
    name = models.CharField(max_length=100)
    tagline = models.TextField()

class Author(models.Model):
    name = models.CharField(max_length=200)
    email = models.EmailField()

class Entry(models.Model):
    blog = models.ForeignKey(Blog, on_delete=models.CASCADE)
    headline = models.CharField(max_length=255)
    body_text = models.TextField()
    pub_date = models.DateField()
    mod_date = models.DateField(default=date.today)
    authors = models.ManyToManyField(Author)
    number_of_comments = models.IntegerField(default=0)
    number_of_pingbacks = models.IntegerField(default=0)
    rating = models.IntegerField(default=5)

2. Creating Objects

To create a new object, instantiate the model class with keyword arguments and call save():


b = Blog(name="Beatles Blog", tagline="All the latest Beatles news.")
b.save()

This triggers an SQL INSERT statement. Django does not touch the database until save() is called.

You can also create and save an object in one step using create():


Blog.objects.create(name="My Blog", tagline="Hello world")

3. Saving Changes to Existing Objects

To update an existing object, modify its attributes and call save() again:


b5.name = "New name"
b5.save()

This triggers an SQL UPDATE statement.

4. Saving ForeignKey and ManyToManyField Values

Updating a ForeignKey

Assign a model instance of the correct type:


entry = Entry.objects.get(pk=1)
cheese_blog = Blog.objects.get(name="Cheddar Talk")
entry.blog = cheese_blog
entry.save()

Updating a ManyToManyField

Use add() to attach related objects:


joe = Author.objects.create(name="Joe")
entry.authors.add(joe)

You can add multiple authors at once:


entry.authors.add(john, paul, george, ringo)

5. Retrieving Objects

To retrieve objects, you use a QuerySet obtained from a model’s Manager. Every model has at least one Manager named objects.


Blog.objects.all()

A Manager is only accessible through the model class, not through instances.

6. Retrieving All Objects

The simplest query retrieves all rows:


all_entries = Entry.objects.all()

7. Filtering QuerySets

To retrieve a subset of objects, use filter() or exclude():

  • filter(**kwargs) → returns objects matching the criteria
  • exclude(**kwargs) → returns objects NOT matching the criteria

Example: entries from 2006


Entry.objects.filter(pub_date__year=2006)

8. Chaining Filters

Each refinement returns a new QuerySet, so you can chain them:


Entry.objects.filter(headline__startswith="What")
    .exclude(pub_date__gte=datetime.date.today())
    .filter(pub_date__gte=datetime.date(2005, 1, 30))

This produces entries whose headline starts with “What” and were published between January 30, 2005 and today.

9. Filtered QuerySets Are Independent

Each filtered QuerySet is a separate object:


q1 = Entry.objects.filter(headline__startswith="What")
q2 = q1.exclude(pub_date__gte=datetime.date.today())
q3 = q1.filter(pub_date__gte=datetime.date.today())

These QuerySets do not affect each other. q1 remains unchanged.

Conclusion

Django’s ORM provides a powerful and intuitive API for interacting with your database. Understanding how to create, save, filter, and chain QuerySets is essential for building dynamic, data‑driven applications. With these tools, you can write expressive, efficient queries without writing SQL manually.

1. QuerySets Are Lazy

QuerySets in Django are lazy, meaning that creating or refining a QuerySet does not immediately hit the database. Django waits until the QuerySet is actually evaluated.

Example:


q = Entry.objects.filter(headline__startswith="What")
q = q.filter(pub_date__lte=datetime.date.today())
q = q.exclude(body_text__icontains="food")
print(q)

Although this looks like three separate queries, Django executes only one SQL query when print(q) forces evaluation.

A QuerySet is evaluated when you:

  • iterate over it,
  • convert it to a list,
  • print it,
  • or otherwise access its results.

2. Retrieving a Single Object with get()

filter() always returns a QuerySet, even if it contains only one object. If you know exactly one object matches your criteria, use get():


one_entry = Entry.objects.get(pk=1)

Difference Between get() and filter()[0]

  • If no object matches:
    • get() → raises DoesNotExist
  • If more than one object matches:
    • get() → raises MultipleObjectsReturned
  • filter()[0] → raises IndexError if empty

3. Limiting QuerySets with Slicing

You can use Python slicing syntax to limit results, similar to SQL’s LIMIT and OFFSET.

First 5 objects (LIMIT 5):


Entry.objects.all()[:5]

Objects 6 through 10 (OFFSET 5 LIMIT 5):


Entry.objects.all()[5:10]

Slicing normally returns a new QuerySet without evaluating it. The exception is when using a step:


Entry.objects.all()[:10:2]

This forces evaluation because Django must fetch results to apply the step.

Retrieving a single object using indexing:


Entry.objects.order_by("headline")[0]

This is equivalent to LIMIT 1 in SQL.

4. Field Lookups

Field lookups define SQL WHERE conditions. They use the syntax field__lookup=value (double underscore).

Example:


Entry.objects.filter(pub_date__lte="2006-01-01")

Equivalent SQL:


SELECT * FROM blog_entry WHERE pub_date <= '2006-01-01';

Common lookup types:

  • exact – exact match
  • iexact – case‑insensitive exact match
  • contains / icontains
  • lt, lte, gt, gte
  • startswith / istartswith
  • in

ForeignKey shortcut:

You can filter by the raw primary key using field_id:


Entry.objects.filter(blog_id=4)

Conclusion

Django’s lazy QuerySets allow efficient query construction, get() provides a clean way to retrieve a single object, slicing enables LIMIT/OFFSET behavior, and field lookups give you expressive control over SQL WHERE clauses. Mastering these tools is essential for writing clean, efficient, and powerful Django queries.

1. Case‑Insensitive and Containment Lookups

iexact

iexact performs a case‑insensitive exact match:


Blog.objects.get(name__iexact="beatles blog")

This matches “Beatles Blog”, “beatles blog”, or even “BeAtlES blOG”.

contains / icontains

contains performs a case‑sensitive substring search:


Entry.objects.get(headline__contains="Lennon")

Equivalent SQL:


SELECT ... WHERE headline LIKE '%Lennon%';

icontains is the case‑insensitive version.

startswith / endswith

These lookups match prefixes and suffixes. Case‑insensitive versions are istartswith and iendswith.

2. Lookups That Span Relationships

Django automatically handles SQL JOINs when you use double underscores to traverse relationships.

Example: filter entries by blog name


Entry.objects.filter(blog__name="Beatles Blog")

Reverse lookups

By default, Django uses the lowercase model name for reverse relations:


Blog.objects.filter(entry__headline__contains="Lennon")

Handling missing intermediate relations

If a related object is missing, Django treats it as NULL rather than raising an error.


Blog.objects.filter(entry__authors__name__isnull=True)

This returns blogs with authors whose name is NULL and blogs whose entries have no authors at all.

3. Spanning Multi‑Valued Relationships

When filtering across ManyToMany or reverse ForeignKey relations, you must decide whether all conditions must match the same related object.

Conditions must match the same related object:


Blog.objects.filter(
    entry__headline__contains="Lennon",
    entry__pub_date__year=2008,
)

Conditions may match different related objects:


Blog.objects.filter(entry__headline__contains="Lennon")
    .filter(entry__pub_date__year=2008)

The second query is more permissive and may return duplicates because it performs multiple JOINs.

Example output:


, , ]>

4. exclude() Does Not Behave Like filter()

Unlike filter(), exclude() does not guarantee that conditions refer to the same related object.

Incorrect (conditions may apply to different entries):


Blog.objects.exclude(
    entry__headline__contains="Lennon",
    entry__pub_date__year=2008,
)

Correct way to exclude entries matching both conditions:


Blog.objects.exclude(
    entry__in=Entry.objects.filter(
        headline__contains="Lennon",
        pub_date__year=2008,
    )
)

5. Comparing Fields with F Expressions

F expressions allow you to compare fields within the same model or perform arithmetic inside queries.

More comments than pingbacks:


Entry.objects.filter(number_of_comments__gt=F("number_of_pingbacks"))

More than twice as many comments as pingbacks:


Entry.objects.filter(number_of_comments__gt=F("number_of_pingbacks") * 2)

Rating less than comments + pingbacks:


Entry.objects.filter(
    rating__lt=F("number_of_comments") + F("number_of_pingbacks")
)

Spanning relationships with F expressions:


Entry.objects.filter(authors__name=F("blog__name"))

Date arithmetic:


Entry.objects.filter(mod_date__gt=F("pub_date") + timedelta(days=3))

Bitwise operations:


F("somefield").bitand(16)

(Oracle does not support XOR.)

Conclusion

Django’s advanced lookup system, relationship spanning, multi‑value filtering rules, and F expressions provide a powerful toolkit for writing expressive and efficient database queries. Mastering these features allows you to build complex logic without writing raw SQL, while keeping your code clean and maintainable.

1. Expressions Can Reference Transforms

Django allows transforms to be used inside F expressions.

Example: entries published in the same year they were modified:


Entry.objects.filter(pub_date__year=F("mod_date__year"))

Finding the earliest publication year:


Entry.objects.aggregate(first_published_year=Min("pub_date__year"))

Using Subquery and OuterRef:

Find the highest rating and total comments for each year:


Entry.objects.values("pub_date__year").annotate(
    top_rating=Subquery(
        Entry.objects.filter(
            pub_date__year=OuterRef("pub_date__year")
        ).order_by("-rating").values("rating")[:1]
    ),
    total_comments=Sum("number_of_comments"),
)

2. The pk Lookup Shortcut

pk stands for “primary key” and is equivalent to id__exact.

These three statements are identical:


Blog.objects.get(id__exact=14)
Blog.objects.get(id=14)
Blog.objects.get(pk=14)

More examples:


Blog.objects.filter(pk__in=[1, 4, 7])
Blog.objects.filter(pk__gt=14)

pk lookups also work across joins:


Entry.objects.filter(blog__pk=3)

3. Escaping % and _ in LIKE Statements

Lookups that translate to SQL LIKE (contains, icontains, startswith, etc.) automatically escape % and _.

Example:


Entry.objects.filter(headline__contains="%")

SQL generated:


WHERE headline LIKE '%\%%'

4. QuerySet Caching

Each QuerySet has an internal cache. The first evaluation populates the cache, and subsequent evaluations reuse it.

Bad example (two queries executed):


print([e.headline for e in Entry.objects.all()])
print([e.pub_date for e in Entry.objects.all()])

Correct approach:


queryset = Entry.objects.all()
print([e.headline for e in queryset])
print([e.pub_date for e in queryset])

5. When QuerySets Are Not Cached

Partial evaluation (such as slicing or indexing) does not populate the cache.

Example:


queryset = Entry.objects.all()
print(queryset[5])  # Hits the database
print(queryset[5])  # Hits the database again

But if the entire QuerySet has been evaluated:


queryset = Entry.objects.all()
list(queryset)  # Populates cache
print(queryset[5])  # Uses cache

Other actions that populate the cache:


[entry for entry in queryset]
bool(queryset)
entry in queryset
list(queryset)

Note: printing the QuerySet does NOT populate the cache.

6. Asynchronous Queries

In async views, you cannot call synchronous ORM methods. Django provides async variants such as aget() and adelete().

Async iteration:


async for entry in Authors.objects.filter(name__startswith="A"):
    ...

Combining filter() with afirst():


user = await User.objects.filter(username=my_input).afirst()

If you forget await, you may see errors like “coroutine object has no attribute …”.

7. Knowing Which Methods Are Async-Safe

Methods that return new QuerySets (filter, exclude, etc.) are non‑blocking and safe in async code.

Methods that evaluate the QuerySet (get, first, delete, etc.) have async versions and must be awaited.

Conclusion

Django’s advanced ORM features—transforms, Subquery, pk shortcuts, LIKE escaping, QuerySet caching, and async query support—provide a powerful toolkit for building efficient, scalable, and expressive database logic. Mastering these tools helps you write clean, optimized, and modern Django applications.

1. Transactions and Asynchronous Code

Django does not currently support database transactions inside asynchronous ORM operations. Attempting to use a transaction in async code raises SynchronousOnlyOperation.

To use transactions safely, place ORM logic inside a synchronous function and call it using sync_to_async.

2. Querying JSONField

JSONField lookups behave differently from other fields because they support key, index, and path transforms.

Example model:


class Dog(models.Model):
    name = models.CharField(max_length=200)
    data = models.JSONField(null=True)

3. Storing and Querying None

Storing None normally results in SQL NULL. To store JSON null explicitly, use:


Value(None, JSONField())

Both SQL NULL and JSON null are returned as Python None, making them indistinguishable at retrieval time.

Query behavior:

  • data=None → matches JSON null
  • data__isnull=True → matches SQL NULL

Example:


Dog.objects.filter(data=None)  # JSON null
Dog.objects.filter(data__isnull=True)  # SQL NULL

To avoid ambiguity, set null=False and provide a default such as default=dict.

4. Key, Index, and Path Transforms

You can query nested JSON structures using double underscores.

Query by key:


Dog.objects.filter(data__breed="collie")

Nested keys:


Dog.objects.filter(data__owner__name="Bob")

Index transforms:


Dog.objects.filter(data__owner__other_pets__0__name="Fishy")

Negative indices:

Not allowed directly, but possible via dictionary unpacking:


Dog.objects.filter(**{"data__owner__other_pets__-1__name": "Fishy"})

Note: MySQL, MariaDB, and Oracle do not support negative JSON indices.

Missing keys:


Dog.objects.filter(data__owner__isnull=True)

5. KT() Expressions

KT() extracts the text value of a JSON key/index/path transform and allows annotation or filtering.

Example:


from django.db.models.fields.json import KT

Dog.objects.annotate(
    first_breed=KT("data__breed__1"),
    owner_name=KT("data__owner__name")
).filter(first_breed__startswith="lhasa", owner_name="Bob")

Warning: Any unknown lookup name is treated as a JSON key. Typos will not raise errors.

6. Backend-Specific Notes

  • MariaDB & Oracle: ordering by JSON transforms sorts by string representation.
  • Oracle: exclude(..., None) behaves differently and may include objects without the path.
  • PostgreSQL: uses -> for single-level and #> for multi-level paths.
  • SQLite: strings "true", "false", "null" are interpreted as JSON booleans/null.

7. Containment and Key Lookups

contains

Matches objects whose JSON contains the given key-value pairs at the top level.


Dog.objects.filter(data__contains={"owner": "Bob"})

Not supported on Oracle or SQLite.

contained_by

The inverse of contains—matches objects whose JSON is a subset of the given dict.


Dog.objects.filter(data__contained_by={"breed": "collie", "owner": "Bob"})

Not supported on Oracle or SQLite.

has_key


Dog.objects.filter(data__has_key="owner")

has_keys


Dog.objects.filter(data__has_keys=["breed", "owner"])

has_any_keys


Dog.objects.filter(data__has_any_keys=["owner", "breed"])

Conclusion

Django’s JSONField querying system is powerful and flexible, supporting nested lookups, index transforms, containment operations, and KT expressions. Understanding SQL NULL vs JSON null, backend differences, and the behavior of key-path lookups is essential for writing correct and efficient JSON queries in Django.

1. Building Complex Queries with Q Objects

In Django, keyword arguments in filter() and exclude() are combined using AND. To build OR or more complex logical expressions, you use Q objects.

Basic Q object:


Q(question__startswith="What")

OR between two conditions:


Q(question__startswith="Who") | Q(question__startswith="What")

Negation (NOT):


Q(question__startswith="Who") | ~Q(pub_date__year=2005)

Using Q objects in queries:


Poll.objects.get(
    Q(question__startswith="Who"),
    Q(pub_date=date(2005, 5, 2)) | Q(pub_date=date(2005, 5, 6)),
)

Important:

Q objects must come before keyword arguments in filter(), get(), or exclude().

2. Comparing Model Instances

To compare two model instances, use the Python equality operator ==. Django compares their primary keys internally.

Example:


some_entry == other_entry
# Equivalent to:
some_entry.id == other_entry.id

If the primary key has a different name, Django still compares that field.

3. Deleting Objects

Deleting a single object:


e.delete()
# Output:
(1, {'blog.Entry': 1})

Bulk delete:


Entry.objects.filter(pub_date__year=2005).delete()

Note: Bulk delete does not call the delete() method on each instance. If you rely on custom delete logic, delete objects individually.

Cascade delete:

Deleting an object also deletes related objects via ForeignKey (default ON DELETE CASCADE).


b = Blog.objects.get(pk=1)
b.delete()  # Deletes Blog and all related Entry objects

4. Copying Model Instances

Django has no built‑in copy method, but you can duplicate an instance by resetting its primary key.

Simple copy:


blog.pk = None
blog._state.adding = True
blog.save()

Copying inherited models:

For inherited models, both pk and id must be set to None:


django_blog.pk = None
django_blog.id = None
django_blog._state.adding = True
django_blog.save()

Copying ManyToMany relationships:


old_authors = entry.authors.all()
entry.pk = None
entry._state.adding = True
entry.save()
entry.authors.set(old_authors)

Copying OneToOne relationships:

You must duplicate the related object to avoid violating the one‑to‑one constraint.

5. Bulk Updating with update()

To update many objects at once, use update().

Example:


Entry.objects.filter(pub_date__year=2007).update(headline="Everything is the same")

Updating a ForeignKey:


Entry.objects.update(blog=b)

Important notes about update():

  • It executes directly as SQL.
  • It does NOT call save().
  • It does NOT trigger pre_save or post_save signals.
  • It does NOT update auto_now fields.
  • It can only update fields on the model’s main table.

Using F expressions in update():


Entry.objects.update(number_of_pingbacks=F("number_of_pingbacks") + 1)

Limitation: F expressions in update() cannot introduce joins:


Entry.objects.update(headline=F("blog__name"))  # Raises FieldError

Conclusion

Q objects allow you to build complex logical queries. Deleting and copying objects requires understanding Django’s cascade and relation behavior. Bulk updates with update() are powerful but bypass model methods and signals. Mastering these tools enables you to write efficient, expressive, and production‑ready Django ORM code.

1. Introduction to Related Objects

Whenever a model defines a relationship (ForeignKey, OneToOneField, ManyToManyField), Django automatically provides an API to access related objects. This includes both forward access (from the model defining the field) and reverse access (from the related model back to the source model).

For example, an Entry instance can access its related Blog via e.blog, while a Blog instance can access all related entries via b.entry_set.all().

2. One‑to‑Many Relationships

Forward Access

When a model has a ForeignKey, each instance can access the related object directly:


e = Entry.objects.get(id=2)
e.blog

You can assign a new related object and save:


e.blog = some_blog
e.save()

If null=True is set, you may assign None to remove the relationship.

Caching Forward Access

The first time a foreign key is accessed, Django hits the database and caches the result. Subsequent access uses the cache:


print(e.blog)  # Database hit
print(e.blog)  # Cached

select_related() prepopulates this cache:


e = Entry.objects.select_related().get(id=2)
print(e.blog)  # No database hit

3. Reverse Access

Reverse access is available through a manager named FOO_set by default:


b = Blog.objects.get(id=1)
b.entry_set.all()
b.entry_set.filter(headline__contains="Lennon")
b.entry_set.count()

Using related_name

You can override entry_set using related_name:


blog = ForeignKey(Blog, related_name="entries", ...)

Then:


b.entries.all()

4. Custom Reverse Managers

You can specify which manager to use for reverse relations:


b.entry_set(manager="entries").all()

This allows custom filtering or custom methods:


b.entry_set(manager="entries").is_published()

Prefetching with a custom manager

Use Prefetch() to prefetch using a custom manager:


prefetch = Prefetch("entry_set", queryset=Entry.entries.all())
Blog.objects.prefetch_related(prefetch)

5. Additional Methods for Handling Related Objects

The reverse manager provides extra methods:

  • add(obj1, obj2, ...) – Add objects to the relation.
  • create(**kwargs) – Create and add a new related object.
  • remove(obj1, obj2, ...) – Remove objects from the relation.
  • clear() – Remove all related objects.
  • set(objs) – Replace the entire related set.

Example:


b.entry_set.set([e1, e2])

These operations write to the database immediately.

6. Many‑to‑Many Relationships

Both sides of a many‑to‑many relationship get access to the related objects.

Examples:


e = Entry.objects.get(id=3)
e.authors.all()
e.authors.filter(name__contains="John")

a = Author.objects.get(id=5)
a.entry_set.all()

Using related_name

If related_name='entries' is set on the ManyToManyField:


a.entries.all()

Primary key support in add(), set(), remove()

Many‑to‑many methods accept both model instances and primary keys:


a.entry_set.set([e1, e2])
a.entry_set.set([e1.pk, e2.pk])

Conclusion

Django provides a powerful and intuitive API for navigating relationships. Forward and reverse access, custom managers, and many‑to‑many utilities make it easy to work with relational data. Understanding these tools helps you write clean, expressive, and efficient ORM code.

1. Filtering on Many‑to‑Many Relationships

When using filter() across a many‑to‑many relationship, Django performs the join between the main model and the intermediary table only once. This creates a restrictive behavior known as a “sticky join”.

Example:

Suppose an Entry is authored by both Anna and Gloria:


anna.entry_set.filter(authors__name="Gloria")

You might expect this to return the shared Entry, but it returns an empty QuerySet. Why?

  • The join happens only once.
  • No single row in the intermediary table represents both Anna and Gloria at the same time.

Workaround: chain two filters


anna.entry_set.filter().filter(authors__name="Gloria")

This forces two separate joins and returns the expected result.

exclude() is also sticky

exclude() behaves similarly in this scenario:


anna.entry_set.exclude(authors__name="Gloria")

This incorrectly keeps the Entry, even though Gloria is a co‑author.

But chaining two excludes works:


anna.entry_set.exclude().exclude(authors__name="Gloria")

Other cases may differ; see Django’s documentation on multi‑valued relationships.

2. One‑to‑One Relationships

One‑to‑one relations behave like ForeignKey but guarantee a single related object.

Example:


class EntryDetail(models.Model):
    entry = models.OneToOneField(Entry, on_delete=models.CASCADE)
    details = models.TextField()

Forward access:


ed.entry

Reverse access:


e.entrydetail

If no related object exists, Django raises DoesNotExist.

3. How Backward Relations Are Created

Django follows the DRY principle: you define the relationship on only one side, and Django automatically creates the reverse relation.

This is possible because of the app registry:

  • Django loads all apps in INSTALLED_APPS.
  • It imports each app’s models.py.
  • Whenever a model is created, Django registers its relationships.
  • If the related model hasn’t been loaded yet, Django defers the relation until it is.

Therefore, all models must be inside apps listed in INSTALLED_APPS.

4. Querying Related Objects

You can filter using either the related object itself or its primary key.

These three queries are identical:


Entry.objects.filter(blog=b)
Entry.objects.filter(blog=b.id)
Entry.objects.filter(blog=5)

5. Falling Back to Raw SQL

If a query is too complex for Django’s ORM, you can write raw SQL manually using Django’s raw SQL APIs.

Remember: Django is just an interface. Your database remains fully accessible through any other tool or language.

Conclusion

Filtering across many‑to‑many relationships requires understanding Django’s single‑join behavior. One‑to‑one relations behave similarly to ForeignKey but return a single object. Django automatically builds reverse relations using the app registry. And when ORM limitations arise, raw SQL is always an option.

Written & researched by Dr. Shahin Siami