Django模型中的循环检测

2 投票
1 回答
855 浏览
提问于 2025-04-16 22:46

我有一个模型,它和自己之间有多对多的关系。

我想在这个模型上设置一个验证,防止一个组成为它自己的子组,或者成为它子组的子组,等等。这样做的目的是为了避免出现循环或无限递归的情况。

我尝试在模型的 clean() 方法中实现这个验证,下面是我的代码。

我也尝试在模型的 save() 方法中使用事务来实现这个验证。

但在这两种情况下,我都遇到了问题,导致一些无效的更改(错误的)被保存到了数据库中。如果我尝试对任一实例进行进一步的更改,验证会检测到错误,但此时错误的数据已经在数据库里了。

我在想,这种验证是否可能实现,如果可以的话,能否在模型的验证中完成,这样我就不需要确保团队中的每个人都记得在他们未来创建的所有表单中调用这些验证。

不再耽搁,下面是代码:

class Group(models.Model):
    name = models.CharField(max_length=200)
    sub_groups = models.ManyToManyField('self', through='SubGroup', symmetrical=False)

    def validate_no_group_loops(self, seen=None):
        if seen is None:
            seen = []
        if self.id in seen:
            raise ValidationError("LOOP DETECTED")
        seen.append(self.id)
        for sub_group in self.target.all():
            sub_group.target.validate_no_group_loops(seen)

    # I thought I would use the standard validation mechanism in the clean()
    # method, but it appears that when I recurse back to the group I started 
    # with, I do so with a query to the database which retreives the data before
    # it's been modified. I'm still not 100% sure if this is the case, but
    # regardless, it does not work.
    def clean(self):
        self.validate_no_group_loops()

    # Suspecting that the problem with implementing this in clean() was that 
    # I wasn't testing the data with the pending modifications due to the 
    # repeated queries to the database, I thought that I could handle the
    # validation in save(), let the save actually put the bad data into the
    # database, and then roll back the transaction if I detect a problem.
    # This also doesn't work.
    def save(self, *args, **kwargs):
        super(Group, self).save(*args, **kwargs)
        try:
            self.validate_no_group_loops()
        except ValidationError as e:
            transaction.rollback()
            raise e
        else:
            transaction.commit()


class SubGroup(models.Model):
    VERBS = { '+': '+', '-': '-' }
    action = models.CharField(max_length=1, choices=VERBS.items(), default='+')
    source = models.ForeignKey('Group', related_name='target')
    target = models.ForeignKey('Group', related_name='source')

提前感谢你们提供的任何帮助。

[编辑] 顺便说一下,如果你能根据我使用的事务管理机制看出来,我目前使用的是 django 1.2,因为这是在 RHEL6 的 Fedora EPEL 仓库中可用的版本。如果有解决方案但需要升级到 1.3,我也没问题升级。我还在使用 python 2.6.6,因为这是 RedHat 在 RHEL6 中提供的版本。我更希望避免升级 python,但我怀疑这是否相关。

1 个回答

0

你觉得代码里的“target.”真的应该放在这个循环里面吗?看起来这样做会让它跳过一个层级。

    for sub_group in self.target.all():
        sub_group.target.validate_no_group_loops(seen)

撰写回答