Unit 5: Models and Migrations and Django Admin
I. Orientation: The Model Layer in Django's MTV Architecture
Django's model layer (introduced with Django 0.90, 2005; ORM matured through the django.db.models API) is the single, authoritative definition of application data. Django follows the DRY principle: you write the schema once as a Python class, and Django derives the database tables, the SQL, the Python query API, the admin interface and form validation from it.
Defining properties and conventions the rest of the unit relies on:
- Model = table: One class subclassing
django.db.models.Modelmaps to one database table; one class attribute maps to one column; one instance maps to one row. - Active Record pattern: Instances carry their own persistence methods —
obj.save(),obj.delete()— rather than requiring a separate session/unit-of-work object. - Implicit primary key: Django adds
id = models.AutoField(primary_key=True)(orBigAutoField, perDEFAULT_AUTO_FIELD) unless you declareprimary_key=Trueyourself. - Default table naming:
<app_label>_<lowercased model name>— modelBookin applibrarybecomes tablelibrary_book. Overridable withclass Meta: db_table = '...'. - Database-agnostic: The same model definitions run on PostgreSQL, MySQL, SQLite, Oracle; Django's backend translates to dialect-specific SQL.
- Migrations as version control for schema: Schema changes are Python files stored in
<app>/migrations/, committed to git alongside code. - App registration is mandatory: A model is invisible to
makemigrationsunless its app appears inINSTALLED_APPS.
II. Creating Models — Declaring the Schema in Python
A. Anatomy of a Model Class
A model is a plain Python class whose attributes are Field instances describing column type and constraints.
# blog/models.py
from django.db import models
class Author(models.Model):
name = models.CharField(max_length=100)
email = models.EmailField(unique=True)
class Post(models.Model):
title = models.CharField(max_length=200)
slug = models.SlugField(unique=True)
body = models.TextField()
price = models.DecimalField(max_digits=6, decimal_places=2, default=0)
published = models.BooleanField(default=False)
created_at = models.DateTimeField(auto_now_add=True)
updated_at = models.DateTimeField(auto_now=True)
class Meta:
ordering = ['-created_at']
verbose_name_plural = 'posts'
def __str__(self):
return self.title- Field types map to SQL types:
CharField(max_length=200)→varchar(200);TextField→text;IntegerField→integer;DateTimeField→timestamp;BooleanField→boolean. max_lengthis compulsory onCharField: Omitting it raises system check errorfields.E120at startup.auto_now_addvsauto_now: The former stamps the time only at row creation; the latter rewrites on everysave()— used forcreated_at/updated_atrespectively.- Common field options:
null=True— permits SQLNULLin the column;blank=True— permits an empty value in form validation. Avoidnull=Trueon string fields (two "empty" states).default=...,unique=True,db_index=True,choices=[('D','Draft'),('P','Published')](addsget_<field>_display()).
class Meta: Non-column configuration —ordering,db_table,unique_together/constraints,verbose_name.__str__: Controls the human-readable label shown in the shell and throughout the admin; without it objects display asPost object (1).
III. Working with Migrations — Versioned Schema Evolution
A. Purpose and Principle
Migrations translate the difference between the current model state and the last recorded state into ordered, replayable operations, so every developer and every server converges on the same schema.
- Two-step workflow:
BASHpython manage.py makemigrations blog # write 0001_initial.py python manage.py migrate # execute SQL against the DB makemigrationscompares models to the historical state built from existing migration files and emits e.g.CreateModel,AddField,AlterField,RemoveField,RenameField,RunPython.migrateapplies unapplied migrations in dependency order and records each in thedjango_migrationstable (columns: app, name, applied).- Inspection and control:
python manage.py showmigrations—[X]marks applied.python manage.py sqlmigrate blog 0001— prints the exact SQL without running it.python manage.py migrate blog 0002— rolls back to 0002 by reversing later migrations.python manage.py migrate --fake— marks as applied without executing (for pre-existing schemas).
- Adding a non-nullable field to a populated table: Django prompts interactively for a one-off default, or you supply
default=/null=Truein the field. - Data migrations: Use
RunPython(forwards, backwards)and access models viaapps.get_model('blog', 'Post')— never import the live model class, whose definition may have moved on.
IV. Using the Django Shell to Explore Models — Insert, Update and Delete
A. Launching and Importing
python manage.py shell starts a Python REPL with DJANGO_SETTINGS_MODULE configured and the app registry loaded; shell_plus (django-extensions) auto-imports models.
B. Insert
- Two-step:
p = Post(title='Hello', body='...'); p.save()— issuesINSERT, then populatesp.id. - One-step:
Post.objects.create(title='Hello', body='...')instantiates and saves in one call. - Bulk:
Post.objects.bulk_create([Post(title='A'), Post(title='B')])— a single INSERT statement, but skipssave()and signals. - Idempotent:
obj, created = Post.objects.get_or_create(slug='hello', defaults={'title':'Hello'}).
C. Update
- Instance-level: mutate then save —
p.title = 'Edited'; p.save()issues anUPDATEon all columns (limit withp.save(update_fields=['title'])). - Queryset-level:
Post.objects.filter(published=False).update(published=True)— one SQLUPDATE, returns the number of rows affected, bypassessave()and signals. - Atomic arithmetic:
Post.objects.filter(pk=1).update(views=F('views') + 1)avoids read-modify-write races.
D. Delete
- Single row:
p.delete()returns(1, {'blog.Post': 1}). - Set:
Post.objects.filter(published=False).delete(). - Cascade: deleting an
Authordeletes dependentPostrows when the FK useson_delete=models.CASCADE.
V. Using Object Relational Mapping (ORM) — Querying Without SQL
A. Managers and QuerySets
Every model gets a default manager objects; calling a method on it returns a QuerySet, which is lazy — no SQL runs until the result is iterated, sliced, printed, or passed to len()/list().
- Retrieval:
Post.objects.all();Post.objects.get(pk=3)raisesDoesNotExistorMultipleObjectsReturned;first()/last()returnNonewhen empty. - Field lookups (double-underscore syntax):
PYTHONPost.objects.filter(title__icontains='django') Post.objects.filter(created_at__year=2024, published=True) Post.objects.exclude(price__gte=100).order_by('-created_at')[:5]
__exact,__iexact,__contains,__in,__gt,__lte,__startswith,__isnull,__range. - Complex conditions:
Qobjects give OR/NOT —Post.objects.filter(Q(published=True) | Q(author__name='Ada')). - Aggregation and annotation:
PYTHONfrom django.db.models import Count, Avg Author.objects.annotate(n=Count('post')).filter(n__gt=2) Post.objects.aggregate(Avg('price')) # {'price__avg': 240.5} - Performance:
select_related('author')performs a SQL JOIN for forward FKs;prefetch_related('post_set')issues a second query for reverse/M2M sets — both cure the N+1 query problem. Inspect generated SQL withprint(qs.query). - Escape hatch:
Post.objects.raw('SELECT * FROM blog_post')for hand-written SQL.
VI. Models Using Foreign Keys — Modelling Relationships
A. The Three Relationship Fields
ForeignKey(many-to-one):author = models.ForeignKey(Author, on_delete=models.CASCADE, related_name='posts')creates columnauthor_idwith aREFERENCESconstraint.OneToOneField: aForeignKeywithunique=True— used for profile extensions ofUser.ManyToManyField:tags = models.ManyToManyField(Tag)creates a hidden join tableblog_post_tagswithpost_id/tag_id.
B. on_delete Behaviours (mandatory since Django 2.0)
CASCADE: delete children with the parent.PROTECT: raiseProtectedError.SET_NULL: requiresnull=True.SET_DEFAULT,DO_NOTHING.
C. Traversal in Both Directions
- Forward:
post.author.email— one extra query unlessselect_relatedwas used. - Reverse:
author.posts.all()whenrelated_name='posts'; otherwise the defaultauthor.post_set.all(). - Cross-relation lookups:
Post.objects.filter(author__email__endswith='@x.com')— the ORM builds the JOIN automatically. - Self- and lazy references:
models.ForeignKey('self', ...)for tree structures;'app.Model'as a string when the target is defined later.
VII. Django Admin — The Auto-Generated Management Interface
A. Enabling and Registering
The admin (django.contrib.admin, enabled by default in startproject) reads model metadata and builds full CRUD screens at /admin/.
- Prerequisites:
django.contrib.admin,auth,contenttypes,sessions,messagesinINSTALLED_APPS;path('admin/', admin.site.urls)inurls.py;python manage.py createsuperuser. - Registration:
PYTHON# blog/admin.py from django.contrib import admin from .models import Post @admin.register(Post) class PostAdmin(admin.ModelAdmin): list_display = ('title', 'author', 'published', 'created_at') list_filter = ('published', 'created_at') search_fields = ('title', 'body') prepopulated_fields = {'slug': ('title',)} list_editable = ('published',) ordering = ('-created_at',) - Related editing:
admin.TabularInline/StackedInlineembed child rows (comments) inside the parent form. - Customisation:
readonly_fields,fieldsets,raw_id_fieldsfor large FK sets, and customactionsfor bulk operations.
B. Adding Groups and Users
- Where: the Authentication and Authorization block on the admin index exposes
UsersandGroupsfromdjango.contrib.auth. - Adding a user: username + password on the add form, then a change form for
first_name,email,is_active,is_staff,is_superuser, group membership and per-user permissions. is_staffvsis_superuser:is_staff=Truealone grants login to/admin/but shows only models the user has permissions for;is_superuser=Trueimplicitly grants every permission.- Groups: a named bundle of permissions (
Editors,Moderators); assigning a user to a group is the maintainable alternative to per-user grants. - Programmatically:
PYTHONfrom django.contrib.auth.models import User, Group u = User.objects.create_user('ravi', 'ravi@x.com', 'Pass@123') g = Group.objects.get(name='Editors') u.groups.add(g); u.is_staff = True; u.save()
C. Users and Permissions
- Automatic permissions: for each model Django creates four
Permissionrows —add_post,change_post,delete_post,view_post(the last added in Django 2.1) — named<app_label>.<codename>. - Checking:
user.has_perm('blog.change_post'); in templates{% if perms.blog.add_post %}; in views@permission_required('blog.add_post')and@login_required. - Resolution order: superuser → user's own
user_permissions→ permissions of all groups the user belongs to. - Custom permissions: declared in
Meta—
PYTHONclass Meta: permissions = [('can_publish', 'Can publish a post')] - Object-level control: override
ModelAdmin.get_queryset()so editors see only their own rows, since built-in permissions are per-model, not per-row.
VIII. Database Configuration — Configuring and Setting Up the Database Connection
A. The DATABASES Setting
All connection details live in the DATABASES dictionary in settings.py; the default alias is compulsory.
DATABASES = {
'default': {
'ENGINE': 'django.db.backends.postgresql',
'NAME': 'blogdb',
'USER': 'bloguser',
'PASSWORD': os.environ['DB_PASSWORD'],
'HOST': '127.0.0.1',
'PORT': '5432',
'CONN_MAX_AGE': 600,
'OPTIONS': {'connect_timeout': 10},
}
}ENGINEvalues:...backends.sqlite3(default;NAMEis a file path such asBASE_DIR / 'db.sqlite3'),...postgresql,...mysql,...oracle.- Drivers: PostgreSQL needs
psycopg2-binary; MySQL needsmysqlclient; SQLite is bundled with Python — install before the firstmigrate, elseImproperlyConfigured. CONN_MAX_AGE: seconds a connection is reused;0closes after each request,Nonekeeps it forever.- Verification:
python manage.py dbshellopens the native client;python manage.py check --database defaultvalidates settings. - Secrets: keep
PASSWORDout of source control via environment variables ordjango-environ.
B. Beyond a Single Database
- Multiple aliases: add e.g.
'replica'alongside'default'and route withDATABASE_ROUTERSorModel.objects.using('replica'). - Legacy schemas:
python manage.py inspectdb > models.pyreverse-engineers models withmanaged = False, so migrations will not attempt to alter those tables. - Transactions: default is autocommit; wrap multi-step writes in
with transaction.atomic():so a failure rolls the whole block back, or setATOMIC_REQUESTS = Trueto wrap every request.
Did this save you a night before the exam?
LPU Notes is free, and it stays free. Ads cover part of the server bill. The rest comes out of a student's own pocket: the domain, the storage, and keeping the site up through the weeks everyone needs it at once.
The payment button didn't load. An ad blocker or a filtered network is the usual reason. to try again.
Nothing here is ever locked, and nothing unlocks. Chip in only if it was worth it. What it pays for →