Welcome to the first lesson of our course on "Advanced Database Schema Design in Django." In this lesson, we will introduce you to the concept of one-to-one relationships in databases. Understanding how to create and manage these relationships is essential for building robust and efficient applications.
By the end of this lesson, you will know how to:
- Create a
Note
model. - Link the
Note
model to an existingTodo
model using a one-to-one relationship. - Create serializers for both models.
- Use fixtures to pre-load data.
- Write tests to ensure everything works correctly.
Let's get started!
Before we dive into creating new models, let's quickly recap our existing setup. In previous lessons, you learned how to create a Todo
model. Here is what the model looks like:
Python1from django.db import models 2 3class Todo(models.Model): 4 task = models.CharField(max_length=255) 5 completed = models.BooleanField(default=False) 6 priority = models.IntegerField() 7 assignee = models.CharField(max_length=255, blank=True, null=True) 8 group = models.CharField(max_length=255, blank=True, null=True)
This model represents tasks that need to be done, with fields for the task name, completion status, priority, assignee, and group information.
Now, let's create a Note
model to represent notes that can be linked to Todo
tasks.
Here's how you can create the Note
model:
Python1from django.db import models 2 3class Note(models.Model): 4 content = models.TextField() 5 6 def __str__(self): 7 return self.content[:50]
content
: ATextField
to store the note's text content.__str__
method: Returns the first 50 characters of the note's content for easy identification.
Now, imagine that in your app, one note
item always corresponds to exactly one todo
item. For example, notes can be additional details added to a Todo
item card. Why don't we make a note of a simple CharField
inside the Todo
model? Well, there could be several reasons not to do this. The most simple one is that the note could be created by another user, and the creator of a card shouldn't have access to edit or remove a note, while the note's creator shouldn't have access to modify todo's fields. In this case, to control the access rights, it would be the best option to create a separate Note
model and link it to the Todo
model.
To link a Note
model with a Todo
model using a one-to-one relationship, we will add a OneToOneField
to the Todo
model.
Here is the updated Todo
model:
Python1from django.db import models 2 3class Todo(models.Model): 4 task = models.CharField(max_length=255) 5 completed = models.BooleanField(default=False) 6 priority = models.IntegerField() 7 assignee = models.CharField(max_length=255, blank=True, null=True) 8 group = models.CharField(max_length=255, blank=True, null=True) 9 note = models.OneToOneField(Note, on_delete=models.SET_NULL, null=True, blank=True) 10 11 def __str__(self): 12 return self.task
note
is aOneToOneField
that links eachTodo
instance to aNote
instance.on_delete=models.SET_NULL
: If theNote
instance is deleted, set thenote
field in theTodo
model toNone
.null=True, blank=True
: Allows thenote
field to be optional.
Django provides several other options for the on_delete
argument, which defines what happens when the referenced object (in this case, Note
) is deleted. Here are some alternatives:
-
CASCADE
: Deletes theTodo
instance when the associatedNote
is deleted.Python1note = models.OneToOneField(Note, on_delete=models.CASCADE)
-
PROTECT
: Prevents deletion of theNote
if it is referenced by aTodo
instance.Python1note = models.OneToOneField(Note, on_delete=models.PROTECT)
-
SET_DEFAULT
: Sets thenote
field to its default value when theNote
instance is deleted.Python1note = models.OneToOneField(Note, on_delete=models.SET_DEFAULT, default=None)
-
SET
: Sets thenote
field to a specified value or function when theNote
instance is deleted. Here's a concrete example of using a function:Python1def get_placeholder_note(): 2 return Note.objects.create(content='Placeholder note') 3 4note = models.OneToOneField(Note, on_delete=models.SET(get_placeholder_note))
In this example, when a
Note
is deleted, theget_placeholder_note
function is called to create and assign a new placeholderNote
to theTodo
. -
DO_NOTHING
: Does nothing on deletion and lets you handle the deletion logic manually.Python1note = models.OneToOneField(Note, on_delete=models.DO_NOTHING)
Each of these options serves different use cases, and the choice depends on your application's requirements.
To ensure the on_delete
behavior is working as expected, we can write tests using Django's TestCase
. For instance, to test the PROTECT
option, you can use assertRaises
to check that attempting to delete a Note
linked with a Todo
raises a ProtectedError
.
Here's an example test:
Python1from django.db import models 2from django.test import TestCase 3from .models import Todo, Note 4 5class OnDeleteOptionTestCase(TestCase): 6 fixtures = ['one_to_one.json'] 7 8 def test_protected_note_deletion(self): 9 note = Note.objects.get(pk=1) 10 with self.assertRaises(models.ProtectedError): 11 note.delete()
In this example:
test_protected_note_deletion
: A test method to verify thePROTECT
option.note = Note.objects.get(pk=1)
: Retrieves aNote
instance with primary key1
.with self.assertRaises(models.ProtectedError)
: Checks that attempting to execute the code inside this block (which deletes theNote
instance) raises aProtectedError
.
To handle the creation of Note
instances via an API, we'll create views in Django using Django Rest Framework.
Here's how to do it:
- Create a view for the
Note
model.
Python1from rest_framework import viewsets 2from .models import Note 3from .serializers import NoteSerializer 4 5class NoteViewSet(viewsets.ModelViewSet): 6 queryset = Note.objects.all() 7 serializer_class = NoteSerializer
- Include the
NoteViewSet
in yoururls.py
.
Python1from django.urls import path, include 2from rest_framework.routers import DefaultRouter 3from .views import NoteViewSet, TodoViewSet 4 5router = DefaultRouter() 6router.register(r'notes', NoteViewSet) 7router.register(r'todos', TodoViewSet) 8 9urlpatterns = [ 10 path('', include(router.urls)), 11]
DefaultRouter
is a class provided by Django Rest Framework. It automatically generates URL patterns for the registered viewsets. When you register a viewset with DefaultRouter
, it creates a set of URLs that handle standard CRUD operations (create, retrieve, update, and delete) on the registered model.
The r
before string patterns like r'notes'
denotes a raw string literal in Python. Raw strings treat backslashes as literal characters and do not interpret them as escape characters. This is especially useful in situations where backslashes are common, such as in regular expressions or Windows file paths.
Fixtures allow us to pre-load data into our database. Here’s how to create a JSON fixture file to load initial data for the Note
and Todo
models.
Create a file named one_to_one.json
in the fixtures
directory with the following content:
JSON1[ 2 { 3 "model": "myapp.note", 4 "pk": 1, 5 "fields": { 6 "content": "This is a note for the first task." 7 } 8 }, 9 { 10 "model": "myapp.todo", 11 "pk": 1, 12 "fields": { 13 "task": "Task with a note", 14 "completed": false, 15 "priority": 1, 16 "note": 1 17 } 18 } 19]
model
: Specifies the model to which the data belongs (myapp.note
ormyapp.todo
).pk
: Primary key for the model instance.fields
: Defines the data fields and their values. Note that we represent atodo
's note with its primary key. Django will automatically find thetodo
's note by the provided id and fetch its information.
Writing tests ensures our code works as expected. Here are example test cases to validate the one-to-one relationship between the Todo
and Note
models.
Python1from django.test import TestCase 2from .models import Todo, Note 3 4class OneToOneRelationshipTestCase(TestCase): 5 fixtures = ['one_to_one.json'] 6 7 def test_todo_has_note(self): 8 todo = Todo.objects.get(pk=1) 9 self.assertIsNotNone(todo.note) 10 self.assertEqual(todo.note.content, "This is a note for the first task.") 11 12 def test_create_todo_with_note_via_api(self): 13 # Create the Note object first 14 note_data = { 15 'content': 'This is a new note.' 16 } 17 note_response = self.client.post('/api/notes/', data=note_data, format='json') 18 note_id = note_response.data['id'] 19 20 # Create the Todo object and associate it with the created Note 21 todo_data = { 22 'task': 'New task with note', 23 'completed': False, 24 'priority': 2, 25 'note': note_id 26 } 27 response = self.client.post('/api/todos/', data=todo_data, format='json') 28 self.assertEqual(response.status_code, 201) 29 self.assertEqual(Todo.objects.count(), 2) 30 self.assertEqual(Note.objects.count(), 2)
class OneToOneRelationshipTestCase(TestCase)
: Defines a test case for testing our models.fixtures = ['one_to_one.json']
: Specifies the JSON fixture file to pre-load initial data.
The first test method checks if the Todo
instance retrieved via get(pk=1)
has an associated Note
. It then asserts that the note
field of the Todo
is not None
and validates the note's content.
The second test method simulates creating a Note
instance via an API call, followed by a Todo
instance that associates with the newly created Note
. First, it constructs the note_data
dictionary and sends a POST request to '/api/notes/'. It then retrieves the note_id
from the response. Next, it embeds the note_id
into the todo_data
dictionary and sends another POST request to '/api/todos/' with the todo_data
. The test verifies that the response status code is 201
(Created) and also checks that the counts of Todo
and Note
instances in the database have increased accordingly.
In this lesson, we covered:
- Creating a
Note
model. - Establishing a one-to-one relationship between
Todo
andNote
. - Creating views to handle creation of
Note
instances. - Serializing the models.
- Using fixtures to load initial data.
- Writing test cases to validate the relationship.
Take your time to review the code examples and understand how each part contributes to the overall functionality. Now, it's your turn to practice and reinforce what you've learned. Good luck, and happy coding!