Initializing objects belonging to a ManyToManyField from data in memory

I am working with database model instances where I need to construct an object from data in memory (using Python style o = Object()

, not ModelClass.objects.create()

). Whether the data will be stored in the database will be decided later when the call is used o.save()

.

These models have ManyToManyField

and have multiple children. The problem is that I can not add()

until ManyToManyField

as long as the child objects are not saved. How can I construct these objects in a way that save()

can be called later? Every potential solution I have found for this problem doesn't actually do what I want.

Here is some sample code showing what I am trying to do:

class Author:
  # ...
  @classmethod
  def create(cls, data):
    # ...
    pass

class Book(models.Model):
  title = models.CharField(max_length=128)
  pages = models.PositiveIntegerField()
  authors = models.ManyToManyField(Author)

  @classmethod
  @transaction.atomic
  def create(cls, data):
    try:
      with transaction.atomic():
        b = cls(title=data["title"],
                pages=data["pages"])

        # This works, but has an unwanted side effect: authors are saved to the database
        # as they're created here while the Book is not saved.
        b.authors = Author.objects.bulk_create([Author.create(a) for a in data["authors"]])
        return b

    except Exception:
      # ...
      raise

################### Later on...
# This data is NOT static - it formed from JSON which comes from an API. Just is here as an example.
data = {
  "title": 1,
  "pages": 934,
  "authors": [
    {
      "name": "John Smith",
      # ...
    }
  ]
}

# We're going to use this now, but we're unsure if we want to actually save
# the object to the database.
b = Book.create(data)

# Save the data to the database if we want to.
b.save()

      

+3


source to share


2 answers


The only solution I can think of is ordering the operations and executing them when you call save()

.

class PostponedOpMixin(models.Model):
    def __init__(self, *args, **kwargs):
        self._postponed_ops = []
        super(PostponedOpMixin, self).__init__(*args, **kwargs)

    def _getattr(self, attr):
        result = self
        for part in attr.split('.'):
            result = getattr(result, part)
        return result

    def postpone(self, op, *args, **kwargs):
        if self.pk:  # execute now if self already has a pk
            return self._getattr(op)(*args, **kwargs)             
        self._postponed_ops.append((op, *args, **kwargs))

    def save(self, *args, *kwargs):
        super(PostponedOpMixin, self).save(*args, **kwargs)
        while self._postponed_ops:
            op, args, kwargs = self._postponed_ops.pop(0):
                self._getattr(op)(*args, **kwargs)

    def Meta:
        abstract = True

      

This way you can:



class Book(PostponedOpMixin):
    ...
    authors = models.ManyToManyField(Author)
    ...

instance = Book()
instance.title = "Romeo and Juliet"
instance.postpone('authors.add', shakespeare)
...

# and some time later:
instance.save()

      

This code is untested and intended as a starting point. Any mistake is left as an exercise for the reader.

+2


source


Perhaps you could put aside the authors of the book by adding to the point where you are sure you want to save this book in the database.
In the meantime, you can save a list of processed (but not yet saved) author objects for each book object.

class Book(models.Model):
    title = models.CharField(max_length=128)
    pages = models.PositiveIntegerField()
    authors = models.ManyToManyField(Author)

    @classmethod
    @transaction.atomic
    def create(cls, data):
        try:
            b = cls(title=data["title"],
                    pages=data["pages"])
            b.author_list = list()
            for a in data["authors"]:
                b.authors_list.append(Author.create(a))
            return b
        except Exception:
            # ...
            raise

      



Then, when you are sure that you want to save the object, you must save all the authors in this list, and then add them to the "authors" field in the corresponding book object.

+2


source







All Articles