Django

[DRF] Serializer relations

r잡초처럼 2023. 2. 26. 20:06

Model 간의 관계를 Serializer를 통해 표현하려면 어떻게 해야 할까? 한 번 알아보자.

데이터 간의 관계는 OneToOne, ForeignKey, ManyToMany 등 다양하다. 이를 Serializer에서 표현하려면 어떻게 할까?

 

NOTE! 
DRF에서는 쿼리 최적화까진 해주지 않는다. 이는 프로그래머의 몫이므로 쿼리 최적화를 하자.

API Reference

앨범과 트랙 모델을 통해 자세히 살펴보자

class Album(models.Model):
    album_name = models.CharField(max_length=100)
    artist = models.CharField(max_length=100)

class Track(models.Model):
    album = models.ForeignKey(Album, related_name='tracks', on_delete=models.CASCADE)
    order = models.IntegerField()
    title = models.CharField(max_length=100)
    duration = models.IntegerField()

    class Meta:
        unique_together = ['album', 'order']
        ordering = ['order']

    def __str__(self):
        return '%d: %s' % (self.order, self.title)

StringRelatedField

관계의 대상을 __str__ 메서그를 통해 string으로 표현할 수 있다.

class AlbumSerializer(serializers.ModelSerializer):
    tracks = serializers.StringRelatedField(many=True)

    class Meta:
        model = Album
        fields = ['album_name', 'artist', 'tracks']

다음과 같이 표현된다.

{
    'album_name': 'Things We Lost In The Fire',
    'artist': 'Low',
    'tracks': [
        '1: Sunflower',
        '2: Whitetail',
        '3: Dinosaur Act',
        ...
    ]
}

PrimaryKeyRelatedField

PK를 관계의 대상을 나타내는 데 사용할 수 있다.

class AlbumSerializer(serializers.ModelSerializer):
    tracks = serializers.PrimaryKeyRelatedField(many=True, read_only=True)

    class Meta:
        model = Album
        fields = ['album_name', 'artist', 'tracks']
{
    'album_name': 'Undun',
    'artist': 'The Roots',
    'tracks': [
        89,
        90,
        91,
        ...
    ]
}

 

기본적으로 이 필드는 read-write 이지만, read_only 플래그를 사용할 수 있다.

HyperlinkedRelatedField

하이퍼링크된 관련 필드로 대상을 나타낼 수 있다.

class AlbumSerializer(serializers.ModelSerializer):
    tracks = serializers.HyperlinkedRelatedField(
        many=True,
        read_only=True,
        view_name='track-detail'
    )

    class Meta:
        model = Album
        fields = ['album_name', 'artist', 'tracks']
{
    'album_name': 'Graceland',
    'artist': 'Paul Simon',
    'tracks': [
        'http://www.example.com/api/tracks/45/',
        'http://www.example.com/api/tracks/46/',
        'http://www.example.com/api/tracks/47/',
        ...
    ]
}

Nested relationships

사실 이거 때문에 이 포스팅을 쓰게 되었다. 중첩 관계를 표현할 때 유용한다. 필드가 to-many 관계를 나타내는 데 사용되는 경우 serializer 필드에 many=True 플래그를 추가해야 한다.

class TrackSerializer(serializers.ModelSerializer):
    class Meta:
        model = Track
        fields = ['order', 'title', 'duration']

class AlbumSerializer(serializers.ModelSerializer):
    tracks = TrackSerializer(many=True, read_only=True)

    class Meta:
        model = Album
        fields = ['album_name', 'artist', 'tracks']
>>> album = Album.objects.create(album_name="The Grey Album", artist='Danger Mouse')
>>> Track.objects.create(album=album, order=1, title='Public Service Announcement', duration=245)
<Track: Track object>
>>> Track.objects.create(album=album, order=2, title='What More Can I Say', duration=264)
<Track: Track object>
>>> Track.objects.create(album=album, order=3, title='Encore', duration=159)
<Track: Track object>
>>> serializer = AlbumSerializer(instance=album)
>>> serializer.data
{
    'album_name': 'The Grey Album',
    'artist': 'Danger Mouse',
    'tracks': [
        {'order': 1, 'title': 'Public Service Announcement', 'duration': 245},
        {'order': 2, 'title': 'What More Can I Say', 'duration': 264},
        {'order': 3, 'title': 'Encore', 'duration': 159},
        ...
    ],
}

Writable

중첩 관계는 read-only이다. 따라서 쓰기 작업을 지원하려면 create 및 update 메서드를 만들어야 한다.

class TrackSerializer(serializers.ModelSerializer):
    class Meta:
        model = Track
        fields = ['order', 'title', 'duration']

class AlbumSerializer(serializers.ModelSerializer):
    tracks = TrackSerializer(many=True)

    class Meta:
        model = Album
        fields = ['album_name', 'artist', 'tracks']

    def create(self, validated_data):
        tracks_data = validated_data.pop('tracks')
        album = Album.objects.create(**validated_data)
        for track_data in tracks_data:
            Track.objects.create(album=album, **track_data)
        return album

>>> data = {
    'album_name': 'The Grey Album',
    'artist': 'Danger Mouse',
    'tracks': [
        {'order': 1, 'title': 'Public Service Announcement', 'duration': 245},
        {'order': 2, 'title': 'What More Can I Say', 'duration': 264},
        {'order': 3, 'title': 'Encore', 'duration': 159},
    ],
}
>>> serializer = AlbumSerializer(data=data)
>>> serializer.is_valid()
True
>>> serializer.save()
<Album: Album object>

Custom relational fields

필요한 표현에 맞는 관계형 스타일이 없는 경우 구현할 수 있다. RelatedField를 재정의하고 to_representation() 메서드를 구현해야 한다. 이 메서드는 필드의 대상을 값 인수로 사용하며 대상을 직렬화하는 데 사용해야 하는 표현을 반환해야 한다. 값 인수는 일반적으로 모형 인스턴스가 된다. 읽기-쓰기 관계형 필드를 구현하려면 .to_internal_value() 메서드도 구현해야 한다. 콘텍스트를 기반으로 동적 쿼리 집합을 제공하려면 클래스에서 .queryset을 지정하거나 필드를 초기화할 때 대신 .get_queryset(self)을 재지정할 수도 있다

import time

class TrackListingField(serializers.RelatedField):
    def to_representation(self, value):
        duration = time.strftime('%M:%S', time.gmtime(value.duration))
        return 'Track %d: %s (%s)' % (value.order, value.name, duration)

class AlbumSerializer(serializers.ModelSerializer):
    tracks = TrackListingField(many=True)

    class Meta:
        model = Album
        fields = ['album_name', 'artist', 'tracks']
{
    'album_name': 'Sometimes I Wish We Were an Eagle',
    'artist': 'Bill Callahan',
    'tracks': [
        'Track 1: Jim Cain (04:39)',
        'Track 2: Eid Ma Clack Shaw (04:19)',
        'Track 3: The Wind and the Dove (04:34)',
        ...
    ]
}

Reverse relations

역방향 관계는 ModelSerializer 및 HyperlinkedModelSerializer 클래스에 자동으로 포함되지 않는. 역방향 관계를 포함하려면 필드 목록에 역방향 관계를 명시적으로 추가해야 한다

class AlbumSerializer(serializers.ModelSerializer):
    class Meta:
        fields = ['tracks', ...]

일반적으로 필드 이름으로 사용할 수 있는 관계에 대한 적절한 related_name 인수를 설정해야 한다. 

class Track(models.Model):
    album = models.ForeignKey(Album, related_name='tracks', on_delete=models.CASCADE)
    ...

만약 이름을 설정하지 않은 경우 인수에서 자동으로 생성된 이름을 사용해야 한다.

class AlbumSerializer(serializers.ModelSerializer):
    class Meta:
        fields = ['track_set', ...]