user7429643
user7429643

Reputation:

Dynamic table creation on a Django Model save

I would like to create additional tables when I call model.save() (INSERT). But I keep getting this error:

django.db.transaction.TransactionManagementError: Executing DDL statements while in a transaction on databases that can't perform a rollback is prohibited.

I tried to create additional tables inside model.save() and using a pre_save() signal, I get the same error.

This is a pre_save solution attempt:

from django.db import connection
from django.db.models.signals import pre_save
from django.dispatch import receiver


@receiver(pre_save, sender=MyModel, dispatch_uid="create_tags")
def create_tags(sender, instance, **kwargs):
    print("debug, signal pre_save works") 
    try:
        # if obj exists in MyModel table, skip tag table creation
        existing_obj = MyModel.objects.get(name=instance.name)
        print("debug, obj exists")
    except MyModel.DoesNotExist:
        with connection.schema_editor() as schema_editor:
            schema_editor.create_model(MyModel2)

Stack: Django, MySQL.

What I want to implement is to create additional tables for the instance that is being inserted.

To clarify how the miscellaneous per-instance tables are generated, this is the code to derive a per-instance table. For example, for every inserted car instance, there are to be generated miscellaneous Tag tables with sensor measurements:

def get_tag(car_url, car_type):
    class Tag(models.Model):
        time = models.PositiveIntegerField(primary_key=True)  # unix time in seconds  # noqa
        value = models.FloatField()

        class Meta:
            db_table = car_url + "_" + car_type

        def __str__(self) -> str:
            return str(self.time) + "," + str(self.value)

    return Tag

I think that the probable solution is to use "nonatomic" somewhere in the code.

Upvotes: 1

Views: 712

Answers (2)

Swift
Swift

Reputation: 1711

Following on from Aaron's answer, we could also make the functions he has proposed as a wrapper like so:

def wrap_non_atomic(func):
    def wrapper(*callback_args, **callback_kwargs):
        with connection.schema_editor() as schema_editor:
            with non_atomic(schema_editor):
                return func(*callback_args, **callback_kwargs)
    return wrapper

Which can then be used in any scenario where you want this behaviour, whether that be a standalone function or a class based function.

@non_atomic_wrapper
def some_ddl_task(*args, **kwargs):
    # some code
    return True

Upvotes: 0

aaron
aaron

Reputation: 43083

Temporarily set schema_editor.connection.in_atomic_block = False.

with connection.schema_editor() as schema_editor:
    in_atomic_block = schema_editor.connection.in_atomic_block
    schema_editor.connection.in_atomic_block = False
    try:
        schema_editor.create_model(MyModel2)
    finally:
        schema_editor.connection.in_atomic_block = in_atomic_block

Using a context manager:

with connection.schema_editor() as schema_editor:
    with non_atomic(schema_editor):
        schema_editor.create_model(MyModel2)
from contextlib import contextmanager


@contextmanager
def non_atomic(schema_editor):
    in_atomic_block = schema_editor.connection.in_atomic_block
    schema_editor.connection.in_atomic_block = False
    try:
        yield
    finally:
        schema_editor.connection.in_atomic_block = in_atomic_block

Upvotes: 3

Related Questions