Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Django: Two different child classes point to same parent class

Tags:

orm

django

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>
like image 334
Furbeenator Avatar asked Aug 08 '13 15:08

Furbeenator


4 Answers

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.

like image 108
Furbeenator Avatar answered Nov 11 '22 14:11

Furbeenator


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 !

like image 31
Ricola3D Avatar answered Nov 11 '22 15:11

Ricola3D


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.

like image 1
Mike DeSimone Avatar answered Nov 11 '22 13:11

Mike DeSimone


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.

like image 1
Kound Avatar answered Nov 11 '22 14:11

Kound