:bulb: Django Serializer로 데이터를 저장할 때 ForeignKey 값이 NULL로 인식되어 Not null constraint failed 오류가 발생합니다. 필드에 null=True를 추가하는 대신, Serializer에서 SerializerMethodField()를 제거해 원인을 없애는 방법을 정리합니다.

[01] Not NULL constraint failed: client_testclient.host_server_id

오류 해석

  • Model의 Field가 NOT NULL인데, NULL 값을 입력
  • client_testclient.host_server_id
    • client = App 이름
    • testclient = Client App의 models.py에 작성한 table 명
    • host_server = testclient의 필드명
    • id = host_server는 ForeignKey로 다른 테이블의 id 값을 연결

testclient의 host_server에 입력할 값이 NULL이어서 오류가 발생한다.

일반적인 해결법 = 해당 필드에 null=True 추가

  • 입력 데이터에 null이 들어올 수 있다면, 해당 필드가 null을 허용하도록 모델을 수정한다.
1
2
3
4
5
6
7
8
9
10
11
12
# ex) client/models.py

class HostServer(models.Model):
    address = models.GenericIPAddressField(unique=True, verbose_name='IP Address on Host SErver')

    def __str__(self):
        return self.address
    
class TestClient(models.Model):
    address = models.GenericIPAddressField(null=True, verbose_name='Client IP')
    # host_server = models.ForeignKey(HostServer, on_delete=models.CASCADE)
    host_server = models.ForeignKey(HostServer, null=True, on_delete=models.CASCADE)
1
2
3
4
# ex) Model 수정사항 적용
# django 프로젝트 폴더 경로에서 실행
python manage.py makemigrations
python manage.py migrate

[02] Django Model ForeignKey

  • testclient 테이블의 host_server 필드는 ForeignKey로 구성한다.
    • 한 테이블의 필드가 다른 테이블의 기본 키(Primary Key)를 참조한다.
    • 주로 일 대 다(One-to-Many) 관계에서 사용한다.
      • 고객 테이블 한 건에 주문 테이블 여러 건이 연결된다.
      • 고객 테이블의 고객ID가 기본 키이고, 주문 테이블에는 이 고객ID를 참조하는 ForeignKey를 만든다.
    • 주요 옵션
      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      14
      15
      
      # 부모 객체가 삭제될 때, 이를 참조하는 모든 객체의 처리
      # 함께 삭제
      on_delete=models.CASCADE
      # 부모 객체가 삭제되지 않도록 함
      on_delete=models.PROTECT
      # 부모 객체가 삭제되면 이를 참조하는 외래 키를 Null 설정
      on_delete=models.SET_NULL
      # 부모 객체가 삭제되면 외래 키를 특정 값으로 설정
      on_delete=models.SET_DEFAULT
      
      # Null 값 허용
      null=True
      
      # 중복 값 허용하지 않음
      unique=True
      
  • [01]의 구조에서는 TestClient 테이블의 host_server 필드가 HostServer의 id 값을 참조한다.
    • Django는 모델에 기본 키를 정의하지 않으면 id 필드를 자동으로 만들어 기본 키로 사용한다.

[03] Django REST Framework의 Serializers

  • serializers.py는 Django REST Framework(DRF)에서 활용
    • API 요청과 응답 데이터를 직렬화, 역직렬화하는 모듈
    • 직렬화(serialization)는 데이터 구조를 JSON, XML 등 특정 포맷으로 변환하는 작업이다.
    • 역직렬화는 그 반대 방향의 변환이다.
    • DRF의 serializer 모듈은 이 변환과 함께 데이터 유효성 검사를 수행한다.
  • class Meta:
    • 직렬화 대상을 지정한다.
      • 직렬화할 모델과 필드, 읽기 전용 필드를 지정한다.
      • extra_kwargs로 필드별 필수 여부, 읽기 전용 여부, 쓰기 전용 여부, 기본값을 지정한다.
      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      14
      15
      16
      17
      18
      19
      20
      21
      22
      23
      24
      25
      26
      27
      28
      29
      30
      31
      32
      33
      34
      35
      36
      37
      38
      39
      
      # models.py
      from django.db import models
      
      class MyModel(models.Model):
          name = models.CharField(max_length=100)
          age = models.IntegerField()
          address = models.CharField(max_length=100)
          email = models.EmailField()
          is_active = models.BooleanField(default=True)
      
      # serializers.py
      from rest_framework import serializers
      from myapp.models import MyModel
      
      class MyModelSerializer(serializers.ModelSerializer):
          class Meta:
              model = MyModel  # 직렬화할 모델을 지정
              fields = '__all__'  # 모든 필드를 직렬화
              # fields = ['name', 'age']  # 특정 필드만 직렬화
              read_only_fields = ['age']  # 읽기 전용 필드를 지정
              extra_kwargs = {  # 추가 설정을 지정
                  'name': {'required': True},  # name 필드를 필수로 설정
                  'address': {'read_only': True},  # address 필드를 읽기 전용으로 설정
                  'email': {'write_only': True},  # email 필드를 쓰기 전용으로 설정
                  'is_active': {'default': False},  # is_active 필드의 기본값을 False로 설정
              }
      
      # views.py 에서 사용 예
      data = {
        'name': 'John Doe',
        'email': 'john@example.com'
      }
      
      serializer = MyModelSerializer(data=data)
      if serializer.is_valid():
          instance = serializer.save()
          print(instance)
      else:
          print(serializer.errors)
      
  • serializers.SerializerMethodField()
    • 특정 필드를 사용자 정의 방식으로 직렬화할 때 사용한다.
    • 이 필드는 읽기 전용이다.
      • 예를 들어 User 모델의 시리얼라이저에 full_name 필드를 추가하고, first_namelast_name을 결합한 값을 반환하게 만든다.
    • 값을 반환하는 메서드는 get_필드명(self, obj) 형태로 정의한다.
      • 메서드 이름의 필드명 부분은 SerializerMethodField()로 선언한 필드 이름과 일치해야 한다.
      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      14
      15
      16
      17
      18
      
      '''
      User 모델에 대한 Serializer에 `full_name`이라는 추가 필드를 생성
      이 필드는 User모델의 first_name과 last_name을 결합하여 데이터를 생성
      '''
      
      # serializers.py
      from rest_framework import serializers
      from django.contrib.auth.models import User
      
      class UserSerializer(serializers.ModelSerializer):
          full_name = serializers.SerializerMethodField()
      
          class Meta:
              model = User
              fields = ['username', 'email', 'full_name']
      
          def get_full_name(self, obj):
              return f'{obj.first_name} {obj.last_name}'
      
  • serializers.PrimaryKeyRelatedField()
    • 연결된 객체를 기본 키(id) 값으로 직렬화한다.
    • SerializerMethodField()와 달리 쓰기가 가능하므로, ForeignKey 값을 입력받아야 하는 필드에 사용한다.

[04] Serializer를 활용할 때, Not NULL constraint failed 오류 이유

TestClientSerializer의 잘못된 구성

  • host_server 필드를 그대로 직렬화하면 HostServer 테이블의 id 값이 출력된다.
  • 응답을 읽기 쉽게 만들려고 SerializerMethodField()를 써서 HostServer의 address 값을 반환하도록 구성했다.

    SerializerMethodField()로 정의한 필드는 읽기 전용이다. views.py에서 이 필드에 값을 입력해도 반영되지 않으므로, 저장 시 host_server가 NULL이 되어 오류가 발생한다.

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    
    # client/models.py
    
    class HostServer(models.Model):
        address = models.GenericIPAddressField(unique=True, verbose_name='IP Address on Host SErver')
    
    
    class TestClient(models.Model):
        address = models.GenericIPAddressField(null=True, verbose_name='Client IP')
        host_server = models.ForeignKey(HostServer, on_delete=models.CASCADE)
    
    
    # client/serializers.py
    from rest_framework import serializers
    from .models import HostServer, Testclient
    
    class HostServerSerializer(serializers.ModelSerializer):
        class Meta:
            model = HostServer
            fields = ['address']
    
    
    class TestClientSerializer(serializers.ModelSerializer):
        host_server = serializers.SerializerMethodField()
          
        class Meta:
            model = TestClient
            fields = '__all__'
    
        def get_host_server(self, obj):
            return obj.host_server.address
    

오류 수정

  • 데이터를 읽을 때 id 값 대신 address가 출력되게 하려면 모델에 __str__ 메서드를 정의한다.
  • Serializer에서는 SerializerMethodField() 관련 코드를 삭제한다.

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    
    # client/models.py
    
    class HostServer(models.Model):
        address = models.GenericIPAddressField(unique=True, verbose_name='IP Address on Host SErver')
    
        def __str__(self):
          return self.address
    
    class TestClient(models.Model):
        address = models.GenericIPAddressField(null=True, verbose_name='Client IP')
        host_server = models.ForeignKey(HostServer, on_delete=models.CASCADE)
    
    
    # client/serializers.py
    from rest_framework import serializers
    from .models import HostServer, Testclient
    
    class HostServerSerializer(serializers.ModelSerializer):
        class Meta:
            model = HostServer
            fields = ['address']
              
    
    class TestClientSerializer(serializers.ModelSerializer):
        class Meta:
            model = TestClient
            fields = '__all__'