Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Django Multi-Table Inheritance VS Specifying Explicit OneToOne Relationship in Models

Hope all this makes sense :) I'll clarify via comments if necessary. Also, I am experimenting using bold text in this question, and will edit it out if I (or you) find it distracting. With that out of the way...

Using django.contrib.auth gives us User and Group, among other useful things that I can't do without (like basic messaging).

In my app I have several different types of users. A user can be of only one type. That would easily be handled by groups, with a little extra care. However, these different users are related to each other in hierarchies / relationships.

Let's take a look at these users: -

Principals - "top level" users

Administrators - each administrator reports to a Principal

Coordinators - each coordinator reports to an Administrator

Apart from these there are other user types that are not directly related, but may get related later on. For example, "Company" is another type of user, and can have various "Products", and products may be supervised by a "Coordinator". "Buyer" is another kind of user that may buy products.

Now all these users have various other attributes, some of which are common to all types of users and some of which are distinct only to one user type. For example, all types of users have to have an address. On the other hand, only the Principal user belongs to a "BranchOffice".

Another point, which was stated above, is that a User can only ever be of one type.

The app also needs to keep track of who created and/or modified Principals, Administrators, Coordinators, Companies, Products etc. (So that's two more links to the User model.)

In this scenario, is it a good idea to use Django's multi-table inheritance as follows: -

from django.contrib.auth.models import User
class Principal(User):
    #
    #
    #    
    branchoffice = models.ForeignKey(BranchOffice)
    landline = models.CharField(blank=True, max_length=20)    
    mobile = models.CharField(blank=True, max_length=20)
    created_by = models.ForeignKey(User, editable=False, blank=True, related_name="principalcreator")    
    modified_by = models.ForeignKey(User, editable=False, blank=True, related_name="principalmodifier")
    #
    #
    #

Or should I go about doing it like this: -

class Principal(models.Model):
    #
    #
    #
    user = models.OneToOneField(User, blank=True)
    branchoffice = models.ForeignKey(BranchOffice)
    landline = models.CharField(blank=True, max_length=20)    
    mobile = models.CharField(blank=True, max_length=20)
    created_by = models.ForeignKey(User, editable=False, blank=True, related_name="principalcreator")    
    modified_by = models.ForeignKey(User, editable=False, blank=True, related_name="principalmodifier")
    #
    #
    #

Please keep in mind that there are other user types that are related via foreign keys, for example: -

class Administrator(models.Model):
    #
    #
    #
    principal = models.ForeignKey(Principal, help_text="The supervising principal for this Administrator")
    user = models.OneToOneField(User, blank=True)
    province = models.ForeignKey(         Province)
    landline = models.CharField(blank=True, max_length=20)    
    mobile = models.CharField(blank=True, max_length=20)
    created_by = models.ForeignKey(User, editable=False, blank=True, related_name="administratorcreator")    
    modified_by = models.ForeignKey(User, editable=False, blank=True, related_name="administratormodifier")

I am aware that Django does use a one-to-one relationship for multi-table inheritance behind the scenes. I am just not qualified enough to decide which is a more sound approach.

like image 997
chefsmart Avatar asked Nov 25 '09 12:11

chefsmart


3 Answers

I'd like to expand on the solution by @thornomad.

Extending Django's User class directly can cause all kinds of trouble with the internal django.auth mechanisms. What I've done in a similar situation is precisely what @thornomad suggests - I made my own UserProfile model linked one-to-one with the Django User model, in which I held additional user data and from which I inherited models for different types of users.

Something to fit what you described:

class UserProfile(models.Model):
    user = models.OneToOneField(User, blank=True, related_name='profile')
    class Meta:
        abstract = True


class PositionHolderUserProfile(UserProfile):
    first_name = models.CharField(max_length=30)
    last_name = models.CharField(max_length=30)
    landline = models.CharField(blank=True, max_length=20)    
    mobile = models.CharField(blank=True, max_length=20)
    created_by = models.ForeignKey(PositionHolderUserProfile, editable=False, blank=True, related_name="created_users")    
    modified_by = models.ForeignKey(PositionHolderUserProfile, editable=False, blank=True, related_name="modified_users")

class Principal(PositionHolderUserProfile):
    branchoffice = models.ForeignKey(BranchOffice)

class Administrator(PositionHolderUserProfile):
    superior = models.ForeignKey(Principal, related_name="subordinates")
    province = models.ForeignKey(Province)

class Coordinator(PositionHolderUserProfile):
    superior = models.ForeignKey(Administrator, related_name="subordinates")


class Company(UserProfile):
    name = models.CharField(max_length=50)

class Product(models.Model):
    name = models.CharField(max_length=50)
    produced_by = models.ForeignKey(Company)

class Buyer(UserProfile):
    first_name = models.CharField(max_length=30)
    last_name = models.CharField(max_length=30)
    products_bought = models.ManyToManyField(Product)
like image 109
taleinat Avatar answered Oct 18 '22 07:10

taleinat


I recently switched over to using models that inherit off of contrib.auto.models.User. My general observation is that in theory they're great, but sometimes they don't get auto-magically handled like they're supposed to.

I think your decision regarding inheritance vs. OneToOne comes down to this:

  • Do I want to have Django automatically do something right 95% of the time, and need to debug that other 5%

-OR-

  • Do I want to do something manually myself 100% of the time

If you haven't seen it, the Scott Barham blog has a great post about inheriting off of User, and also building a custom back end to make sure that your custom object is being returned -- Extending the Django User.

Additionally of interest would be the AutoOneToOne field provided by django-annoying. It's sort of a hybrid of the two approaches here -- there's no inheritance taking place, but Django is taking care of creating the matching OneToOneField if it's not present.

Also, thornomad does make a good point about the redundancy in your models. You could easily implement an abstract class to clean that up as so (assuming you're doing manual OneToOne):

class BaseExtendedUser(models.Model):
    user = models.OneToOneField(User, blank=True, related_name='profile')
    landline = models.CharField(blank=True, max_length=20)    
    mobile = models.CharField(blank=True, max_length=20)
    created_by = models.ForeignKey(User, editable=False, blank=True, related_name="created_users")    
    modified_by = models.ForeignKey(User, editable=False, blank=True, related_name="modified_users")

    class Meta:
        abstract = True

class Administrator(BaseExtendedUser):
    province = models.ForeignKey(Province)

class Principal(BaseExtendedUser):
    branchoffice = models.ForeignKey(BranchOffice)
like image 35
T. Stone Avatar answered Oct 18 '22 07:10

T. Stone


I don't think I would inherit the User model, rather use a custom UserProfile - leaving the contrib.auth model alone. With the custom UserProfile model, you could setup a base user profile model that can be a part of all your different user types.

Just looking at it quickly, too, I would look carefully at any models that repeat all the same fields (like your last two Principle and Administrator models). Combining the built in group functionality with the user profile idea may do what you are looking for.

like image 2
thornomad Avatar answered Oct 18 '22 08:10

thornomad