Lesson 1
One-to-One Relationship in Django
Introduction

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:

  1. Create a Note model.
  2. Link the Note model to an existing Todo model using a one-to-one relationship.
  3. Create serializers for both models.
  4. Use fixtures to pre-load data.
  5. Write tests to ensure everything works correctly.

Let's get started!

Recap: Project Setup

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:

Python
1from 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.

Creating the Note Model

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:

Python
1from django.db import models 2 3class Note(models.Model): 4 content = models.TextField() 5 6 def __str__(self): 7 return self.content[:50]
  • content: A TextField to store the note's text content.
  • __str__ method: Returns the first 50 characters of the note's content for easy identification.
Establishing the One-to-One Relationship

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:

Python
1from 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 a OneToOneField that links each Todo instance to a Note instance.
  • on_delete=models.SET_NULL: If the Note instance is deleted, set the note field in the Todo model to None.
  • null=True, blank=True: Allows the note field to be optional.
Other on_delete Options

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 the Todo instance when the associated Note is deleted.

    Python
    1note = models.OneToOneField(Note, on_delete=models.CASCADE)
  • PROTECT: Prevents deletion of the Note if it is referenced by a Todo instance.

    Python
    1note = models.OneToOneField(Note, on_delete=models.PROTECT)
  • SET_DEFAULT: Sets the note field to its default value when the Note instance is deleted.

    Python
    1note = models.OneToOneField(Note, on_delete=models.SET_DEFAULT, default=None)
  • SET: Sets the note field to a specified value or function when the Note instance is deleted. Here's a concrete example of using a function:

    Python
    1def 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, the get_placeholder_note function is called to create and assign a new placeholder Note to the Todo.

  • DO_NOTHING: Does nothing on deletion and lets you handle the deletion logic manually.

    Python
    1note = 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.

Testing on_delete Options

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:

Python
1from 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 the PROTECT option.
  • note = Note.objects.get(pk=1): Retrieves a Note instance with primary key 1.
  • with self.assertRaises(models.ProtectedError): Checks that attempting to execute the code inside this block (which deletes the Note instance) raises a ProtectedError.
Creating Views to Handle Creation of Note Instances

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.
Python
1from 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 your urls.py.
Python
1from 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.

Loading Initial Data with Fixtures

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:

JSON
1[ 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 or myapp.todo).
  • pk: Primary key for the model instance.
  • fields: Defines the data fields and their values. Note that we represent a todo's note with its primary key. Django will automatically find the todo's note by the provided id and fetch its information.
Testing the One-to-One Relationship

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.

Python
1from 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.

Summary and Preparing for Practice

In this lesson, we covered:

  1. Creating a Note model.
  2. Establishing a one-to-one relationship between Todo and Note.
  3. Creating views to handle creation of Note instances.
  4. Serializing the models.
  5. Using fixtures to load initial data.
  6. 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!

Enjoy this lesson? Now it's time to practice with Cosmo!
Practice is how you turn knowledge into actual skills.