본문 바로가기
파이썬(Python)/장고(Django)

[기초] 파이썬 장고 REST API - Relationship 설정하기

by 기계공학 주인장 2024. 2. 5.
반응형

이전 포스팅에서 Model Serializers를 사용하여 Model을 사용하는 방법에 대해 알아봤습니다.

 

https://android-developer.tistory.com/84

 

[기초] 장고 REST API 프레임워크 - Model Serializer

이번 포스팅에서는 Model을 만들 때 보편적으로 사용하는 Model Serialize에 대해 말해보겠습니다. 지금까지는 일반적인 Serialize를 커스텀해서 사용했지만 실제 개발에는 Model Serialize를 주로 사용합니

android-developer.tistory.com

 

이번 포스팅에서는 다수의 Model에 관계를 부여하는 방법에 대해 알아보겠습니다.

 

Model의 관계(Relationship)은 다음과 같은 종류가 있습니다.

  • Many-to many relationships
  • One-to One relationships
  • Many to one relationships

Model 하나 더 생성하기

이전 방법과 똑같이 Model을 하나 더 생성하면 됩니다.

 

단, 조건은 다음과 같습니다.

 

  • 모델 이름은 StreamPlatform
  • id, name, about, website라는 아이템을 가짐
  • movie/stream 에서 해당 Model의 리스트를 볼 수 있어야함(= 현재 movie/list와 동일하게)

정답은 아래의 풀코드를 확인하시길 바랍니다.

 

# api/serializers.p

from rest_framework import serializers
from watchlist_app.models import Movie, StreamPlatform


class StreamPlatfromSerializer(serializers.ModelSerializer):
    class Meta:
        model = StreamPlatform
        fields = "__all__"
    

class MovieSerializer(serializers.ModelSerializer):
    len_name = serializers.SerializerMethodField()
    
    class Meta:
        model = Movie
        # 모든 Field를 사용하게 한다.
        fields = "__all__"
        # 사용하고 싶은 Field를 지정하고 싶으면 다음과 같이 한다
        # fields = ['id', 'name', 'description']
        
        # 또는 특정 Field만 제외하고싶다면 다음과 같이 한다.
        # exclude = ['activity']
        
    def get_len_name(self, object):
        return len(object.name)
        
    def validate(self, data):
        if data['name'] == data['description']:
            raise serializers.ValidationError("이름과 내용이 같아선 안됩니다.")
        else:
            return data
    
    # field level validation
    # validate_필드 이름으로 만들면 자동으로 오버라이드 해준다.
    def validate_name(self, value):
        if len(value) < 2:
        	raise serializers.ValidationError("이름이 너무 짧습니다.")
        else :
            return value

 

 

# api/urls.py

from django.urls import path
from api.views import MovieListAV, MovieDetailAV, StreamPlatformAV, StreamPlatformDetailAV

urlpatterns = [
    path('list/', MovieListAV.as_view(), name='movie-list'),
    path('list/<int:pk>', MovieDetailAV.as_view(), name='movie-detail'),
    path('stream/', StreamPlatformAV.as_view(), name='stream-list'),
    path('stream/<int:pk>', StreamPlatformDetailAV.as_view(), name='stream-detail'),
]

 

 

# api/views.py

from watchlist_app.models import Movie, StreamPlatform
from api.serializers import MovieSerializer, StreamPlatfromSerializer
from rest_framework.response import Response
from rest_framework.decorators import api_view
from rest_framework import status
from rest_framework.views import APIView

class StreamPlatformDetailAV(APIView):
    def get(self, request, pk):
        try:
            platform = StreamPlatform.objects.get(pk=pk)
        except StreamPlatform.DoesNotExist:
            return Response({'Error': '존재하지 않는 데이터를 참조했습니다'}, status=status.HTTP_404_NOT_FOUND)
        
        serializer = StreamPlatfromSerializer(platform)
        return Response(serializer.data)
    
    def put(self, request, pk):
        platform = StreamPlatform.objects.get(pk=pk)
        serializer = StreamPlatfromSerializer(movie, data=request.data)
        if serializer.is_valid():
            serializer.save()
            return Response(serializer.data)
        else:
            return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)
        
    def delete(self, request, pk):
        platform = StreamPlatform.objects.get(pk=pk)
        platform.delete()
        return Response(status=status.HTTP_204_NO_CONTENT)
    

class StreamPlatformAV(APIView):
    def get(self, request):
        platform = StreamPlatform.objects.all()
        serializer = StreamPlatfromSerializer(platform, many=True)
        return Response(serializer.data)
    
    def post(self, request):
        serializer = StreamPlatfromSerializer(data=request.data)
        if serializer.is_valid():
            serializer.save()
            return Response(serializer.data)
        else:
            return Response(serializer.errors)


class MovieListAV(APIView):
    def get(self, request):
        movies = Movie.objects.all()
        serializer = MovieSerializer(movies, many=True)
        return Response(serializer.data)
    
    def post(self, request):
        serializer = MovieSerializer(data=request.data)
        if serializer.is_valid():
            serializer.save()
            return Response(serializer.data)
        else:
            return Response(serializer.errors)
    

class MovieDetailAV(APIView):
    def get(self, request, pk):
        try:
            movie = Movie.objects.get(pk=pk)
        except Movie.DoesNotExist:
            return Response({'Error': '존재하지 않는 데이터를 참조했습니다'}, status=status.HTTP_404_NOT_FOUND)
        
        serializer = MovieSerializer(movie)
        return Response(serializer.data)
    
    def put(self, request, pk):
        movie = Movie.objects.get(pk=pk)
        serializer = MovieSerializer(movie, data=request.data)
        if serializer.is_valid():
            serializer.save()
            return Response(serializer.data)
        else:
            return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)
        
    def delete(self, request, pk):
        movie = Movie.objects.get(pk=pk)
        movie.delete()
        return Response(status=status.HTTP_204_NO_CONTENT)

 

 

# 해당앱폴더/admin.py

from django.contrib import admin
from watchlist_app.models import Movie, StreamPlatform

# Register your models here.
admin.site.register(Movie)
admin.site.register(StreamPlatform)

 

 

# 해당앱폴더/models.py

from django.db import models


class StreamPlatform(models.Model):
    # Model(=DB)에서 사용할 Column 정의
    id = models.BigAutoField(primary_key=True)
    name = models.CharField(max_length=50)
    about = models.TextField(max_length=200)
    website = models.URLField(max_length=100)

    def __str__(self):
        return self.name


class Movie(models.Model):
    # Model(=DB)에서 사용할 Column 정의
    id = models.BigAutoField(primary_key=True)
    name = models.CharField(max_length=50)
    description = models.TextField(max_length=200)
    active = models.BooleanField(default=True)
    created = models.DateTimeField(auto_now_add=True)

    def __str__(self):
        return self.name

Many-to many relationships

장고의 공식 문서에 따르면 

 

https://docs.djangoproject.com/en/5.0/topics/db/examples/many_to_many/

 

Django

The web framework for perfectionists with deadlines.

docs.djangoproject.com

 

Many-to-many relationships는 2개 이상의 Model이 여러 Model의 아이템으로써 사용되는 관계입니다.

 

예를 들어 다음과 같은 Model의 관계를 생각해볼 수 있습니다.

 

from django.db import models


class Publication(models.Model):
    title = models.CharField(max_length=30)

    class Meta:
        ordering = ["title"]

    def __str__(self):
        return self.title


class Article(models.Model):
    headline = models.CharField(max_length=100)
    # Article Model 안에서 Publication Model과 관계를 맺음
    # ManyToManyField를 사용할 경우 반대로 Publication에서 article_set이라는 항목을 자동으로 추가함
    publications = models.ManyToManyField(Publication)

    class Meta:
        ordering = ["headline"]

    def __str__(self):
        return self.headline

 

ManyToManyField 같이 다수의 Model을 참조하는 함수를 사용하면 반대 Model에서도 자동으로 "Model이름_set"이라는 이름으로 항목을 생성합니다.

 

신문사(Publication)은 여러 개의 신문 기사(Article)을 낼 수 있고 신문 기사는 여러 신문사에서 발간될 수 있습니다. 

 

그렇기 때문에 Many-to-many relationships라고 할 수 있습니다.

 

예를 들어 다음과 같이 여러 신문사를 만듭니다.

 

>>> p1 = Publication(title="The Python Journal")
>>> p1.save()
>>> p2 = Publication(title="Science News")
>>> p2.save()
>>> p3 = Publication(title="Science Weekly")
>>> p3.save()

 

그리고 다음과 같이 신문 기사를 하나 생성합니다. 

 

>>> a1 = Article(headline="Django lets you build web apps easily")
>>> a1.save()

 

그리고 해당 신문 기사에 신문사를 추가해줍니다.

 

>>> a1.publications.add(p1)

 

그리고 신문을 하나 더 생성하고 해당 신문을 여러 신문사에서 발간했다고 생각합니다.

 

>>> a2 = Article(headline="NASA uses Python")
>>> a2.save()
>>> a2.publications.add(p1, p2)
>>> a2.publications.add(p3)

 

여기까지 보면 현재 3개의 신문사와 2개의 신문 기사가 있고 신문 기사 2개를 여러 신문사가 발간했다는 것을 알 수 있습니다.

 

그렇기 때문에 하나의 신문 기사를 어떤 신문사에서 발간했는지 다음과 같은 방법으로 확인할 수 있습니다.

 

>>> a1.publications.all()
<QuerySet [<Publication: The Python Journal>]>
>>> a2.publications.all()
<QuerySet [<Publication: Highlights for Children>, <Publication: Science News>, <Publication: Science Weekly>, <Publication: The Python Journal>]>

 

그리고 반대로 특정 신문사에서 발간한 모든 뉴스 기사를 확인할 수 있습니다.

 

>>> p2.article_set.all()
<QuerySet [<Article: NASA uses Python>]>
>>> p1.article_set.all()
<QuerySet [<Article: Django lets you build web apps easily>, <Article: NASA uses Python>]>
>>> Publication.objects.get(id=4).article_set.all()
<QuerySet [<Article: NASA uses Python>]>

One-to One relationships

One-to One relationships은 특정 Model과 1:1 관계를 갖는 갖는 것을 의미합니다.

 

OneToOneField 함수를 통해 구현할 수 있습니다.

 

예시는 다음과 같습니다.

 

from django.db import models


class Place(models.Model):
    name = models.CharField(max_length=50)
    address = models.CharField(max_length=80)

    def __str__(self):
        return f"{self.name} the place"


class Restaurant(models.Model):
    place = models.OneToOneField(
        Place,
        on_delete=models.CASCADE,
        primary_key=True,
    )
    serves_hot_dogs = models.BooleanField(default=False)
    serves_pizza = models.BooleanField(default=False)

    def __str__(self):
        return "%s the restaurant" % self.place.name


class Waiter(models.Model):]
	# models.CASCADE는 부모 모델이 삭제될 때 자식 모델의 행동을 정의한다.
    restaurant = models.ForeignKey(Restaurant, on_delete=models.CASCADE)
    name = models.CharField(max_length=50)

    def __str__(self):
        return "%s the waiter at %s" % (self.name, self.restaurant)

 

Place는 장소를 의미하고 해당 장소에는 하나의 Restaurant만 있을 수 있습니다.

 

그리고 Restaurant에는 Waiter가 존재합니다.

 

하나의 Restaurant에는 다수의 Waiter가 존재할 수 있습니다.

위 코드에서 각각의 Model은 다음과 같은 특징을 갖습니다.

 

Place

  • 3개의 항목을 갖고 있다
  • Restaurant의 models.OneToOneField 함수에 의해 restraurant라는 항목이 자동으로 생성된다.

 

Restarurant

  • Place Model과 1:1 관계를 가지는 항목을 하나 갖고 있다.(models.OneToOneField)
    • Place가 삭제되면 해당 Restaurant도 삭제(on_delete=models.CASCADE)
      • 반대로 특정 restaurant가 삭제된다고 해서 해당 place가 삭제되지는 않는다.
    • restaurant의 각 아이템이 place의 각 아이템과 정확히 일치해야한다.(primary_key=True)
      • 즉, restaurant의 1번 아이템 = place의 1번 아이템
      • 각 Model의 각 아이템이 1:1 매핑되는 것을 의미
  • 총 항목을 3개 갖고 있다.

 

Waiter

  • restaurant Model과 외래키 관계를 갖고 있다.
    • 특정 restaurant가 삭제되면 해당 waiter Model도 삭제됨을 의미한다.(on_delete=models.CASCADE)
  • 총 2개의 항목을 갖고 있다

다음과 같이 Place 아이템을 2개 생성합니다.

 

>>> p1 = Place(name="Demon Dogs", address="944 W. Fullerton")
>>> p1.save()
>>> p2 = Place(name="Ace Hardware", address="1013 N. Ashland")
>>> p2.save()

 

그리고 p1에 대해 restaurant Model을 하나 생성해줍니다.

 

>>> r = Restaurant(place=p1, serves_hot_dogs=True, serves_pizza=False)
>>> r.save()

 

만약 Place Model에 등록되지 않은 Restaurant가 있다면 다음과 같은 방법으로 확인할 수 있습니다.

 

>>> from django.core.exceptions import ObjectDoesNotExist
>>> try:
...     p2.restaurant
... except ObjectDoesNotExist:
...     print("There is no restaurant here.")
...
There is no restaurant here.

 

또는 hasattr 함수를 사용해서 해당 Model에서 특정 항목에 대한 값을 갖고 있는지 확인할 수 있다.

 

>>> hasattr(p2, "restaurant")
False

 

그렇기 때문에 다음과 같이 place를 restaurant에 등록하거나

 

>>> r.place = p2
>>> r.save()
>>> p2.restaurant
<Restaurant: Ace Hardware the restaurant>
>>> r.place
<Place: Ace Hardware the place>

 

반대로 restaurant를 place에 등록할 수 있다.

 

>>> p1.restaurant = r
>>> p1.restaurant
<Restaurant: Demon Dogs the restaurant>

Many to one relationships

Many to one relationships는 ForeignKey 함수로 사용할 수 있습니다.

 

예시 코드는 다음과 같습니다.

 

from django.db import models


class Reporter(models.Model):
    first_name = models.CharField(max_length=30)
    last_name = models.CharField(max_length=30)
    email = models.EmailField()

    def __str__(self):
        return f"{self.first_name} {self.last_name}"


class Article(models.Model):
    headline = models.CharField(max_length=100)
    pub_date = models.DateField()
    reporter = models.ForeignKey(Reporter, on_delete=models.CASCADE)

    def __str__(self):
        return self.headline

    class Meta:
        ordering = ["headline"]

 

Reporter는 여러 개의 Article을 쓸 수 있지만

 

Article에는 하나의 Reporter만 존재할 수 있습니다.

 

각 Model은 다음과 같은 특징을 가집니다.

 

Reporter

  • 4개의 항목을 갖고 있습니다.
  • Article에서 ForeignKey를 사용하여 다수의 Model 아이템을 참조할 수 있게 했기 때문에 "모델이름_set"이라는 이름으로 항목이 자동으로 추가된다
    • 그래서 여기서는 article_set이라는 이름으로 항목이 추가될 예정

Article

  • 3개의 항목을 갖고 있습니다.
  • Repoter Model과 연결된 항목을 갖습니다
    • 해당 Reporter Model이 삭제되면 해당 Article도 삭제됩니다.(on_delete=models.CASCADE)

 

다음과 2개의 Reporter 데이터를 생성합니다.

 

>>> r = Reporter(first_name="John", last_name="Smith", email="john@example.com")
>>> r.save()

>>> r2 = Reporter(first_name="Paul", last_name="Jones", email="paul@example.com")
>>> r2.save()

 

다음과 같이 Article 데이터를 1개 생성합니다.

 

>>> from datetime import date
# 반드시 이미 저장되어있는 reporter를 여기서 사용해야한다.
>>> a = Article(id=None, headline="This is a test", pub_date=date(2005, 7, 27), reporter=r)
>>> a.save()

>>> a.reporter.id
1

>>> a.reporter
<Reporter: John Smith>

 

반대로 Article에 아이템을 만들 때도 Reporter의 아이템을 참조할 수 있습니다.

 

>>> new_article = r.article_set.create(
...     headline="John's second story", pub_date=date(2005, 7, 29)
... )
>>> new_article
<Article: John's second story>
>>> new_article.reporter
<Reporter: John Smith>
>>> new_article.reporter.id
1

 

반응형


"이 포스팅은 쿠팡 파트너스 활동의 일환으로, 이에 따른 일정액의 수수료를 제공받습니다."


댓글