Django:合并对象

8 投票
5 回答
13405 浏览
提问于 2025-04-16 02:13

我有这样一个模型:

class Place(models.Model):
    name = models.CharField(max_length=80, db_index=True)
    city = models.ForeignKey(City)
    address = models.CharField(max_length=255, db_index=True)
    # and so on

因为我从很多地方导入这些数据,而且我的网站用户可以添加新的地点,所以我需要一个方法来通过管理界面合并这些地点。问题是,地点的名称不太可靠,因为它们可能有很多不同的拼写方式等等。

我习惯使用这样的方式:

class Place(models.Model):
    name = models.CharField(max_length=80, db_index=True) # canonical
    city = models.ForeignKey(City)
    address = models.CharField(max_length=255, db_index=True)
    # and so on

class PlaceName(models.Model):
    name = models.CharField(max_length=80, db_index=True)
    place = models.ForeignKey(Place)

像这样查询

Place.objects.get(placename__name='St Paul\'s Cathedral', city=london)

然后像这样合并

class PlaceAdmin(admin.ModelAdmin):
    actions = ('merge', )

    def merge(self, request, queryset):
        main = queryset[0]
        tail = queryset[1:]

        PlaceName.objects.filter(place__in=tail).update(place=main)
        SomeModel1.objects.filter(place__in=tail).update(place=main)
        SomeModel2.objects.filter(place__in=tail).update(place=main)
        # ... etc ...

        for t in tail:
            t.delete()

        self.message_user(request, "%s is merged with other places, now you can give it a canonical name." % main)
    merge.short_description = "Merge places"

正如你所看到的,我必须用新的值更新所有与地点相关的其他模型。但是这不是一个很好的解决方案,因为我必须把每个新模型都添加到这个列表中。

我该如何在删除某些对象之前“级联更新”所有外键?

或者,也许还有其他方法可以避免合并。

5 个回答

2

根据在接受的回答中评论里提供的代码片段,我开发了以下内容。这个代码没有处理通用外键(GenericForeignKeys)。我不太喜欢使用通用外键,因为我觉得这说明你使用的模型有问题。

在这个回答中,我用了很多代码来实现这个功能,但我已经更新了我的代码,使用了在这里提到的django-super-deduper。那时候,django-super-deduper对不受管理的模型处理得不好。我提交了一个问题,看起来很快就会修复。我还使用了django-audit-log,我不想把那些记录合并在一起。我保留了函数的签名和@transaction.atomic()这个装饰器。这在出现问题时是很有帮助的。

from django.db import transaction
from django.db.models import Model, Field
from django_super_deduper.merge import MergedModelInstance


class MyMergedModelInstance(MergedModelInstance):
    """
        Custom way to handle Issue #11: Ignore models with managed = False
        Also, ignore auditlog models.
    """
    def _handle_o2m_related_field(self, related_field: Field, alias_object: Model):
        if not alias_object._meta.managed and "auditlog" not in alias_object._meta.model_name:
            return super()._handle_o2m_related_field(related_field, alias_object)

    def _handle_m2m_related_field(self, related_field: Field, alias_object: Model):
        if not alias_object._meta.managed and "auditlog" not in alias_object._meta.model_name:
            return super()._handle_m2m_related_field(related_field, alias_object)

    def _handle_o2o_related_field(self, related_field: Field, alias_object: Model):
        if not alias_object._meta.managed and "auditlog" not in alias_object._meta.model_name:
            return super()._handle_o2o_related_field(related_field, alias_object)


@transaction.atomic()
def merge(primary_object, alias_objects):
    if not isinstance(alias_objects, list):
        alias_objects = [alias_objects]
    MyMergedModelInstance.create(primary_object, alias_objects)
    return primary_object
3

现在有两个库提供了最新的模型合并功能,这些功能可以处理相关的模型:

一个是Django Extensions里的merge_model_instances管理命令。

另一个是Django Super Deduper

6

如果有人感兴趣,这里有一段非常通用的代码:

def merge(self, request, queryset):
    main = queryset[0]
    tail = queryset[1:]

    related = main._meta.get_all_related_objects()

    valnames = dict()
    for r in related:
        valnames.setdefault(r.model, []).append(r.field.name)

    for place in tail:
        for model, field_names in valnames.iteritems():
            for field_name in field_names:
                model.objects.filter(**{field_name: place}).update(**{field_name: main})

        place.delete()

    self.message_user(request, "%s is merged with other places, now you can give it a canonical name." % main)

撰写回答