Setting default value for Foreign Key attribute in Django
Asked Answered
B

10

111

What is the best way to set a default value for a foreign key field in a model? Suppose I have two models, Student and Exam with student having exam_taken as foreign key. How would I ideally set a default value for it? Here's a log of my effort

class Student(models.Model):
   ....
   .....
   exam_taken = models.ForeignKey("Exam", default=1)

Works, but have a hunch there's a better way.

def get_exam():
    return Exam.objects.get(id=1)
    
class Student(models.Model):
    ....
    .....
    exam_taken = models.ForeignKey("Exam", default=get_exam)

But this fails with tables does not exist error while syncing.

Any help would be appreciated.

Boilermaker answered 16/2, 2012 at 13:20 Comment(7)
how about: #938454Scorpaenid
@NitzanTomer It for a AdminModelField. Had seen it previously.Boilermaker
@TomIngram: default=get_exam() will call get_exam immediately and store the value permanently, whereas default=get_exam stores the method which would later be called each time the default attribute is used, to get the value at that moment. It's often used with datetime, i.e. default=datetime.now, not default=datetime.now().Figuration
@TomIngram: I'm not debating the merits of one approach over another. My point was only that it is valid, and the author seems to want it that way.Figuration
@ChrisPratt "This can be a value or a callable object. If callable it will be called every time a new object is created." so this means default=get_exam() and default=get_exam effectively is the same, unless you can re-call default?Malina
@TomIngram: The difference is that when you add the parenthesis, it's no longer a "callable"; it's a static value, no different that just putting an integer there.Figuration
@ChrisPratt hello I understand the difference etc, what I guess I'm struggling with is working out why. Giving it a bit of thought I'm thinking that as external states change perhaps the required default value will also and you would need to re-evaluate this value using the callback you passed, maybe I'm also over emphasising or misinterpreting default as to me it reads as something that should be relatively static, thanks it's been interesting and thought provoking for me at least :)Malina
W
32

As already implied in @gareth's answer, hard-coding a default id value might not always be the best idea:

If the id value does not exist in the database, you're in trouble. Even if that specific id value does exist, the corresponding object may change. In any case, when using a hard-coded id value, you'd have to resort to things like data-migrations or manual editing of existing database content.

To prevent that, you could use get_or_create() in combination with a unique field (other than id).

Here's one way to do it:

from django.db import models

 
class Exam(models.Model):
    title = models.CharField(max_length=255, unique=True)
    description = models.CharField(max_length=255)
    
    @classmethod
    def get_default_pk(cls):
        exam, created = cls.objects.get_or_create(
            title='default exam', 
            defaults=dict(description='this is not an exam'),
        )
        return exam.pk
    
    
class Student(models.Model):
    exam_taken = models.ForeignKey(
        to=Exam, on_delete=models.CASCADE, default=Exam.get_default_pk
    )

Here an Exam.title field is used to get a unique object, and an Exam.description field illustrates how we can use the defaults argument (for get_or_create) to fully specify the default Exam object.

Note that we return a pk, as suggested by the docs:

For fields like ForeignKey that map to model instances, defaults should be the value of the field they reference (pk unless to_field is set) instead of model instances.

Also note that default callables are evaluated in Model.__init__() (source). So, if your default value depends on another field of the same model, or on the request context, or on the state of the client-side form, you should probably look elsewhere.

Wolverhampton answered 2/7, 2020 at 13:48 Comment(12)
It doesn't work if U have not any migrations yet for me. I use field role for User, where I use get_default_user_role callback as default parameter like in your example. So if I have no migrations I got no such table: users_role on python manage.py makemigration. But if I comment role field in User model and run makemigrations - it is no problems. After what I uncomment role field and run migrations again with no problems. Maybe U can halp me with that?Ethyl
@parfeniukink: I would be happy to help, but it is a bit difficult to tell what is going wrong without seeing your actual code. Perhaps you could create a new Question with some more details?Wolverhampton
Oh, Thanks a lot, but I've finished this task )))Ethyl
I like the approach but get_or_create returns a tuple object and tuple have no pk ! exam[0] would have pk !Terpsichorean
@Terpsichorean If you look closely, the tuple is unpacked in the example: exam, created = cls.objects.get_or_create(...), so the first item is assigned to exam, the second item is assigned to created. Exactly like the example in the Django docs.Wolverhampton
You're correct, I have not followed (copy paste) your code 100% so I got different results, adding created make sense now. Thanks for share and the explanation..Terpsichorean
@Wolverhampton I Had one doubt about your answer. (This might be a silly question). Generally, we use parenthesis when calling a function in Python. But there isn't any parenthesis in (default=Exam.get_default_pk). Can you please explain why this is the case?Centerpiece
@DigvijayNikam Exam.get_default_pk (without parentheses) is a "reference" to the method itself: it does not call the method. Later, if you instantiate a Student() object, without specifying a value for the exam_taken field, it will call Exam.get_default_pk() (with parentheses) to determine the actual default value, at that time. Also see the example in the documentation.Wolverhampton
@Ethyl I've the same problem here.. how could you solve that?Ohare
@EbramShehata Yo! I did not use Django since 2021 I guess, so honestly I am not ready to tell you, buut you can provide more info about your case and I will try to figure it outEthyl
@EbramShehata I just verified that migrations can be created, for the example above, in a fresh project without any existing (custom) migrations. If you get "no such table" there's probably a dependency issue. If you need help with this, please post your code in a new question (and link here).Wolverhampton
I think it could be a bug in Django, I create a ticket here: code.djangoproject.com/ticket/35410Ohare
H
25

I would modify @vault's answer above slightly (this may be a new feature). It is definitely desirable to refer to the field by a natural name. However instead of overriding the Manager I would simply use the to_field param of ForeignKey:

class Country(models.Model):
    sigla   = models.CharField(max_length=5, unique=True)

    def __unicode__(self):
        return u'%s' % self.sigla

class City(models.Model):
    nome   = models.CharField(max_length=64, unique=True)
    nation = models.ForeignKey(Country, to_field='sigla', default='IT')
Hadden answered 14/8, 2018 at 2:18 Comment(0)
G
14

I use natural keys to adopt a more natural approach:

<app>/models.py

from django.db import models

class CountryManager(models.Manager):
    """Enable fixtures using self.sigla instead of `id`"""

    def get_by_natural_key(self, sigla):
        return self.get(sigla=sigla)

class Country(models.Model):
    objects = CountryManager()
    sigla   = models.CharField(max_length=5, unique=True)

    def __unicode__(self):
        return u'%s' % self.sigla

class City(models.Model):
    nome   = models.CharField(max_length=64, unique=True)
    nation = models.ForeignKey(Country, default='IT')
Gash answered 21/1, 2014 at 16:34 Comment(4)
I tried this in Django 1.6 but I get the error, "Invalid literal for int() with base 10: 'IT'. (My string is different, but you get the idea.)Riendeau
This worked fine, though, and seems more Pythonic: default=lambda: Country.objects.filter(sigla='IT').first()Riendeau
Strange, I remember to have tested this with django 1.6. Googling I found it's a tuple problem, try: def get_by_natural_key(self, sigla): return (self.get(sigla=sigla),) or also with default=('IT',). I'm just guessing ;)Gash
"Natural" is a problematic concept here. What you seem to be saying is that you're forcing the user to choose a five-character ID for each item, but that is not inherently more natural than a serial number.Haven
S
11

The issue with most of these approaches are that they use HARD CODED values or lambda methods inside the Model which are not supported anymore since Django Version 1.7.

In my opinion, the best approach here is to use a sentinel method which can also be used for the on_delete argument.

So, in your case, I would do

# Create or retrieve a placeholder
def get_sentinel_exam():
    return Exam.objects.get_or_create(name="deleted",grade="N/A")[0]

# Create an additional method to return only the id - default expects an id and not a Model object
def get_sentinel_exam_id():
    return get_sentinel_exam().id

class Exam(models.Model):
    ....
    # Making some madeup values
    name=models.CharField(max_length=200) # "English", "Chemistry",...
    year=models.CharField(max_length=200) # "2012", "2022",...

class Student(models.Model):
    ....
    .....
    exam_taken = models.ForeignKey("Exam",    
                       on_delete=models.SET(get_sentinel_exam),
                       default=get_sentinel_exam_id
                 )

Now, when you just added the exam_taken field uses a guaranteed existing value while also, when deleting the exam, the Student themself are not deleted and have a foreign key to a deleted value.

Smaltite answered 28/4, 2021 at 10:43 Comment(5)
This worked perfectly — I had a website admin where there were overall site settings, and one of them was the default language. I don't know in advance which languages will be available, and this enabled me to provide a default of "undefined".Milling
What about performance? It doesn't execute a database hit for every call?Heffernan
@Heffernan It's been some time that I used it, so this may be opinionated. Calling the get_sentinel_exam method executes a query whenever a referential action is invoked (ON DELETE in this case). So to answer your question - unless you don't delete a row, a second query is not performedSmaltite
@Smaltite Had one doubt in your answer. (This might be a silly question). Generally, we use parenthesis when calling a function in Python. But there isn't any parenthesis in (default=get_sentinel_exam_id). Can you please explain why this is the case?Centerpiece
@DigvijayNikam, been some time i worked in Python, but I would believe that this is a method reference. With parenthesis, you're asking for it to execute immediately, except when the model is actually used. The fallback exam id could change for some reason. (Somebody correct me if I'm mistaken)Smaltite
C
9

In my case, I wanted to set the default to any existing instance of the related model. Because it's possible that the Exam with id 1 has been deleted, I've done the following:

class Student(models.Model):
    exam_taken = models.ForeignKey("Exam", blank=True)

    def save(self, *args, **kwargs):
        try:
            self.exam_taken
        except:
            self.exam_taken = Exam.objects.first()
        super().save(*args, **kwargs)

If exam_taken doesn't exist, django.db.models.fields.related_descriptors.RelatedObjectDoesNotExist will be raised when a attempting to access it.

Choreograph answered 25/11, 2016 at 23:43 Comment(0)
E
4

You could use this pattern:

class Other(models.Model):
    DEFAULT_PK=1
    name=models.CharField(max_length=1024)

class FooModel(models.Model):
    other=models.ForeignKey(Other, default=Other.DEFAULT_PK)

Of course you need to be sure that there is a row in the table of Other. You should use a datamigration to be sure it exists.

Ethics answered 4/6, 2014 at 9:57 Comment(0)
S
1

You need to use get_or_create() in get_exam() and on_delete for models.ForeignKey() as shown below. *Don't forget to put .id just after get_or_create(id=1)[0] because default in models.ForeignKey() needs id of an Exam object otherwise there is an error and you can see my answer explaining about get_or_create():

def get_exam():        # Here                # ↓ Don't forget
    return Exam.objects.get_or_create(id=1)[0].id

class Student(models.Model):
    # ...
    exam_taken = models.ForeignKey(
        "Exam", 
        default=get_exam, 
        on_delete=models.CASCADE # Here
    )

And, you can put get_exam() in Student class as shown below:

class Student(models.Model):
    # ...
    def get_exam(): # Here
        return Exam.objects.get_or_create(id=1)[0].id

    exam_taken = models.ForeignKey(
        "Exam", 
        default=get_exam, 
        on_delete=models.CASCADE
    )

I recommend to use @classmethod for get_exam() in Student class as shown below so that other classes can also use get_exam() by class name. *@classmethod can do more things than @staticmethod according to my answer:

class Student(models.Model):
    # ...
    @classmethod # Here
    def get_exam(cls):
        return Exam.objects.get_or_create(id=1)[0].id

    exam_taken = models.ForeignKey(
        "Exam", 
        default=get_exam, 
        on_delete=models.CASCADE
    )

And instead of get_exam, you can assign get_exam() to default as shown below:

def get_exam():
    return Exam.objects.get_or_create(id=1)[0].id

class Student(models.Model):
    # ...
    exam_taken = models.ForeignKey(
        "Exam", # ↓ Here ↓
        default=get_exam(),
        on_delete=models.CASCADE
    )
class Student(models.Model):
    # ...
    def get_exam():
        return Exam.objects.get_or_create(id=1)[0].id

    exam_taken = models.ForeignKey(
        "Exam", # ↓ Here ↓
        default=get_exam(), 
        on_delete=models.CASCADE
    )
Spit answered 11/5, 2023 at 7:36 Comment(0)
P
0

I'm looking for the solution in Django Admin, then I found this:

class YourAdmin(admin.ModelAdmin)

    def get_changeform_initial_data(self, request):
        return {'owner': request.user}

this also allows me to use the current user.

see django docs

Padegs answered 3/4, 2020 at 3:32 Comment(0)
C
0

the best way I know is to use lambdas

class TblSearchCase(models.Model):
    weights = models.ForeignKey('TblSearchWeights', models.DO_NOTHING, default=lambda: TblSearchWeights.objects.get(weight_name='value_you_want'))

so you can specify the default row..

default=lambda: TblSearchWeights.objects.get(weight_name='value_you_want')
Cioffred answered 12/8, 2020 at 8:24 Comment(2)
Error: Cannot serialize function 'lambda'Voyeurism
that was Django 2, I'm not sure about 3 or 4.Cioffred
W
0

I'm adding something on top of @djvg answer and providing some illustration to @Super Kai one's concerning the class method way of solving this problem:

Using a class method is also useful in this case, and probably in a lot of other cases, because you may have to add other ForeignKey relationships to your model later on like so:

from django.db import models


class Exam(models.Model):
    title = models.CharField(max_length=255, unique=True)
    description = models.CharField(max_length=255)

    @classmethod
    def get_default_pk(cls):
        exam, created = cls.objects.get_or_create(
            title='default exam', 
            defaults=dict(description='this is not an exam'),
        )
        return exam.pk


class Student(models.Model):
    exam_taken = models.ForeignKey(
        to=Exam, on_delete=models.CASCADE, default=Exam.get_default_pk
    )

# NEW
class Teacher(models.Model):
        exam_given = models.ForeignKey(
            to=Exam, on_delete=models.CASCADE, default=Exam.get_default_pk
        )

In my case, I came here to figure out how to provide a default image to a Photo model whom I wanted to be used by several other models that would also use that default image like so:

class Photo(models.Model):

    title = models.CharField(max_length=200, unique=True)
    image = models.ImageField(upload_to='photos',
        default="default_photo_folder/default_image")

    @classmethod
    def get_or_create_default_photo_pk(cls):
        obj, created_bool = cls.objects.get_or_create(
            title='default photo', # ↓ title value will be automatically included in defaults ↓
            defaults=dict(image="default_photo_folder/default_image"),
            )
    return obj.pk

------------------------------------------------------------------
# From photoapp.models import Photo

class Event(models.Model):
    photo = models.ForeignKey(Photo,
                        on_delete=models.SET_DEFAULT,
                        related_name='event_photos',
                        default=Photo.get_or_create_default_photo_pk)

class Place(models.Model):
    photo = models.ForeignKey(Photo,
                        on_delete=models.SET_DEFAULT,
                        related_name='place_photos',
                        default=Photo.get_or_create_default_photo_pk)

Please note that in this example I already have a default photo in my media/default_photo folder. I also used models.SET_DEFAULT to set my default photo as the object's photo when its original photo would be deleted. As stated in the doc, I also used a unique field lookup in my get_or_create() method to avoid extra headache .

Washtub answered 5/10, 2023 at 7:51 Comment(0)

© 2022 - 2025 — McMap. All rights reserved.