Django Views – The Right Way
spookylukey.github.io
spookylukey.github.io
But either way if your views are getting complicated you're doing it wrong. Views should just be plumbing to glue together the data you want for the template. All your business logic should be methods on your models, and if those are getting too complex they should be importing separate modules. The views should mostly be boilerplate.
The reason everything should be on the model (or atleast a separate module if you're a functional zealot) is so you can have your same logic available in your async queue, the api, cron jobs, and the shell for troubleshooting. If you have any logic tied up in the view it's a huge pain to access from anywhere else. And I don't know about you, but I always end up needing the logic somewhere else.
Complications probably come from coordinating more models. So you introduce a Service layer. So Models house logic specific to handling the data within that Model. And Services can orchestrate across multiple Models, or Multiple Services. But you try to keep it as a DAG.
There's always room for pragmatic compromise though...
I've also found that it's good practice to impose boundaries between apps so that they are not strongly coupled together but communicate through an interface. That way, if you need to split an app out into its own project at some point, the migration is significantly easier from a code perspective, since the implementation of the interface function is all that needs changing on the consumer side.
>in practice, changing your data model in some way is actually pretty common
I find this to be very true on some projects and very not true on others.
Where it is true the abstraction would be beneficial but if you assume all projects have this problem and therefore jam the abstraction into a project that doesn't have it you're just creating overhead.
For e.g. say you have a User model and a corresponding “Profile” model. If you need to create the profile object when the user is created, it doesn’t really feel appropriate to put that in a UserModelManager directly coupled to the User object. This is the example given from the Hacksoft Django style guide to advocate creating a service layer.
In terms of the abstraction, I totally agree and tend to be pretty pragmatic about this sort of thing.
They're definitely necessary though, most of what we were doing didn't map cleanly to a single model/manager so it would have been a confusing mess to shoehorn it into there.
CBVs are ok for simple use cases, but the fact that FBVs work fine for both suggests to me that CBVs are probably better not used at all.
>you're doing it wrong
Over the years I've come to view the frequent appearance of this phrase when discussing the merits of any kind of tech, process or abstraction as a red flag that something nonobvious is badly wrong with it.
With CBVs I think there is a slight impedance mismatch between classes and views - not quite enough to cause trouble with simple use cases but enough to create an ugly mess when view complexity ramps up.
But in my experience, FBVs become a mess even if they're relatively simple, because of the temptation to keep it all within a single function. If you take the time to break things down into separate functions and try to keep it dry, while passing around the related objects then FBVs are ok, but then you're basically recreating classes.
In any case, my point is that function or class views should never be complicated, they should just be taking a request, and returning an appropriate object. If the process for creating the appropriate object is complex, that should be handled outside the view.
Oh and my use of the phrase "you're doing it wrong" is kind of tongue in cheek, and comes from my time doing tech support in my early 20s, where we all thought it was hilariously unhelpful. I agree it's a bit obnoxious, I almost edited it out last night. But I made myself giggle enough that I left it in.
No, not really. Multiple layers of inheritance naturally causes code to lose cohesion no matter how familiar you are with it.
It's one of the reasons for the maxim "prefer composition over inheritance".
>But in my experience, FBVs become a mess even if they're relatively simple, because of the temptation to keep it all within a single function.
If calling functions or classes from within functions is a struggle I dare say you might have bigger problems than whether to use FBVs or CBVs.
Ninja looks neat, though.
Django ninja also is build with async in mind fron the start so immediately opens up possibilities that are not possible with DRF.
Seems like as of 4mo ago the maintainers of DRF added async support via a separate module.
But it's actually quite good and I came to similar conclusions after trying to make CBV work for years.
Plus it's well written.
Damn.
The Django fat models and (lack of) code structure in the Django project I'm working on fixing is painful for me!
Here's another opinionated Django guide: https://github.com/HackSoftware/Django-Styleguide if anyone's interested
It was a refreshing approach rather than tracking down logic scattered between models, views, managers etc.
For very simple projects it can add a bit of bloat/boilerplate but as complexity grows it's great to have reusable selectors & service functions to use.
Since using such an IDE I pretty much never have to rely on the documentation because it’s much easier to Cmd+click into a symbol and look at its implementation.
People like to think CBV offer a lot of free things but they miss the trade off. CBV makes it easier to write code but harder to read the code. CBV should only be used for the simplest of cases IMO.
Class based views for DRF are awesome because they represent a resource. It has been a great experience writing REST APIs with class based views, and it reduces boilerplate with a tolerable amount of implicit logic.
But as soon as I need to render a simple html page from a request, I share the frustration of the author.
The DRF Generic ViewSets and Views provide maximum magic plus they also shift part of the creation process into Model Serializers. Doing anything custom with this setup involves overriding bunch of methods and chasing their original definitions through an handfull of mixins.
The only thing beyond that that I'd recommend is specifying a get_serializer_class method since it simplifies your boilerplate and plays well with drf-spectacular for generating your API documentation. I generally don't use ModelSerializer unless the logic is very simple, I've been meaning to write a blog post for a long while about avoiding it when your data model is split across two database tables.
I found greater success and clarity with the @property and its cached brother @cached_property decorators. Using these two, you can access the view instance and its properties via just, e.g., 'view.foo' in the template.
@cached_property from what I understand is in fact originally a Django creation too (or at least its first popularising use https://github.com/python/cpython/issues/65344).
in my personal experience this can also happen when using class based. It depends on how much your application is "CRUD-y"
with a CRUD, it's easier to go CBV and have minimal branching, and use the different HTTP verbs based on the CRUD operations.
but sometimes you have apps where you have a bunch of different operations, and maybe all of those operations are just types of updates to the same type "Foo".
in that case, FBVs work best, because you can just do
def update_foo_expiry()
def renew_foo()
def rename_foo()
etc. etc. - all of the above would have ended up being if/elif/else in a put() method in a CBV
FBV are superior in most cases. Less magic, just code.
CBV are better when you have complex operations. Not everything is just CRUD, and sometimes you do need a long POST method and a long GET. Sometimes the two share a bunch of functionality that makes sense to break out into its own function(s) but that functionality clearly belongs in the view layer.
Recently I worked on a system where my models all shared a fairly complex system of being versioned and having start_on and ends_on dates to make them temporal. In fact each concept had two models: a root that most other models refer to and a version. While the dozen models were all different the logic of processing creating and updating these models instances was the same between all of them but each model had its own unique additional rules for how to process them. So I created a pair of base CBV (one for a listing and one for a single object) that implemented CRUD methods for them and then each object got its own CBV pair based on those. I tried this with FBV and it was much messier, especially with form validation shared between POST and PUT. Sometimes a complicated setup like this requires CBV to create neat and concise code (relatively speaking).
On moving logic to the model layer: my litmus test for this is whether the logic is used more than once. I’d you have a view that fetches model instances based on a complex filter (dates, user permissions, etc.), but it literally is the only one of its kind, why spread logic out though model code? But if you have either multiple views that do this, or more likely views that share logic with management commands and/or background tasks then it absolutely makes sense.
Service layers leak abstractions. Django doesn’t support this as a first class citizen, so why should you? You won’t be able to switch to a different framework just by rewriting what your service layer talks to so just don’t do it.
DRF is a CPU hog even at mild loads. I use a home grown model-to-JSON serializer that is quite a bit faster and Django forms to validate input.
It’s not scalable long-term, but if your objective is to either slowly migrate those views to true Django models, or for few, specific tables, then I think it’s a good workaround.
But Django is definitely opinionated on things
> The Django ORM is close to useless if your database is not generated by the web app
to which I replied
> not in my experience
I have a legacy database derived from an outside source which is running in production and all my code interacts with it via the Django ORM.
And I'm sure the Django ORM works fine for you if you're only doing read operations and the legacy database schema never migrates.
In out case we don't write to the same tables but that's only because data is overwritten nightly for the tables with an external source. Assuming a situation where that wasn't happening, writing data wouldn't be a problem.
As for schema changes - you just have to decide who is in charge of the schema. In this case the authoratative source would be outside of Django so you just set those tables to managed=False - the only manual task is to update your models.py when the external schema changes.
See here: https://docs.djangoproject.com/en/4.2/howto/legacy-databases...