0

I have a Person model.

class Person(models.Model):
    name_first = models.CharField(max_length=100)
    name_middle = models.CharField(max_length=100, null=True, blank=True)
    name_last = models.CharField(max_length=100)
    date_birth = models.DateField(null=True, blank=True)
    date_death = models.DateField(null=True, blank=True)

I'm trying to extend it to different roles in the music world: a composer, a performer, and a patron.

A person can be one, two or all three roles. If a person is a performer, I also need to assign one or more instrument for that person. You may not know if a person is a performer at the time of instantiation. Or their roles can change over time.

I want to be able to search and display that a person is a composer, pianist and a patron, if he's all three. For example: Beethoven is a conductor, composer, and pianist.

My initial thought on implementation is to inherit the Person class.

class Composer(Person):
    pass

class Performer(Person):
    instrument = models.ManyToManyField(Instrument, verbose_name=_('instrument'), blank=True,)

class Patron(Person):
    pass

class Instrument(models.Model):
    name = models.CharField(max_length=100, null=True, blank=True)

Question 1: should I use some sort of abstract model instead? If so, how would I go about it?

Question 2: how can I search for a person and know whether they are a composer, patron and/or performer and the kind of performer they are.

Thanks.

H C
  • 1,138
  • 4
  • 21
  • 39

3 Answers3

7

My approach would be this: define a base Person class and make your other models have a foreign key to that model.

First, create your Person class.

class Person(models.Model):
    name_first = models.CharField(max_length=100)
    name_middle = models.CharField(max_length=100, null=True, blank=True)
    name_last = models.CharField(max_length=100)
    date_birth = models.DateField(null=True, blank=True)
    date_death = models.DateField(null=True, blank=True)

    @property
    def is_performer(self):
       return hasattr(self, 'performer_role')

    @property
    def is_patron(self):
        return hasattr(self, 'patron_role')

    @property
    def is_composer(self):
        return hasattr(self, 'composer_role')

    @property
    def roles(self):
        roles = []
        if self.is_performer:
            roles.append('performer')
        if self.is_patron:
            roles.append('patron')
        if self.is_composer:
            roles.append('composer')
        return roles

Second, make your specific models and set up a foreign key to the Person.

class ComposerRole(models.Model):
    person = models.OneToOneField(Person, on_delete=models.CASCADE, related_name='composer_role')
    # other fields as necessary

class PerformerRole(models.Model):
    person = models.OneToOneField(Person, on_delete=models.CASCADE, related_name='performer_role')
    instrument = models.CharField(max_length=100, blank=True)
    performer_type = models.CharField(max_length=100, blank=True) //conductor, performer
    # other fields as necessary

class PatronRole(models.Model):
    person = models.OneToOneField(Person, on_delete=models.CASCADE, related_name='patron_roll')
    # other fields as necessary

To instantiate a new person with their correct roles, you could add them like this:

p = Person.objects.create(...)
PerformerRole.objects.create(person=p, instrument='...')

To check whether someone has a role, just do person_instance.is_performer. To get the data for that role, do person_instance.performer_role.instrument.

An added benefit of this approach is that it allows you to query for composers, patrons, and performers, eg ComposerRole.objects.all() or ComposerRole.objects.filter(person__last_name='Beethoven').

Hope that helps.

EDIT: added roles to Person. This way you can get all of the roles a person has by doing person_instance.roles.

inostia
  • 7,777
  • 3
  • 30
  • 33
  • 1
    Thanks! why not just inherit the Person model like class Composer(Person): ? – H C Sep 17 '19 at 05:29
  • 2
    With the inheritance from a concrete class, you can't create a `Person` that has all three roles. When you create a `Composer` in the db, it'll create a unique row with the same id in the `Person` table **and** the `Composer` table. But you won't be able to create a `Performer` for that same `Person`. Class inheritance can't do that for you. – dirkgroten Sep 17 '19 at 15:59
  • @dirkgroten Got it. But it seems like using the abstraction model will only create one entry on one table for a person? – H C Sep 17 '19 at 17:23
  • @HC I think maybe you're thinking about the problem the wrong way. What inheritance is mostly good for is it allows you to share _properties_ between classes. An "abstract" class in the strict Django sense [is not saved to the database](https://docs.djangoproject.com/en/2.2/topics/db/models/#abstract-base-classes), it's just used for DRYing up code. [Concrete inheritance in Django is an anti-pattern](https://stackoverflow.com/a/23549428/2212678). There is no need for inheritance here (concrete or abstract), because none of the models share the same fields. – inostia Sep 17 '19 at 18:07
  • in short, just use foreign keys :) this is what Django simplifies and what the foreign key fields are for – inostia Sep 17 '19 at 18:13
  • @HC just look at it the same way as you learned OOP: a parent class Shape can have subclasses Circle and Square but you can’t have an instance of Shape that is both Circle and Square. It’s either or. Same with model inheritance, regardless whether the parent class is abstract or concrete. In the end it’s just python subvlassing. – dirkgroten Sep 17 '19 at 18:25
  • @dirkgroten OK I think it makes sense. As a followup, is there a way to create a queryset that a Person instance and get all of the role the person is connected to, instead of iteratively go and check if that person has an attribute? – H C Sep 18 '19 at 02:08
  • @HC i'm not sure what your specific need is, but I edited my answer to have some methods on `Person` that should help. – inostia Sep 18 '19 at 22:03
  • My concern with this solution would be in the future if someone is capable of more than one `PerformerRole`? This translates to the `is_performer` property to always returning `True`. I'd also have a concern and have to do some due diligence if the properties defined here take advantage of things like `select_related` and `prefetch_related` as it's not obvious at first look. – Scott Woodall Sep 21 '19 at 15:04
  • @ScottWoodall I have an additional question. I have made instrument a separate table with a ManyToMany relations with PerformerRole. For the situation where someone played for than one instrument. Is there a way to create an attr under PerformerRole, that returns all of the instruments a performer plays? – H C Sep 21 '19 at 17:29
  • @inostia please see my comment above as well. I've updated my model. – H C Sep 21 '19 at 17:29
  • @HC your solution won't give you multiple instruments per person, it just means that the same instrument can apply to different roles. To allow multiple performer roles for a person, you need to change from a `OneToOneField` on the performer role to a `ForeignKey`. You can still make `PerformerRole.instrument` a m2m – inostia Sep 24 '19 at 18:43
  • @inostia would it be possible to pull the instruments the person plays under the Person property? So instead of Composer. Performer. I have Composer. Pianist. Conductor. – H C Sep 29 '19 at 02:16
0

Create a single Person model with BooleanFields if they are that type of person then add related models as needed.

For me, it makes sense to have concrete fields on the Person model to be explicit if this person is a composer, performer or patron. Instead of an implicit assumption that someone is a Performer only because they might have a row in the PerformerRole, ComposerRole or PatronRole table.

class Person(models.Model):
    name_first = models.CharField(max_length=100)
    name_middle = models.CharField(max_length=100, null=True, blank=True)
    name_last = models.CharField(max_length=100)
    date_birth = models.DateField(null=True, blank=True)
    date_death = models.DateField(null=True, blank=True)

    is_composer = models.BooleanField(default=False)
    is_performer = models.BooleanField(default=False)
    is_patron = models.BooleanField(default=False)


class PerformerRole(models.Model):
    person = models.ForeignKey(
        Person,
        on_delete=models.CASCADE,
    )

    instrument = models.CharField(max_length=100)
    type = models.CharField(max_length=100)


class ComposerRole(models.Model):
    person = models.ForeignKey(
        Person,
        on_delete=models.CASCADE,
    )


class PatronRole(models.Model):
    person = models.ForeignKey(
        Person,
        on_delete=models.CASCADE,
    )
Scott Woodall
  • 10,456
  • 6
  • 39
  • 37
  • What do you think about creating a single Role class, with ManyToMany field to Person with choices of "composer", "performer", "patron", "instrument". Instrument is also a ManyToMany field to the Instrument table? If performer is selected as a choice, then you have to add an instrument? What are the pros and cons of this implementation? – H C Sep 21 '19 at 16:35
  • @dirkgroten What about the above implementation? – H C Sep 21 '19 at 16:36
  • @inostia please see above? – H C Sep 21 '19 at 16:36
  • I think the `ManyToMany` solution sounds reasonable and allows you to grow beyond the three roles we have been discussing here. – Scott Woodall Sep 21 '19 at 19:36
  • Yes the `is_composer`, etc should be boolean fields instead of just computed methods if you want to query them. – inostia Sep 24 '19 at 21:34
0

I would use two ManyToMany fields here:

class Role(models.Model):
    name = models.CharField(max_length=80)
    created = models.DateTimeField(auto_now_add=True)

class Instrument(models.Model):
    name = models.CharField(max_length=80)
    created = models.DateTimeField(auto_now_add=True)

class Person(models.Model):
    name_first = models.CharField(max_length=100)
    name_middle = models.CharField(max_length=100, null=True, blank=True)
    name_last = models.CharField(max_length=100)
    date_birth = models.DateField(null=True, blank=True)
    date_death = models.DateField(null=True, blank=True)
    role = models.ManyToManyField(Role)
    instrument = models.ManyToManyField(Instrument)

You can add as many roles and instruments to a Person as you would like.

joshlsullivan
  • 1,375
  • 2
  • 14
  • 21