Joshua's Docs - Django

Resources

What & Link Type
Django Project - Docs Official Docs
Django ORM Cookbook Cheatsheet on ORM Syntax and Tricks

You might also find my general Python cheatsheet useful.

Misc / To Sort

  • How to return specific status code?
    • return HttpResponse(status=STATUS_CODE)
  • Where can I find a list of included / bundled icons?
    • There are a set of icons accessible under /static/admin/img/{ICON}, which you can find here
  • CommandError: Unable to serialize database: cursor "...sync...." does not exist
    • Check for unapplied migrations, conflicts with models and database
  • How to get the names of all the fields of a model
    • You can use "concrete fields" -> [f.name for f in my_model._meta.concrete_fields]

Dealing with Requests

Dealing with QueryDict

The QueryDict class in Django (from django.http) is an interesting class, with a lot of hidden complexity. There are two critical distinctions to note when dealing with them:

  • They are (often) immutable by default. If you want to mutate them, you need to override the immutable flag (and/or clone first)
  • Keys can be repeated - meaning that the same key could have mappings to two (or more) different values. This is unlike any other dictionary and adds a bunch of complexity when trying to retrieve or store values.

To expand on the multiple-values-per-key part of QueryDicts, here is an example of what this means:

query_dict = QueryDict("id=123&id=abc&color=red&color=blue")
print(query_dict["id"])
# > abc
# ^ Only returns a single value, which is deceiving...
print(query_dict.dict())
# > {'id': 'abc'}
# ^ Only returns a single value, which is deceiving...
print(query_dict.getlist("id"))
# > ['123', 'abc']
# ^ Ah! There are actually two mappings for this single key
print(list(query_dict.lists()))
# > [('id', ['123', 'abc']), ('color', ['red', 'blue'])]
# ^ Get all key->lists mappings

For some utilities that can help with working with QueryDicts, check out:

Useful Entrypoints

  • For executing code only once, on startup
    • wsgi.py

Routing and URLs

  • Dictionary to query string?
    • This is not Django specific - just use something like urlencode({'my_arg': my_val})
    • If you need to combine it with resolving to a URL in your settings, you can do something like: final_url = reverse(...) + urlencode(...)
  • Capturing query parameters / querystring in URL patterns
    • You don't - if you really want a value captured in a pattern, move it to be part of the path instead of the query string. For query params, instead use request.GET.* methods to access params inside request handler, without explicit capture syntax

Database, Models, and ORM

Models - Using Base Models

Django's model system is generally enjoyable to use, but there are a few common things that I almost always want my models to have that the default model omits:

  • Timestamps, for creation and modification
  • Validation of model fields before saving (.save()) completes

With this in mind, I often implement a base model that can be subclassed, with something like this:

import uuid
from django.db import models

class BaseModel(models.Model):
	# Sometimes it is nice to have ID be a UUID,
	# instead of using an auto-incrementing int
	# as the PK
	id = models.UUIDField(
		primary_key=True, default=uuid.uuid4, unique=True, editable=False
	)

	# Django doesn't include timestamps by default
	created_date = models.DateTimeField(auto_now_add=True)
	modified_date = models.DateTimeField(auto_now=True)

	# Django' doesn't call `full_clean` on saves by default
	def save(self, *args, **kwargs) -> None:
		self.full_clean()
		return super().save(*args, **kwargs)

	class Meta:
		abstract = True

ORM - Misc Tips

  • Get and update in same query?
    • my_models.objects.filter(...).update() (no need to call save())
  • Assigning relationship via FK ID instead of model instance
    • Assign by my_model.${RELATED}_id = fk_id instead my_model.${RELATED} = ${INSTANCE}
      • This also works for retrieval
  • Only getting back certain fields (instead of SELECT *)
  • Delete all objects of a given model?
    • MyModel.objects.all().delete()
  • Filter for not matching?
    • Options:
      • .exclude(prop=val)
      • .filter(~Q(prop=val))
  • Check if an object exists matching filter?
    • MyModel.objects.filter(myprop=myval).exists()
  • Get an object that might or might not exist, without throwing
    • If you are unsure if a queryset will return a result, you can use queryset.first(), as it will return None if there are no results, without throwing an exception. You can also combine this with filters - e.g. MyModel.objects.filter(pk=25).first()
    • Beware: the get method - queryset.get() - will raise a Model.DoesNotExist exception if no object can be found.
    • Another option, if you want to return a 404 response instead of throwing an exception, is to use the get_object_or_404() method instead
  • Beware that using MyModel(param=val).save() returns None, not a model instance.
    • You can use MyModel.objects.create(param=val) instead, which does return an instance
    • Or, if you really want to use this approach but get the instance, you have to do it as two separation operations:
      instance = MyModel(param=val)
      instance.save()
  • Error cannot assign <MyModel object (1)> ...
    • Make sure the instance you are assigning to the model actually has been saved to the database and has a primary key before assigning. In some cases, you might need to call .save() first.
    • In some instances, you might need to use (related_obj_id=my_obj.pk) instead of (related_obj=my_obj) when assigning on creation
  • _get_target_ids, etc. , TypeError: 'MyModel' instance expected, got <MyModel: my_instance> instead
    • If you are writing a migration, and passing in an object to a related field (e.g. my_model.related_field.add(my_related_model)), you either need to make sure that the related model instance was instantiated via a class created with app.get_model() (instead of an import) or add the model instance by primary key (e.g. my_model.related_field.add(my_related_model.pk)). See this StackOvrrflow for example.

Database - Django Model Relationship Fields

Django ORM Relations: One-to-One

OneToOneField, examples

Django ORM Relations: Many-to-Many

ManyToManyField

A many-to-many relationship in Django works with an intermediate join table. When writing the migration to add this linkage, you can specify the join table explicitly. For example:

class User(AbstractUser, BaseModel):
	companies = models.ManyToManyField(
		Company,
		related_name="users",
		through="my_app.CompanyMappings"
	)

class CompanyMappings(BaseModel):
	user = models.ForeignKey(User, on_delete=models.CASCADE)
	company = models.ForeignKey(Company, on_delete=models.CASCADE)

For retrieving items via a many-to-many relationship, you have a few different options

  • For getting get the intermediate (through / join) object, you can use my_model.intermediate_name_set. Example:
    • my_memberships = my_user.companymappings_set.all()
    • acme_memberships = acme_company.companymappings_set.all()
  • For getting the related items:
    • For the model that defines the ManyToManyField, you can use the field name directly - my_model.related_thing.all()
      • my_companies = my_user.companies.all()
    • For the reverse, you can access though the related_name property
      • acme_users = acme_company.users.all()
      • support_users = Group.objects.get(name="Support").user_set.all()

For adding new linkages, it depends on how you are storing the many-to-many. If it is a simple many-to-many join table, with no other data, you can use your_model.linked_thing.add(new_linked_thing). For example:

my_user.companies.add(acme_corp)

However, if you are using a many-to-many with extra metadata attached, you'll generally want to access the join model directly:

CompanyMappings.objects.create(
	user=my_user,
	company=acme_corp,
	notes="Extra notes..."
)

Django ORM Relations: Many-to-One

Example: Author <-> Book. Each author can have multiple books, but each book has only one author.

In the child model, which can only have one parent, use parent_field_name = models.ForeignKey(ParentModel, on_delete={YOUR_CHOSEN_DELETE_OPTION}).

Continuing our example,

# In Book model
author = models.ForeignField(Author, on_delete=models.CASCADE)

With a child model instance, the parent field should be automatically accessible as a model instance (e.g. my_book.author). For setting or updating the parent of the child instance, you can set to None (if nullable is allowed), set to an actual model instance, or use the magic *_id property to set the related model by ID alone (e.g. my_book.author_id = 4).

With a parent model instance, which can have multiple children, you can access the children with {PARENT_MODEL_INSTANCE}.{CHILD_MODEL_NAME}_set. With our example, that would be my_authors_books = my_author.book_set.all().

While the relationship is hard-coded in the child model (you must add a ForeignKey field), it is implicit on the parent; you don't actually have to add any code to the parent model. If you wish to, you could make the relationship more explicit in the parent by adding it to the serialization method, or as a dynamic property.

Django ORM Relations: Adding Relationship Items in Bulk

students = Student.objects.all()
for student in students:
	student.degree_id = getDegree(student)
Student.objects.bulk_update(students, fields=['degree'])

ORM "Magic"

Both a pro and a con of the Django ORM is the amount of "magic" it tries to inject. I consider these things primarily exist only at runtime and are hard to statically type. For example:

  • ${FIELD}_set injected model members (via related_name absence), for reverse lookups
    • Thankfully django-stubs supports typing for these
  • The ${FIELD}_id for relationship fields
  • Many constraints aren't actually created at the DB level...

Migrations

Q&A:

  • How do I roll back ALL migrations?
    • manage.py migrate my_app zero
    • Or, assuming you have no data outside of Django, drop and rebuild the database before running the migrate command
  • How do I create a new migration based on model changes?
    • manage.py makemigrations

Debug SQL

  • For specific queries
    • If you want to see the SQL generated:
      • str(myQuerySet.query)
    • If you want the SQL explained
      • myQuerySet.explain()
  • For Q() objects
    • Pass to a model first, then you can use the above methods
    • E.g.:
      q_obj = Q(name="Ozma") | Q(location="Oz")
      queryset = User.objects.filter(q_object)
      sql_str = str(queryset.query)
  • For monitoring / debugging all queries made
    from django.db import connection
    print(connection.queries)

How does get_or_create work?

Use regular kwargs for the fields to evaluate for similarity, and use defaults= for the fields that will combined with the filtering fields for the actual object creation if it doesn't already exist.

For example, if we want to create a new menu item if one doesn't already exist with the same name, but we don't care about the price matching:

bagel, had_to_create_bagel = MenuItem.objects.get_or_create(
	name="bagel",
	defaults={"price": 1.99}
)

Making Ad-Hoc Queries

Generally, you should stick with the Django ORM whenever possible. Django offers a lot of utilities to hand-tune queries while staying within the ORM ecosystem, so there aren't many good reasons to bypass this.

However, if you really do want to bypass the ORM and make queries that are more raw (e.g. bypassing models and just using table names), while reusing the database driver connection, you can do so in a few different ways.

https://docs.djangoproject.com/en/5.0/topics/db/sql/#executing-custom-sql-directly

If you have the right database driver / binary installed locally, for an interactive shell you can use:

python manage.py dbshell

If you don't have the DB driver installed, or prefer a code-based approach, you can use the connection object from django.db. A small example:

from django.db import connection

c = connection.cursor()
c.execute("SELECT * FROM my_table")
res = c.fetchall()
# Do something with results
c.close()

JSON

Django and rest_framework sending back escaped JSON

There are a few reasons why your API endpoint might be sending back JSON as an escaped (or even double escaped) string.

The first thing you should double check is that the model class has the field in question declared as a models.JSONField(). E.g.:

from django.db import models

class Game(models.Model):
	state = models.JSONField()

And when returning the model from the API endpoint as JSON, you probably want to use something like a rest_framework.serializers.ModelSerializer based class. For example:

from .models import Game
from rest_framework import serializers

class GameSerializer(serializers.ModelSerializer):
	class Meta:
		model = Game
		fields = ["state"]

... and return it with something like

return JsonResponse(GameSerializer(gameInstance).data, safe=False)

However, if you are still getting back escaped JSON as a string, especially if it is happening with a nested sub-field of the model, consider how the data is getting into the database before questioning the retrieval method. For example, I accidentally wrote a migration that introduced this issue, by using (WRONG) this:

# WRONG
seed_game = Game(
	state="""{"id": 24}"""
)

The above code will permanently add an escaped JSON string into the DB. Don't do this.

...when I should have been using:

seed_game = Game(
	state=json.loads("""{"id": 24}""")
)

StartProject

django-admin must have the most confusing docs and instructions for startproject...

By default, using the command django-admin startproject backend will create a structure like:

./backend
  - manage.py
  - ./backend
    - settings.py
    - wsgi.py
    - ... (etc.)

I usually prefer to rename the backend/backend directory to something like backend/rootproject, since that really is what that directory is - the root Django project that contains the shared settings, wsgi entrypoint, etc. I also usually like to name my core "app" with the name "common", since it usually contains a lot of shared business logic, and then specialized sub-app would get a unique name. So my final structure sometimes looks something like this:

./backend/
  - manage.py
  - rootproject/
    - settings.py
    - wsgi.py
    - ... (etc.)
  - common/
    - migrations/
    - views.py
    - ... (etc.)

Another option, if you want to create without myproj/myproj nesting, you need to use the . trick, but so many tutorials leave out where to execute this command. If you already have a /myproj directory, you don't cd to it first and then execute, you stay in the root directory and run:

django-admin startproject myproj .

If you were to run that after first using cd myproj, you would still end up with the double myproj/myproj creation. What the above command really does is avoids the "directory already exists" type error.

manage.py will still be placed in the current directory, instead of myproj, and moving it into the same directory takes some changes:

  • change lines that have myproj.submodule to submodule (e.g. change myproj.settings to settings)
  • Change BASE_DIR = Path(__file__).resolve().parent.parent to just BASE_DIR = Path(__file__).resolve().parent

Note: With the above strategy, Django will allow the directory to already exist and contain files. This is is very beneficial, as it allows you to setup a venv within that directory before targeting it to install to

HTML Templating

  • How to conditionally render an attribute?
    • Wrap in {% if %} ... {% endif %} blocks

Interacting from the Command Line

  • manage.py shell will let you use the Python interpreter within your Django environment
  • To change a setting from the management shell and have it persist / apply to the live instance, ... well, the short answer is that you can't. You might find some instances in which this works, but most parts of Django won't play nice with this and it is strongly recommended against.

Django-Rest-Framework (DRF)

DRF Serializers

  • How to represent a field that can be repeated multiple times (e.g. multiple files for a single upload)?
    • Use serializers.ListField
    • image_file = serializers.ListField(child=serializers.ImageField(), allow_empty=False)
  • How to represent a nested model that can be repeated multiple times
    • Use the existing serializer, but with many=True
    • books = BookSerializer(many=True)
  • How to represent a one-to-many (reversed via FK)?
    • One option is to add a property equal to a serializer of that linked item, with source="{model_name}_set" and many=True. Or, with `source="{}"
      • Example:
      # In `models.py`
      class Article():
      	...
      
      class Comment():
      	article = models.ForeignKey(Article, on_delete=models.CASCADE)
      
      # In your serializers:
      class Article(serializers.ModelSerializer[Article]):
      	comments = CommentSerializer(source="comment_set", many=True)
    • If you used an explicit related_name= on the linked item, use that as the source value.
    • Tip: You can use required=False if this serializer is used reused for input validation, and it isn't required at input time
  • How to represent a many-to-many?
    • Use the many=true trick (see above), but you don't need the source={} argument if the field name matches, since the field should explicitly exist on the class you are serializing, as opposed to a reversed FK lookup
Markdown Source Last Updated:
Tue Oct 01 2024 03:28:35 GMT+0000 (Coordinated Universal Time)
Markdown Source Created:
Wed Apr 17 2024 00:45:46 GMT+0000 (Coordinated Universal Time)
© 2024 Joshua Tzucker, Built with Gatsby
Feedback