Django "ValueError: Can't bulk create a multi-table inherited model"

6.6k Views Asked by At

Problem

I am using the django-model-utils InheritanceManager. I have a super Notification(models.Model) class which I use to create many notification subclasses such as PostNotification(Notification), CommentNotification(Notification), etc., and when trying to run CommentNotification.objects.bulk_create(list_of_comment_notification_objects), i get the following traceback:

File "/home/me/.virtualenvs/project/local/lib/python2.7/site-packages/django/db/models/query.py", line 429, in bulk_create
    raise ValueError("Can't bulk create a multi-table inherited model")
ValueError: Can't bulk create a multi-table inherited model

and upon inspecting the query.py file, we get this causes the error:

for parent in self.model._meta.get_parent_list():
      if parent._meta.concrete_model is not self.model._meta.concrete_model:
           raise ValueError("Can't bulk create a multi-table inherited model")

Environment Django Model Utils version: 3.1.1 Django version: 1.11.7 Python version: 2.7.3

Example

PostNotification.objects.bulk_create(
   [PostNotification(related_user=user, post=instance) for user in users]
)

throws the above exception

What I have tried and though was a success originally:

I though that simply running: BaseClass.objects.bulk_create(list_of_SubClass_objects) instead of SubClass.objects.bulk_create(list_of_SubClass_objects) would work and return a list of SubClass values, but subsequently running SubClass.objects.all() would return an empty result. The bulk_create() would only create a Notification base class object for each item in the list.

3

There are 3 best solutions below

0
On

Found a hacky solution. I hope it works in your case. The trick is create a model (which is not an inherited one) dynamically that has some meta (db_table) set. And use this dynamic model to create Child objects in bulk (in other words write into Child's DB table).

    class Parent(models.Model):
        name = models.CharField(max_length=10)


    class Child(Parent):
        phone = models.CharField(max_length=12)
# just an example. Should be expanded to work properly.
field_type_mapping = {
    'OneToOneField': models.IntegerField,
    'CharField': models.CharField,
}

def create_model(Model, app_label='children', module='', options=None):
    """
    Create specified model
    """
    model_name = Model.__name__
    class Meta:
       managed = False
       db_table = Model._meta.db_table

    if app_label:
        # app_label must be set using the Meta inner class
        setattr(Meta, 'app_label', app_label)

    # Update Meta with any options that were provided
    if options is not None:
        for key, value in options.iteritems():
            setattr(Meta, key, value)

    # Set up a dictionary to simulate declarations within a class
    attrs = {'__module__': module, 'Meta': Meta}

    # Add in any fields that were provided
    fields = dict()
    for field in Model._meta.fields:
        if field.attname == 'id':
            continue
        if field.model.__name__ == model_name:
            field_class_name = type(field).__name__
            print(field.attname)
            fields[field.attname] = field_type_mapping[field_class_name]()
    # Create the class, which automatically triggers ModelBase processing
    attrs.update(fields)

    model = type(f'{model_name}Shadow', (models.Model,), attrs)


    return model

mod = create_model(Child)
parents = [Parent(name=i) for i in range(15)]
parents = Parent.objects.bulk_create(parents)
children = [mod(phone=parent.name, parent_ptr_id=parent.id) for parent in parents]
mod.objects.bulk_create(children)
0
On

I've done a custom implementation of bulk_create that seems to be working for my case (only one parent relationship and not autoincremented pk):

from django.db import models


class MultiTableChildQueryset(models.QuerySet):

    def bulk_create(self, objs, batch_size=None):
        assert batch_size is None or batch_size > 0
        if not objs:
            return objs

        self._for_write = True
        objs = list(objs)
        parent_model = self.model._meta.pk.related_model

        parent_objs = []
        for obj in objs:
            parent_values = {}
            for field in [f for f in parent_model._meta.fields if hasattr(obj, f.name)]:
                parent_values[field.name] = getattr(obj, field.name)
            parent_objs.append(parent_model(**parent_values))
            setattr(obj, self.model._meta.pk.attname, obj.id)
        parent_model.objects.bulk_create(parent_objs, batch_size=batch_size)

        with transaction.atomic(using=self.db, savepoint=False):
            self._batched_insert(objs, self.model._meta.local_fields, batch_size)

        return objs
1
On

A slightly easier to read version of Moises:

from typing import TypeVar
from django.db.models import Model

M = TypeVar('M', bound=Model)

def multi_inheritance_table_bulk_insert(data: List[M]) -> None:
    """
    Bulk insert data into a multi-inheritance table.
    """
    if not data:
        return

    model = data[0].__class__
    local_fields = model._meta.local_fields
    parent_model = model._meta.pk.related_model
    parent_fields = parent_model._meta.local_fields

    parent_objects = [
        parent_model(**{field.name: getattr(obj, field.name) for field in parent_fields})
        for obj in data
    ]
    parent_model.objects.bulk_create(parent_objects)
    for parent, obj in zip(parent_objects, data):
        obj.pk = parent.pk

    queryset = QuerySet(model)
    queryset._for_write = True
    with transaction.atomic(using=queryset.db, savepoint=False):
        queryset._batched_insert(
            data,
            local_fields,
            batch_size=None,
        )