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

5

18

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.

Doormat answered 25/11, 2009 at 12:18 Comment(0)
M
17

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)
Maren answered 5/12, 2009 at 13:7 Comment(4)
That seems like a good implementation to me. But why have both the UserProfile class and the PositionHolderUserProfile class? Will it not be simpler to get rid of the latter and push everything in it to the former model?Doormat
I thought it best to encapsulate data relevant to all position holders, e.g. Principal, Administrator and Coordinator, but not relevant to other users such as Buyer and Company. Such data could include which branch they work in, how long they've been employed, etc.Maren
this doesn't seems work.CommandError: One or more models did not validate: app1.positionholderuserprofile: Accessor for field 'user' clashes with related field 'User.profile'. Add a related_name argument to the definition for 'user'.Cawnpore
Django's User model has changed considerably in the 4.5 years since I posted this answer!Maren
A
3

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)
Alloplasm answered 25/11, 2009 at 16:25 Comment(3)
>but sometimes they don't get auto-magically handled like they're supposed to. Could you expand on this and let us know how they did not behave as expected?Doormat
It's hard to qualify, but most of the problems are with the contrib.admin I believe. Occasionally you hit odd problems, like if you override the AdminModel.model_save() I found (through an hour+ of experimenting) you have to manually specify force_insert or force_update otherwise it won't create the matching OneToOne. Occasionally the actual admin interface wigs out and says there is already a record that exists with that ID when you just updated some value.Alloplasm
I'm gonna have to look into that if that is the case, but I am not using contrib.admin anyway. Still trying to find out the pros and cons of Multi-Table Inheritance VS Explicit OneToOne RelationshipDoormat
C
2

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.

Carincarina answered 25/11, 2009 at 12:36 Comment(2)
There are other types of users that I would really like to keep separate. When I look at my models, I would like to see that Principal, Administrator, Coordinator are related, but also that Company and Buyer are not directly related to any of these.Doormat
Just to clarify, I do intend to use abstract models where beneficial. Also, I intend to have separate models for separate types of users.Doormat
B
0

Please consider what happens in the data model when a Coordinator gets promoted to a Principal. I would not use inheritance in this case at all. Please reconsider the previous poster's suggestion "Combining the built in group functionality with the user profile idea may do what you are looking for."

Barthold answered 29/11, 2009 at 11:53 Comment(1)
There is no promotion taking place. "Combining the built in group functionality with the user profile idea may do what you are looking for." is good practice and I follow it, however i want to know what are the pros and cons of inheritance vs explicit onetoone in this particular scenario...Doormat
T
0

Do you need objects of your user classes to act like an auth.User anywhere? That would be the most obvious reason to use inheritance over OneToOne. One pro of the OneToOne approach would be the ease in which you can change over to another User model, if that's a concern.

The real issue I see with what you have above (by either method) is that there doesn't appear to be anything stopping you from having a Principal object and an Administrator object share the same User. OneToOneField can only guarantee a one-to-one mapping between any two relations.

Toothed answered 4/12, 2009 at 18:20 Comment(1)
I am going with OneToOne now because I know exactly what is going on there, instead of having to guess all the repercussions of inheritance (like the UserManager for example). Having greater control seems like an attractive proposition.Doormat

© 2022 - 2024 — McMap. All rights reserved.