Есть ли способ получить первичные ключи элементов, которые вы создали, используя функцию bulk_create в django 1.4+?
Как получить первичные ключи объектов, созданных с помощью django bulk_create
Ответы (10)
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: добавлена поддержка установки первичных ключей для объектов, созданных с помощью bulk_create() при использовании PostgreSQL.
bulk_create
, совпадает с тем, который вы предоставили, и для локальных объектов (членов этого списка) он не установлен как пирику демонстрирует в своем ответе.
- person Yushin Washio; 15.02.2019
Согласно документации вы не можете этого сделать: 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
, чтобы надежно получить созданные идентификаторы?
- person DanH; 08.06.2013
Два подхода, о которых я могу думать:
а) Вы могли бы сделать
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)
Это может быть немного дорого, если набор запросов очень велик.
б) Если в модели есть поле 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
.
- person DanH; 09.06.2013
select max(id) is better
- person deathangel908; 04.05.2018
max(id)
, я попробовал и столкнулся с проблемами. В документации MariaDB прямо указано, что нельзя предполагать ничего другого в отношении PK, кроме уникальности.
- person Patrick; 17.09.2020
На самом деле мой коллега предложил следующее решение, которое сейчас кажется таким очевидным. Добавьте новый столбец с именем 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 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.
Я рекомендую вам прочитать Обработка AUTO_INCREMENT в 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
, убедитесь, что никакие записи не прерывают ваш objs
из ПК cursor.lastrowid - len(objs) + 1 to cursor.lastrowid
(cursor.lastrowid).
Бонус: в моей компании запущено производство. Но вам нужно позаботиться о size affect
, почему Django его не поддерживает.
Я испробовал множество стратегий, чтобы обойти это ограничение MariaDB/MySQL. Единственным надежным решением, которое я придумал в конце, было создание первичных ключей в приложении. НЕ генерируйте поля INT AUTO_INCREMENT
PK самостоятельно, это не сработает даже в транзакции с уровнем изоляции serializable
, потому что счетчик PK в MariaDB не защищен блокировками транзакций.
Решение состоит в том, чтобы добавить в модели уникальные поля UUID
, сгенерировать их значения в классе модели, а затем использовать их в качестве своего идентификатора. Когда вы сохраняете кучу моделей в базу данных, вы все равно не получите обратно их фактический PK, но это нормально, потому что в последующих запросах вы можете однозначно идентифицировать их по их UUID.
документация по django в настоящее время в ограничениях указано:
Если первичный ключ модели является AutoField, он не извлекает и не устанавливает атрибут первичного ключа, как это делает
save()
.
Но есть и хорошие новости. Было несколько билетов, говорящих о bulk_create
по памяти. заявка, указанная выше, скорее всего, имеет решение, которое вскоре будет реализовано, но, очевидно, нет гарантии вовремя или если он когда-либо сделает это.
Таким образом, есть два возможных решения,
Подождите и посмотрите, выйдет ли этот патч в производство. Вы можете помочь с этим, протестировав заявленное решение и сообщив сообществу django о своих мыслях/проблемах. https://code.djangoproject.com/attachment/ticket/19527/bulk_create_and_create_schema_django_v1.5.1.patch
Переопределите/напишите собственное решение для массовой вставки.
Вероятно, самым простым обходным решением является назначение первичных ключей вручную. Это зависит от конкретного случая, но иногда достаточно начать с max(id)+1 из таблицы и присвоить числа, увеличивающиеся на каждом объекте. Однако, если несколько клиентов могут вставлять записи одновременно, может потребоваться некоторая блокировка.
Это не работает в стандартном Django, но есть патч в системе отслеживания ошибок Django, который делает bulk_create устанавливает первичные ключи для создаваемых объектов.
Подход, предложенный @Or Duan, работает для PostgreSQL при использовании bulk_create
с ignore_conflicts=False
. Когда установлено ignore_conflicts=True
, вы не получаете значения для AutoField
(обычно ID) в возвращаемых объектах.