Lesson 2
One-to-Many Relationship in Django
Introduction

Welcome back! In our previous lesson, we explored how to implement a one-to-one relationship in Django by linking a Note model to an existing Todo model. This relationship allowed each Todo item to have a unique note associated with it.

Today, we will advance to another fundamental concept in database schema design: the one-to-many relationship. This type of relationship is essential in many real-world scenarios, such as associating multiple tasks with a single project or user. By the end of this lesson, you will learn how to define a one-to-many relationship in Django, serialize the data, and perform CRUD (Create, Read, Update, Delete) operations through a REST API.

Model Design for One-to-Many Relationship

Let's imagine that we want to store task groups as a separate model to bind more information to a group, such as a group's access rights or settings. In this case, each Group instance can have multiple corresponding Todo instances (as each group stores multiple items), but each Todo instance can be inside only one group. This is called a one-to-many relationship.

Here's how you can define this in models.py:

Python
1from django.db import models 2 3class Group(models.Model): 4 name = models.CharField(max_length=255) 5 6 def __str__(self): 7 return self.name 8 9class Todo(models.Model): 10 task = models.CharField(max_length=255) 11 completed = models.BooleanField(default=False) 12 priority = models.IntegerField() 13 assignee = models.CharField(max_length=255, blank=True, null=True) 14 group = models.ForeignKey(Group, on_delete=models.CASCADE, null=True, blank=True) 15 16 def __str__(self): 17 return self.task
  1. Group Model:

    • name: A character field to hold the group name.
  2. Todo Model:

    • group: A foreign key to the Group model, establishing a one-to-many relationship. If a Group is deleted, all the associated Todo tasks will be deleted (on_delete=models.CASCADE). This field can be null or left blank, allowing creation of Todo instances without a specified group.

This setup allows each Todo to be associated with one Group, but each Group can have multiple Todos.

One-to-Many Relationship in SQL

In SQL databases, a one-to-many relationship is implemented using a foreign key. The foreign key field in the child table references the primary key of the parent table. In our case, the Todo table has a foreign key referencing the Group table. Here’s an illustration of how these tables might look:

idname
1Group A
2Group B
idtaskcompletedpriorityassigneegroup_id
1Task in group AFalse11
2Another task in ATrue21
3Task in group BFalse12

Here, we have two tasks (1 and 2) that belong to one group (they both have group_id = 1).

Serializers for One-to-Many Relationship

Serializers help convert complex data types, such as querysets and model instances, into native Python data types that can then be easily rendered into JSON and vice versa.

Here's how to create serializers for the Group and Todo models in serializers.py:

Python
1from rest_framework import serializers 2from .models import Todo, Group 3 4class GroupSerializer(serializers.ModelSerializer): 5 class Meta: 6 model = Group 7 fields = ['id', 'name'] 8 9class TodoSerializer(serializers.ModelSerializer): 10 class Meta: 11 model = Todo 12 fields = '__all__'
  1. GroupSerializer:

    • This serializer converts the Group model into JSON format and vice versa. The fields being serialized are id and name.
  2. TodoSerializer:

    • This serializer handles the Todo model. By using fields = '__all__', it includes all fields of the Todo model for serialization and deserialization.

Serializers are crucial for ensuring that data sent to and received from the API is in the correct format.

Fixtures for Testing One-to-Many Relationship

Fixtures are a convenient way to add sample data to your tests. They allow you to create a consistent testing environment.

Here's a fixture that will create initial data to test our one-to-many relationship, to be placed in fixtures/one_to_many.json:

JSON
1[ 2 { 3 "model": "myapp.group", 4 "pk": 1, 5 "fields": { 6 "name": "Group A" 7 } 8 }, 9 { 10 "model": "myapp.todo", 11 "pk": 1, 12 "fields": { 13 "task": "Task in group A", 14 "completed": false, 15 "priority": 1, 16 "group": 1 17 } 18 } 19]
  1. Group:

    • Creates a group with pk (primary key) of 1 and the name "Group A".
  2. Todo:

    • Adds a task associated with "Group A". It is not completed, has priority 1, and references the created group using group: 1.

Fixtures help streamline testing by loading predefined sets of data into the database.

Testing the One-to-Many Relationship: Basic Test

It is important to test your models and their relationships to ensure they work as intended. Here is a test case in tests.py to check if a Todo item has an associated Group:

Python
1from django.test import TestCase 2from .models import Todo, Group 3 4class OneToManyRelationshipTestCase(TestCase): 5 fixtures = ['one_to_many.json'] 6 7 def test_todo_has_group(self): 8 todo = Todo.objects.get(pk=1) 9 self.assertIsNotNone(todo.group) 10 self.assertEqual(todo.group.name, "Group A")

This test checks if the Todo item has an associated Group and verifies the group's name. It uses the fixture data we defined earlier.

Testing the One-to-Many Relationship: Advanced Test

The following test case in tests.py goes further to create new Group and Todo records and verify their relationships:

Python
1from django.test import TestCase 2from .models import Todo, Group 3 4class OneToManyRelationshipTestCase(TestCase): 5 fixtures = ['one_to_many.json'] 6 7 def test_create_todos_with_group(self): 8 group_data = { 9 'name': 'Group B' 10 } 11 group_response = self.client.post('/api/groups/', data=group_data, format='json') 12 self.assertEqual(group_response.status_code, 201) 13 group_id = group_response.data['id'] 14 15 todo_data_1 = { 16 'task': 'Task 1 with group B', 17 'completed': False, 18 'priority': 1, 19 'group': group_id 20 } 21 todo_response_1 = self.client.post('/api/todos/', data=todo_data_1, format='json') 22 self.assertEqual(todo_response_1.status_code, 201) 23 todo_id_1 = todo_response_1.data['id'] 24 25 todo_data_2 = { 26 'task': 'Task 2 with group B', 27 'completed': True, 28 'priority': 2, 29 'group': group_id 30 } 31 todo_response_2 = self.client.post('/api/todos/', data=todo_data_2, format='json') 32 self.assertEqual(todo_response_2.status_code, 201) 33 todo_id_2 = todo_response_2.data['id'] 34 35 todo_1 = Todo.objects.get(pk=todo_id_1) 36 todo_2 = Todo.objects.get(pk=todo_id_2) 37 self.assertIsNotNone(todo_1.group) 38 self.assertIsNotNone(todo_2.group) 39 self.assertEqual(todo_1.group.id, group_id) 40 self.assertEqual(todo_2.group.id, group_id)

This test case creates a new Group named 'Group B' and associates two new Todo items with this group. It verifies the creation by checking the status codes of the responses. The test then retrieves the Todo items from the database and asserts that they are associated with the correct group.

Testing ensures the integrity and correctness of your application's relationships and functionalities.

Summary and Preparation for Practice

In this lesson, we delved into implementing a one-to-many relationship between Group and Todo models in Django. We covered:

  • Defining the models and their relationships,
  • How one-to-many relationships are stored in SQL tables,
  • Creating serializers to handle the data,
  • Using fixtures to pre-load data for testing,
  • Writing test cases to ensure the integrity of the relationship,
  • Setting up views and URLs for CRUD operations.

Now, it's time to put this knowledge into practice. Proceed to the exercises that follow to reinforce your understanding and gain hands-on experience. Happy coding!

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