【Django】 ManyToManyField 获取数据时 按添加时间排列

本文围绕Django中ManyToManyField展开,该字段用于实现数据库多对多模型,但获取数据的排序有时不符合预期。提出无需改变数据库结构的解决方案,利用Django引入的“中间表”和隐藏的“Modal”,通过自增索引实现按添加时间排列,并解释了相关原理和“隐藏的Modal”的含义。
部署运行你感兴趣的模型镜像

在Django中,ManyToManyField是常用的Field,实现数据库中多对多模型。

例如,人与团体的关系(一个人可加入多个团体,一个团体有多个人):

class Person(models.Model):
    name = models.CharField(max_length=50)

class Group(models.Model):
    name = models.CharField(max_length=128)
    members = models.ManyToManyField(Person, related_name='groups')

但是,当我们通过group.members(或person.groups)获取一个组织的所有成员(或获取这个人参加的所有组织)时,拿到的数据是按照group_id (或person_id)排列的,有时这样的行为不符合预期:

比如前端展示这个团体的人,希望是按照加入顺序来展示,而非按照人的id排序。
试想,当团队人数很多,你加了一个人后,他不是出现在最后,而是钻在一群人的中央,你很可能找不到他了!甚至怀疑自己有没有添加成功。这种类似的场合很常见

解决方案(无需改变数据库结构)

Django在实现ManyToManyField时,会默默引入一张“中间表”,它默认包括3列:idmodel1_idmodel2_id。以上面的Person和Group为例,这3列就是:idperson_idgroup_id。这个id是自增索引,当然也意味着:建立的时间晚,id相对就大。我们可以通过id来实现“按添加时间排列”,而无需对数据库结构做任何改变!

为了实现它,我们获取数据的方式需要适当改变:

group1 = Group.objects.get(id=1)
# 之前的方式,数据按 person_id 排列
group1.members.all()
# 改变后,数据按添加时间排列
Group.members.through.objects.filter(group=group1).order_by('id').all()
原理是什么?

刚才提到,Django会默默引入一张“中间表”,这张“中间表”其实也是对应着一个隐藏的"Modal"的(生成顺序其实是Django先生成隐藏的Modal,再根据这个Modal生成那个数据库表)。Group.members.through这句话其实是引用了那个隐藏的"Modal"。它后面的.objects,就像对待普通Modal一样,对待它就可以了。

“隐藏的Modal”是什么?

在本文开头的例子,隐藏的"Modal"长这个样子:

class Person_Group(models.Model):
    group = models.ForeignKey(Group, on_delete=models.CASCADE)
    person = models.ForeignKey(Person, on_delete=models.CASCADE)

它生成的数据库表就是3列:idperson_idgroup_id

其实,这张表你也可以自己定义出来,不让Django自动生成,只是你需要多提供一个参数throuth给ManyToManyField:

class Person(models.Model):
    name = models.CharField(max_length=50)

class Group(models.Model):
    name = models.CharField(max_length=128)
    members = models.ManyToManyField(Person, through='Membership')

class Membership(models.Model):
    group = models.ForeignKey(Group, on_delete=models.CASCADE)
    person = models.ForeignKey(Person, on_delete=models.CASCADE)
    invite_reason = models.CharField(max_length=64)

这里我们给中间表起了名字Membership,名字随意起,through参数跟名字是对应的就行。

为什么through是写的字符串'Membership',而非直接写Membership
因为Python解析器解析类的定义时,会把类的属性、方法都定义完,才会继续解析下一个类(在定义members时,Membership尚未被解析,此时如果引用它会报错,所以Django面对这种问题的解决方案就是——先用字符串代替,这是很常见的做法)

您可能感兴趣的与本文相关的镜像

EmotiVoice

EmotiVoice

AI应用

EmotiVoice是由网易有道AI算法团队开源的一块国产TTS语音合成引擎,支持中英文双语,包含2000多种不同的音色,以及特色的情感合成功能,支持合成包含快乐、兴奋、悲伤、愤怒等广泛情感的语音。

Django 中,`ManyToManyField` 用于表示多对多关系,例如角色(Role)与权限(Permission)之间的关联。当尝试修改或添加 `permissions` 字段的数据,可能会遇到无法更新的情况。以下是一些常见原因及其解决方法。 ### 数据未正确保存导致操作失败 如果尝试使用 `add()`、`set()` 或 `remove()` 方法修改多对多关系,涉及的对象尚未保存到数据库中,则会引发异常。因此,在执行这些操作之前,必须确保对象已经被保存[^1]。 ```python role = Role(title='管理员') role.save() # 必须先保存角色对象 permission = Permission(name='查看权限', url='/view/') permission.save() # 必须先保存权限对象 role.permissions.add(permission) # 此可以正常添加权限 ``` ### 参数类型错误导致方法调用失败 `add()` 方法接受的是模型实例或主键值(id),若传入了其他类型(如字符串、None)会导致操作失败。类似地,`remove()` 和 `clear()` 方法也要求正确的参数格式[^2]。 ```python # 正确使用方式 role.permissions.add(permission) # 传入 Permission 实例 role.permissions.add(permission.id) # 传入 Permission 的主键值 role.permissions.remove(permission) # 移除特定权限 role.permissions.clear() # 清空所有权限 ``` ### 缓存机制导致获取旧数据 由于 Django ORM 的查询缓存机制,可能在某些情况下获取到的是旧数据。为避免此类问题,可以在操作前重新获取对象以确保其状态是最新的: ```python role = Role.objects.get(id=role.id) permission = Permission.objects.get(id=permission.id) role.permissions.add(permission) ``` ### 自定义中间表结构不一致或未同步数据库结构 如果使用了自定义中间表(通过 `through` 参数指定),则需要确保该表的字段定义与模型中的 `ManyToManyField` 定义保持一致。此外,运行 `makemigrations` 和 `migrate` 命令是必要的,以确保数据库结构与模型定义同步[^3]。 ```bash python manage.py makemigrations python manage.py migrate ``` ### 使用 set() 替代 add() 如果希望一次性替换某个角色的所有权限,可以使用 `set()` 方法代替 `add()`。需要注意的是,`set()` 接受一个可迭代对象,并且要求所有元素都是模型实例或主键值: ```python permissions = [p1, p2, p3] # 假设 p1, p2, p3 是已存在的 Permission 实例 role.permissions.set(permissions) ``` ### 启用事务支持保证数据一致性 在涉及多个数据库操作,建议启用事务以保证数据一致性。如果在操作过程中发生异常,可以通过事务回滚来避免部分更新带来的问题: ```python from django.db import transaction try: with transaction.atomic(): role = Role.objects.create(title='测试角色') permission = Permission.objects.create(name='测试权限', url='/test/') role.permissions.add(permission) except Exception as e: pass ``` ### 确保 ManyToManyField 配置正确 检查模型中的 `ManyToManyField` 定义是否正确,特别是字段名称和目标模型是否匹配。例如,在 `Role` 模型中定义的 `permissions` 字段应该指向 `Permission` 模型: ```python class Role(models.Model): title = models.CharField(max_length=32, verbose_name='身份名称') permissions = models.ManyToManyField(to='Permission') # 确保指向正确的模型 ``` 如果字段定义有误,例如拼写错误或指向不存在的模型,则无法正常工作[^2]。 ---
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

打赏作者

Hull Qin

你的鼓励将是我创作的最大动力

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

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

打赏作者

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

抵扣说明:

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

余额充值