3 Mistakes That I Find in Most Django-Based Projects by Sebastian Opałczyński Apr, 2021 Better Programming
3 Mistakes That I Find in Most Django-Based Projects by Sebastian Opałczyński Apr, 2021 Better Programming
+",,"-'.-%/-0112&/
<,*+2$(D)%(6%)1%"??$%'F 56$&(+&("66 B$#('#"%#$,
9)::);
G)(%$'6)&,(#)(#A+'('#)%-H
56$&(+&("66
!"#$%&'()%"*+'&","-$./"$."#0%&"12'.3045'%)/
1$#(#A$(D%$$(I$,+.?("66F
6702)8&%
!"##$%&'(#)("*)+,(+&(-).%(/0"&1)(2),$(3"'$'
4$3"'#+"&(56"ł27-ń'8+ 9)::);
<6%(=(·(>(?+&(%$",
4$$(&)($*+:(@(6A)#)(3-(B$)%1$(C$28$%(D%)?(!$E$:'F
I am old enough to remember working with version 1.3 of the Django framework. It was
2011 and I was just getting started with professional web development in the Python
ecosystem. Later, the framework matured (it’s already 3.2), and I matured as well.
During this time, I probably worked on more than 50 projects where Django was used as
the main framework. That’s because it’s a good framework that enables developers to
deliver features faster, with good tooling, ORM, and a really great community.
Nevertheless, I am still Lnding solutions in Django-based code bases that are simply bad
or unacceptable despite the fact that we even have anti-patterns in Django web pages
now. In this article, I would like to discuss a few of them to help developers understand
what I mean — especially the young ones.
.-%3"4,'5",6$
If you would like to follow along with the code, here is how to set up the project. You
can also skip this part and go to the next section.
Run:
Add the articles app to the INSTALLED_APPS in your Django settings Lle, create the
migrations, and apply them.
We are all set. The rest of the code for the experiments will be provided in the sections
below.
78'92:";'<=%24"#'+0#";'%&'>%;"?
Sometimes, you would like to use a custom form in Django to handle diVerent logic or
UX. Generally, you end up in a situation where ModelForm is not an option. This is
where the fun begins. In the model above’s deLnitions, you can see that the Article
instance can have multiple tags assigned via the ManyToManyField . I’ve simpliLed it on
the form level to allow only one tag per article to show you something.
AddArticleForm has a ChoiceField with custom choices that is taken from the Tag
model explicitly, and the list is built there. On the page, it looks more or less like this:
<,,(&$;("%#+2:$(D)%?
I’ve deLned a new tag in the admin panel to register the above models there. You need
to do the following in your articles/admin.py :
Now, why is this bad? Well, when you now add a new tag in the admin panel (or the
other way around), the form will not see it until you restart the development server (or
any other, if this hits other environments for some reason). And why is that? Such code
is executed on the class deLnition level, which means it will be done when we import
the form AddArticleForm and those choices will live there forever until the application
server reloads.
It seems a little bit counterintuitive, and I think the main reason for that is developers
still have issues with fully understanding how ORM and Python work. Another reason is
that developers are used to putting something in choices , like choices=[("EUR",
"EURO"), ("USD", "DOLLAR")] , and it makes perfect sense in this case (as we do not
expect this to be changed often). But with entities that are stored on the DB level, it is a
diVerent story.
There’s one more — less obvious — problem with this approach (choices built from
model instances on the class level): It will most likely break your initial project setup, as
it will require you to have Tag already in the database. With minimal eVort, a bad
developer can prevent that new developer from joining without having the database
dump.
You would be surprised by how many variations of this issue I’ve found in existing,
serious, production-ready code bases.
)8'@/&%-2&/'#"?"4,A-"?0,";'0&;'$-"B",4=A-"?0,";
To set up the playground here, open a Django shell ( python manage.py shell ) and
import the Article and Tag model ( from articles.models import Article, Tag ).
import logging
l = logging.getLogger('django.db.backends')
l.setLevel(logging.DEBUG)
l.addHandler(logging.StreamHandler())
So we have one SELECT query and printed the content attribute for each Article
instance.
Now, what will happen if we want to access the tags attribute? Yes, you are right. We
will have additional queries for each for loop:
As you can see here, we Lrst have SELECT to fetch all of the articles from the database.
Later in each for loop, we have an additional query to Lnd the Lrst tag.
I had only two articles and two tags in this example, but now imagine that someone is
operating on thousands of entries. As a backend developer, you should always try to
keep the number of queries minimal. It’s an I/O operation that is costly and will
in_uence the application’s performance.
There are tools out there that will help you to understand this part (e.g. django-silk).
C8'D%,'E&%F2&/'G=0,'6$;0,"AB2"?;#'@#'9%-
It’s simple. It will update only the Lelds on the model that are speciLed on the list. For
example:
article.save(update_fields=["content"])
Where is it useful? Well, having concurrent writes of the same object is normal in web
development. It’s not a huge issue when you always do full saves. Then the latest one
wins, but what is important here is that the two same objects can be in a totally diVerent
state on diVerent nodes.
Imagine that the Lrst node of your application fetched the object from the database and
then transformed it to a Python object using ORM. Then it has some time-consuming
operations, and on the Lnish, it saves the modiLed object to the database. In this time-
consuming window, the second node fetched the object and also loaded it to the Python
object. And the second node is doing the save slightly after the Lrst node. Then the
changes done on the Lrst node will be overwritten, and this is a case where
update_fields save us (there are also diVerent techniques to handle that).
update_fields will be especially helpful when the Lrst and second nodes are doing
totally diVerent things with the object and are interested in diVerent attributes (but you
need to think about it and design the system like this).
This is particularly important when you have places in the code where you simply
update a single Leld. The status is a good example. If no other Lelds are updated
during the process, it should be:
instance.save(update_fields=["status"])
As such Lelds often store important business logic, it’s even more important not to lose
it.
52/&'6$'B%-'H="'+"#,'%B'+",,"-'.-%/-0112&/
C-(C$##$%(!%)1%"??+&1
<(;$$8:-(&$;':$##$%('$&#($*$%-(9%+,"-(;+#A(#A$(3$'#("%#+2:$'(;$(6.3:+'A$,(#A"#(;$$8F(O),$(#.#)%+":'H
",*+2$H(2"%$$%()66)%#.&+#+$'H("&,(?)%$P(G"8$("(:))8F
J).%($?"+:
B$#(#A+'(&$;':$##$%
C-('+1&+&1(.6H(-).(;+::(2%$"#$("(I$,+.?("22).&#(+D(-).(,)&Q#(":%$",-(A"*$()&$F(R$*+$;().%(!%+*"2-(!):+2-(D)%(?)%$(+&D)%?"#+)&
"3).#().%(6%+*"2-(6%"2#+2$'F
LMN L
SRTGGUV(CJ
5"I0#,20&'J$0ł4KLń#M2 9)::);
4)D#;"%$($&1+&$$%H(#$2A&):)1-($&#A.'+"'#H(2)??)&('$&'$(:)*$%H('$%+":(2)KD).&,$%H("&,("(D"#A$%()D
#;)F
+",,"-'.-%/-0112&/ 9)::);
<,*+2$(D)%(6%)1%"??$%'F
>%-"'9-%1'>";261
N%&O,'P#"'@BQR?#"'0&;'5F2,4='2&'S0T054-2$,U'P#"'JI3"4,
V2,"-0?#
W"28(G"-:)%(+&(C$##$%(!%)1%"??+&1
7W'P#"B6?'.L,=%&'5&2$$",#'H%'<%;"'V2M"'0'.-%
<%##.%+(W"::+(+&(C$##$%(!%)1%"??+&1
X'>2#,0M"#'@'>0;"'0#'0'.-%/-011"-U'I6,'@'Y0;'H%
+"4%1"'0'<HJ'H%'5""'H="1
W"8.3(Bó%);'8+(+&(C$##$%(!%)1%"??+&1
7Z'!"0?2#,24'Y0I2,#'H%'@1$-%T"'5%B,F0-"'N"T"?%$1"&,
X$'8(KYK(+&(C$##$%(!%)1%"??+&1
H%$'['52/&#'%B'0'Y2/=?L'R:$"-2"&4";'5%B,F0-"'R&/2&""-
Z)8"0+#(G+8"-"#%"-(+&(C$##$%(!%)1%"??+&1
7W'S0T054-2$,'5&2$$",#'B%-'<?"0&"-QV%%M2&/'<%;"
<%##.%+(W"::+(+&(C$##$%(!%)1%"??+&1
Y%F'\%2&/'+04M',%'<%;2&/']B,"-'7W'^"0-#']?1%#,
<-6#=";'>"
/).1(9))(+&(C$##$%(!%)1%"??+&1
C'5""12&/?L'521$?"'.L,=%&'9"0,6-"#'H=0,'<%&B6#"
+"/2&&"-#
J)&1(O.+(+&(C$##$%(!%)1%"??+&1
B$#(#A$(I$,+.?("66