save a model with foreign key at the same time

I have a model called Profile. Profile model has a foreign key to a model Location, which in turn has a city field that is a foreign key to a City model which has foreign keys to State and Country

How do I save the Profile so the Location is also saved correctly and the country state and city for the location is correct. The Country, State and City tables are already populated with data so the user will send an id to the back end and the views should then match the id’s with what is in the database.

Do I have to take out the parts for Location from the validated_data object and then save the location separately or how do I do it please?

Location model:

class Location(models.Model):
    name = models.CharField(max_length=50, default=None)
    slug = AutoSlugField(populate_from=["name"])
    street = models.CharField(max_length=100)
    additional = models.CharField(max_length=100)
    zip = models.CharField(max_length=20)
    city = models.ForeignKey(City, on_delete=models.CASCADE, default=None)
    phone = models.CharField(max_length=15)

    created_at = models.DateTimeField(auto_now_add=True, verbose_name="created at")
    updated_at = models.DateTimeField(auto_now=True, verbose_name="updated at")

    class Meta:
        verbose_name = "location"
        verbose_name_plural = "locations"
        db_table = "locations"
        ordering = ["zip"]

    def __str__(self):
        return self.name

    def get_absolute_url(self):
        return self.slug

City model:

class City(models.Model):
    name = models.CharField(max_length=50)
    slug = AutoSlugField(populate_from=["name"])
    country = models.ForeignKey(Country, on_delete=models.CASCADE, default=None)
    state = models.ForeignKey(State, on_delete=models.CASCADE, default=None)
    created_at = models.DateTimeField("date post was created", auto_now_add=True)
    updated_at = models.DateTimeField("date post was updated", auto_now=True)

    class Meta:
        verbose_name = "city"
        verbose_name_plural = "cities"
        db_table = "cities"
        unique_together = ["country", "state", "name"]
        ordering = ["name"]

    def __str__(self):
        return self.name

    def get_absolute_url(self):
        return self.slug

The profile model:

class Profile(models.Model):

    # Gender
    M = 'M'
    F = 'F'
    GENDER = [ 
        (M, "male"),
        (F, "female"),
    ]

    # Basic information
    background = models.FileField(upload_to=background_to)
    photo = models.FileField(upload_to=image_to)
    slug = AutoSlugField(populate_from=['first_name', 'last_name', 'gender', 'location'])
    first_name = models.CharField(max_length=100)
    middle_name = models.CharField(max_length=100, null=True, blank=True)
    last_name = models.CharField(max_length=100)
    birthdate = models.DateField()
    gender = models.CharField(max_length=1, choices=GENDER, default=None)
    bio = models.TextField(max_length=5000)
    languages = ArrayField(models.CharField(max_length=30), null=True, blank=True)

    # Location information
    location = models.ForeignKey(Location, on_delete=models.DO_NOTHING)
    website = models.URLField(max_length=256)

    # user information 
    user = models.ForeignKey(User, on_delete=models.CASCADE)
    created_at = models.DateTimeField(auto_now_add=True, verbose_name="created at")
    updated_at = models.DateTimeField(auto_now=True, verbose_name="updated at")

    class Meta:
        verbose_name = "profile"
        verbose_name_plural = "profiles"
        db_table = "user_profiles"

    def __str__(self):
        return self.first_name + ' ' + self.last_name

    def get_absolute_url(self):
        return self.slug    

Any advice will be greatly appreciated. Just want to know if my approach is correct and how I get the data for the location model I must save and if i need to remove the location data from validated data first or what is the best approach. I am using Django Rest Framework

You should not be putting the location field in the profile model. You actually don’t seem to have any FKs to the profile model. You should also have a State model with a FK to the Country model with an FK to the Profile model. (If your not dealing with a lot of countries you might just add a country field in the state model, similarly if you don’t expect a lot of cities you could store state and country as simple fields in the City model to simplify. What ever seems to make sense based on your expected data, but at some point you’ll need a FK to the profile (you already have profile pointing to user).

Foreign keys are used in the child to point to the parent. (It helped me think of the ForeignKey field as containing an object not just an integer field pointing to a pk in the parent.) So when you save a location in a city for a profile (yes you must “save the location separately” as you would any input), the location record will have a City object to which it pertains (FK), which (ignoring State and Country) has a FK to the Profile object. So assigning a location FK in the profile wouldn’t make sense since everything is already “linked up”. I hope I didn’t make things more confusing.

Location is a foreign key on the profile model. It points to the location model and here is the entire locations model file:

from django.db import models
from django_extensions.db.fields import AutoSlugField


class Country(models.Model):
    name = models.CharField(max_length=50)
    slug = AutoSlugField(populate_from=["name"])
    country_code = models.CharField(max_length=5)
    dial_code = models.CharField(max_length=5)
    created_at = models.DateTimeField(auto_now_add=True)
    updated_at = models.DateTimeField(auto_now=True)

    class Meta:
        verbose_name = "country"
        verbose_name_plural = "countries"
        db_table = "countries"
        ordering = ["name"]

    def __str__(self):
        return self.name

    def get_absolute_url(self):
        return self.slug


class State(models.Model):
    name = models.CharField(max_length=50)
    slug = AutoSlugField(populate_from=["name"])
    country = models.ForeignKey(Country, on_delete=models.CASCADE, default=None)
    created_at = models.DateTimeField("date post was created", auto_now_add=True)
    updated_at = models.DateTimeField("date post was updated", auto_now=True)

    class Meta:
        verbose_name = "state"
        verbose_name_plural = "states"
        db_table = "states"
        unique_together = ["country", "name"]
        ordering = ["name"]

    def __str__(self):
        return self.name

    def get_absolute_url(self):
        return self.slug


class City(models.Model):
    name = models.CharField(max_length=50)
    slug = AutoSlugField(populate_from=["name"])
    country = models.ForeignKey(Country, on_delete=models.CASCADE, default=None)
    state = models.ForeignKey(State, on_delete=models.CASCADE, default=None)
    created_at = models.DateTimeField("date post was created", auto_now_add=True)
    updated_at = models.DateTimeField("date post was updated", auto_now=True)

    class Meta:
        verbose_name = "city"
        verbose_name_plural = "cities"
        db_table = "cities"
        unique_together = ["country", "state", "name"]
        ordering = ["name"]

    def __str__(self):
        return self.name

    def get_absolute_url(self):
        return self.slug


class Location(models.Model):
    name = models.CharField(max_length=50, default=None)
    slug = AutoSlugField(populate_from=["name"])
    street = models.CharField(max_length=100)
    additional = models.CharField(max_length=100)
    zip = models.CharField(max_length=20)
    city = models.ForeignKey(City, on_delete=models.CASCADE, default=None)
    phone = models.CharField(max_length=15)

    created_at = models.DateTimeField(auto_now_add=True, verbose_name="created at")
    updated_at = models.DateTimeField(auto_now=True, verbose_name="updated at")

    class Meta:
        verbose_name = "location"
        verbose_name_plural = "locations"
        db_table = "locations"
        ordering = ["zip"]

    def __str__(self):
        return self.name

    def get_absolute_url(self):
        return self.slug

In the database it will will have every country, state and city in the world. A profile might also have more than one location. If I look at the validated data using a breakpoint I do not see all the fields there. So in my view do i have to include the location serializer as well?

profile views.py:

class ProfileViewSet(viewsets.ModelViewSet):
    permission_classes = [permissions.IsAuthenticated]
    queryset = Profile.objects.all()
    serializer_class = ProfileSerializer

How do you handle a model with 5 or 10 foreign keys to 10 different models in 10 different apps?

“Location is a foreign key on the profile model.” — which would make Location the parent. Is that what you want? It should be the other way, indirectly, through Location->City->State->Country->Profile. Then Profile can have as many Locations as you want.

Thanks for the help really appreciate it.

I changed my profile model to this:

from django.db import models
from django_extensions.db.fields import AutoSlugField
from django.contrib.postgres.fields import ArrayField
from django.contrib.auth import get_user_model
from locations.models import Location

User = get_user_model()

def image_to(instance, filename):
    return 'profiles/images/{id}/{filename}'.format(
        id=instance.user.id, filename=filename)

def background_to(instance, filename):
    return 'profiles/backgrounds/{id}/{filename}'.format(
        id=instance.user.id, filename=filename)

# User profiles
class Profile(models.Model):

    # Gender
    M = 'M'
    F = 'F'
    O = 'O'
    GENDER = [ 
        (M, "male"),
        (F, "female"),
        (O, "Other")
    ]

    # Basic information
    background = models.FileField(upload_to=background_to, null=True, blank=True)
    photo = models.FileField(upload_to=image_to, null=True, blank=True)
    slug = AutoSlugField(populate_from=['first_name', 'last_name', 'gender', 'location'])
    first_name = models.CharField(max_length=100)
    middle_name = models.CharField(max_length=100, null=True, blank=True)
    last_name = models.CharField(max_length=100)
    birthdate = models.DateField()
    gender = models.CharField(max_length=1, choices=GENDER, default=None)
    bio = models.TextField(max_length=5000, null=True, blank=True)
    languages = ArrayField(models.CharField(max_length=30, null=True, blank=True))

    # Location information
    website = models.URLField(max_length=256, null=True, blank=True)

    # author information 
    user = models.OneToOneField(User, on_delete=models.CASCADE)
    created_at = models.DateTimeField(auto_now_add=True, verbose_name="created at")
    updated_at = models.DateTimeField(auto_now=True, verbose_name="updated at")

    class Meta:
        verbose_name = "profile"
        verbose_name_plural = "profiles"
        db_table = "user_profiles"

    def __str__(self):
        return self.first_name + ' ' + self.last_name

    def get_absolute_url(self):
        return self.slug    

class ProfileLocation(models.Model):
    location = models.OneToOneField(Location, on_delete=models.CASCADE)
    profile = models.ForeignKey(Profile, on_delete=models.CASCADE)

    class Meta:
        db_table = "profile_locations"

    def __str__(self):
        return "{}' location".format(self.profile.first_name)   

and the Locations model to this:

from django.db import models
from django_extensions.db.fields import AutoSlugField


class Country(models.Model):
    name = models.CharField(max_length=50)
    slug = AutoSlugField(populate_from=["name"])
    country_code = models.CharField(max_length=5)
    dial_code = models.CharField(max_length=5)
    created_at = models.DateTimeField(auto_now_add=True)
    updated_at = models.DateTimeField(auto_now=True)

    class Meta:
        verbose_name = "country"
        verbose_name_plural = "countries"
        db_table = "countries"
        ordering = ["name"]

    def __str__(self):
        return self.name

    def get_absolute_url(self):
        return self.slug


class State(models.Model):
    name = models.CharField(max_length=50)
    slug = AutoSlugField(populate_from=["name"])
    country = models.OneToOneField(Country, on_delete=models.CASCADE, default=None)
    created_at = models.DateTimeField("date post was created", auto_now_add=True)
    updated_at = models.DateTimeField("date post was updated", auto_now=True)

    class Meta:
        verbose_name = "state"
        verbose_name_plural = "states"
        db_table = "states"
        unique_together = ["country", "name"]
        ordering = ["name"]

    def __str__(self):
        return self.name

    def get_absolute_url(self):
        return self.slug


class City(models.Model):
    name = models.CharField(max_length=50)
    slug = AutoSlugField(populate_from=["name"])
    country = models.OneToOneField(Country, on_delete=models.CASCADE, default=None)
    state = models.OneToOneField(State, on_delete=models.CASCADE, default=None)
    created_at = models.DateTimeField("date post was created", auto_now_add=True)
    updated_at = models.DateTimeField("date post was updated", auto_now=True)

    class Meta:
        verbose_name = "city"
        verbose_name_plural = "cities"
        db_table = "cities"
        unique_together = ["country", "state", "name"]
        ordering = ["name"]

    def __str__(self):
        return self.name

    def get_absolute_url(self):
        return self.slug


class Location(models.Model):
    name = models.CharField(max_length=50, default=None)
    slug = AutoSlugField(populate_from=["name"])
    street = models.CharField(max_length=100)
    additional = models.CharField(max_length=100)
    country = models.OneToOneField(State, on_delete=models.CASCADE, related_name="countries")
    state = models.OneToOneField(State, on_delete=models.CASCADE, related_name="states")
    city = models.OneToOneField(City, on_delete=models.CASCADE, related_name="cities")
    zip = models.CharField(max_length=30)
    phone = models.CharField(max_length=15)

    created_at = models.DateTimeField(auto_now_add=True, verbose_name="created at")
    updated_at = models.DateTimeField(auto_now=True, verbose_name="updated at")

    class Meta:
        verbose_name = "location"
        verbose_name_plural = "locations"
        db_table = "locations"
        ordering = ["zip"]

    def __str__(self):
        return self.name

    def get_absolute_url(self):
        return self.slug

The question I have is how do I save a model that has a foreign key. I want to save a profile and that profile’s location at the same time and how to do that is what I am after. And how do I get all the fields that I send from the front end to show up at the back end? Do I need to make two async calls to the backend simultaneously to two different endpoints, one saving the profile and one saving the location. Really new to all of this thanks

Get rid of your ProfileLocation and add a FK on Location that points to Profile. When you create a Profile with a Location the Location will be linked to the Profile because the Location record will have the Profile object as a FK. Look, I’m not trying to be a jerk but you probably would benefit from reviewing data modeling in general. It would definitely speed you along.

POSTing to profile with the appropriate body will take care of the necessary table entries if that’s what you were asking. You don’t need to make a separate request to a locations endpoint.

edit: should have said profile endpoint.

So you are saying that by simply adding a foreign key field on the location model that points to profile, will the data I send from the front end (formdata) used to create a location for that profile? How does that work?

How does the framework know it must create a location for the profile?

This is the data I send from the front end:

0: photo → ""
1: first_name → "undefined"
2: middle_name → "undefined"
3: last_name → "undefined"
4: birthdate → ""
5: gender → "0"
6: bio → ""
7: languages → ""
8: street → ""
9: additional → ""
10: country → ""
11: state → ""
12: city → ""
13: zip → ""
14: phone → ""
15: website → ""
16: user → "1"

Perhaps I confused things because I said the wrong location. In the case of creating a new profile and adding a location to it that would be 2 steps but they could potentially be handled in a single dynamic form. The Profile must exist for a Location to be related to. When you add a location you “declare” the profile to which it relates by storing the Profile object in the FK field of Location. That Profile object must already exist.
One more thing: It will not work to use a number for user unless you process the input after receiving it because a FK field expects an object not a str or int. You have probably noted that DRF shows foreign keys as html links. That’s because the FK is an object, one whose own endpoint is referenced by the url link shown. (You should probably make the FK relationship explicit. Not sure if OneToOneField does enough because I haven’t used it.)

Ok I could not understand how that would work.

I know you must create a profile first before you can add a location because the foreign key expects a profile object, which is why it would be something like this

new_profile = Profile.objects.create(request.data)
new location = Location.objects.create(profile=new_profile …)

I use the number so I can find the user object quickly
User.objects.get(pk=number from front end)

What I really needed to know is how can i create a profile and location in one single step using DRF views. After reading a bunch i feel I have to create the view as a custom class based view and remove the fields used for the location like

street = request.data.pop('street)
… and so on for every location field

then what’s left over use that to create the profile as in
new_profile = Profile.objects.create(request.data)

then creating the location with the removed data and use the new_profile to populate the profile foreign key?

Does this sound like the right way to do it?

If I’m understanding correctly the answer is yes. You save the profile fields to Profile and location fields to Location. You could just have the create view take you directly to the edit view upon creation.

—Edited to remove folly