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.
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
:
Python1from 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
-
Group Model:
name
: A character field to hold the group name.
-
Todo Model:
group
: A foreign key to theGroup
model, establishing a one-to-many relationship. If aGroup
is deleted, all the associatedTodo
tasks will be deleted (on_delete=models.CASCADE
). This field can be null or left blank, allowing creation ofTodo
instances without a specified group.
This setup allows each Todo
to be associated with one Group
, but each Group
can have multiple Todos
.
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:
id | name |
---|---|
1 | Group A |
2 | Group B |
id | task | completed | priority | assignee | group_id |
---|---|---|---|---|---|
1 | Task in group A | False | 1 | 1 | |
2 | Another task in A | True | 2 | 1 | |
3 | Task in group B | False | 1 | 2 |
Here, we have two tasks (1
and 2
) that belong to one group (they both have group_id = 1
).
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
:
Python1from 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__'
-
GroupSerializer:
- This serializer converts the
Group
model into JSON format and vice versa. The fields being serialized areid
andname
.
- This serializer converts the
-
TodoSerializer:
- This serializer handles the
Todo
model. By usingfields = '__all__'
, it includes all fields of theTodo
model for serialization and deserialization.
- This serializer handles the
Serializers are crucial for ensuring that data sent to and received from the API is in the correct format.
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
:
JSON1[ 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]
-
Group:
- Creates a group with
pk
(primary key) of 1 and the name "Group A".
- Creates a group with
-
Todo:
- Adds a task associated with "Group A". It is not completed, has priority 1, and references the created group using
group: 1
.
- Adds a task associated with "Group A". It is not completed, has priority 1, and references the created group using
Fixtures help streamline testing by loading predefined sets of data into the database.
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
:
Python1from 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.
The following test case in tests.py
goes further to create new Group
and Todo
records and verify their relationships:
Python1from 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.
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!