如何用Django软删除多对多的关系

时间:2022-10-05 13:19:14

In my Django project, all entities deleted by the user must be soft deleted by setting the current datetime to deleted_at property. My model looks like this: Trip <-> TripDestination <-> Destination (many to many relation). In other words, a Trip can have multiple destinations.

在我的Django项目中,必须通过将当前日期时间设置为deleted_at属性来软删除用户删除的所有实体。我的模型如下所示:Trip < - > TripDestination < - > Destination(多对多关系)。换句话说,Trip可以有多个目的地。

When I delete a Trip, the SoftDeleteManager filters out all the deleted trip. However, if I request all the destinations of a trip (using get_object_or_404(Trip, pk = id)), I also get the deleted ones (i.e. TripDestination models with deleted_at == null OR deleted_at != null). I really don't understand why since all my models inherit from LifeTimeTracking and are using the SoftDeleteManager.

当我删除Trip时,SoftDeleteManager会过滤掉所有已删除的行程。但是,如果我请求旅行的所有目的地(使用get_object_or_404(Trip,pk = id)),我也会得到已删除的目的地(即具有deleted_at == null或者deleted_at!= null的TripDestination模型)。我真的不明白为什么因为我的所有模型都继承自LifeTimeTracking并使用SoftDeleteManager。

Can someone please help me to understand why the SoftDeleteManager isn't working for n:m relation?

有人可以帮我理解为什么SoftDeleteManager不能用于n:m关系吗?

class SoftDeleteManager(models.Manager):
    def get_query_set(self):
        query_set = super(SoftDeleteManager, self).get_query_set()
        return query_set.filter(deleted_at__isnull = True)

class LifeTimeTrackingModel(models.Model):
    created_at = models.DateTimeField(auto_now_add = True)
    updated_at = models.DateTimeField(auto_now = True)
    deleted_at = models.DateTimeField(null = True)

    objects = SoftDeleteManager()
    all_objects = models.Manager()

    class Meta:
        abstract = True

class Destination(LifeTimeTrackingModel):
    city_name = models.CharField(max_length = 45)

class Trip(LifeTimeTrackingModel):
    name = models.CharField(max_length = 250)
    destinations = models.ManyToManyField(Destination, through = 'TripDestination')

class TripDestination(LifeTimeTrackingModel):
    trip = models.ForeignKey(Trip)
    destination = models.ForeignKey(Destination)

Resolution I filed the bug 17746 in Django Bug DB. Thanks to Caspar for his help on this.

解决方案我在Django Bug DB中提交了错误17746。感谢Caspar对此的帮助。

2 个解决方案

#1


2  

It looks like this behaviour comes from the ManyToManyField choosing to use its own manager, which the Related objects reference mentions, because when I try making up some of my own instances & try soft-deleting them using your model code (via the manage.py shell) everything works as intended.

看起来这种行为来自ManyToManyField选择使用自己的管理器,相关对象引用了这一点,因为当我尝试编写一些自己的实例并尝试使用模型代码软删除它们时(通过manage.py) shell)一切都按预期工作。

Unfortunately it doesn't mention how you can override the model manager. I spent about 15 minutes searching through the ManyToManyField source but haven't tracked down where it instantiates its manager (looking in django/db/models/fields/related.py).

不幸的是,它没有提到如何覆盖模型管理器。我花了大约15分钟搜索了ManyToManyField源,但没有找到它实例化其管理器的位置(查看django / db / models / fields / related.py)。

To get the behaviour you are after, you should specify use_for_related_fields = True on your SoftDeleteManager class as specified by the documentation on controlling automatic managers:

要获得您所追求的行为,您应该在SoftDeleteManager类上指定use_for_related_fields = True,如控制自动管理器的文档所指定:

class SoftDeleteManager(models.Manager):
    use_for_related_fields = True

    def get_query_set(self):
        query_set = super(SoftDeleteManager, self).get_query_set()
        return query_set.filter(deleted_at__isnull = True)

This works as expected: I'm able to define a Trip with 2 Destinations, each through a TripDestination, and if I set a Destination's deleted_at value to datetime.datetime.now() then that Destination no longer appears in the list given by mytrip.destinations.all(), which is what you are after near as I can tell.

这按预期工作:我能够通过TripDestination定义一个具有2个目的地的Trip,如果我将Destination的deleted_at值设置为datetime.datetime.now(),那么该目的地不再出现在mytrip给出的列表中.destinations.all(),就像我说的那样,这就是你所追求的。

However, the docs also specifically say do not filter the query set by overriding get_query_set() on a manager used for related fields, so if you run into problems later, bear this in mind as a possible cause.

但是,文档还特别指出,不要通过覆盖用于相关字段的管理器上的get_query_set()来过滤查询集,因此如果您以后遇到问题,请记住这一点作为可能的原因。

#2


2  

To enable filtering by deleted_at field of Destinantion and Trip models setting use_for_related_fields = True for SoftDeleteManager class is enough. As per Caspar's answer this does not return deleted Destinations for trip_object.destinations.all().

要通过Destinantion和Trip模型的deleted_at字段启用过滤,设置use_for_related_fields = True对于SoftDeleteManager类就足够了。根据Caspar的回答,这不会返回trip_object.destinations.all()的已删除目标。

However from your comments we can see you would like to filter out Destinations that are linked to Trip via a TripDestination object with a set deleted_at field, a.k.a. soft delete on a through instance.

但是根据您的评论,我们可以看到您希望过滤掉通过TripDestination对象链接到Trip的目标,并在实例上设置deleted_at字段,a.k.a。软删除。

Let's clarify the way managers work. Related managers are the managers of the remote model, not of a through model.

让我们澄清管理者的工作方式。相关经理是远程模型的管理者,而不是直通模型。

trip_object.destinantions.some_method() calls default Destination manager. destinantion_object.trip_set.some_method() calls default Trip manager. TripDestination manager is not called at any time.

trip_object.destinantions.some_method()调用默认目标管理器。 destinantion_object.trip_set.some_method()调用默认的Trip管理器。 TripDestination管理器不会随时被调用。

You can call it with trip_object.destinantions.through.objects.some_method(), if you really want to. Now, what I would do is add an Instance method Trip.get_destinations and a similar Destination.get_trips that filters out deleted connections.

如果你真的想,可以使用trip_object.destinantions.through.objects.some_method()调用它。现在,我要做的是添加一个Instance方法Trip.get_destinations和一个类似的Destination.get_trips来过滤掉已删除的连接。

If you insist on using the manager to do the filtering it gets more complicated:

如果您坚持使用管理器进行过滤,则会变得更加复杂:

class DestinationManager(models.Manager):
    use_for_related_fields = True

    def get_query_set(self):
        query_set = super(DestinationManager, self).get_query_set()
        if hasattr(self, "through"):
            through_objects = self.through.objects.filter(
                destination_id=query_set.filter(**self.core_filters).get().id,
                trip_id=self._fk_val,
                deleted_at__isnull=True)
            query_set = query_set.filter(
                id__in=through_objects.values("destination_id"))

        return query_set.filter(deleted_at__isnull = True)

The same would have to be done for TripManager as they would differ. You may check the performance and look at django/db/models/fields/related.py for reference.

TripManager也必须这样做,因为它们会有所不同。您可以检查性能并查看django / db / models / fields / related.py以供参考。

Modifying the get_queryset method of the default manager may hamper the ability to backup the database and the documentation discourages it. Writing a Trip.get_destinations method is the alternative.

修改默认管理器的get_queryset方法可能会妨碍备份数据库的能力,文档也不鼓励这样做。编写Trip.get_destinations方法是另一种选择。

#1


2  

It looks like this behaviour comes from the ManyToManyField choosing to use its own manager, which the Related objects reference mentions, because when I try making up some of my own instances & try soft-deleting them using your model code (via the manage.py shell) everything works as intended.

看起来这种行为来自ManyToManyField选择使用自己的管理器,相关对象引用了这一点,因为当我尝试编写一些自己的实例并尝试使用模型代码软删除它们时(通过manage.py) shell)一切都按预期工作。

Unfortunately it doesn't mention how you can override the model manager. I spent about 15 minutes searching through the ManyToManyField source but haven't tracked down where it instantiates its manager (looking in django/db/models/fields/related.py).

不幸的是,它没有提到如何覆盖模型管理器。我花了大约15分钟搜索了ManyToManyField源,但没有找到它实例化其管理器的位置(查看django / db / models / fields / related.py)。

To get the behaviour you are after, you should specify use_for_related_fields = True on your SoftDeleteManager class as specified by the documentation on controlling automatic managers:

要获得您所追求的行为,您应该在SoftDeleteManager类上指定use_for_related_fields = True,如控制自动管理器的文档所指定:

class SoftDeleteManager(models.Manager):
    use_for_related_fields = True

    def get_query_set(self):
        query_set = super(SoftDeleteManager, self).get_query_set()
        return query_set.filter(deleted_at__isnull = True)

This works as expected: I'm able to define a Trip with 2 Destinations, each through a TripDestination, and if I set a Destination's deleted_at value to datetime.datetime.now() then that Destination no longer appears in the list given by mytrip.destinations.all(), which is what you are after near as I can tell.

这按预期工作:我能够通过TripDestination定义一个具有2个目的地的Trip,如果我将Destination的deleted_at值设置为datetime.datetime.now(),那么该目的地不再出现在mytrip给出的列表中.destinations.all(),就像我说的那样,这就是你所追求的。

However, the docs also specifically say do not filter the query set by overriding get_query_set() on a manager used for related fields, so if you run into problems later, bear this in mind as a possible cause.

但是,文档还特别指出,不要通过覆盖用于相关字段的管理器上的get_query_set()来过滤查询集,因此如果您以后遇到问题,请记住这一点作为可能的原因。

#2


2  

To enable filtering by deleted_at field of Destinantion and Trip models setting use_for_related_fields = True for SoftDeleteManager class is enough. As per Caspar's answer this does not return deleted Destinations for trip_object.destinations.all().

要通过Destinantion和Trip模型的deleted_at字段启用过滤,设置use_for_related_fields = True对于SoftDeleteManager类就足够了。根据Caspar的回答,这不会返回trip_object.destinations.all()的已删除目标。

However from your comments we can see you would like to filter out Destinations that are linked to Trip via a TripDestination object with a set deleted_at field, a.k.a. soft delete on a through instance.

但是根据您的评论,我们可以看到您希望过滤掉通过TripDestination对象链接到Trip的目标,并在实例上设置deleted_at字段,a.k.a。软删除。

Let's clarify the way managers work. Related managers are the managers of the remote model, not of a through model.

让我们澄清管理者的工作方式。相关经理是远程模型的管理者,而不是直通模型。

trip_object.destinantions.some_method() calls default Destination manager. destinantion_object.trip_set.some_method() calls default Trip manager. TripDestination manager is not called at any time.

trip_object.destinantions.some_method()调用默认目标管理器。 destinantion_object.trip_set.some_method()调用默认的Trip管理器。 TripDestination管理器不会随时被调用。

You can call it with trip_object.destinantions.through.objects.some_method(), if you really want to. Now, what I would do is add an Instance method Trip.get_destinations and a similar Destination.get_trips that filters out deleted connections.

如果你真的想,可以使用trip_object.destinantions.through.objects.some_method()调用它。现在,我要做的是添加一个Instance方法Trip.get_destinations和一个类似的Destination.get_trips来过滤掉已删除的连接。

If you insist on using the manager to do the filtering it gets more complicated:

如果您坚持使用管理器进行过滤,则会变得更加复杂:

class DestinationManager(models.Manager):
    use_for_related_fields = True

    def get_query_set(self):
        query_set = super(DestinationManager, self).get_query_set()
        if hasattr(self, "through"):
            through_objects = self.through.objects.filter(
                destination_id=query_set.filter(**self.core_filters).get().id,
                trip_id=self._fk_val,
                deleted_at__isnull=True)
            query_set = query_set.filter(
                id__in=through_objects.values("destination_id"))

        return query_set.filter(deleted_at__isnull = True)

The same would have to be done for TripManager as they would differ. You may check the performance and look at django/db/models/fields/related.py for reference.

TripManager也必须这样做,因为它们会有所不同。您可以检查性能并查看django / db / models / fields / related.py以供参考。

Modifying the get_queryset method of the default manager may hamper the ability to backup the database and the documentation discourages it. Writing a Trip.get_destinations method is the alternative.

修改默认管理器的get_queryset方法可能会妨碍备份数据库的能力,文档也不鼓励这样做。编写Trip.get_destinations方法是另一种选择。