有没有办法获取您使用 django 1.4+ 中的 bulk_create 功能创建的项目的主键?
11 回答
2016
由于 Django 1.10 -现在支持(仅在 Postgres 上)这里是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() 创建的对象上设置主键的支持
根据文档你不能这样做: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,只是查询没有检索到它们(如果键是 an AutoField
)。如果出于某种原因需要 pks,则需要以经典方式保存对象。
我能想到的两种方法:
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)
这具有存储对象创建时间的字段的限制。
实际上,我的同事提出了以下解决方案,现在看起来很明显。添加一个名为的新列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)
我将与您分享获取主键时AUTO_INCREMENT
的处理方法和方法InnoDB
(MySQL)
bulk_create
(Django)
根据bulk_create doc If 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 中找出问题的原因,然后再寻找解决方案。
在AUTO FIELD
Django 中实际上是AUTO_INCREMENT
在 MySQL 中。它用于为新行生成唯一标识(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.
我鼓励您阅读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
用来执行raw insert sql
然后取回created_records。根据AUTO_INCREMENT handling in InnoDB
,它确保不会有任何记录中断您objs
的 PK cursor.lastrowid - len(objs) + 1 to cursor.lastrowid
( cursor.lastrowid )。
奖励:它正在我的公司进行生产。但是你需要关心size affect
为什么 Django 不支持它。
我尝试了许多策略来解决 MariaDB/MySQL 的这个限制。最后我想出的唯一可靠的解决方案是在应用程序中生成主键。不要INT AUTO_INCREMENT
自己生成 PK 字段,即使在具有隔离级别的事务中也不serializable
行,因为 MariaDB 中的 PK 计数器不受事务锁的保护。
解决方案是向模型添加唯一UUID
字段,在模型类中生成它们的值,然后将其用作它们的标识符。当您将一堆模型保存到数据库时,您仍然无法取回它们的实际 PK,但这很好,因为在随后的查询中,您可以使用它们的 UUID 唯一地标识它们。
# 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)
django 文档目前在限制下声明:
如果模型的主键是 AutoField,它不会像那样检索和设置主键属性
save()
。
但是,有好消息。有几张票bulk_create
是凭记忆在谈论的。上面列出的票最有可能有一个很快就会实施的解决方案,但显然不能保证按时或是否会成功。
所以有两种可能的解决方案,
等着看这个补丁是否可以投入生产。您可以通过测试所述解决方案来帮助解决此问题,并让 django 社区知道您的想法/问题。https://code.djangoproject.com/attachment/ticket/19527/bulk_create_and_create_schema_django_v1.5.1.patch
覆盖/编写您自己的批量插入解决方案。
这在现有的 Django 中不起作用,但Django 错误跟踪器中有一个补丁可以让 bulk_create 设置创建对象的主键。
可能最简单的解决方法是手动分配主键。这取决于特定情况,但有时从表中的 max(id)+1 开始并为每个对象分配递增的数字就足够了。但是,如果多个客户端可能同时插入记录,则可能需要一些锁。
@Or Duan 建议的方法在使用bulk_create
with时适用于 PostgreSQL ignore_conflicts=False
。当ignore_conflicts=True
设置时,您不会AutoField
在返回的对象中获得(通常是 ID)的值。