Opened 8 years ago

Last modified 14 months ago

#7623 new New feature

Multi-table inheritance: Add the ability create child instance from existing parent

Reported by: brooks.travis@… Owned by:
Component: Database layer (models, ORM) Version: master
Severity: Normal Keywords: model-inheritance, multi-table-inheritance
Cc: brooks.travis@…, sirexas@…, anssi.kaariainen@…, mike@…, reza@…, cmawebsite@…, Aron Podrigal Triage Stage: Accepted
Has patch: yes Needs documentation: no
Needs tests: yes Patch needs improvement: yes
Easy pickings: no UI/UX: no

Description

As it exists now, multi-table inheritance does not allow for the creation of a child model instance that inherits from an existing parent model instance. For example:

Parent Class-

class Place(models.Model):
    name = models.CharField(max_length=50)
    address = models.TextField(max_length=150)

Child Classes-

class Restaurant(Place):
    place = models.OneToOneField(Place, parent_link=True)
    cuisine = models.CharField(max_length=75)
    rating = models.IntegerField()

class Bar(Place):
    parent = models.OneToOneField(Place, parent_link=True)
    happy_hour = models.BooleanField()
    beers_on_tap = models.ManyToManyField("Beers", null=True, blank=True)

Sample Use-case-

When the system is first deployed, a restaurant instance is created. Later, the restaurant adds a bar to increase revenue, and we now want to create a Bar model instance for the parent Place for the restaurant. I would propose the following interface for doing so:

parentPlace = Restaurant.objects.get(name__iexact="Bob's Place").parent
barInstance = Bar(parent=parentPlace, happy_hour=True)

However, if you attempt to create an instance in this manner now, you receive a DatabaseIntegrityError, saying that a Place object with that id already exists.

Attachments (1)

makechild.patch (2.6 KB) - added by korpios 8 years ago.

Download all attachments as: .zip

Change History (37)

comment:1 Changed 8 years ago by korpios

I have a Bazaar branch available that solves this issue:

https://code.launchpad.net/~theonion/django/makechild

In short, it adds a couple of new methods to QuerySet / Manager: prepare_child() and create_child(). For an existing Place p that isn't currently a Restaurant, you could do either of the following:

# p is an existing Place
r = Restaurant.objects.prepare_child(p)
r.cusine = "Sushi"
r.rating = 5
r.save()

# or, alternately
r = Restaurant.objects.create_child(p, cusine="Sushi", rating=5)

While create_child() saves to the database, prepare_child() does not; the latter allows you to further alter the instance before saving.

I tried several alternate routes — supporting setting children directly from parents, supporting setting parents directly from children, a single unified child() method — and none worked cleanly. The unified child() method presented the issue of passing a save flag; there was no way to avoid a collision with a field named save in the keyword arguments.

I'll add an initial patch here based on the Bazaar repository for now; I still need to add tests. Any patches here will lag behind the Bazaar repository, as the latter is where my actual development takes place.

comment:2 Changed 8 years ago by korpios

Needs documentation: set
Needs tests: set

Changed 8 years ago by korpios

Attachment: makechild.patch added

comment:3 Changed 8 years ago by korpios

Has patch: set
Patch needs improvement: set

comment:4 Changed 8 years ago by korpios

Cc: korpios@… added

comment:5 Changed 8 years ago by Eric Holscher

milestone: post-1.0
Triage Stage: UnreviewedDesign decision needed

comment:6 Changed 8 years ago by korpios

Cc: korpios@… removed

comment:7 Changed 8 years ago by (none)

milestone: post-1.0

Milestone post-1.0 deleted

comment:8 Changed 7 years ago by Karen Tracey

#11618 was a dup.

comment:9 Changed 6 years ago by sirex

Cc: sirexas@… added

comment:10 Changed 6 years ago by Julien Phalip

Component: Core frameworkDatabase layer (models, ORM)

comment:11 Changed 6 years ago by Luke Plant

Severity: Normal
Type: Bug

comment:12 Changed 5 years ago by Alex Gaynor

Easy pickings: unset
Resolution: needsinfo
Status: newclosed
UI/UX: unset

Discussion with Jacob: closing as needsinfo, the modeling in the ticket does not seem like it *should* work, a model cannot be an instance of multiple subclasses.

comment:13 Changed 5 years ago by Nan

Resolution: needsinfo
Status: closedreopened

Here's a more reasonable example...

# models.py
class Place(models.Model):
    name = models.CharField(max_length=50)
    address = models.CharField(max_length=80)

class Restaurant(Place):
    place = models.OneToOneField(Place, parent_link=True, related_name='restaurant')
    serves_hot_dogs = models.BooleanField()
    serves_pizza = models.BooleanField()

Now, say we have a bunch of Place objects in the database, and a form where users can inform us what sort of business a particular Place is (OK, this is a contrived example, but I'm sure you can see that this could be a reasonable use case for some types of data).

# somewhere in views.py
p = Place.objects.get(pk=1)
restaurant = Restaurant(**{
    'place': p,
    'serves_hot_dogs': False,
    'serves_pizza': True,
})
restaurant.save()

comment:14 Changed 5 years ago by Aymeric Augustin

Resolution: wontfix
Status: reopenedclosed

If multiple related objects are linked to the same object, they should have a foreign key to this object, not "extend" it; that would break the "instance" paradigm.

You can always use a Python mixin if you have related objects of different types, but want to share some methods.

comment:15 Changed 5 years ago by Nan

Resolution: wontfix
Status: closedreopened

Please see my example immediately above. This is not about foreign keys or multiple related objects linked to the same object. It's about multi-table inheritance where the child data isn't always known at the time the parent object is created.

If you have a Place object that is not yet a Restaurant object, the ORM will not allow you to add the data to make it a Restaurant object.

comment:16 Changed 5 years ago by Aymeric Augustin

Resolution: worksforme
Status: reopenedclosed

Ah, I understand. This works:

p = Place.objects.get(pk=1)
restaurant = Restaurant(place_ptr=p, ...)

comment:17 Changed 5 years ago by bunyan

Resolution: worksforme
Status: closedreopened

This leads to the behavior described in #11618 - the parent instance fields get overwritten.

comment:18 Changed 5 years ago by Anssi Kääriäinen

Cc: anssi.kaariainen@… added

If you want to extend an existing instance, you need to copy all fields of the parent to the new child manually. Unfortunately this can't be done in a generic way using only public APIs. Although ._meta is semi-public and doing

child = ChildModel(**childfields) # set child field values
for field in parent._meta.fields:
    setattr(child, field.attname, getattr(parent, field.attname)) # set child values from parent
child.parent_ptr = parent.pk # not sure if this is strictly necessary, probably so...

or something along those lines should work (not tested). Now, I don't think this can be officially documented without making ._meta.fields part of the public API. I would have use for a helper method that does this.

comment:19 Changed 5 years ago by Mike Fogel

Cc: mike@… added

comment:20 Changed 4 years ago by Edward Askew <gnutrino@…>

There's actually an easier workaround for this:

parent = Place.objects.get(...)
child = Restaurant(place_ptr=parent,...)

#Overwrites the parents fields except the primary key with None (because the parent fields in child are not auto filled)
child.save()

#Restores the parent fields to their previous values
parent.save()

#hit the database again to fill the proper values for the inherited fields
child = Restaurant.objects.get(pk=parent.pk) 

comment:21 in reply to:  20 Changed 4 years ago by sammiestoel@…

There is an even easier work around not 100% sure if it works as expected yet though:

child = Restaurant(place_ptr_id=place.pk)
child.__dict__.update(place.__dict__)
child.save()

Tested it myself for my use case as working.
Thanks to: http://stackoverflow.com/questions/4064808/django-model-inheritance-create-sub-instance-of-existing-instance-downcast

comment:22 Changed 4 years ago by loic84

I'm not sure it covers all bases but I use:

child = Restaurant(place_ptr=place)
child.save_base(raw=True)

It would be great to have an obvious and officially blessed way of doing this though.

comment:23 Changed 4 years ago by Aymeric Augustin

Status: reopenednew

comment:24 Changed 4 years ago by Anssi Kääriäinen

Triage Stage: Design decision neededAccepted

I think this makes sense. Unlink child but keep the parent and link child to existing parent would both be useful in some situatios.

comment:25 Changed 4 years ago by Karol Sikora

Owner: changed from nobody to Karol Sikora
Status: newassigned

comment:26 Changed 3 years ago by Aymeric Augustin

#21537 was a duplicate.

comment:27 Changed 3 years ago by eugene kim

I'd like this to be implemented as well. I have the following model relationships and it's just hard to do what django expects me to do when creating objects.

Thread has 1 main post.
Thread has many other posts
Post has 0-1 parent post.

There are multiple type of Threads.

It's just easier to work with when I can create BaseThread and extend it.

Besides, there are cases where I need to switch Thread types.
If it's possible to treat the base and extended separately, I can keep the base part, and create the extension part, and connect them.

comment:28 Changed 3 years ago by Reza Mohammadi <reza@…>

raw=True doesn't always work. raw is used for two different purposes:

The 'raw' argument is telling save_base not to save any parent models and
not to do any changes to the values before save. This is used by fixture loading.

For the purpose of this ticket, we want save_base ignore the first part but we need the second part to be done.

I think raw argument should be split to two different arguments, as its functionality is already separated. Not as a workaround for this bug, but to make the code more readable.

comment:29 Changed 3 years ago by Reza Mohammadi <reza@…>

Cc: reza@… added

comment:30 Changed 3 years ago by juan64645@…

Hello! I am new in django. I have a problem like this.
I want to do something like this(in inheritance multi-table):

in view.py

restaurant = Restaurant(place_ptr=place)
restaurant.save()

assuming that the attributes of restaurant hold null=True.
but Restaurant(place_ptr=place) returns nothing.

My vercion of django is 1.5
They say about this? Is there any alternative?
That is not to create a new restaurant and it was clear the place, because it is very ugly.

From already thank you very much!

comment:31 Changed 23 months ago by Collin Anderson

Cc: cmawebsite@… added
Summary: Multi-table inheritance does not allow linking new instance of child model to existing parent model instance.Multi-table inheritance: create child instance from existing parent

comment:32 Changed 15 months ago by Aron Podrigal

Cc: Aron Podrigal added

comment:33 Changed 15 months ago by Aron Podrigal

Anything wrong with this implementation? (taken from Tom Tobin's branch referenced in this ticket)

Last edited 15 months ago by Aron Podrigal (previous) (diff)

comment:34 Changed 14 months ago by Tim Graham

It's a bit difficult to review without tests. To get a review of the API design, it's a good idea to offer a high level overview on the DevelopersMailingList.

comment:35 Changed 14 months ago by Tim Graham

Needs documentation: unset
Summary: Multi-table inheritance: create child instance from existing parentMulti-table inheritance: Add the ability create child instance from existing parent
Type: BugNew feature

comment:36 Changed 14 months ago by Tim Graham

Owner: Karol Sikora deleted
Status: assignednew
Note: See TracTickets for help on using tickets.
Back to Top