This content originally appeared on DEV Community and was authored by Max Core
Ten Do-Nots
- Do not relay on ORM feature to create multiple models with the same name, controlling its’ table and relations naming.
- Do not rush into
is_deletedfield withon_delete=PROTECTfor all models, especially overriding manager’s methods likeall,filter, etc. - Do not allow duality in filtering.
- Do not ignore the convenience of using the database directly, especially for the sake of technical savings.
- Do not follow specific ORMs’ best practices, follow common best practices.
- Do not build algorithms tied on db-records.
- Do not do anything in the database that is not reflected in the code.
- Do not be shy to denormalize to stay in domain logics and not finish with CQRS.
- Do not edit migrations without an extreme exceptional need.
- Do not use DBMS’ specific features if you allow/assume vendor changes, on-board delivery, or testing using common universal practices (in-memory SQLite).
How-Tos
I. How to name models and db-tables?
Django allows create multiple models with the same class name with db-table names prefixed with app name,
like users.Group —> users_group, posts.Group —> posts_group, etc.
It’s ok in db, but it brings confusion in the code.
It also makes model migration between apps much harder.
So,
- Give model name an app prefix (if needed, or potentially would be). In the example above — better names would be
UserGroupandPostGroup. - Specify db-table name explicitly in
Meta— just translate its’ name to the snake-case. - Describe M2M fields in ‘secondary-table’. Bidirectional solution is also possible: https://stackoverflow.com/a/9341455/4117781.
- Give FKs and M2Ms fields logical names, not technical, e.g.:
author, notuser. Tech name is for explicit m2m-tables describings. - For m2m-fields: use pattern
model_name__field_name, since 2 tables can have an m2m relation for different purposes. - In case separate m2m-table with
through, specify tables’ name inMetawith the same patternmodel_name__field_name. - Always specify
default_related_namein models’Meta— just its’ name in snake case in plural form. - In case default can not be used (multiple FK or M2M) specify fields’
related_nameusing patternfield_name_model_name_plural. - Specify
db_table_comment,verbose_name_plural,default_related_nameinMeta.
Complex example following all steps above:
class Post(models.Model):
author = models.ForeignKey(User)
liked = models.ManyToManyField(User, db_table='post__liked', related_name='liked_posts')
shared = models.ManyToManyField(User, db_table='post__shared', related_name='shared_posts')
viewed = models.ManyToManyField(User, throught=PostViewed, related_name='viewed_posts')
class Meta:
db_table = 'post'
db_table_comment = 'Post'
verbose_name = 'Post'
verbose_name_plural = 'Posts'
default_related_name = 'posts'
class PostViewed(models.Model):
user = models.ForeignKey(User)
post = models.ForeignKey(Post)
class Meta:
db_table='post__viewed'
class Comment(models.Model):
author = models.ForeignKey(User)
post = models.ForeignKey(Post)
text = models.TextField()
class Meta:
db_table='post_comment'
db_table_comment = 'Post Comment'
verbose_name = 'Post Comment'
verbose_name_plural = 'Post Comments'
default_related_name = 'comments'
Notice, that m2m-table need only
db_tableto be specified.
Also, technicallyComment— is also an ‘m2m-table’ betweenUserandPost, but we see it as a separate logical entity, with an another way of access that relation.
II. How to manage deletion of an objects?
The worst practice, as it was mentioned earlier:
Do not rush into
is_deletedfield withon_delete=PROTECTfor all models, especially overriding manager’s methods likeall,filter, etc.
- Sometimes garbage is garbage.
- And there are could also testing purposes, when we have to add and clean some records, even if they considered critical in production.
-
is_deletedbrings no semantic load, it could beis_archived,is_published. -
on_delete=CASCADEwill also clear some historical records we could need later.
So, it could depend on case, but common good practive would be:
class Task(models.Model):
subtask = models.ForeignKey(Task, null=True, on_delete=models.SET_NULL)
is_active = models.BooleanField(default=False, db_default=False)
And querying it simply like:
tasks = Task.objects.filter(is_active=True)
III. How to describe field with default?
- If it has
default, it should also havedb_default.
IV. How to describe a boolean field?
- Always set
default, nevernull=True. - Also set
db_default.
So, in minimal it’s like:
is_active = models.BooleanField(default=False, db_default=False)
IV. How to describe a datetime field?
default=timezone.now — if we want date to be prepolated when page/form is rendered.
auto_now_add=True — if we want date to be prepolated when page/form is submitted, and not managed by any user (even admin).
auto_now=True — if we want date to be updated each time on form edited (also could not be managed by anyone).
V. How to describe varchar and text fields?
There is also a recommendation in Django docs:
“Avoid using null on string-based fields such as CharField and TextField. The Django convention is to use an empty string, not NULL”
https://docs.djangoproject.com/en/5.2/ref/models/fields/#null
Looks ok for TextField, but very rarely actually needed, so let’s omit it.
But it’s totally wrong for CharField in practice.
CharFields are either required (title, slug, username, email), either are storing fixed values via TextChoices, either they are just storing notes, that we are not interesting in filtering/analysing.
So it’s completely safe stay with null=True.
- Avoid
default=''forCharField(and probablyTextField), stay withnull=True. - It’s ok to set
max_length=255if contents in unclear.
So, that’s completely legal:
text = models.CharField(max_length=255, null=True, blank=True)
VI. How to manage and describe fields with choices like statuses?
Do not build algorithms tied on db-records.
Do not ignore the convenience of using the database directly, especially for the sake of technical savings.
- Use
TextChoicesoverIntegerChoicesin most common cases. - Name choices class in plural.
- Make values uppercase to make it clear that this is not just a regular string with any possible value.
- Describe choices class inside model unless it is used in other models’ choices.
So, this:
class MyModel(models.Model):
class Statuses:
CREATED = 'CREATED', 'Created'
COMPLETED = 'COMPLETED', 'Completed'
status = models.CharField(max_length=20, choices=Statuses, default=Statuses.CREATED, db_default=Statuses.CREATED)
Is better than this:
class Status:
CREATED = 1, 'Created'
COMPLETED = 2, 'Completed'
class MyModel(models.Model):
status = models.SmallPositiveIntegerField(choices=Status)
Worst is:
class Status(models.Model):
name = models.ChatField(max_length=255)
class MyModel(models.Model):
status = models.ForeignKey(Status)
Same for rights/permissions.
This content originally appeared on DEV Community and was authored by Max Core