使用 get_or_create 创建带 ManyToMany 字段的 Django 模型

6 投票
2 回答
8630 浏览
提问于 2025-04-16 12:28

假设我有三个 Django 模型:

class Section(models.Model):
    name = models.CharField()

class Size(models.Model):
    section = models.ForeignKey(Section)
    size = models.IntegerField()

class Obj(models.Model):
    name = models.CharField()
    sizes = models.ManyToManyField(Size)

我想导入大量的 Obj 数据,其中很多的尺寸字段会是相同的。不过,由于 Obj 有一个多对多的字段,我不能像平常那样简单地检查是否存在。我希望能做一些类似这样的事情:

try:
    x = Obj(name='foo')
    x.sizes.add(sizemodel1) # these can be looked up with get_or_create
    ...
    x.sizes.add(sizemodelN) # these can be looked up with get_or_create
    # Now test whether x already exists, so I don't add a duplicate
    try:
        Obj.objects.get(x)
    except Obj.DoesNotExist:
        x.save()

但是,我不知道有没有办法这样获取一个对象,你必须传入关键字参数,而这对于多对多字段是行不通的。

有没有什么好的方法可以做到这一点?我唯一想到的办法是构建一组 Q 对象来传递给获取方法:

myq = myq & Q(sizes__id=sizemodelN.id)

但我不确定这样做是否有效……

2 个回答

2

使用一个中间模型,然后对它使用 .get() 方法。

http://docs.djangoproject.com/en/dev/topics/db/models/#extra-fields-on-many-to-many-relationships

一旦你有了这个中间模型,你可以使用 .get()、.filter() 或 .exists() 来检查一个对象是否存在,这样你就可以决定是否需要创建它。需要注意的是,.get() 主要是用来查找数据库中那些唯一的字段 - 如果只是想检查对象是否存在,使用 .exists() 可能会更快。

如果这个方法对你来说太复杂或者不方便,你也可以直接获取 ManyRelatedManager,然后遍历它来判断对象是否存在:

object_sizes = obj.sizes.all()
exists = object_sizes.filter(id__in = some_bunch_of_size_object_ids_you_are_curious_about).exists()
if not exists: 
    (your creation code here)
1

你的例子有点不太合理,因为在一个x还没保存之前,你不能添加多对多的关系。不过,它很好地说明了你想做的事情。你有一个通过get_or_create()创建的Size对象列表,想要在没有重复的obj-size关系时创建一个Obj,对吗?

可惜,这个操作并不是特别简单。用Q(id=F) & Q(id=O) & Q(id=O)这样的方式来连接是无法处理多对多关系的。

你当然可以使用Obj.objects.filter(size__in=Sizes),但这样的话,你会得到一个Obj,它的size在一个庞大的尺寸列表中匹配。

可以看看这个帖子,里面有关于__in的具体问题,是Malcolm回答的,所以我觉得这个答案挺靠谱的。

我写了一些Python代码来处理这个问题。
这是一次性的导入,对吧?

def has_exact_m2m_match(match_list):
    """
    Get exact Obj m2m match 
    """
    if isinstance(match_list, QuerySet):
        match_list = [x.id for x in match_list]

    results = {}
    match = set(match_list)
    for obj, size in \
        Obj.sizes.through.objects.filter(size__in=match).values_list('obj', 'size'):
        # note: we are accessing the auto generated through model for the sizes m2m
        try:
            results[obj].append(size)
        except KeyError:
            results[obj] = [size]

    return bool(filter(lambda x: set(x) == match, results.values()))
    # filter any specific objects that have the exact same size IDs
    # if there is a match, it means an Obj exists with exactly 
    # the sizes you provided to the function, no more.


sizes = [size1, size2, size3, sizeN...]
if has_exact_m2m_match(sizes):
    x = Obj.objects.create(name=foo) # saves so you can use x.sizes.add
    x.sizes.add(sizes)

撰写回答