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.
- This concept doesn’t exist in Laravel; if you wanted to do this, you would add the file column as a VARCHAR, and then handle the storage logic in the business logic layer rather than on the model layer.
- ImageField requires Python Pillow, I’m guessing that this is for validating that it’s an image.
- Looks like it is possible to swap this in and out between s3/local using
DEFAULT_FILE_STORAGE
or by specifyingstorage
in the FileField parameters
- 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 alist()
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)
- Found an article about it here which recommends creating different classes for different permissions and overriding
get_serializer_class
— https://medium.com/aubergine-solutions/decide-serializer-class-dynamically-based-on-viewset-actions-in-django-rest-framework-drf-fb6bb1246af2 - It seems like the common practice is to just make a new Serializers for different usecases
- Found an article about it here which recommends creating different classes for different permissions and overriding
- 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
- Faker & factory_boy for factories:
- Selenium for browser testing:
- Nothing first-party for snapshot testing, did come across this library:
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
- 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 —
- 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.