Как получить первичные ключи объектов, созданных с помощью django bulk_create

Есть ли способ получить первичные ключи элементов, которые вы создали, используя функцию bulk_create в django 1.4+?


person mikec    schedule 10.04.2013    source источник
comment
Я также хотел бы знать, как люди справляются с этим. Я полагаю, вам придется сделать что-то вроде блокировки таблицы, запустить bulk_create, запросить все новые записи, а затем разблокировать таблицу? Из документов кажется совершенно очевидным, что bulk_create не возвращает ключи auto_increment, поэтому единственный способ обойти это — запутанная работа. Другой метод, который я полагаю, заключается в том, чтобы иметь другую таблицу, которую вы используете для отслеживания используемых первичных ключей по порядку, поэтому вы заранее выделяете блок идентификаторов, а затем запускаете bulk_create, и вы должны знать ожидаемые первичные ключи. Я не в восторге ни от одной идеи :(   -  person DanH    schedule 07.06.2013
comment
Кажется, в django dev предпринимаются попытки решить эту проблему code.djangoproject.com/ticket/19527   -  person DanH    schedule 07.06.2013
comment
Ах, да! Похоже, что мое старое предложение ~ 4 года только что растворилось в стандартной версии Django 1.10, позволив всем нам наслаждаться. :-) Работает, я думаю, пока только для postgres.   -  person Tuttle    schedule 02.08.2016
comment
надеюсь, может быть поддержка и для mysql   -  person Roel    schedule 23.12.2016


Ответы (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.

person Or Duan    schedule 04.09.2016
comment
Добро пожаловать в будущее - person Trinh Hoang Nhu; 15.09.2016
comment
грустно я пользователь mysql - person Roel; 23.03.2017
comment
А если в mysql? Имеют ли записи, созданные с помощью bulk_create, значение идентификатора в базе данных? - person Mohammed Shareef C; 16.11.2017
comment
@MohammedShareefC Он получит первичный ключ в базе данных, но список, возвращаемый методом bulk_create, совпадает с тем, который вы предоставили, и для локальных объектов (членов этого списка) он не установлен как пирику демонстрирует в своем ответе. - person Yushin Washio; 15.02.2019
comment
В базах данных, которые его поддерживают (все, кроме PostgreSQL ‹ 9.5 и Oracle), установка для параметра ignore_conflicts значения True указывает базе данных игнорировать ошибки при вставке любых строк, которые не соответствуют ограничениям, таким как повторяющиеся уникальные значения. Включение этого параметра отключает настройку первичного ключа для каждого экземпляра модели (если база данных обычно поддерживает это). - person eugene; 25.05.2020
comment
Кто-нибудь еще получает None в PostgreSQL? - person TechniCollins; 10.05.2021
comment
Я только что заметил, что установка ignore_conflicts в True является причиной этого - person TechniCollins; 10.05.2021
comment
Только что нашел это в документах: в базах данных, которые его поддерживают (все, кроме Oracle), установка для параметра ignore_conflicts значения True указывает базе данных игнорировать сбой при вставке любых строк, которые не соответствуют ограничениям, таким как повторяющиеся уникальные значения. Включение этого параметра отключает настройку первичного ключа для каждого экземпляра модели (если база данных обычно поддерживает это). - person TechniCollins; 10.05.2021
comment
Большое спасибо, искал ответ на эту проблему в течение большого количества часов! - person Kais Ben Daamech; 22.07.2021

Согласно документации вы не можете этого сделать: 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, вам нужно будет сохранить объекты классическим способом.

person pyriku    schedule 07.06.2013
comment
Я думаю, что в этом суть вопроса или, по крайней мере, как я его интерпретирую, то есть: какие методы люди используют, чтобы обойти это ограничение bulk_create, чтобы надежно получить созданные идентификаторы? - person DanH; 08.06.2013
comment
Существует открытый PR для добавления поддержки возврата идентификаторов из bulk_create здесь: github.com/django/django /pull/5166 Примечательно, что Postgres поддерживает возврат идентификаторов, поэтому есть способ немедленно вернуть идентификаторы с помощью необработанной операции SQL. - person gordonc; 08.12.2015

Два подхода, о которых я могу думать:

а) Вы могли бы сделать

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)

Это имеет ограничение наличия поля, в котором хранится время создания объекта.

person karthikr    schedule 09.06.2013
comment
Вы знаете, у меня уже есть поле date_created, так что это может сработать, хотя добавить его в любом случае не составит труда. Меня беспокоит только то, что несколько запросов могут попасть в БД одновременно, поэтому я полагаю, что мне нужно реализовать какой-то механизм блокировки до запроса bulk_create и после запроса created_at. - person DanH; 09.06.2013
comment
Да, атомарные транзакции можно использовать, чтобы избежать условий гонки. - person karthikr; 09.06.2013
comment
Что касается первого подхода, в Django 1.10 values_list('id', flat=True) возвращает набор запросов, который, по-видимому, оценивается после вызова bulk_create - помогает перенос category_ids в list() для принудительного запроса к базе данных. - person George; 12.09.2016
comment
Ужасно, наверное, даже select max(id) is better - person deathangel908; 04.05.2018
comment
@deathangel908 Не делайте 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)
person DanH    schedule 10.06.2013
comment
Не рекомендуется добавлять в модель дополнительные поля для решения проблем с запросами. - person max; 19.07.2015
comment
Хотя это верно, объемные вставки в любом случае следует рассматривать как оптимизацию, которая обязательно может поставить под угрозу конструкцию. Здесь есть противоречие между недостаточно быстрым и несовершенным дизайном, чтобы его можно было сбалансировать. Пока не появится Django PR 5166, это, вероятно, разумный компромисс для команд, которым нужна оптимизация массовой вставки. - person Scott A; 22.01.2016
comment
если массовое создание вызывается в приложении несколько раз в разное время, тогда нам нужно обновлять bulk_ref каждый раз, для чего нам понадобится статическая переменная ref - person varun; 15.04.2016
comment
@varun Я не могу вспомнить, как мы на самом деле реализовали это, bulk_ref мог быть UUID или подобным случайным числом. Не обязательно быть последовательным или относительным к другим bulk_refs - person DanH; 15.04.2016
comment
@DanH, тогда нам придется каждый раз проверять, существует ли случайное число или uuid в базе данных. - person varun; 15.04.2016
comment
+1 Это решение уже сработало для меня, потому что у меня уже был способ однозначно запрашивать вставленные значения. - person Justin Meiners; 22.07.2016
comment
@DanH кажется разумным выбором, чтобы избежать запросов, и добавление дополнительного поля для этой цели может быть очень полезным. - person varun; 11.08.2017

Я поделюсь с вами 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 его не поддерживает.

person Hoang Dung Pham    schedule 15.03.2021

Я испробовал множество стратегий, чтобы обойти это ограничение MariaDB/MySQL. Единственным надежным решением, которое я придумал в конце, было создание первичных ключей в приложении. НЕ генерируйте поля INT AUTO_INCREMENT PK самостоятельно, это не сработает даже в транзакции с уровнем изоляции serializable, потому что счетчик PK в MariaDB не защищен блокировками транзакций.

Решение состоит в том, чтобы добавить в модели уникальные поля UUID, сгенерировать их значения в классе модели, а затем использовать их в качестве своего идентификатора. Когда вы сохраняете кучу моделей в базу данных, вы все равно не получите обратно их фактический PK, но это нормально, потому что в последующих запросах вы можете однозначно идентифицировать их по их UUID.

person Patrick    schedule 17.09.2020

документация по 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. Переопределите/напишите собственное решение для массовой вставки.

person Matt Seymour    schedule 13.06.2013

Вероятно, самым простым обходным решением является назначение первичных ключей вручную. Это зависит от конкретного случая, но иногда достаточно начать с max(id)+1 из таблицы и присвоить числа, увеличивающиеся на каждом объекте. Однако, если несколько клиентов могут вставлять записи одновременно, может потребоваться некоторая блокировка.

person peper0    schedule 15.12.2013

Это не работает в стандартном Django, но есть патч в системе отслеживания ошибок Django, который делает bulk_create устанавливает первичные ключи для создаваемых объектов.

person user3175220    schedule 08.01.2014

Подход, предложенный @Or Duan, работает для PostgreSQL при использовании bulk_create с ignore_conflicts=False. Когда установлено ignore_conflicts=True, вы не получаете значения для AutoField (обычно ID) в возвращаемых объектах.

person Kunal Yadav    schedule 13.07.2020