Django模型关系:从一对多到多对多全解析

一、一对多关系: ForeignKey

一对多是最常见的模型关系,例如 "作者 - 书籍" 场景:假设一个作者可以写多本书,但每本书只能属于一个作者。

定义关系

核心参数说明:

  • on_delete=models.CASCADE:当作者被删除时,关联的书籍也会被自动删除
  • related_name='books':定义反向查询名称,可通过author.books.all()获取作者的所有书籍
python 复制代码
from django.db import models

class Author(models.Model):
    first_name = models.CharField(max_length=100)
    last_name = models.CharField(max_length=100)

    def __str__(self):
        return f"{self.first_name} {self.last_name}"

class Book(models.Model):
    title = models.CharField(max_length=100)
    publication_date = models.DateField()
    # 外键关联Author,级联删除,反向查询名为books
    author = models.ForeignKey(
        Author, 
        on_delete=models.CASCADE, 
        related_name='books'
    )

    def __str__(self):
        return self.title

数据操作示例

创建数据

python 复制代码
# 创建作者
author1 = Author.objects.create(first_name='J.K.', last_name='Rowling')
author2 = Author.objects.create(first_name='George', last_name='Orwell')

# 创建书籍并关联作者
book1 = Book.objects.create(
    title='Harry Potter', 
    publication_date='1997-06-26', 
    author=author1
)
book2 = Book.objects.create(
    title='1984', 
    publication_date='1949-06-08', 
    author=author2
)

查询操作

python 复制代码
# 正向查询:通过书籍找作者
book = Book.objects.get(title='1984')
print(book.author)  # 输出: George Orwell

# 反向查询:通过作者找书籍
author = Author.objects.get(last_name='Rowling')
for book in author.books.all():
    print(book.title)  # 输出: Harry Potter

高级配置

禁用外键约束:当需要灵活管理关联关系(如允许删除存在关联数据的主表记录)时,可关闭数据库级约束

python 复制代码
author = models.ForeignKey(
    Author, 
    on_delete=models.SET_NULL,
    related_name='books',
    db_constraint=False,  # 不创建数据库外键约束
    null=True
)

自定义数据库列名:默认会生成<ClassName>_id列,可通过db_column修改

python 复制代码
dept_id = models.ForeignKey(
    "SystemDept",
    on_delete=models.SET_NULL,
    db_column="dept_id",  # 显式指定数据库列名
    null=True
)

二、多对多关系: ManyToManyField

多对多关系适用于 "作者 - 书籍" 的另一种场景:假设一个作者可以写多本书,一本书也可以有多个作者。

定义关系

Django 会自动创建中间表(默认名为appname_book_authors)存储关联关系,无需手动定义。

python 复制代码
from django.db import models

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

    def __str__(self):
        return self.name

class Book(models.Model):
    title = models.CharField(max_length=200)
    publication_date = models.DateField()
    # 多对多关联Author
    authors = models.ManyToManyField(Author, related_name='books')

    def __str__(self):
        return self.title

数据操作示例

添加 / 移除关联

python 复制代码
# 创建实例
author1 = Author.objects.create(name='Alice', email='alice@example.com')
author2 = Author.objects.create(name='Bob', email='bob@example.com')
book = Book.objects.create(title='Example Book', publication_date='2023-01-01')

# 添加关联
book.authors.add(author1, author2)

# 移除关联
book.authors.remove(author1)

查询操作

python 复制代码
# 正向查询:书籍的所有作者
book = Book.objects.get(title='Example Book')
for author in book.authors.all():
    print(author.name)

# 反向查询:作者的所有书籍
author = Author.objects.get(name='Bob')
for book in author.books.all():  # related_name='books'
    print(book.title)

自定义中间表

当需要存储关联关系的额外信息(如邀请原因、加入时间)时,可自定义中间表

python 复制代码
class Membership(models.Model):
    group = models.ForeignKey(Group, on_delete=models.CASCADE)
    person = models.ForeignKey(Person, on_delete=models.CASCADE)
    inviter = models.ForeignKey(Person, related_name="invites", on_delete=models.CASCADE)
    invite_reason = models.CharField(max_length=64)  # 额外信息

class Group(models.Model):
    name = models.CharField(max_length=128)
    members = models.ManyToManyField(
        Person,
        through="Membership",  # 指定中间表
        through_fields=("group", "person"),  # 关联字段
    )

三、性能优化技巧

select_related:用于一对多关系,提前加载关联对象,减少数据库查询

python 复制代码
# 普通查询(N+1问题)
entries = Entry.objects.all()
for entry in entries:
    print(entry.blog.name)  # 每次循环都会触发新查询

# 优化后(仅1次查询)
entries = Entry.objects.select_related('blog').all()
for entry in entries:
    print(entry.blog.name)  # 使用缓存数据

批量操作:利用update()进行批量更新,避免循环操作

python 复制代码
# 批量标记站内信为已读
SystemNotifyMessage.objects.filter(
    id__in=ids.split(",")
).update(
    read_status=True, 
    read_time=timezone.now()
)

四、关于是否使用外键约束

在实际项目中,是否使用数据库外键约束需要权衡利弊

使用外键的优势

  • 数据完整性:数据库级别的约束保证关联数据一致性
  • 开发效率:ORM 自动处理关联查询和级联操作
  • 查询便捷:支持select_related等优化方法,简化多表查询

禁用外键的场景

  • 高并发系统:外键会增加数据库锁竞争,影响写入性能
  • 分布式架构:分库分表环境下,跨库外键无法生效
  • 复杂迁移:避免循环依赖导致的迁移失败问题

折中方案:使用db_constraint=False 参数

  • 数据库层面:无外键约束,数据库不会强制校验关联数据的存在性
  • Django ORM 层面:保留逻辑关联,ORM仍将字段视为外键关系(逻辑关联),支持 ORM 查询、操作语法
特性 db_constraint=True (默认) db_constraint=False
数据库外键约束 创建,强制数据一致性 不创建
级联操作 数据库自动处理 仅由 Django ORM 处理
关联数据存在性校验 数据库强制校验 不校验(需应用层保障)
ORM 查询支持 完整支持 完整支持(逻辑外键保留)
性能影响 外键约束带来额外开销 无约束开销
适用场景 强数据一致性需求 高频写入/跨库/历史数据迁移

五、多对多关系实战

实战场景:在一个后台管理系统中,用户与角色往往是多对多关系。一个用户可以分配多个角色,一个角色也可以属于多个用户。

模型定义:点击查看完整代码

python 复制代码
class SystemUsers(BaseModel, AbstractBaseUser):
    id = models.BigAutoField(primary_key=True, db_comment="用户ID", help_text="用户ID")
    username = models.CharField(
        max_length=30, unique=True, db_comment="用户账号", help_text="用户账号"
    )
    # ...
    # 与角色多对多关系
    roles = models.ManyToManyField(
        "SystemRole",
        through="SystemUserRole",
        through_fields=("user_id", "role_id"),
        related_name="users",
    )
    # ...    
    
class SystemUserRole(BaseModel):
    """用户和角色关联中间表"""
    id = models.BigAutoField(primary_key=True, db_comment="id")
    user_id = models.ForeignKey(
        "SystemUsers",
        on_delete=models.CASCADE,
        db_constraint=False,
        db_column="user_id",
        db_comment="用户ID",
    )
    role_id = models.ForeignKey(
        "SystemRole",
        on_delete=models.CASCADE,
        db_constraint=False,
        db_column="role_id",
        db_comment="角色ID",
    )

    class Meta:
        managed = True
        db_table = "system_user_role"
        db_table_comment = "用户和角色关联表"
        ordering = ["-id"]

system_user_role数据库生成的中间表


您正在阅读的是《Django从入门到实战》专栏!关注不迷路~

相关推荐
xmjd msup3 小时前
mysql的分区表
数据库·mysql
Lyyaoo.3 小时前
【JAVA Spring面经】Spring 事务失效情况
java·数据库·spring
MeAT ITEM4 小时前
MySQL Workbench菜单汉化为中文
android·数据库·mysql
dovens4 小时前
PostgreSQL 中进行数据导入和导出
大数据·数据库·postgresql
IOT.FIVE.NO.14 小时前
claude code desktop cowork报错解决和记录Workspace..The isolated Linux environment ...
linux·服务器·数据库
Rick19934 小时前
mysql 慢查询怎么快速定位
android·数据库·mysql
科技小花11 小时前
全球化深水区,数据治理成为企业出海 “核心竞争力”
大数据·数据库·人工智能·数据治理·数据中台·全球化
X566112 小时前
如何在 Laravel 中正确保存嵌套动态表单数据(主服务与子服务)
jvm·数据库·python
虹科网络安全13 小时前
艾体宝干货|数据复制详解:类型、原理与适用场景
java·开发语言·数据库
2301_7717172113 小时前
解决mysql报错:1406, Data too long for column
android·数据库·mysql