7

I have a model Person which stores all data about people. I also have a Client model which extends Person. I have another extending model OtherPerson which also extends the Person model. I want to create a Client pointing to a Person, and ALSO create an OtherPerson record which points to that Person. Basically, I want one Person object to be viewed as a Client and and an OtherPerson, depending on the current view. Is this possible with Django's ORM, or do I need to somehow write a Raw query to create this scenario. I am quite certain it is possible from the database side, because both child classes would just point to the parent Person class with their person_ptr_id field.

Simply put, if I create a Client (and thus a Person), can I also create an OtherPerson object using the base Person from the Client. That way I can view them as a Client OR as an OtherPerson, and saving one will affect the Person fields of each?

"Horizontal" Polymorphism?

Here is a cut down version of my models in case that clarifies:

class Person(models.Model):
    """
        Any person in the system will have a standard set of details, fingerprint hit details, some clearances and items due, like TB Test.
    """
    first_name = models.CharField(db_index=True, max_length=64, null=True, blank=True, help_text="First Name.")
    middle_name = models.CharField(db_index=True, max_length=32, null=True, blank=True, help_text="Middle Name.")
    last_name = models.CharField(db_index=True, max_length=64, null=True, blank=True, help_text="Last Name.")
    alias = models.CharField(db_index=True, max_length=128, null=True, blank=True, help_text="Aliases.")
    .
    .
    <some person methods like getPrintName, getAge, etc.>

class Client(Person):
    date_of_first_contact = models.DateField(null=True, blank=True)
    .
    .
    <some client methods>


class OtherPerson(Person):
    active_date = models.DateField(null=True, blank=True)
    termination_date = models.DateField(null=True, blank=True)
    .
    .
    <some other person methods>
Furbeenator
  • 8,106
  • 4
  • 46
  • 54

4 Answers4

5

Okay, I hate to answer my own question, especially since it is sort of a repeat of (Django model inheritance: create sub-instance of existing instance (downcast)?

@Daniel Roseman got me out of a jam AGAIN. Gotta love that guy!

person = Person.objects.get(id=<my_person_id>)
client = Client(person_ptr_id=person.id)
client.__dict__.update(person.__dict__)
client.save()
other_person = OtherPerson(person_ptr_id=person.id)
other_person.__dict__.update(person.__dict__)
other_person.save()

If I have an existing Client and want to make an OtherPerson from them, which is my exact use-case, I just do this:

client_id = <ID of Client/Person I want to create an OtherPerson with>
p = Person.objects.get(id=client_id)
o = OtherPerson(person_ptr_id=p.id) # Note Person.id and Client.id are the same.
o.__dict__.update(p.__dict__)
o.save()

Now the person shows up as a Client on the clients screen and as an OtherPerson on the other person screen. I can get the OtherPerson version of the Person which has all the OtherPerson details and functions or I can get a Client version of that Person which has all the Client details and functions.

Community
  • 1
  • 1
Furbeenator
  • 8,106
  • 4
  • 46
  • 54
  • 1
    This is the PITA I mentioned. Next time don't use subclassing where a OneToOneField will do better. – Mike DeSimone Aug 09 '13 at 00:44
  • Why shouldn't one use inheritance here? A Client is a Person, an OtherPerson is a Person. To me it looks like a missing feature in Django. – Se Norm Sep 04 '13 at 10:01
2

What you are doing is not possible as you do it, Django has specific rules for inheritance

The only possible schema is:

class Parent(models.Model):
    class Meta:
        abstract = True # MUST BE !!! This results in no relation generated in your DB

    field0 = models.CharField(...
    ...

    # here you're allowed to put some functions and some fields here


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

    # Anything you want, this model will create a relation in your database with field0, field1, ...


class GrandChild(models.Model):
    class Meta:
        proxy = True # MUST BE !!! This results in no relation generated in your DB

    # here you're not allowed to put DB fields, but you can override __init__ to change attributes of the fields: choices, default,... You also can add model methods.

This is because there is no DB inheritance in most DBGS. Thus you need to make you parent class abstract !

Ricola3D
  • 2,402
  • 17
  • 16
  • Note you can have many abastract classes (but won't only inherit from the 1st methods found from left to right), many Child classes (and thus many tables in your DB), and many proxies (and thus many admin, ...) – Ricola3D Aug 08 '13 at 16:08
  • Thanks for the answer, Ricola3D. Unfortunately, the parent `Person` table already exists, so I cannot change to an abstract table. Any way you know I can take a `Person` object that exists and use them as a super to a new child `Client` object? – Furbeenator Aug 08 '13 at 16:19
  • do your client model need tout store additionnal fields un thé database ? – Ricola3D Aug 12 '13 at 06:54
1

You can't really do that with subclassing. When you subclass Person, you're implicitly telling Django that you'll be creating subclasses, not Person objects. It's a PITA to take a Person and transmogrify it into a OtherPerson later.

You probably want a OneToOneField instead. Both Client and OtherPerson should be subclasses of models.Model:

class Client(models.Model):
    person = models.OneToOneField(Person, related_name="client")
    # ...

class OtherPerson(models.Model):
    person = models.OneToOneField(Person, related_name="other_person")
    # ...

Then you can do things like:

pers = Person(...)
pers.save()
client = Client(person=pers, ...)
client.save()
other = OtherPerson(person=pers, ...)
other.save()

pers.other.termination_date = datetime.now()
pers.other.save()

See https://docs.djangoproject.com/en/dev/topics/db/examples/one_to_one/ for more.

Mike DeSimone
  • 41,631
  • 10
  • 72
  • 96
  • 1
    I really like the idea of having the O2O extrapolated to the sub classes, unfortunately, I already have hundreds of Person objects already created. I found a suggested patch for creating child objects with the parent class defined in the constructor (https://code.djangoproject.com/ticket/7623). This is exactly what I want to do, but it is 5 years old, so I'm not sure it will work. Thanks for your answer, it would be perfect for future implementations and good to know; this paradigm is definitely superior to what I have. Hopefully I can find a way to work around my previously created scenario. – Furbeenator Aug 08 '13 at 16:17
  • Well, if you're stuck, you're stuck. FYI, I've converted things from subclass to non-subclass before, and the South migration is hard to get right. – Mike DeSimone Aug 09 '13 at 00:46
1

As mentioned in a comment already, there is an open ticket for this very question: https://code.djangoproject.com/ticket/7623

In the meanwhile there is a proposed patch (https://github.com/django/django/compare/master...ar45:child_object_from_parent_model) which not using obj.__dict__ but creates an dictionary with all field values cycling over all fields. Here a simplified function:

def create_child_from_parent_model(parent_obj, child_cls, init_values: dict):
    attrs = {}
    for field in parent_obj._meta._get_fields(reverse=False, include_parents=True):
        if field.attname not in attrs:
            attrs[field.attname] = getattr(parent_obj, field.attname)
    attrs[child_cls._meta.parents[parent_obj.__class__].name] = parent_obj
    attrs.update(init_values)
    print(attrs)
    return child_cls(**attrs)

person = Person.objects.get(id=<my_person_id>)
client = create_child_from_parent_model(person, Client, {})
client.save()

If you want to create a sibling:

client_person = getattr(person, person._meta.parents.get(Person).name)
other_person = create_child_from_parent_model(person, OhterPerson, {})
other_person.save()

This method has the advantage that methods that are overwritten by the child are not replaced by the original parent methods. For me using the original answers obj.__dict__.update() led to exceptions as I was using the FieldTracker from model_utils in the parent class.

Kound
  • 1,835
  • 1
  • 17
  • 30