58

I have two models in different apps: ModelA and ModelB. They have a one-to-one relationship. Is there a way django can automatically create and save ModelB when ModelA is saved?

class ModelA(models.Model):
    name = models.CharField(max_length=30)

class ModelB(models.Model):
    thing = models.OneToOneField(ModelA, primary_key=True)
    num_widgets = IntegerField(default=0)

When I save a new ModelA I want a entry for it to be saved automatically in ModelB. How can I do this? Is there a way to specify that in ModelA? Or is this not possible, and I would just need to create and save ModelB in the view?

Edited to say the models are in different apps.

djvg
  • 11,722
  • 5
  • 72
  • 103
vagabond
  • 581
  • 1
  • 4
  • 4
  • 1
    Possible duplicate of [Create OneToOne instance on model creation](http://stackoverflow.com/questions/5608001/create-onetoone-instance-on-model-creation) – Don Kirkby Apr 15 '16 at 21:08

9 Answers9

49

Take a look at the AutoOneToOneField in django-annoying. From the docs:

from annoying.fields import AutoOneToOneField

class MyProfile(models.Model):
    user = AutoOneToOneField(User, primary_key=True)
    home_page = models.URLField(max_length=255)
    icq = models.CharField(max_length=255)

(django-annoying is a great little library that includes gems like the render_to decorator and the get_object_or_None and get_config functions)

John Paulett
  • 15,596
  • 4
  • 45
  • 38
  • 1
    It's worth noting that creating a new user in the admin panel will not create MyProfile right away. It's created in a lazy way (the first time you actually access that profile object). – kszl Feb 04 '18 at 15:46
34

Like m000 pointed out:

... The catch in the question is that the models belong to different apps. This matches the use case for signals: "allow decoupled applications get notified when actions occur elsewhere in the framework". Other proposed solutions work but introduce an unnecessary A->B dependency, essentially bundling the two apps. Signals allows A to remain decoupled from B.

Your models exist in different apps. Often you use apps you didn't write, so to allow updates you need a decoupled way to create logically related models. This is the preferred solution in my opinion and we use it in a very large project.

By using signals:

In your models.py:

from django.db.models import signals


def create_model_b(sender, instance, created, **kwargs):
    """Create ModelB for every new ModelA."""
    if created:
        ModelB.objects.create(thing=instance)

signals.post_save.connect(create_model_b, sender=ModelA, weak=False,
                          dispatch_uid='models.create_model_b')

You can create a separate app to hold this models.py file if both of the apps are off-the-shelf.

Dmitry
  • 2,068
  • 2
  • 21
  • 30
  • 3
    +1 for this. The catch in the question is that the models belong to different apps. This matches the use case for signals: "allow decoupled applications get notified when actions occur elsewhere in the framework". Other proposed solutions work but introduce an unecessary A->B dependency, essentially bundling the two apps. Signals allows A to remain decoupled from B. – m000 Jun 22 '12 at 09:03
  • @m000 Thanks for this! If you don't mind I'll update the description of my solution as you've summarized it very nicely. – Dmitry Jun 24 '12 at 14:37
  • This method breaks tests that use fixtures to provide both ModelA and related ModelB objects. Any suggestions? – Marius Gedminas Sep 16 '13 at 11:26
  • 2
    @MariusGedminas from the docs: `Note also that Django stores signal handlers as weak references by default, so if your handler is a local function, it may be garbage collected. To prevent this, pass weak=False when you call the signal’s connect().` – Dmitry Sep 17 '13 at 11:23
  • @MariusGedminas `get_or_create` could provide the necessary tweak in your circumstances. – Dmitry Sep 17 '13 at 11:25
15

The most straightforward way is to override the save method of ModelA:

class ModelA(models.Model):
    name = models.CharField(max_length=30)

    def save(self, force_insert=False, force_update=False):
        is_new = self.id is None
        super(ModelA, self).save(force_insert, force_update)
        if is_new:
            ModelB.objects.create(thing=self)
Jarret Hardie
  • 95,172
  • 10
  • 132
  • 126
  • 2
    The trouble with doing it this way is it unfortunately breaks if you've got an inline form in the admin and use it to create a ModelB instance at the same time - it'll try to create two ModelBs and die horribly. – Daniel Roseman Oct 31 '09 at 09:37
  • Yup, but I'd consider this a hack. – Dmitry Jun 24 '12 at 14:36
  • 3
    Might want to be more future-proof by not naming the args to super. I'll suggest an edit. – hobs Apr 24 '13 at 01:23
9

I know it's a bit late, but I came up with a cleaner and more elegant solution. Consider this code:

class ModelA(models.Model):
    name = models.CharField(max_length=30)

    @classmethod
    def get_new(cls):
        return cls.objects.create().id



class ModelB(models.Model):
    thing = models.OneToOneField(ModelA, primary_key=True, default=ModelA.get_new)
    num_widgets = IntegerField(default=0)

Of course any callable (except lambdas) can be used to supply the default, as long as you return integer id of related object :)

powderflask
  • 371
  • 1
  • 12
realmaniek
  • 465
  • 5
  • 11
  • nice use of class method, however, I think signal might be more straightforward – Luk Aron Nov 11 '19 at 09:03
  • 2
    In my case, this finishes creating 2 ModelA records... I do not know why... The way Django creates models is pretty strange. – J_Zar Sep 30 '21 at 13:37
  • This is awesome. This should be the accepted answer. – Janaka Chathuranga Feb 05 '22 at 04:43
  • This answer is nifty for some use cases - thanks. Last comment in answer is incorrect though. From django docs: "lambdas can’t be used for field options like default because they can’t be serialized by migrations. " https://docs.djangoproject.com/en/3.2/ref/models/fields/#default – powderflask Sep 14 '22 at 18:00
8

I assembled a few different answers (because none of them worked straight out of the box for me) and came up with this. Thought it's pretty clean so I'm sharing it.

from django.db.models.signals import post_save
from django.dispatch import receiver

@receiver(post_save, sender=ModelA)
def create_modelb(sender, instance, created, **kwargs):
    if created:
        if not hasattr(instance, 'modelb'):
            ModelB.objects.create(thing=instance)

It's using Signal as @Dmitry suggested. And as @daniel-roseman commented in @jarret-hardie's answer, Django Admin does try to create the related object for you sometimes (if you change the default value in the inline form), which I ran into, thus the hasattr check. The nice decorator tip is from @shadfc's answer in Create OneToOne instance on model creation

jtlai
  • 779
  • 7
  • 7
0

You could use the post_save-hook which is triggered after a record has been saved. For more documentation on django signals, see here. On this page, you find an example on how to apply the hook on your model.

schneck
  • 10,556
  • 11
  • 49
  • 74
0

I think you want to use django's model inheritance. This is useful if the following statement is true: ModelA is a ModelB (like, Restaurant is a Location).

You can define:

class ModelB(models.Model):
    field1 = models.CharField(...)

class ModelA(ModelB):
    field2 = models.CharField(...)

Now you can create an instance of ModelA and set field2 and field1. If this model is saved it will also create an instance of ModelB which gets the value of field1 assigned. This is all done transparently behind the scenes.

Though you can do the following:

a1 = ModelA()
a1.field1 = "foo"
a1.field2 = "bar"
a1.save()
a2 = ModelA.objects.get(id=a1.id)
a2.field1 == "foo" # is True
a2.field2 == "bar" # is True
b1 = ModelB.objects.get(id=a1.id)
b1.field1 == "foo" # is True
# b1.field2 is not defined
Gregor Müllegger
  • 4,973
  • 1
  • 23
  • 22
0

Just create a function that creates and returns an empty ModelA, and set the default named argument on "thing" to that function.

0

If you are using InlineForm in admin panel, than you can do like this.

But of course in other cases need to check too(like in DRF or manual model instance creation)

from django.contrib import admin
from django.forms.models import BaseInlineFormSet, ModelForm

class AddIfAddParentModelForm(ModelForm):
    def has_changed(self):
        has_changed = super().has_changed()

        if not self.instance.id:
            has_changed = True

        return has_changed

class CheckerInline(admin.StackedInline):
    """ Base class for checker inlines """
    extra = 0
    form = AddIfAddParentModelForm
MegaJoe
  • 575
  • 6
  • 11