Django ORM Cross-Table Association and Query Operation Manual

One-to-One Association (OneToOneField)

from django.db import models

class Author(models.Model):
    full_name = models.CharField(max_length=32)
    profile = models.OneToOneField(to="AuthorProfile", on_delete=models.CASCADE)

class AuthorProfile(models.Model):
    residential_addr = models.CharField(max_length=128)

Object-based cross-table query

Forward query (use the association field deefined on the current model) Requirement: Fetch the residential address of every registered author

all_authors = Author.objects.all()
for author in all_authors:
    print(author.full_name, author.profile.residential_addr)

Reverse query (use lowercase model name of the associated table) Requirement: Get names of all authors living in Guangzhou

gz_profiles = AuthorProfile.objects.filter(residential_addr="Guangzhou")
for profile in gz_profiles:
    print(profile.author.full_name)

Double underscore cross-table query (for filter, values, values_list methods)

Forward query syntax: association_field__related_table_field Requirement: Directly get the address of author named "Li Ming"

li_addr = Author.objects.filter(full_name="Li Ming").values("profile__residential_addr")
print(li_addr)
# Output example: <QuerySet [{'profile__residential_addr': 'Tianhe District, Guangzhou'}]>

Reverse query syntax: lowercase_related_model_name__field Requirement: Get names of all authors based in Guangzhou

gz_author_names = AuthorProfile.objects.filter(residential_addr="Guangzhou").values("author__full_name")

Delete operation

Cascade delete is enabled by default, deleting a record from the main table will remove associated dependent table records:

# Delete the profile record with primary key 8, associated author record will also be removed
AuthorProfile.objects.filter(pk=8).delete()

Add operation (update follows the same logic, replace create with update)

Create main table record first, then create associated dependent table record: Method 1: Use create method directly

new_profile = AuthorProfile.objects.create(residential_addr="Xihu District, Hangzhou")
new_author = Author.objects.create(full_name="Zhang Wei", profile=new_profile)

Method 2: Initialize model instance then call save method

new_profile = AuthorProfile(residential_addr="Xihu District, Hangzhou")
new_profile.save()

new_author = Author(full_name="Zhang Wei", profile=new_profile)
new_author.save()

One-to-Many Association (ForeignKey)

class Book(models.Model):
    title = models.CharField(max_length=64)
    price = models.DecimalField(max_digits=8, decimal_places=2)
    publisher = models.ForeignKey(to="Publisher", on_delete=models.CASCADE)

class Publisher(models.Model):
    pub_name = models.CharField(max_length=64)
    contact_email = models.EmailField()

Object-based cross-table query

Forward query Requirement: Query the publisher name for each stored book

all_books = Book.objects.all()
for book in all_books:
    print(book.title, book.publisher.pub_name)

Reverse query: use lowercase_related_model_name_set to get associated queryset Requirement: Get all book titles published by the publisher with id 2

target_pub = Publisher.objects.filter(id=2).first()
published_books = target_pub.book_set.all()
for book in published_books:
    print(book.title)

Double underscore cross-table query

Forward query Requirement: Fetch publisher names for all books

book_pub_list = Book.objects.values("title", "publisher__pub_name")

Reverse query Requirement: Get all book titles published by the publisher with id 2

pub_book_names = Publisher.objects.filter(id=2).values("book__title")

Delete operation

# Delete publisher with primary key 5, all associated book records will be removed
Publisher.objects.filter(pk=5).delete()

Add operation

Method 1: Pass associated object directly

new_pub = Publisher.objects.create(pub_name="Sci-Fi Literature Press", contact_email="edit@scifibooks.com")
Book.objects.create(title="Dune", price=49.8, publisher=new_pub)

Method 2: Pass primary key of associated record with [association_field]_id

existing_pub = Publisher.objects.filter(pub_name="Sci-Fi Literature Press").first()
Book.objects.create(title="Dune Messiah", price=42.5, publisher_id=existing_pub.pk)

Method 3: Initialize instance and save

new_pub = Publisher(pub_name="Sci-Fi Literature Press", contact_email="edit@scifibooks.com")
new_pub.save()

new_book = Book(title="Children of Dune", price=45.0, publisher=new_pub)
new_book.save()

Many-to-Many Association

Auto-generated junction table (ManyToManyField)

Django automatically creates a hidden junction table when ManyToManyField is defined:

class Book(models.Model):
    title = models.CharField(max_length=64)
    written_by = models.ManyToManyField(to="Author")

class Author(models.Model):
    full_name = models.CharField(max_length=32)

Query operation

Object-based forward query Requirement: Get all authors of the book with primary key 3

target_book = Book.objects.filter(pk=3).first()
print(target_book.written_by.all())
# Output example: <QuerySet [<Author: Frank Herbert>, <Author: Brian Herbert>]>

Object-based reverse query Requirement: Get all books written by the author with primary key 2

target_author = Author.objects.filter(pk=2).first()
print(target_author.book_set.all())
# Output example: <QuerySet [<Book: Dune>, <Book: Dune Messiah>]>

Double underscore cross-table query

# Get names of all authors for the book with primary key 3
book_authors = Book.objects.filter(pk=3).values("written_by__full_name")

# Get all book titles written by the author with primary key 4
author_books = Author.objects.filter(pk=4).values("book__title")

Junction table operation methods

add operation: no return value, skips duplicate existing associations

# Forward add by primary key
target_book = Book.objects.filter(id=3).first()
target_book.written_by.add(5, 7)

# Forward add by model objects
author_list = Author.objects.filter(id__in=[5,7,9])
target_book.written_by.add(*author_list)

# Reverse add
target_author = Author.objects.filter(id=2).first()
target_author.book_set.add(4,6)

remove operation: same usage as add

target_book.written_by.remove(7)

clear operation: remove all associations for the current record

target_book.written_by.clear()

set operation: accepts iterable parameter, replaces all existing associations

target_book = Book.objects.filter(id=3).first()
new_authors = Author.objects.filter(id__in=[2,4])
target_book.written_by.set(new_authors)

Custom junction table

Define explicit junction table when you need to store extra association fields:

class Book(models.Model):
    title = models.CharField(max_length=64)

class Author(models.Model):
    full_name = models.CharField(max_length=32)

class BookAuthorMap(models.Model):
    book = models.ForeignKey(to="Book", on_delete=models.CASCADE)
    author = models.ForeignKey(to="Author", on_delete=models.CASCADE)
    # Extra fields can be added here, e.g. cooperation_role, join_date

Cross-table query example: Get all authors of the book named "Foundation" Method 1: Query book first, reverse to junction table, then forward to author

target_book = Book.objects.filter(title="Foundation").first()
mapping_records = target_book.bookauthormap_set.all()
for record in mapping_records:
    print(record.author.full_name)

Method 2: Query junction table directly with filter conditions

mapping_records = BookAuthorMap.objects.filter(book__title="Foundation").all()
for record in mapping_records:
    print(record.author.full_name)

Method 3: Cross query directly from author table

foundation_authors = Author.objects.filter(bookauthormap__book__title="Foundation").values("full_name")

Combine custom junction table with ManyToManyField

Use through parameter to link custom junction table to ManyToManyField for simplified query:

class Book(models.Model):
    title = models.CharField(max_length=64)
    written_by = models.ManyToManyField(
        to="Author",
        through="BookAuthorMap",
        through_fields=("book", "author")
    )

class BookAuthorMap(models.Model):
    book = models.ForeignKey(to="Book", on_delete=models.CASCADE)
    author = models.ForeignKey(to="Author", on_delete=models.CASCADE)

class Author(models.Model):
    full_name = models.CharField(max_length=32)

Configuration notes:

  1. Simplified query syntax is still supported, e.g. Book.objects.first().written_by.all()
  2. Only clear operation is supported for association modification, add, remove, set are unavailable
  3. All add/update/delete operations for the junction table need to be done direct via the custom junction table model

Database Transactions

Use Django's atomic context manager to wrap operations requiring transaction guarantee:

from django.db import transaction

with transaction.atomic():
    # All ORM operations inside this block are executed as a single transaction
    # Any exception triggers full rollback of all changes
    pass

MVC vs MTV Pattern

Django follows the MTV architecture, a variant of the classic MVC pattern:

MTV (Django):

  • M: Models layer, responsible for database data interaction
  • T: Templates layer, responsible for front-end page rendering
  • V: Views layer, responsible for business logic processing and request response

Classic MVC:

  • M: Models layer, same as MTV's M
  • V: Views layer, responsible for front-end page display (corresponds to Django's Templates layer)
  • C: Controller layer, responsible for request distribution and business logic processing (corresponds to Django's url routing + Views layer)

Tags: Django ORM Backend Development python Database Operations

Posted on Sat, 13 Jun 2026 16:35:33 +0000 by Shawn Jetton