如何获取使用 django bulk_create 创建的对象的主键

Posted

技术标签:

【中文标题】如何获取使用 django bulk_create 创建的对象的主键【英文标题】:How to get primary keys of objects created using django bulk_create 【发布时间】:2013-04-02 18:10:22 【问题描述】:

有没有办法获取您使用 django 1.4+ 中的 bulk_create 功能创建的项目的主键?

【问题讨论】:

我也很想知道人们是如何解决这个问题的。我想您必须执行诸如锁定表、运行 bulk_create、查询所有新记录然后解锁表之类的操作?从文档中似乎很清楚 bulk_create 不返回 auto_increment 键,因此解决此问题的唯一方法是进行复杂的工作。我想的另一种方法是使用另一个表来按顺序跟踪使用的主键,因此您预先分配一个 ID 块,然后运行 ​​bulk_create,您应该知道预期的主键。我对这两个想法都不满意:( django dev code.djangoproject.com/ticket/19527 似乎正在努力解决这个问题 哦,是的!看来我 4 年前的提议刚刚融入了 Django 1.10,让我们所有人都可以享受。 :-) 我想现在只适用于 postgres。 现在可以使用 Django 1.10 和 PostgreSQl:docs.djangoproject.com/en/dev/ref/models/querysets/#bulk-create 希望也能支持 mysql 【参考方案1】:

2016

自 Django 1.10 - 现在支持(仅在 Postgres 上)这里是 link to the doc。

>>> 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() 创建的对象设置主键的支持

【讨论】:

欢迎来到未来 很遗憾我是 mysql 用户 如果在mysql中呢? bulk_create 创建的条目在数据库中是否有 id 值? @MohammedShareefC 会得到数据库中的一个主键,但是bulk_create方法返回的列表和你提供的一样,并且本地对象(那个列表的成员)没有它设置为pyriku demonstrates in his answer。 在支持它的数据库上(除 PostgreSQL 【参考方案2】:

根据文档你不能这样做: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)。如果您出于某种原因想要 pks,则需要以经典方式保存对象。

【讨论】:

我认为这是问题的重点,或者至少我会如何解释它,即:人们使用什么技术来解决bulk_create 的限制,以便检索创建的ID 可靠吗? 这里有一个开放的 PR 来添加对从 bulk_create 返回 ID 的支持:github.com/django/django/pull/5166 值得注意的是 Postgres 支持返回 ID,因此有一种方法可以通过原始 sql 操作立即取回 ID。【参考方案3】:

我能想到的两种方法:

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)

这具有存储对象创建时间的字段的限制。

【讨论】:

你知道,我已经有一个date_created 字段,所以这可以工作,尽管无论如何添加一个字段是最小的努力。我唯一担心的是多个查询可能同时命中数据库,所以我想我需要在bulk_create 之前和created_at 查询之后实现某种锁定机制。 是的,原子事务可用于确保避免竞争条件。 关于第一种方法,在 Django 1.10 中,values_list('id', flat=True) 返回一个查询集,这似乎是在调用 bulk_create 之后评估的 - 在 list() 中包装 category_ids 以强制数据库查询,帮助。 太可怕了,我猜我什至select max(id) is better @deathangel908 不要这样做max(id),我试过了,但遇到了问题。 MariaDB 文档明确声明,除了唯一性之外,不对 PK 做任何其他假设。【参考方案4】:

实际上,我的同事提出了以下解决方案,现在看起来很明显。添加一个名为 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)

【讨论】:

向模型添加额外字段来解决查询问题不是一个好习惯。 虽然这是事实,但无论如何都应该将批量插入视为一种优化,这可能必然会损害设计。 “不够快”和“不是完美的设计”之间存在一种张力,需要在这里加以平衡。在 Django PR 5166 加入之前,对于需要优化批量插入的团队来说,这可能是一个合理的折衷方案。 如果批量创建在应用程序的不同时间被多次调用,那么我们需要每次更新 bulk_ref ,为此我们需要一个统计变量 ref @varun Thus, for there to be a one in a billion chance of duplication, 103 trillion version 4 UUIDs must be generated. @DanH 似乎是避免查询的合理选择,为此添加额外字段实际上可能非常有帮助。【参考方案5】:

我将分享AUTO_INCREMENTInnoDB(MySQL) 中的处理以及bulk_create(Django) 时获取主键的方法

根据bulk_create docIf the model’s primary key is an AutoField it does not retrieve and set the primary key attribute, as save() does, unless the database backend supports it (currently PostgreSQL).所以我们需要在Django或MySQL中找出问题的原因,然后再寻找解决方案。

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

你想要bulk_create 对象(Django)意味着insert multiple rows in a single SQL query。但是如何检索最近自动生成的 PK(主键)?感谢LAST_INSERT_ID。 It returns first value automatically generated of the most recently executed INSERT statement...This value cannot be affected by other clients, even if they generate AUTO_INCREMENT values of their own. This behavior ensures that each client can retrieve its own ID without concern for the activity of other clients, and without the need for locks or transactions.

我鼓励您阅读 AUTO_INCREMENT Handling in InnoDB 和 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 执行raw insert sql 然后取回created_records。根据AUTO_INCREMENT handling in InnoDB,它确保不会有任何记录从PKcursor.lastrowid - len(objs) + 1 to cursor.lastrowid(cursor.lastrowid)打断你的objs

奖励:它正在我的公司进行生产。但是你需要关心size affect,这就是为什么 Django 不支持它。

【讨论】:

【参考方案6】:
# 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:%Sr".%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)

【讨论】:

【参考方案7】:

我尝试了很多策略来解决 MariaDB/MySQL 的这个限制。最后我想出的唯一可靠的解决方案是在应用程序中生成主键。不要自己生成INT AUTO_INCREMENT PK 字段,即使在隔离级别为serializable 的事务中也不行,因为MariaDB 中的PK 计数器不受事务锁的保护。

解决方案是向模型添加唯一的UUID 字段,在模型类中生成它们的值,然后将其用作它们的标识符。当您将一堆模型保存到数据库时,您仍然无法取回它们的实际 PK,但这很好,因为在后续查询中您可以使用它们的 UUID 唯一地标识它们。

【讨论】:

【参考方案8】:

django documentation 目前表示受到限制:

如果模型的主键是 AutoField,它不会检索并且 像save()那样设置主键属性。

但是,有个好消息。有几张票从记忆中谈论bulk_create。 ticket listed above 最有可能有一个很快就会实施的解决方案,但显然不能保证按时或是否会成功。

所以有两种可能的解决方案,

    等一下,看看这个补丁是否可以投入生产。您可以通过测试所述解决方案来帮助解决此问题,并让 django 社区知道您的想法/问题。 https://code.djangoproject.com/attachment/ticket/19527/bulk_create_and_create_schema_django_v1.5.1.patch

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

【讨论】:

【参考方案9】:

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

【讨论】:

【参考方案10】:

这在 Django 中不起作用,但有一个 patch in the Django bug tracker 可以让 bulk_create 设置创建对象的主键。

【讨论】:

【参考方案11】:

@Or Duan 建议的方法在使用 bulk_createignore_conflicts=False 时适用于 PostgreSQL。如果设置了ignore_conflicts=True,则在返回的对象中您不会获得AutoField(通常是ID)的值。

【讨论】:

以上是关于如何获取使用 django bulk_create 创建的对象的主键的主要内容,如果未能解决你的问题,请参考以下文章

Django插入多条数据—bulk_create

Django model中数据批量导入bulk_create()

Django bulk_create 忽略导致 IntegrityError 的行?

django-pyodbc bulk_create 坏了

从 django bulk_create 中选择/查询对象?

在外键中使用 Django bulk_create 对象?