Sarah Ting

Scraps: Django notes

Trying to get my head around Django and try figuring out how to write “Django-ish” code. As part of the learning process I’ve been noting down Django concepts as Laravel equivalents — I understand that a lot of these are not true 1:1 mappings but it makes it easier for me to transfer some knowledge over 🤭


URLConf

  • Routes → Django URL config

  • Routes are done on the URL level rather than the method level so view functions end up with control structures that look like this —

    def api_detail(request, pk):
      if request.method == 'GET':
         # do stuff
      elif request.method == 'PUT':
         # do stuff
      elif request.method == 'DELETE':
         # do stuff
    
  • Django does have class based views which seems to alleviate this, but the route method does seem to be fundamentally tied to the view level —

    class ApiDetailView(APIView):
    	def get(self, request):
        # do stuff
      def delete(self, request):
        # do stuff
      def put(self, request):
        # do stuff
    
  • Django doesn’t appear to have anything similar to Laravel’s implicit model binding (in Django terms, I guess I would describe it as a middleware that handles automatically resolving a PK from the URL into a model and injects it as a Model object into the view layer).

    • I tried emulating a generic solution with function decorators which is good enough for my side project but I’m sure there must be a more conventional way of doing this.

      def find_or_fail(model, var_name, key='pk'):
          def decorator(view_fn):
              def wrapper(*args, **kwargs):
                  try:
                      kwargs[var_name] = model.objects.get(id=kwargs[key])
                  except model.DoesNotExist:
                      return Response(status=status.HTTP_404_NOT_FOUND)
                  return view_fn(*args, **kwargs)
              return wrapper
          return decorator
      
      class AdoptApiDetailView(APIView):
          @find_or_fail(Adopt, 'adopt')
          def get(self, request, pk, adopt):
              return Response(AdoptSerializer(adopt).data)
      
      

Models & migrations

  • Based off the documentation, convention for datetimes seems to be date_xxx, eg. date_created
  • Django appears to encourage use of FKs for relationships which I’m not fond of. FKs will automatically generate an underlying index.
  • Laravel enums → Django choices, though it looks like Django also has enums
  • Laravel model observers → Django doesn’t have a direct equivalent baked in but it can do model events/listeners with signals/receivers
  • Django has a File/Image upload field for models — this represents the file path using a string in the database.
  • Laravel query builder → Django model objects
  • Laravel Model collections → Django query set
  • M2M relationships are expected to only be defined on one side and then you use a reverse M2M to traverse the other side of the relationship

Django Model relationship caching

adopt = Adopt.objects.get(id=1)

# 1:1 relationship
print(adopt.creator) # this will trigger a db call the first time
print(adopt.creator) # cached; no db call

# has many relationship
print(adopt.genes.all()) # db call
print(adopt.genes.all()) # db call -- there is no cache
  • One to one relationships cache but anything that returns a collection will instead return a RelatedManager object which will be re-evaluated every time the results are needed
  • Even if you assign this to another variable, it will still not evaluate itself until required. To be honest, I don’t have a strong understanding of what mechanism is being causing the DB call to be triggered and will need to do a deep dive, but for the purposes of this project I’ve just taken dumping the query into a list() when I need the output to be evaluated once only.
adopt = Adopt.objects.get(id=1)
genes = adopt.genes.all() # no db call

print(genes) # db call
print(genes) # db call

genes = list(adopt.genes.all()) # db call

print(genes) # no call
print(genes) # no call
  • I was trying to get eager loading working with prefetch_related but kept running into the queries getting re-evaluated. After I wrapped everything in a list() it started working as expected.

  • Speaking of prefetch_related, it’s pretty flexible in complex queries —

    self.gene_pools = list(self.adopt.gene_pools.prefetch_related(
                Prefetch(
                    "genes",
                    queryset=Gene.objects.active().order_by("name"),
                )
            ).prefetch_related("color_pool").prefetch_related("genes__color_pool").all())
    

Django Rest Framework

  • Looks like this is what I should be using for setting up a REST API?
  • I mostly wanted to see if there was any quick equivalent of Laravel Resources, and it looks like this package provides Serializers
  • Looking for the cleanest way to alter Serializer behaviour based on logged in user… (eg. admin can see X Y Z but users can only see X)
  • Serializers can both be used for formatting output and for validating input as a replacement for Django Forms (oh my god!)
    • This sounds quite absurd at a glance but I imagine the rationale is that Django ties validation rules to the model. In comparison, Laravel’s data validation is handled separately from the model and data presentation layers.

Authentication & validation

  • Auth/authorization can be done on both the view or URL level
  • Laravel policies → Django Model Permissions
  • Django supports Form classes which can be used for validation and for building a form template
    • For REST API, use Rest Framework’s Serializers it has the same behaviour

Image manipulation

  • Python Pillow (PIL) — For my needs it seems to work very similarly to IM (just doing basic alpha composites & masking). Was honestly surprised how easy it was to use, didn’t have to do anything fancy to work with alpha channels.

Testing

Impressions

  • I think my overall impression is that Django has less opinions about code styles than Laravel and there’s more freedom for developer’s choice of code patterns and structure.
  • Django seems to couple basically everything to the model. Django’s documentation explicitly advocates for fat models, and a lot of framework features are directly dependent on the Model configuration.
    • I can understand the appeal of being able to get code magically working by pointing at a Model or to be able to set up automated scaffolding very quickly. It makes development quicker and cooler. That said, I don’t like how coupled the code gets.
    • I noticed some concepts in Django (eg. having the model be responsible for input validation, doing sanitizer/validation/data presentation in the same Serializer object, etc) are concepts that I built out when I first started working with Laravel 5, then ended up throwing out when the codebases I was working with got larger. When the classes proximate to the Model class are responsible for so many things the code inevitably turns into spaghetti. I would prefer to cop the extra development time for more cleanly de-coupled code and enforcement of SRP.
  • I followed the folder structure recommended by the Django documentation and set up a project with a separate app for each domain. I understand the mentality here (keeping domain code separated from each other), but my folder structure ended up feeling very unpleasant to look at.
    • I’d like to try structuring future projects more similarly to how Spatie recommends structuring Laravel apps in Laravel Beyond Crud. In Django I imagine this would look something like —
      • 1 app for handling migrations, tests, and whatever else Django specifically needs apps for
      • 1 app for the frontend
      • Separate all the other domain logic out to a domains folder
  • Python is much more pleasant to work with than PHP. If I had to summarise I’d say I enjoy Python more than PHP, but I enjoy Laravel more than Django.