Movatterモバイル変換


[0]ホーム

URL:


Skip to content
DEV Community
Log in Create account

DEV Community

Daniel Coker
Daniel Coker

Posted on

     

Updating A Many-To-Many Relationship In Django

There is this feeling you get when you're stuck on a problem and you search Google for an answer but don't find it. It’s even more frustrating when it is a seemingly common problem. That was me recently when I was attempting to update a many-to-many relationship in Django

When developing a Django app, many-to-many relationship use cases will arise at various points. Blog posts and tags, books and authors, and so on are examples of many-to-many relationships. A many-to-many relationship is one where multiple records in one table can be related to multiple records of another table. A book, for example, can have multiple authors, and an author can write multiple books.

The relationship between blog posts and tags will be used in this article. Consider a blog app with multiple posts. Multiple tags can be attached to a post, and a tag can belong to multiple posts. Adding tags to the post was simple for me; I simply needed to use theadd method. Where I struggled was in keeping their relationship up to date. I'd like to be able to remove any tags associated with the post that isn't in the list of tags sent with the update post request and then create new associations with new tags in the list of tags.

There are several approaches that can be taken to accomplish this. One approach is to delete all of the tags associated with the post and then re-add them. However, in this article, we'll use theset method to update their relationship.

To follow this article, I'm assuming you have a Django project with a Django Rest Framework installed. Let's get started.


Setting Up Models and Migrations

To begin, we'll need to create the two models that will be required for this project—TheTag and thePost models. TheTag model only contains the tag's title. While thePost model contains information about the post. TheTag and thePost have a many-to-many relationship.

# blog/models.pyfromdjango.dbimportmodelsclassTag(models.Model):title=models.CharField(max_length=150)def__str__(self):returnself.titleclassPost(models.Model):title=models.CharField(max_length=150)body=models.TextField()tags=models.ManyToManyField(Tag)created_at=models.DateTimeField(auto_now_add=True)updated_at=models.DateTimeField(auto_now=True)def__str__(self):returnself.title
Enter fullscreen modeExit fullscreen mode

Run the following commands to create and run the migrations.

>>> python manage.py makemigrations>>> python manage.py migrate
Enter fullscreen modeExit fullscreen mode

Adding Serializers

We'll add serializers for the models we've created.

# blog/serializers.pyfromrest_frameworkimportserializersfrom.modelsimportPost,TagclassTagSerializer(serializers.ModelSerializer):classMeta:model=Tagfields=('id','title',)classPostSerializer(serializers.ModelSerializer):tags=TagSerializer(many=True,read_only=True)classMeta:model=Postfields=('id','title','body','tags','created_at','updated_at',)
Enter fullscreen modeExit fullscreen mode

Creating A Post With Tags

Before we go on to add the view function to create a post, we will need to add tags to the database using the shell. Run:

>>>pythonmanage.pyshell>>>fromblog.modelsimportTag>>>Tag.objects.create(title='Django')>>>Tag.objects.create(title='Database')
Enter fullscreen modeExit fullscreen mode

To make a post with tags, we must first create a new view function calledcreate_post.

# blog/views.py@api_view(['POST'])defcreate_post(request):...
Enter fullscreen modeExit fullscreen mode

We're making a post with tags in this view function. We begin by passing the request data to thePostSerializer and checking to see if it is valid.

serializer=PostSerializer(data=request.data)serializer.is_valid(raise_exception=True)
Enter fullscreen modeExit fullscreen mode

Once the data is set and valid, we save the post.

post=serializer.save()
Enter fullscreen modeExit fullscreen mode

The following step is to include the tags in the post. Before they can be related in a many-to-many relationship, both records must exist in the database. That is why we had to first create the tags and post.

By looping through the tag ids in the request data, we add the tag to the post. To avoid errors, we check to see if the tag already exists in the database, and then we add the tag to the post using theadd method. We raise aNotFound exception if a tag does not exist in the database.

fortag_idinrequest.data.get('tags'):try:tag=Tag.objects.get(id=tag_id)post.tags.add(tag)exceptTag.DoesNotExist:raiseNotFound()
Enter fullscreen modeExit fullscreen mode

After adding the tags to the post in the above snippet, we will return to the client the response containing the newly created post.

returnResponse(data=serializer.data,status=status.HTTP_201_CREATED)
Enter fullscreen modeExit fullscreen mode

Updating A Post With Tags

This is the primary purpose of this article—updating the many to many relationship between the post and tags. To update the post along with the tags, we need to add another view function calledupdate_post.

# blog/views.py@api_view(['PUT'])defupdate_post(request,pk=None):...
Enter fullscreen modeExit fullscreen mode

In this view function, we begin by retrieving the post using the primary key. If the post does not exist, we raise aNotFound exception.

try:post=Post.objects.get(id=pk)exceptPost.DoesNotExist:raiseNotFound()
Enter fullscreen modeExit fullscreen mode

The post and request data are then passed to thePostSerializer, and the request data is validated. We save the post to the database if it is valid.

serializer=PostSerializer(post,data=request.data)serializer.is_valid(raise_exception=True)serializer.save()
Enter fullscreen modeExit fullscreen mode

We'll need to use theset method to sync the relationship. However, theset method accepts a list of objects. As a result, we must first generate a list of tag objects and then pass it to theset method. To generate this list of objects, we'll loop through the tag ids in the request data, just like we did when we made the post. The tag is then checked to see if it exists in the database (to avoid any errors). If the tag already exists, we add it to the tags list. We raise aNotFound exception if a tag does not exist in the database.

tags=[]fortag_idinrequest.data.get('tags'):try:tag=Tag.objects.get(id=tag_id)tags.append(tag)except:raiseNotFound()
Enter fullscreen modeExit fullscreen mode

We can now set the posts after we've finished creating the list of tags. The set method deletes the association for tags that are no longer in the list and creates new associations for tags that are added to the list.

post.tags.set(tags)
Enter fullscreen modeExit fullscreen mode

After using theset method to sync the posts and tags. We can send the client the response containing the updated post.

returnResponse(data=serializer.data,status=status.HTTP_200_OK)
Enter fullscreen modeExit fullscreen mode

Add Tests

Let's add some test cases to ensure that everything works as expected. First, include the required imports, define the test class, and add a setup to create three tags for us when each test case is run.

# blog/tests.pyimportjsonfromdjango.urlsimportreversefromrest_frameworkimportstatusfromrest_framework.testimportAPITestCasefrom.modelsimportPost,TagclassPostTest(APITestCase):defsetUp(self):self.tag1=Tag.objects.create(title='Django')self.tag2=Tag.objects.create(title='Database')self.tag3=Tag.objects.create(title='Relationship')
Enter fullscreen modeExit fullscreen mode

This test case asserts that the create post endpoint is able to create a new post.

deftest_can_create_post(self):url=reverse('posts-create')data={'title':'Test Post','body':'The body of the test post.','tags':[self.tag1.id,self.tag2.id],}response=self.client.post(url,data,format='json')self.assertEqual(response.status_code,status.HTTP_201_CREATED)
Enter fullscreen modeExit fullscreen mode

This test case ensures that the update post endpoint updates the post details while also establishing the proper relationship between the tags and the updated post.

deftest_can_update_post(self):post=Post.objects.create(title='Test Post',body='The body of the test post.')post.tags.set([self.tag1,self.tag3])url=reverse('posts-update',kwargs={'pk':post.id})data={'title':'Updated Test Post','body':'The body of the updated test post.','tags':[self.tag2.id,self.tag3.id],}response=self.client.put(url,data,format='json')response_data=json.loads(response.content)post=Post.objects.get(id=post.id)self.assertEqual(response.status_code,status.HTTP_200_OK)self.assertEqual(response_data['title'],post.title)self.assertEqual(response_data['body'],post.body)self.assertEqual(len(response_data['tags']),2)self.assertEqual(response_data['tags'][0]['id'],self.tag2.id)self.assertEqual(response_data['tags'][1]['id'],self.tag3.id)
Enter fullscreen modeExit fullscreen mode

Runpython manage.py test to execute tests.


Conclusion

In this article, we discussed what a many-to-many relationship is and how to update a many-to-many relationship using Django. We also added tests to ensure that the implementation works properly. The complete code is available in the GitHub repohere. View the API documentation for sample requestshere.

Top comments(1)

Subscribe
pic
Create template

Templates let you quickly answer FAQs or store snippets for re-use.

Dismiss
CollapseExpand
 
junedang profile image
June Dang
A programmer try to simplify everything

Good article 👍

Are you sure you want to hide this comment? It will become hidden in your post, but will still be visible via the comment'spermalink.

For further actions, you may consider blocking this person and/orreporting abuse

  • Location
    Lagos, Nigeria
  • Joined

Trending onDEV CommunityHot

DEV Community

We're a place where coders share, stay up-to-date and grow their careers.

Log in Create account

[8]ページ先頭

©2009-2025 Movatter.jp