Django常见问题之模型

一、同模型多外键引用冲突及解决方案

1.1 问题现象

当模型中出现多个字段引用同一个模型作为外键时(如下方代码示例),Django 会抛出 clash 冲突错误:

from django.db import models
from django.contrib.auth.models import User
from utile.model import timeFiledMixin, statusFiledMixin


class ProductTypeModel(timeFiledMixin.CreateTimeFiled, timeFiledMixin.UpdateTimeFiled,
                       models.Model):
    name = models.CharField(max_length=200, verbose_name='产品类型名称', db_index=True, unique=True)
    types = models.CharField(max_length=128, verbose_name='产品类型')

    class Meta:
        db_table = 'product_type'
        verbose_name = '产品类型'
        verbose_name_plural = verbose_name

    def __str__(self):
        return self.name


class Products(timeFiledMixin.CreateAndUpdateTimeFiled, statusFiledMixin.ActiveFiled, models.Model):
    name = models.CharField(max_length=200, verbose_name='产品名称', db_index=True, unique=True)
    product_type = models.ForeignKey(ProductTypeModel, on_delete=models.CASCADE)
    introduction = models.CharField(max_length=255, verbose_name='产品介绍', blank=True, null=True)
    administrator = models.ManyToManyField(User)
    creator = models.ManyToManyField(User, verbose_name='创建者')

    class Meta:
        db_table = 'product'
        verbose_name = '产品'
        verbose_name_plural = verbose_name

    def __str__(self):
        return self.name

模型Products字段 administrator 和 creator 同时引用 User 作为外键,导致运行异常

1.2 原因分析

Django 自动为关联字段创建的反向关系名称(related_name)默认采用 <model_name>_set 格式。当多个字段引用同一模型时,Django 无法自动区分反向关系名称,导致命名冲突。

1.3 解决方案

通过显式声明 related_name 参数为每个字段定义唯一反向关系名称:

分别添加 related_name 参数,分别自定义不同名称即可

1.4 使用规范与注意事项

  1. 命名规范:推荐使用 <app_label>_<model_name>_<field_name> 格式

    • 示例:products_product_administrators

  2. 查询关系:

     # 正向查询
     product = Products.objects.first()
     admins = product.administrator.all()
     ​
     # 反向查询(通过 related_name)
     user = User.objects.first()
     created_products = user.product_creators.all()

  3. 级联关系:使用 on_delete 参数明确删除行为(适用于 ForeignKey)

    • CASCADE:级联删除

    • PROTECT:阻止删除

    • SET_NULL:设为 NULL(需允许空值)

二、新增非空外键字段的默认值问题(You are trying to add a non-nullable field 'name' to contact without a default)

2.1 问题现象

当向已存在数据的模型中新增非空外键字段时,会触发 NOT NULL constraint 错误:

Products 模型已迁移,表格中已有数据,此时新增外键administrator,执行迁移时报错

class Products(timeFiledMixin.CreateAndUpdateTimeFiled, statusFiledMixin.ActiveFiled, models.Model):
    name = models.CharField(max_length=200, verbose_name='产品名称', db_index=True, unique=True)
    product_type = models.ForeignKey(ProductTypeModel, on_delete=models.CASCADE)
    introduction = models.CharField(max_length=255, verbose_name='产品介绍', blank=True, null=True)
    creator = models.ManyToManyField(User, verbose_name='创建者', related_name='creator')
    administrator = models.ForeignKey(User, related_name='administrator', on_delete=models.DO_NOTHING)

    class Meta:
        db_table = 'product'
        verbose_name = '产品'
        verbose_name_plural = verbose_name

    def __str__(self):
        return self.name

2.2 原因解析

数据库要求新增字段必须为现有记录提供默认值,但Django无法自动确定关联模型的默认值。

2.3 解决方案汇总

方案一:迁移时交互设置默认值
  1. 执行迁移命令:

     python manage.py makemigrations
     python manage.py migrate

  2. 当提示 Please select a fix: 时选择选项 1

  3. 输入有效的用户ID作为默认值(需确保该ID存在)

方案二:模型字段预设默认值
        在Products模型中外键creator字段添加defunct字段设置默认,再进行迁移即可

方案二:模型字段预设默认值
  1. 首次迁移允许空值:

     administrator = models.ForeignKey(
         User,
         on_delete=models.SET_NULL,
         null=True,
         blank=True
     )

  2. 执行数据填充脚本:

     # 在migrations中创建RunPython操作
     def init_administrators(apps, schema_editor):
         Product = apps.get_model('appname', 'Products')
         default_user = User.objects.get_or_create(username='admin')[0]
         for product in Product.objects.all():
             product.administrator = default_user
             product.save()
  3. 二次迁移移除空值约束:

     administrator = models.ForeignKey(
         User,
         on_delete=models.SET_NULL,
         null=False
     )

三、模型继承方式选择不当导致数据冗余

3.1 问题现象

  • 使用多表继承 (Meta: abstract = False) 导致生成过多数据表

  • 子类重复存储父类字段数据,产生冗余

  • 查询父类时产生不必要的 JOIN 操作

3.2 解决方案:合理选择继承模式

 # 抽象基类(字段不生成实际表)
 class BaseModel(models.Model):
     created_at = models.DateTimeField(auto_now_add=True)
     
     class Meta:
         abstract = True  # 核心配置
 ​
 class Product(BaseModel):
     name = models.CharField(max_length=100)
 ​
 # 代理模型(仅扩展行为)
 class FeaturedProduct(Product):
     class Meta:
         proxy = True  # 不生成新表
     
     def mark_featured(self):
         self.featured = True
         self.save()

四、N+1 查询性能问题

4.1 问题现象

 # 触发 N+1 查询
 products = Product.objects.all()
 for p in products:
     print(p.category.name)  # 每次循环执行新查询

4.2 优化方案:预加载关联数据

 # 单次查询解决外键
 products = Product.objects.select_related('category')
 ​
 # 预取多对多/反向关系
 products = Product.objects.prefetch_related('tags')
 ​
 # 复杂预取(自定义查询集)
 from django.db.models import Prefetch
 products = Product.objects.prefetch_aching_related(
     Prefetch('reviews', queryset=Review.objects.filter(rating__gt=3))

五、字段选项配置混淆

5.1 null vs blank 的区别

配置组合数据库约束表单验证典型场景
null=True允许 NULL不验证数值型可选字段
blank=True无影响允许空值表单中的可选输入项
同时设置允许 NULL允许空值完全可选字段

5.2 时间字段配置陷阱

 # 自动更新时间(适合最后修改时间)
 last_modified = models.DateTimeField(auto_now=True)
 ​
 # 仅首次设置时间(适合创建时间)
 created_at = models.DateTimeField(auto_now_add=True)
 ​
 # 重要提醒:设置auto_now/add后,字段不可手动编辑

六、信号使用不当导致性能问题

6.1 典型错误场景

  • post_save 中执行耗时同步操作(如发送邮件)

  • 未正确断开信号导致重复触发

  • 信号处理中修改实例引发递归保存

6.2 最佳实践方案

 
from django.db.models.signals import post_save
 from django.dispatch import receiver
 from .tasks import send_welcome_email  # Celery 任务
 ​
 @receiver(post_save, sender=User)
 def user_created_handler(sender, instance, created, **kwargs):
     if created:
         # 异步处理耗时操作
         send_welcome_email.delay(instance.id)
         
     # 避免递归:检查字段变化
     if hasattr(instance, '_dirty'):
         return
     instance._dirty = True
     instance.save()

七、数据库索引配置优化

7.1 索引类型选择

索引方式适用场景示例代码
字段级索引高频查询的单个字段price = models.IntegerField(db_index=True)
组合索引多字段联合查询Meta: index_together = ['category', 'status']
条件索引部分数据高频访问Meta: indexes = [models.Index(fields=['name'], condition=Q(stock__gt=0))]

7.2 索引优化建议

  1. 读多写少的字段优先添加索引

  2. 避免在频繁更新的字段上建索引

  3. 使用 explain() 分析查询计划:

     Product.objects.filter(category=5).explain()


八、多数据库路由配置

8.1 错误现象

  • 模型未正确指定数据库导致写入默认库

  • 跨库关联查询引发 Foreign key mismatch 错误

8.2 正确配置示例

 # settings.py
 DATABASE_ROUTERS = ['path.to.ProductRouter']
 ​
 # routers.py
 class ProductRouter:
     def db_for_read(self, model, **hints):
         if model._meta.app_label == 'products':
             return 'products_db'
         return None
 ​
     def allow_relation(self, obj1, obj2, **hints):
         # 禁止跨库关联
         if obj1._meta.app_label == 'products' and obj2._meta.app_label != 'products':
             return False
         return None

九、文件上传配置安全漏洞

9.1 危险配置

 # models.py(漏洞示例)
 class UserProfile(models.Model):
     avatar = models.FileField(upload_to='/var/www/uploads')  # 绝对路径不安全

9.2 安全配置方案

 # 动态路径 + 文件名处理
 def user_directory_path(instance, filename):
     return f'users/{instance.user.id}/{hashlib.md5(filename.encode()).hexdigest()}.{filename.split(".")[-1]}'
 ​
 class UserProfile(models.Model):
     avatar = models.ImageField(
         upload_to=user_directory_path,
         validators=[FileExtensionValidator(allowed_extensions=['jpg', 'png'])]
     )
 ​
 # settings.py 配置媒体文件隔离
 MEDIA_ROOT = '/secure/media/'  # 禁止Web直接访问
 MEDIA_URL = '/internal/media/' 

十、单元测试中的模型陷阱

10.1 常见问题

  • 测试数据库未正确隔离导致数据污染

  • 未使用 factory_boy 造成测试数据创建缓慢

  • 未清理临时文件导致存储空间耗尽

10.2 优化测试方案

 
# 使用事务加速测试
 from django.test import TransactionTestCase
 ​
 class ProductTest(TransactionTestCase):
     def test_inventory(self):
         with transaction.atomic():
             prod = Product.objects.create(name="Test")
             # 测试代码...
 ​
 # 使用工厂批量创建
 from factory.django import DjangoModelFactory
 class ProductFactory(DjangoModelFactory):
     class Meta:
         model = Product
     
     name = "Default Product"
     price = 100
 ​
 def test_bulk_operations(self):
     products = ProductFactory.create_batch(100)

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

Yant224

点滴鼓励,汇成前行星光🌟

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值