使用Django bulk_create创建对象后如何获取其主键?

96

在Django 1.4及以上版本中使用bulk_create功能创建的项目中,有没有一种方法可以获取这些项目的主键?


我也想知道人们是如何解决这个问题的。我想你得做一些像锁定表格,运行bulk_create,查询所有新记录,然后解锁表格的事情吧?从文档中很清楚地看出,bulk_create不会返回auto_increment键,所以唯一的方法就是通过复杂的解决办法来解决这个问题。另一种方法可能是拥有另一个表格来跟踪按顺序使用的主键,因此你预先分配一个ID块,然后运行bulk_create,你应该知道预期的主键。我对这两个想法都不满意 :( - DanH
2
似乎在Django开发中有一个解决这个问题的努力 https://code.djangoproject.com/ticket/19527 - DanH
1
哦耶!看起来我的大约4年前的提案刚好融入了Django 1.10,让我们所有人都能够享受。 :-) 目前似乎只适用于Postgres。 - Tuttle
现在使用Django 1.10和PostgreSQL已经成为可能:https://docs.djangoproject.com/en/dev/ref/models/querysets/#bulk-create - Maxime R.
希望也能支持MySQL。 - Shift 'n Tab
11个回答

89

2016

自Django 1.10开始,它现在受到支持(仅限Postgres),这是一个文档链接

>>> list_of_objects = Entry.objects.bulk_create([
...     Entry(headline="Django 2.0 Released"),
...     Entry(headline="Django 2.1 Announced"),
...     Entry(headline="Breaking: Django is awesome")
... ])
>>> list_of_objects[0].id
1

从变更日志中:

在Django 1.10版本中进行了更改: 当使用PostgreSQL时,支持通过bulk_create()方法设置对象的主键


Django 1.10中,现已支持使用bulk_create()方法创建对象并在PostgreSQL数据库中设置主键。


3
我是一个MySQL用户,感到难过。 - Shift 'n Tab
5
在MySQL中,使用bulk_create创建的条目会在数据库中有一个id值吗? - Mohammed Shareef C
2
@MohammedShareefC 它将在数据库中获取一个主键,但bulk_create方法返回的列表与您提供的列表相同,并且该列表中的本地对象(即该列表的成员)未设置它,就像pyriku在他的答案中展示的那样 - Yushin Washio
4
还有其他人在使用PostgreSQL时遇到返回None的情况吗? - The Voyager
5
刚在文档中找到了这个:对于支持它的数据库(除了Oracle),将ignore_conflicts参数设置为True会告诉数据库忽略插入任何违反约束条件的行,如重复唯一值。启用此参数会禁用在每个模型实例上设置主键(如果数据库通常支持它)。 - The Voyager
显示剩余4条评论

41
根据文档,您无法这样做:https://docs.djangoproject.com/en/dev/ref/models/querysets/#bulk-create
bulk-create的作用是高效地创建大量对象并节省很多查询。但这意味着您得到的响应有一定的不完整性。如果您执行以下操作:
>>> categories = Category.objects.bulk_create([
    Category(titel="Python", user=user),
    Category(titel="Django", user=user),
    Category(titel="HTML5", user=user),
])

>>> [x.pk for x in categories]
[None, None, None]

这并不意味着你的分类没有主键(pk),只是查询没有检索到它们(如果键是AutoField)。如果由于某种原因您想要pk,您将需要经典方式保存对象。


20
我认为这个问题的重点就在于如何绕过bulk_create这个限制,以便可靠地检索已创建的ID。至少这是我对这个问题的理解。请问人们都使用哪些技巧来实现这一点? - DanH
3
有一个开放的 PR,旨在添加批量创建时返回 IDs 的支持:https://github.com/django/django/pull/5166值得注意的是,Postgres 支持返回 IDs,因此可以通过原始 SQL 操作立即获取 IDs。 - gordonc

36

我能想到两种方法:

a)你可以做...

category_ids = Category.objects.values_list('id', flat=True)
categories = Category.objects.bulk_create([
    Category(title="title1", user=user, created_at=now),
    Category(title="title2", user=user, created_at=now),
    Category(title="title3", user=user, created_at=now),
])
new_categories_ids = Category.objects.exclude(id__in=category_ids).values_list('id', flat=True)

如果查询集非常庞大,这可能会有些昂贵。

b)如果模型有一个created_at字段,

now = datetime.datetime.now()
categories = Category.objects.bulk_create([
    Category(title="title1", user=user, created_at=now),
    Category(title="title2", user=user, created_at=now),
    Category(title="title3", user=user, created_at=now),
])

new_cats = Category.objects.filter(created_at >= now).values_list('id', flat=True)

这有一个限制,即有一个字段存储对象的创建时间。


3
你知道,我已经有一个 date_created 字段了,所以虽然最小的努力是无论如何都要添加一个。我唯一担心的是多个查询可能会同时访问数据库,因此我想在 bulk_create 之前和 created_at 查询之后实现某种锁定机制。 - DanH
1
是的,原子事务可以用来确保避免竞态条件。 - karthikr
关于第一种方法,在 Django 1.10 中,使用 values_list('id', flat=True) 返回 queryset 对象,bulk_create 调用后似乎才会被评估。将 category_ids 包装在 list() 中来强制进行数据库查询,这将有所帮助。 - George
糟糕透了,我想甚至“选择max(id)更好”。 - deathangel908
1
@deathangel908 不要使用 max(id),我尝试过并遇到了问题。MariaDB文档明确指出不要假设PK除唯一性外还有其他任何属性。 - Patrick
你如何知道 Django 的批量创建是否完成? - Mathijs

14

其实我的同事已经建议了以下解决方案,现在看来这个方案显得非常明显。添加一个新列名叫做bulk_ref,你需要为每一行填入一个唯一的值。之后只需预先设置好bulk_ref并查询表格,你就可以轻松检索到插入的记录了。例如:

cars = [Car(
    model="Ford",
    color="Blue",
    price="5000",
    bulk_ref=5,
),Car(
    model="Honda",
    color="Silver",
    price="6000",
    bulk_ref=5,
)]
Car.objects.bulk_create(cars)
qs = Car.objects.filter(bulk_ref=5)

21
为了解决查询问题而向模型添加额外字段并不是一种好的做法,应该避免这样做。 - max
2
虽然如此,批量插入应该被视为一种优化,这可能会妥协设计。在“不够快”和“不完美的设计”之间需要平衡。在Django PR 5166实现之前,对于需要批量插入优化的团队来说,这可能是一个合理的妥协。 - Scott A
@varun 我记不清我们最终是如何实现的,bulk_ref 可能是 UUID 或类似的随机数。它不需要是顺序的或相对于其他 bulk_refs 的。 - DanH
1
@varun 【因此,要使重复的概率为十亿分之一,必须生成103万亿个版本4 UUID。】(来源:https://en.wikipedia.org/wiki/Universally_unique_identifier#Collisions) - DylanYoung
1
@DanH 看起来是一个合理的选择,避免查询并添加一个额外的字段可能会非常有帮助。 - varun
显示剩余3条评论

4

我将为您分享在InnoDB (MySQL)中处理AUTO_INCREMENT以及在bulk_create (Django)中获取主键的方法。

根据bulk_create文档,如果模型的主键是AutoField,则不会像save()函数一样检索并设置主键属性,除非数据库后端支持它(目前只有PostgreSQL支持)。所以我们需要先找出在Django或MySQL中引起问题的原因,然后再寻找解决方案。

Django中的AUTO FIELD实际上就是MySQL中的AUTO_INCREMENT。它用于为新行生成唯一标识(参考文献)

当您想要bulk_create对象(Django)时,意味着在单个SQL查询中插入多行。但是如何检索最近自动生成的PK(主键)呢?感谢LAST_INSERT_ID它返回最近执行的INSERT语句的首个自动生成的值......即使其他客户端生成了自己的AUTO_INCREMENT值,此值也不会受到影响。这种行为确保每个客户端都可以检索其自己的ID,而无需关注其他客户端的活动,也无需锁定或事务。

我鼓励您阅读InnoDB中的AUTO_INCREMENT处理和Django代码django.db.models.query.QuerySet.bulk_create以了解为什么Django尚未支持MySQL。很有意思。请回来并在评论中发表您的想法。

接下来,我将向您展示示例代码:

from django.db import connections, models, transaction
from django.db.models import AutoField, sql

def dict_fetch_all(cursor):
    """Return all rows from a cursor as a dict"""
    columns = [col[0] for col in cursor.description]
    return [
        dict(zip(columns, row))
        for row in cursor.fetchall()
    ]

class BulkQueryManager(models.Manager):
    def bulk_create_return_with_id(self, objs, batch_size=2000):
        self._for_write = True
        fields = [f for f in self.model._meta.concrete_fields if not isinstance(f, AutoField)]
        created_objs = []
        with transaction.atomic(using=self.db):
            with connections[self.db].cursor() as cursor:
                for item in [objs[i:i + batch_size] for i in range(0, len(objs), batch_size)]:
                    query = sql.InsertQuery(self.model)
                    query.insert_values(fields, item)
                    for raw_sql, params in query.get_compiler(using=self.db).as_sql():
                        cursor.execute(raw_sql, params)
                    raw = "SELECT * FROM %s WHERE id >= %s ORDER BY id DESC LIMIT %s" % (
                        self.model._meta.db_table, cursor.lastrowid, cursor.rowcount
                    )
                    cursor.execute(raw)
                    created_objs.extend(dict_fetch_all(cursor))

        return created_objs

class BookTab(models.Model):
    name = models.CharField(max_length=128)
    bulk_query_manager = BulkQueryManager()

    class Meta:
        db_table = 'book_tab'


def test():
    x = [BookTab(name="1"), BookTab(name="2")]
    create_books = BookTab.bulk_query_manager.bulk_create_return_with_id(x)
    print(create_books)  # [{'id': 2, 'name': '2'}, {'id': 1, 'name': '1'}]

使用游标(cursor)来执行原始插入SQL语句,然后获取已创建记录(created_records)。根据InnoDB中的AUTO_INCREMENT处理方式,确保没有记录会中断您从主键(PK)“cursor.lastrowid - len(objs) + 1到cursor.lastrowid” (cursor.lastrowid) 中提取出的objs。 (注意:这个方法正在我们公司的生产环境下运行。但你需要关注大小影响,这就是为什么Django不支持它的原因)

我在哪里可以找到有关Django不支持bulk_create的解释,以及您所说的大小影响是什么意思? - Mathijs
我的表格中有超过1.4亿行数据,每天还会新增100万行。我能使用这个实现方式而不会出现问题吗? - Mathijs
我已经尝试过这个解决方案,但它并不总是有效。看起来lastrowid没问题,但有时会返回错误的创建对象。例如,lastrowid = 10,限制为20,我会得到ID为12到22的行,而不是10到20。(这种情况很少发生) - Mathijs

1
我尝试了许多方法来克服MariaDB/MySQL的限制。最终,我找到的唯一可靠的解决方案是在应用程序中生成主键。不要自己生成INT AUTO_INCREMENT PK字段,它不起作用,即使在隔离级别为serializable的事务中也不行,因为MariaDB中的PK计数器没有受到事务锁的保护。
解决方案是向模型添加唯一的UUID字段,在模型类中生成其值,然后将其用作标识符。当您将一堆模型保存到数据库中时,仍然无法获取它们的实际PK,但这没关系,因为在后续查询中,您可以使用它们的UUID唯一地标识它们。

1
# datatime.py
# my datatime function
def getTimeStamp(needFormat=0, formatMS=True):
    if needFormat != 0:
        return datetime.datetime.now().strftime(f'%Y-%m-%d %H:%M:%S{r".%f" if formatMS else ""}')
    else:
        ft = time.time()
        return (ft if formatMS else int(ft))


def getTimeStampString():
    return str(getTimeStamp()).replace('.', '')


# model
    bulk_marker = models.CharField(max_length=32, blank=True, null=True, verbose_name='bulk_marker', help_text='ONLYFOR_bulkCreate')



# views
import .........getTimeStampString

data_list(
Category(title="title1", bulk_marker=getTimeStampString()),
...
)
# bulk_create
Category.objects.bulk_create(data_list)
# Get primary Key id
Category.objects.filter(bulk_marker=bulk_marker).values_list('id', flat=True)

0

可能最简单的解决方法是手动分配主键。这取决于具体情况,但有时从表中开始使用max(id)+1并为每个对象分配递增的数字就足够了。然而,如果多个客户端同时插入记录,则可能需要一些锁定。


0

这在原版的Django中不起作用,但是在Django错误跟踪器中有一个补丁(patch)可以使bulk_create设置创建对象的主键。


0

Django文档目前在限制部分声明:

如果模型的主键是AutoField,则不会检索和设置主键属性,就像save()一样。

但是,有好消息。已经有几个关于从内存中bulk_create的票据。上面列出的票据最有可能有一个解决方案,很快将被实现,但是显然不能保证时间或是否会实现。

因此,有两种可能的解决方案,

  1. 等待并观察此补丁是否能够进入生产环境。您可以通过测试所述解决方案并让 Django 社区了解您的想法/问题来帮助实现这一点。https://code.djangoproject.com/attachment/ticket/19527/bulk_create_and_create_schema_django_v1.5.1.patch

  2. 覆盖/编写自己的批量插入解决方案。


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接