Django Views – The Right Way

(spookylukey.github.io)

134 points | by sgt 317 days ago

11 comments

  • dec0dedab0de 317 days ago
    I like class based views.

    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.

    • kamikaz1k 317 days ago
      There should be an intermediate layer between views and models, especially if they're getting complicated.

      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...

      • Izkata 317 days ago
        We could never figure out what to call these and just ended up with "utils.py"

        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.

      • gmt2027 317 days ago
        There's an argument against service layers in Django. The recommendation is to use instance and manager methods.

        https://www.b-list.org/weblog/2020/mar/16/no-service/

        • rpep 317 days ago
          I disagree with the idea of not having a service layer at all, because in practice, changing your data model in some way is actually pretty common, even if the API remains the same so the data coming in/out isn't changing. I've had to split models into multiple parts to add audit history on certain fields, create notifications on object creation or modification (possible but hard to follow and a visibility problem when using signals), change the normalisation on fields, etc. etc. and at this point, turning to a service layer is much more appropriate than putting methods on the model since often the data is split across multiple database tables. I also would advocate this in projects where it's Django + DRF since you generally want exactly same logic to be run through whether submitting via a form or submitting via an API.

          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.

          • hitchstory 317 days ago
            It sounds to me like you could use model managers and just call it a service layer.

            >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.

            • rpep 317 days ago
              ModelManagers are directly coupled to one model though. You mix concerns if you need to reference other models in them.

              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.

              • dec0dedab0de 315 days ago
                i don't see anything wrong with it if there are relationships between Profile and User, but if you feel bad about abusing a manager, you could just do a classmethod, or a separate function all together.
        • btown 317 days ago
          Over time, though, this approach leads to massive model/manager files with dozens of groupable methods. Sometimes a service is a useful abstraction as a way to encourage placing related functionality in a dedicated file. The one-minute learning curve for telling new team members what that services directory does, may save hours that would have otherwise been necessary to grok a thousand-line model file.
      • dec0dedab0de 317 days ago
        I tend to handle that with an extra model that just has relationships to the other models and some methods to make it work
    • hitchstory 317 days ago
      I've never seen complex CBVs that werent a mess. FBVs yes, CBVs no.

      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.

      • dec0dedab0de 317 days ago
        I think that comes down to how comfortable you are with classes and multiple inheritance and whatnot. Django is a shining example of how to do these things right, but yes it can be a bit daunting if you're not used to them.

        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.

        • hitchstory 317 days ago
          >I think that comes down to how comfortable you are with classes and multiple inheritance and what

          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.

          • dec0dedab0de 316 days ago
            Preferring composition over inheritance is good for languages that don't do inheritance well. Python does it well. But more importantly, Django is an object oriented framework that makes heavy use of multiple inheritance, and the code is gorgeous. It seems silly to choose a big framework like Django and not take advantage of it's features.
  • jerrygenser 317 days ago
    Or just use django-ninja if you are writing an API. Maybe it's just because I came from teams that used tornado and then fastapi but when I started using django which was about 8 months ago, I simply grabbed django-ninja and haven't thought much about it. It seems like everything in this article would be solved by using a simpler interface for writing endpoints with body parsing, output schemas, and url parsing.

    https://django-ninja.rest-framework.com/

    • winrid 317 days ago
      The article isn't really about APIs. If you're writing APIs, most people will reach for DRF.

      Ninja looks neat, though.

    • xwowsersx 317 days ago
      How does Django Ninja compare to Django Rest Framework (DRF)? Is there much of a difference in terms of performance?
      • jerrygenser 317 days ago
        Django ninja performs better primarily due to relying on pydantic for parsing and validating requests. In a high throughput scenario that's going to be the primary CPU bound area. However usually network or file io is going to be a limiting factor far before CPU.

        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.

        • xwowsersx 315 days ago
          Thanks very much for the reply. I actually had not seen that DRF added async support. I must take a look at that! I'm very curious to know how big of an impact switching to AsyncViews would have. Any experience on that front?
        • xwowsersx 315 days ago
          I haven't tested very thoroughly yet, but I'm seeing large improvements in latency and throughput by switching to async views!
  • BiteCode_dev 317 days ago
    Ok I was ready to hate this article the second I read the title, and I clicked on it just to get enough ammunition to crap on it.

    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.

    • firecall 317 days ago
      As a Rails dev that is having to work with Django lately, I'll give it a read.

      The Django fat models and (lack of) code structure in the Django project I'm working on fixing is painful for me!

    • scrollaway 317 days ago
      There's a lesson to learn, here.
  • whakim 317 days ago
    I agree with the author about 99% of what's written here. Except for the preference for thin views (i.e. thin controllers and fat models). It seems to be me that the author goes to great lengths to decry the abstractions inherent in CBVs (and other patterns) which hide logic from the user and make code harder to read all in the name of "DRY". Except that pre-emptively moving your queryset methods into another model does exactly that! Sure, if you're repeatedly doing some super-complicated joining and filtering - by all means move your logic elsewhere. But my experience with this pattern (mostly in Rails codebases which seem to love it) has been tons of extra functions scattered around models that don't actually get used every time and often themselves become out-of-date.
  • willmeyers 317 days ago
    I think this is a great resource. The only comment I have is on the Thin Views chapter. Instead of attaching logic to the models, I like to make a services.py file in my app that has functions that satisfy all sorts of business logic that I can plop in anywhere I need them.

    Here's another opinionated Django guide: https://github.com/HackSoftware/Django-Styleguide if anyone's interested

    • kisamoto 317 days ago
      Having worked on Django projects of multiple sizes I really like the HackSoftware Django Styleguide.

      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.

    • noisy_boy 317 days ago
      This is how I do things in Spring Boot too. Model specific stuff stays in the model, cross model business logic goes into services.
  • adamckay 317 days ago
    I'd agree that CBV's can be difficult to reason with (though I personally still use them for most views) but the following two websites have massively helped me make sense of them. The only thing missing really is the actual flow of a request, such as knowing `dispatch() -> get() -> get_context_data()` but it's not terribly difficult to work out and learn.

    https://ccbv.co.uk/

    https://www.cdrf.co/

    • Nextgrid 317 days ago
      For anyone struggling with remembering class based view mixins, I suggest you get yourself a proper IDE that allows you to trivially look into the implementation of anything as well as lookup any symbol with fuzzy search. PyCharm/IntelliJ is one.

      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.

      • nouveaux 317 days ago
        The problem with relying on fancy IDE to untangle your classes is that it breaks on GitHub. Also it's harder to discuss snippets of code asynchronously.

        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.

      • kodah 317 days ago
        Properly configured VSCode with the Python Virtual Environment Manager plugin also makes this easy.
    • code_biologist 317 days ago
      I came here to post that first link. I don't mind working with CBVs if that's the existing style in a project, but the very existence of CCBV highlights the pain of lots of indirection. Certainly a lot of power, but not trivial to remember how everything connects either.
  • sergioisidoro 317 days ago
    The most important caveat that the author mentions: this advice may not apply to Django Rest Framework.

    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.

    • pdhborges 317 days ago
      I read the article and I'm left scratching my head why this advice doesn't apply to DRF.

      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.

      • rpep 317 days ago
        CBV doesn't require you to do anything in particular other than implement retrieve/list/etc. methods on the GenericViewSet as needed, and get/post/etc. on GenericAPIView. You can use the convenience methods in Django and mixins, but as soon as you get beyond a simple case, they start to become a hindrance.

        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.

        • pdhborges 313 days ago
          In my current codebase there is a lot more overriding going on than simply implementing the retrieve/list/etc... methods. On a quick review I found overrides for: - get_object - get_serializer_class - perform_create - get_queryset - get_serializer_context - perform_destroy
          • rpep 312 days ago
            It depends on how much you want to rely on Mixins and using Serializer classes to do more than just serialise JSON. Neither of those are compulsory to use, and the more you lean into them the more inheritance you’re forced to do as soon as you need to customise the behaviour.
  • evrimoztamur 317 days ago
    I'm not so sure about the 'including data in your template' section. It shows the regular way of adding context fields directly.

    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).

  • kolanos 317 days ago
    Function based views are good until you have to handle many request methods, then they become a hideous series of if/elif/else.
    • mastazi 317 days ago
      > series of if/elif/else

      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

      • ehutch79 317 days ago
        If you would have made them separate function views, they should be separate class views.
    • notagoodidea 317 days ago
      Or a good opportunity for match/case if >3.9.
  • IgorPartola 317 days ago
    This is a great guide. Some thoughts having been working with Django for a while:

    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.

  • jgalt212 317 days ago
    The Django ORM is close to useless if your database is not generated by the web app.
    • Nextgrid 317 days ago
      You can “cheat” by having fake models backed by database views. Declare your view creation/deletion as SeparateDatabaseAndState migrations (with RunSQL operations creating the actual views).

      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.

    • Takennickname 317 days ago
    • ehutch79 317 days ago
      If you’re not all in on the Orem I’m not sure I’d recommend django
      • jgalt212 317 days ago
        If you have a database-driven web app, and you like Python, what would use then?
        • ehutch79 317 days ago
          Probably something like flask/sql alchemy. It’s been a bit since I’ve looked at the field.

          But Django is definitely opinionated on things

        • 7373737373 317 days ago
          If it's a simple app, check out this flask+dataset(sqlite) approach: https://github.com/void4/lazyweb/
      • ehutch79 317 days ago
        On mobile. I meant ORM
    • andybak 317 days ago
      This doesn't match my experience.
      • jgalt212 317 days ago
        so you do your online and offline database operations using the Django ORM?
        • andybak 317 days ago
          I'm not sure I understand what you mean by online and offline in this context?
          • jgalt212 316 days ago
            do you have CRUD code running on the web app, and offline / batch jobs where both sets of jobs / functions / processes modify the same database that Django wants to ORM and your data scientists and ETL folks don't want to do Django ORM - or ORM at all.
            • andybak 316 days ago
              The original comment was:

              > 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.

              • jgalt212 316 days ago
                > 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.

                • andybak 316 days ago
                  We do write to the database and the schema does change.

                  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.