1

I am trying to wrap my head around how proxy model works. Supposed I have a base class called Animal, and I would like to implement two sub-classes: Dog and Cow. They have the same data requirements, so I don't really want to create two separate tables. So I am trying using proxy models:

class Animal(models.Model):
    name = models.CharField()
    animal_type = models.CharField(choices=(('Dog','Dog'),('Cow','Cow')))

    def get_sound(self):
        if animal_type == 'Dog':
            return self.dog.get_sound()    #doesn't work
        elif self.animal_type == 'Cow':
            return self.cow.get_sound()    #doesn't work

class Dog(Animal):
    class Meta:
        proxy=True

    def get_sound(self):
        return 'Woof'

class Cow(Animal):
    class Meta:
        proxy=True

    def get_sound(self):
        return 'Moo'

The problem I have is, how can I access the sub-class methods from the parent class? I have it as self.dog.get_sound(). This is possible in multi-table inheritance, but does not work in a proxy model.

>>obj = Animal.objects.create(name='Max', animal_type='Dog')
>>obj.get_sound()
'Woof'    <-- what I'd like it to return

Is proxy model the wrong way to do this? Would prefer to keep one table.

user3628119
  • 355
  • 5
  • 15
  • I think you don't understand object inheritance. Try to make new object `obj = Dog.objects.create(name='Max', animal_type='Dog')` and you will get Woof – Ilko Oct 13 '18 at 17:54
  • 1
    Yes, I can do that. But supposed I have an object id, but I don't know what type of animal the object is and all I care is to run `get_sound()`. Currently, I would have to get `Animal.objects.get(id=id).animal_type`, then once I figure out it's a dog (using if/then), do `Dog.objects.get(id=id).get_sound()`. Is it possible to do this from within the Animal class? – user3628119 Oct 14 '18 at 01:15
  • I guess I can do this in Animal get_sound(). `if animal_type == 'Dog': return Dog.objects.get(id=self.id).get_sound()`. That seems inefficient to have to initiate the object twice (once as an Animal and once as a Dog). – user3628119 Oct 14 '18 at 01:20
  • https://stackoverflow.com/questions/50525301/django-inheritance-and-polymorphism-with-proxy-models – ndpu Oct 05 '22 at 02:52

2 Answers2

1

Here you go:

class Animal(models.Model):
    class Type(models.TextChoices):
        Dog = ('dog', 'Dog')
        Cow = ('cow', 'Cow')

    name = models.CharField()
    animal_type = models.CharField(choices=Type.choices)

    def get_subclass_instance(self):
        subclasses = {
            self.Type.choices.Dog: Dog,
            self.Type.choices.Cow: Cow,
        }
        instance = subclasses[self.type].objects.get(id=self.id)
        return instance

    def get_sound(self):
        self.get_subclass_instance().get_sound()


class Dog(Animal):
    class Meta:
        proxy=True

    def get_sound(self):
        return 'Woof'

class Cow(Animal):
    class Meta:
        proxy=True

    def get_sound(self):
        return 'Moo'
swinters
  • 133
  • 1
  • 6
0

Yeah, that's not going to work. Behind the scenes Django creates a OneToOne foreign key for each inherited model, you're right about it, except in the case of proxy models (docs). Proxy models are not meant to be used this way, they work exactly like the original model, sometimes in a subset of the original table (like your example), extra methods, etc. If you use a proxy model you shouldn't need to use the original table, as you want.

I have a few suggestions for you:

class DogManager(Manager):

    def get_queryset(self):
        queryset = super().get_queryset()
        return queryset.filter(kind='Dog')  # Literal is not good

    def create(self, **kwargs):
        kwargs.update({'kind': 'Dog'})
        return super().create(**kwargs)


class CowManager(Manager):

    def get_queryset(self):
        queryset = super().get_queryset()
        return queryset.filter(kind='Cow')  # Literal is not good

    def create(self, **kwargs):
        kwargs.update({'kind': 'Cow'})
        return super().create(**kwargs)


class Animal(models.Model):
    name = models.CharField()
    # I've changed the name because I don't like variables with 'type' in the name
    kind = models.CharField(choices=(('Dog','Dog'),('Cow','Cow')))


class Dog(Animal):

    objects = DogManager()

    class Meta:
        proxy=True

    def get_sound(self):
        return 'Woof'


class Cow(Animal):

    objects = CowManager()

    class Meta:
        proxy=True

    def get_sound(self):
        return 'Moo'

And now you don't need to call Animal class, as you have a subset of your original data in those two proxy models proxy managers.

instance = Dog.objects.create(name='Max')
instance.get_sound()  # Woof

Dog.objects.all() # all animals with kind 'Dog'
Cow.objects.all() # all animals with kind 'Cow'
gmacro
  • 397
  • 3
  • 6
  • Essentially, I'd like to be able to call all animals and get their sound `for animal in Animals.objects.all(): animal.get_sound()` and I think the approach above doesn't do that as I would need to do it for each kind of animal. – user3628119 Oct 15 '18 at 09:36
  • 1
    I do have a solution, which is once I get an Animal instance, I look at the type of animal and creates an instance of that animal. However, instead of using `Dog.objects.get(id=self.id)` which would hit the database a second time, I would do `Dog(**attrs)` where `attrs={f.name: getattr(self, f.name) for f in self._meta.fields}`. `self` here refers to the Animal instance. I think this works but what I'd like to know is whether I should be using a model other than the proxy model to accomplish my purpose. – user3628119 Oct 15 '18 at 09:42