Best practices working with Django models in Python
1. Correct Model Naming
It is generally recommended to use singular nouns for model naming, for example: User
, Post
, Article
. That is, the last component of the name should be a noun, e.g.: Some New Shiny Item. It is correct to use singular numbers when one unit of a model does not contain information about several objects.
2. Relationship Field Naming
For relationships such as ForeignKey
, OneToOneKey
, ManyToMany
it is sometimes better to specify a name. Imagine there is a model called Article
, - in which one of the relationships is ForeignKey
for model User
. If this field contains information about the author of the article, then author
will be a more appropriate name than user
.
3. Correct Related-Name
It is reasonable to indicate a related-name in plural
as related-name addressing returns queryset. Please, do set adequate related-names. In the majority of cases, the name of the model in plural will be just right. For example:
class Owner(models.Model):
pass
class Item(models.Model):
owner = models.ForeignKey(Owner, related_name='items')
4. Do not use ForeignKey with unique=True
There is no point in using ForeignKey
with unique=True
as there exists OneToOneField
for such cases.
5. Attributes and Methods Order in a Model
Preferable attributes and methods order in a model (an empty string between the points).
- constants (for choices and other)
- fields of the model
- custom manager indication
meta
def __unicode__
(python 2) ordef __str__
(python 3)- other special methods
def clean
def save
def get_absolut_url
- other methods
Please note that the given order was taken from documentations and slightly expanded.
6. Adding a Model via Migration
If you need to add a model, then, having created a class of a model, execute serially manage.py
commands makemigrations
and migrate
(or use South
for Django 1.6 and below).
7. Denormalisations
You should not allow thoughtless use of denormalization in relational databases. Always try to avoid it, except for the cases when you denormalise data consciously for whatever the reason may be (e.g. productivity). If at the stage of database designing you understand that you need to denormalise much of the data, a good option could be the use of NoSQL. However, if most of data does not require denormalisation, which cannot be avoided, think about a relational base with JsonField to store some data.
8. BooleanField
Do not use null=True
or blank=True
for BooleanField
. It should also be pointed out that it is better to specify default values for such fields. If you realise that the field can remain empty, you need NullBooleanField
.
9. Business Logic in Models
The best place to allocate business logic for your project is in models, namely method models and model manager. It is possible that method models can only provoke some methods/functions. If it is inconvenient or impossible to allocate logic in models, you need to replace its forms or serializers in tasks.
10. Field Duplication in ModelForm
Do not duplicate model fields in ModelForm
or ModelSerializer
without need. If you want to specify that the form uses all model fields, use MetaFields. If you need to redefine a widget for a field with nothing else to be changed in this field, make use of Meta widgets to indicate widgets.
11. Do not use ObjectDoesNotExist
Using ModelName.DoesNotExist
instead of ObjectDoesNotExist
makes your exception intercepting more specialised, which is a positive practice.
12. Use of choices
While using choices
, it is recommended to:
- keep strings instead of numbers in the database (although this is not the best option from the point of optional database use, it is more convenient in practise as strings are more demonstrable, which allows the use of clear filters with get options from the box in REST frameworks).
- variables for variants storage are constants. That is why they must be indicated in uppercase.
- indicate the variants before the fields lists.
- if it is a list of the statuses, indicate it in chronological order (e.g.
new
,in_progress
,completed
). - you can use
Choices
from themodel_utils
library. Take modelArticle
, for instance:
from model_utils import Choices
class Article(models.Model):
STATUSES = Choices( (0, 'draft', _('draft')), (1, 'published', _('published')) ) status = models.IntegerField(choices=STATUSES, default=STATUSES.draft)
…
13. Why do you need an extra .all()?
Using ORM, do not add an extra method call all
before filter()
, count()
, etc.
14. Many flags in a model?
If it is justified, replace several BooleanFields
with one field, status
-like. e.g.
class Article(models.Model):
is_published = models.BooleanField(default=False)
is_verified = models.BooleanField(default=False)
…
Assume the logic of our application presupposes that the article is not published and checked initially, then it is checked and marked is_verified
in True
and then it is published. You can notice that article cannot be published without being checked. So there are 3 conditions in total, but with 2 boolean fields we do not have 4 possible variants, and you should make sure there are no articles with wrong boolean fields conditions combinations. That is why using one status field instead of two boolean fields is a better option:
class Article(models.Model):
STATUSES = Choices('new', 'verified', 'published')
status = models.IntegerField(choices=STATUSES, default=STATUSES.draft)
…
This example may not be very illustrative, but imagine that you have 3 or more such boolean fields in your model, and validation control for these field value combinations can be really tiresome.
15. Redundant model name in a field name
Do not add model names to fields if there is no need to do so, e.g. if table User
has a field user_status
- you should rename the field into status
, as long as there are no other statuses in this model.
16. Dirty data should not be found in a base
Always use PositiveIntegerField
instead of IntegerField
if it is not senseless, because “bad” data must not go to the base. For the same reason, you should always use unique
,unique_together
for logically unique data and never use required=False
in every field.
17. Getting the earliest/latest object
You can use ModelName.objects.earliest('created'/'earliest')
instead of order_by('created')[0]
and you can also put get_latest_by
in Meta
model. You should keep in mind that latest
/earliest
as well as get
can cause an exception DoesNotExist
. Therefore, order_by('created').first()
is the most useful variant.
18. Never make len(query set)
Do not use len
to get queryset’s objects amount. The count
method can be used for this purpose. Like this: len(ModelName.objects.all())
, firstly the query for selecting all data from the table will be carried out, then this data will be transformed into a Python object, and the length of this object will be found with the help of len
. It is highly recommended not to use this method as count
will address to a corresponding SQL function COUNT()
. With count
, an easier query will be carried out in that database and fewer resources will be required for python code performance.
19. if queryset is a bad idea
Do not use queryset as a boolean value: instead of if queryset: do something
use if queryset.exists(): do something
. Remember, that querysets are lazy, and if you use queryset as a boolean value, an inappropriate query to a database will be carried out.
20. Using help_text as documentation
Using model help_text
in fields as a part of documentation will definitely facilitate the understanding of the data structure by you, your colleagues, and admin users.
21. Money Information Storage
Do not use FloatField
to store information about the quantity of money. Instead, use DecimalField
for this purpose. You can also keep this information in cents, units, etc.
22. Don't use null=true if you don't need it
null=True - Allows column to keep
null
value.
blank=True - Will be used only if Forms for validation and not related to the database.
In text-based fields, it's better to keepdefaultvalue.
blank=True
default=''
This way you'll get only one possible value for columns without data.
23. Remove _id
Do not add _id
suffix to ForeignKeyField
and OneToOneField
.
24. Define __unicode__ or __str__
In all non-abstract models, add methods __unicode__
(python 2) or __str__
(python 3). These methods must always return strings.
25. Transparent fields list
Do not use Meta.exclude
for a model’s fields list description in ModelForm
. It is better to use Meta.fields
for this as it makes the fields list transparent. Do not use Meta.fields=”__all__”
for the same reason.
26. Do not heap all files loaded by user in the same folder
Sometimes even a separate folder for each FileField
will not be enough if a large amount of downloaded files is expected. Storing many files in one folder means the file system will search for the needed file more slowly. To avoid such problems, you can do the following:
def get_upload_path(instance, filename):
return os.path.join('account/avatars/', now().date().strftime("%Y/%m/%d"), filename)
class User(AbstractUser):
avatar = models.ImageField(blank=True, upload_to=get_upload_path)
27. Use abstract models
If you want to share some logic between models, you can use abstract models.
class CreatedatModel(models.Model):
created_at = models.DateTimeField(
verbose_name=u"Created at",
auto_now_add=True
)
class Meta:
abstract = True
class Post(CreatedatModel):
...
class Comment(CreatedatModel):
...
28. Use custom Manager and QuerySet
The bigger project you work on, the more you repeat the same code in different places.
To keep your code DRY and allocate business logic in models, you can use custom Managers and Queryset.
For example. If you need to get comments count for posts, from the example above.
class CustomManager(models.Manager):
def with_comments_counter(self):
return self.get_queryset().annotate(comments_count=Count('comment_set'))
Now you can use:
posts = Post.objects.with_comments_counter()
posts[0].comments_count
If you want to use this method in chain with others queryset methods,
you should use custom QuerySet:
class CustomQuerySet(models.query.QuerySet):
"""Substitution the QuerySet, and adding additional methods to QuerySet
"""
def with_comments_counter(self):
"""
Adds comments counter to queryset
"""
return self.annotate(comments_count=Count('comment_set'))
Now you can use:
posts = Post.objects.filter(...).with_comments_counter()
posts[0].comments_count