Is there a way to optimize the creation and retrieval of users (that are also part of another model) in Django with just one request?

Viewed 280

My app has Users that can be Doctors/Patients/Secretaries. To create a Doctor, therefore, I perform two POST requests: one for the creation of a User and one for a Doctor. The way I do this, the User has to be created first so that I can later create the Doctor (Doctor requires a 'User' field). I am using Django Rest Framework to create the API.

class User(AbstractUser):
    # defined roles so when I retrieve user, I know to perform a
    # request to api/doctors/ or api/secretaries/ etc depending on role.
    ROLES = (
        ('d', 'Doctor'),
        ('s', 'Secretary'),
        ('p', 'Patient'),
    )
    role = models.CharField(
        max_length=1, choices=ROLES, blank=True, default='p', help_text='Role')

class Doctor(models.Model):
    user = models.ForeignKey(User, on_delete=models.CASCADE)
    national_id =  models.CharField(max_length=32, blank=False)
    ...

Since I'm new to Django, I don't know if two requests is the standard/best way of creating this User/Doctor.

This comes to mind as I am also thinking of the GET methods which will be performed later on (two GET requests when a Doctor logs in if I want to retrieve all of their info (User/Doctor)?)

I read about subclassing, which would be something like Doctor(User), then the only necessary request would be a single POST to create a Doctor (which would alongside create the User). I am, however, skeptical of subclassing the User as I read at least 3 SO answers stating it could cause problems in the future.

4 Answers

have a look at this good tutorial https://simpleisbetterthancomplex.com/tutorial/2018/01/18/how-to-implement-multiple-user-types-with-django.html which explain 2 different approches

  • extend AbstractUser with flags is_doctor, is_secretary, is_patient
class User(AbstractUser):
    is_doctor = models.BooleanField('Doctor status', default=False)
    is_secretary  = models.BooleanField('Secretary status', default=False)
    is_patient = models.BooleanField('Patient status', default=False)
  • using roles which suites your case:
class Role(models.Model):
  '''
  The Role entries are managed by the system,
  automatically created via a Django data migration.
  '''

  ROLE_CHOICES = (
        ('d', 'Doctor'),
        ('s', 'Secretary'),
        ('p', 'Patient'),
  )

  id = models.PositiveSmallIntegerField(choices=ROLE_CHOICES, primary_key=True)

  def __str__(self):
      return self.get_id_display()


class User(AbstractUser):
  roles = models.ManyToManyField(Role)

Problem

Determine best practices for handling multiple user types and adding attributes to a user in Django.

Solution

The following is are recommendations based on design patterns that have been commonly used in Django since version 0.96. These recommendations are present in Django’s documentation (see: References).

Roles

Use Django’s built in permissions module for role and group management instead of rolling your own role and group management.

Instead of a model for each type, create a single UserProfile model, relegating user types to being managed by permissions model.

I recommend using a data migration to add groups so that default groups are automatically seeded on initial migrate call—this reduces overhead for anyone setting up your project for the first time.

OneToOne

Use Django OneToOne field instead of ForeignKey.

UserProfile and Signals

Create a signal that creates a UserProfile on User create.

Example

class UserProfile(models.Model):
    user = models.OneToOne(“User”, on_delete=models.CASCADE)
    national_id =  models.CharField(max_length=32, 


def create_profile(sender, **kwargs):
    user = kwargs["instance"]
    if kwargs["created"]:
        user_profile = UserProfile(user=user)
        user_profile.save()

post_save.connect(create_profile, sender=User)

References

Django permissions (groups): https://docs.djangoproject.com/en/3.1/topics/auth/default/#groups

Django Data Migrations: https://docs.djangoproject.com/en/3.1/topics/migrations/#data-migrations

Django extending User model recommendation: https://docs.djangoproject.com/en/1.8/topics/auth/customizing/#extending-django-s-default-user

Django post_save signal: https://docs.djangoproject.com/en/3.1/ref/signals/#post-save

in your case (the model you made) you can create the doctor and the user in one post request to the doctor creation by overriding the create function for the

from rest_framework.generics import CreateAPIView, ListAPIView

class CreateDoctorViewSet(CreateAPIView, ListAPIView):
    def create(self, request, *args, **kwargs):
        data = self.request.data
        user_dict_keys = ["username", "email", "first_name", "last_name"]
        user_dict = {key: data.pop(key, None) for key in user_dict_keys}
        user_dict['role'] = "d"
        user_serializer = UserSerializer(data=user_dict)
        # if it's not valid it will return the exception details for the requester
        user_serializer.is_valid(raise_exception=True)
        user = user_serializer.create(user_dict)
        user.set_password(data['password'])
        data.pop("password", None)
        user.save()

        response = super().create(request, *args, **kwargs)
        if response.statu_code == 201:
            return response 
        # if an error happened while in the doctor model (model error or serializer error) >> delete the created user
        user.delete()
        return response

or made a little more DRY :

from rest_framework.generics import CreateAPIView, ListAPIView


def create_user(self):
    data = self.request.data
    user_dict_keys = ["username", "email", "first_name", "last_name"]
    user_dict = {key: data.pop(key, None) for key in user_dict_keys}
    user_dict['role'] = "d"
    user_serializer = UserSerializer(data=user_dict)
    # if it's not valid it will return the exception details for the requester
    user_serializer.is_valid(raise_exception=True)
    user = user_serializer.create(user_dict)
    user.set_password(data['password'])
    data.pop("password", None)
    user.save()

    return user


class CreateDoctorViewSet(CreateAPIView, ListAPIView):
    def create(self, request, *args, **kwargs):
        user = create_user(request)

        response = super().create(request, *args, **kwargs)
        if response.statu_code == 201:
            return response
            # if an error happened while in the doctor model (model error or serializer error) >> delete the created user
        user.delete()
        return response

Personal Advices:

  • in the case you're providing the User model has one role so it's better to make the user field in the Doctor class OneToOne instead of ForeignKey.

  • Of course if you have cases where there are people for example converting from Doctor to Secretary and you want them to switch between roles on the same account you can keep the ForeignKey on Doctor model but you have to make multiple roles possible in the user model.

What you mean by subclassing it is called Multi table inheritance. And there is no problem in using it, no side effects and it is perfectly compatible with Django Rest Framework (which you have tagged). This is the way it works:

class User(AbstractUser):
    # Your common fields for all user types.

class Doctor(User):
    national_id =  models.CharField(max_length=32, blank=False)

class Secretary(User):
    # Your specific fields for secretary model

class Patient(User):
    # Your specifict fields for patient model

In background, it uses a OneToOne relationship for each subtype.

Advantajes of using Multi table inheritance:

It is simple and elegant: you don't have to take care of different tables, queries, etc; Django does it for you. It also unsures a good and formalized structure of your database: different tables for common and specific data in OneToOne relationship.

It is suitable for your needs? That depends.

  • If each subtype has its own specific fields -> use multi table inheritance without doubt.
  • If each subtype has the same set of fields but different behaviours (different class/model methods, code, etc) -> use proxy models.
  • If all the subtypes have the same set of fields and the same behaviour (same class/model methods, code, etc) -> use role based approach (one field identifying the role).
  • (extra) If you have dozens of subtypes, each one of them has different fields and you don't care too much about database formalization -> Don't use Multi table inheritance. In this case, you can use a mix of role based approach with JSON fields (for storing all the specifict fields) and proxy models (for handling different behaviours)**.
Related