【问题标题】:Proper way to bulk_create for ManyToMany field, Django?Django 为 ManyToMany 字段批量创建的正确方法?
【发布时间】:2015-12-04 14:28:52
【问题描述】:

我有这个用于表格填充的代码。

def add_tags(count):
    print "Add tags"
    insert_list = []
    photo_pk_lower_bound = Photo.objects.all().order_by("id")[0].pk
    photo_pk_upper_bound = Photo.objects.all().order_by("-id")[0].pk
    for i in range(count):
        t = Tag( tag = 'tag' + str(i) )
        insert_list.append(t)
    Tag.objects.bulk_create(insert_list)
    for i in range(count):
        random_photo_pk = randint(photo_pk_lower_bound, photo_pk_upper_bound)
        p = Photo.objects.get( pk = random_photo_pk )
        t = Tag.objects.get( tag = 'tag' + str(i) )
        t.photos.add(p)

这是模型:

class Tag(models.Model):
    tag = models.CharField(max_length=20,unique=True)
    photos = models.ManyToManyField(Photo)

据我了解这个答案:Django: invalid keyword argument for this function 我必须先保存标签对象(由于 ManyToMany 字段),然后通过add() 将照片附加到它们。但是对于大的count,这个过程需要的时间太长了。有什么方法可以重构此代码以使其更快?

一般来说,我想用随机虚拟数据填充标签模型。

EDIT 1(照片模型)

class Photo(models.Model):
    photo = models.ImageField(upload_to="images")
    created_date = models.DateTimeField(auto_now=True)
    user = models.ForeignKey(User)

    def __unicode__(self):
       return self.photo.name

【问题讨论】:

    标签: python django many-to-many


    【解决方案1】:

    TL;DR 使用 “through” 模型批量插入 m2m 关系。

    "Tag.photos.through" => Django generated Model with 3 fields [ id, photo, tag ]
    photo_tag_1 = Tag.photos.through(photo_id=1, tag_id=1)
    photo_tag_2 = Tag.photos.through(photo_id=1, tag_id=2)
    Tag.photos.through.objects.bulk_insert([photo_tag_1, photo_tag_2, ...])
    

    这是我所知道的最快的方法,我一直使用它来创建测试数据。我可以在几分钟内生成数百万条记录。

    Georgy 编辑:

    def add_tags(count):
        Tag.objects.bulk_create([Tag(tag='tag%s' % t) for t in range(count)])
    
        tag_ids = list(Tag.objects.values_list('id', flat=True))
        photo_ids = Photo.objects.values_list('id', flat=True)
        tag_count = len(tag_ids)
           
        for photo_id in photo_ids:
            tag_to_photo_links = []
            shuffle(tag_ids)
    
            rand_num_tags = randint(0, tag_count)
            photo_tags = tag_ids[:rand_num_tags]
    
            for tag_id in photo_tags:
                # through is the model generated by django to link m2m between tag and photo
                photo_tag = Tag.photos.through(tag_id=tag_id, photo_id=photo_id)
                tag_to_photo_links.append(photo_tag)
    
            Tag.photos.through.objects.bulk_create(tag_to_photo_links, batch_size=7000)
    

    我没有创建要测试的模型,但结构已经存在,您可能需要调整一些东西才能使其正常工作。如果您遇到任何问题,请告诉我。

    [编辑]

    【讨论】:

    • 您好,很抱歉回答延迟。我可以说您确实有使用through 的正确想法,我确实为自己找到了相同的解决方案,尽管此功能缺少文档,您能给我一些建议吗?至少与我相比,您是一名高级 Python 开发人员,我必须阅读一些文档才能完全理解您的答案,尽管我不得不承认简单的复制和过去对我不起作用。非常感谢你的帮助!我稍后会尝试添加其他信息。
    • 你能发布照片的模型定义吗?
    • 抱歉,我花了点时间。该模型在编辑部分。
    • 嗨。我之前对您的答案进行了编辑,但被拒绝了。您的代码有一些错误,请您修改一下以备不时之需? 1) 不是Tag.Photos.through,而是Tag.photos.through。 2) Photo.objects.value_listPhoto.objects.values_list(这里是错字)。 3)你不能这样洗牌tag_ids,使用list()转换它。 4)并且您必须将最后一行bulk_create() 移出forloop,否则代码会尝试添加重复项。提前感谢您的宝贵时间!
    • 只是一个更新.. Tag.photos.through.bulk_insert() 将导致 SOMETHING 没有属性 bulk_insert()。相反,我们应该使用 Tag.photos.through.objects.bulk_create()。
    【解决方案2】:

    如 Du D 的回答所示,Django ManyToMany 字段使用一个名为 through 的表,其中包含三列:关系的 ID、to 链接的对象的 ID 和对象链接来自。您可以在 through 上使用 bulk_create 批量创建多对多关系。

    作为一个简单的示例,您可以像这样批量创建标签到照片的关系:

    tag1 = Tag.objects.get(id=1)
    tag2 = Tag.objects.get(id=2)
    photo1 = Photo.objects.get(id=1)
    photo2 = Photo.objects.get(id=2)
    
    
    through_objs = [
        Tag.photos.through(
            photo_id=photo1.id,
            tag_id=tag1.id,
        ),
        Tag.photos.through(
            photo_id=photo1.id,
            tag_id=tag2.id,
        ),
        Tag.photos.through(
            photo_id=photo2.id,
            tag_id=tag2.id,
        ),
    ]
    Tag.photos.through.objects.bulk_create(through_objs)
    

    一般解决方案

    这是一个通用解决方案,您可以运行它来在任何对象对列表之间建立多对多关系。

    from typing import Iterable
    from collections import namedtuple
    
    
    ManyToManySpec = namedtuple(
        "ManyToManySpec", ["from_object", "to_object"]
    )
    
    
    def bulk_create_manytomany_relations(
        model_from,
        field_name: str,
        model_from_name: str,
        model_to_name: str,
        specs: Iterable[ManyToManySpec]
    ):
        through_objs = []
        for spec in specs:
            through_objs.append(
                getattr(model_from, field_name).through(
                    **{
                        f"{model_from_name.lower()}_id": spec.from_object.id,
                        f"{model_to_name.lower()}_id": spec.to_object.id,
                    }
                )
            )
        getattr(model_from, field_name).through.objects.bulk_create(through_objs)
    

    示例用法

    tag1 = Tag.objects.get(id=1)
    tag2 = Tag.objects.get(id=2)
    photo1 = Photo.objects.get(id=1)
    photo2 = Photo.objects.get(id=2)
    
    bulk_create_manytomany_relations(
        model_from=Tag,
        field_name="photos",
        model_from_name="tag",
        model_to_name="photo",
        specs=[
            ManyToManySpec(from_object=tag1, to_object=photo1),
            ManyToManySpec(from_object=tag1, to_object=photo2),
            ManyToManySpec(from_object=tag2, to_object=photo2),
        ]
    )
    

    【讨论】:

    • 问题不在于这些对象没有保存因此没有id吗?我看不出这将如何工作?它仍然会创建一个ValueError?
    • 问题中的问题是他们正在迭代`for i in range(count):`,然后为每个项目执行t.photos.add(p)bulk_create 并不昂贵,这不是问题所在。诚然,要使用我的解决方案,他们必须迭代 Photo.objects.all()Tag.objects.all() 或一些 filter 版本。我不记得这是一个查询还是多个查询……也许他们需要将其转换为一个列表。
    猜你喜欢
    • 2020-11-15
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2020-02-01
    • 2016-06-06
    • 1970-01-01
    • 2018-05-05
    相关资源
    最近更新 更多