modelform 에서 form field 만 update 하기

종욱·2020년 4월 8일
0

django

목록 보기
5/5
post-thumbnail

장고 form 을 사용하면서 ModelForm 을 안 쓰는 사람은 아마 없을거다.


문제 상황

근데 모델 폼의 인스턴스를 save() 할 때
폼에서 다루지않는 필드 모두를 기존과 같은 값으로 업데이트를 한다.
문제는 하나의 row에 대해 통째로 덮어쓰기 작업을 한다는 것.

UpdateView 등의 제네릭 뷰의 post 함수 내부 동작은 대략 이렇다.

  • form 의 유효성 검사가 성공했을 때
  • form_valid 함수 내부의 form.save() 를 호출

아래 예시를 보면 모델과 모델 폼이 있다.

# 동아리 모델
class Club(models.Model):
    name = models.CharField('이름', max_length=100, validators=[validate_group_name])
    year = models.PositiveIntegerField('유효 연도')
    REGURAL = 'R'
    AUTONOMY = 'A'
    CLUBTYPE_CHOICES = (
        (REGURAL, '정규'),
        (AUTONOMY, '자율'),
    )
    club_type = models.CharField('동아리 타입', max_length=1, choices=CLUBTYPE_CHOICES, default=REGURAL)
    teachers = models.ManyToManyField(Teacher, verbose_name='담당 교사')
    students = models.ManyToManyField(Student, through='ClubMember', blank=True)

    def __str__(self):
        return self.name
        
    def get_absolute_url(self):
        return reverse('teacher:club', args=[self.id])



# 동아리 정보 수정 폼
class ClubUpdateForm(forms.ModelForm):

    class Meta:
        model = Club
        exclude = ['year', 'students']

이 경우에 동아리 수정 제네릭 뷰를 다음과 같이 만들면,

class ClubUV(UpdateView):
    model = Club
    form_class = ClubForm

DB에 보내는 쿼리는 한 row 를 모두 업데이트하는 형태이다.

// 폼의 필드 외에 업데이트 된 항목이 있음 (year)
UPDATE "teacher_club"
   SET "name" = '야구',
       "year" = 2020,
       "club_type" = 'R'
 WHERE "teacher_club"."id" = 16
 
INSERT INTO "teacher_club_teachers" ("club_id", "teacher_id") 
   SELECT 16, 2

이 문제는 뷰의 form_valid 함수를 재정의하여 해결할 수 있다.
모델의 임시 인스턴스를 저장할 때 update_fields 에 변경할 컬럼만 넘겨주면 된다.

class ClubUV(UpdateView):
    model = Club
    form_class = ClubForm
    
    def form_valid(self, form):
    	self.object = form.save(commit=False)
        self.object.save(update_fields=list(form.fields))
        return HttpResponseRedirect(self.get_success_url())

폼에 ManyToManyField 가 포함된 경우

이 예제에서는 teachers 가 m2m 이라서 위와 같이 수정한 후 업데이트 뷰를 호출하면 아래 에러가 뜬다.

ValueError: The following fields do not exist in this model or are m2m fields: teachers

에러발생 이유는,
teachers 는 Club 모델의 실제 필드가 아니고 관계를 맺는 링크 테이블의 컬럼이기 때문이다.

위에 지나온 쿼리에서 teacher_club_teachers 테이블에 동아리 담당 교사 정보가 따로 추가/삭제 되는 것을 볼 수 있다.

github: django.forms.models 의 save 함수를 보면,
commit=True 인 경우에만 save_m2m 함수를 직접 호출해준다.
이 부분을 고려해서 다시 재정의 해보자.

class ClubUV(UpdateView):
    model = Club
    form_class = ClubForm
    
    def form_valid(self, form):
    	# [A] 폼 필드의 집합
        form_fieldset = set(form.fields)
        # [B] 모델 폼의 모델 중 m2m 필드의 집합
        m2m_fieldset = set(field.name for field in form._meta.model._meta.many_to_many)
        # [A-B] 폼 필드 중 m2m 필드를 제거한 집합
        update_fieldset = form_fieldset - m2m_fieldset
        self.object = form.save(commit=False)
        self.object.save(update_fields=update_fieldset)
        # [A] 와 [B] 의 교집합이 있는 경우,
        # (즉, 폼 필드 중 m2m 필드가 있는 경우)
        if form_fieldset & m2m_fieldset:
            form.save_m2m()
        return HttpResponseRedirect(self.get_success_url())

이제 원하던 대로 된다.

// 폼의 필드에 대해서만 업데이트 됨
UPDATE "teacher_club"
   SET "name" = '야구',
       "club_type" = 'R'
 WHERE "teacher_club"."id" = 16
 
// m2m 필드 추가,삭제도 정상 동작
DELETE
  FROM "teacher_club_teachers"
 WHERE ("teacher_club_teachers"."club_id" = 16 AND "teacher_club_teachers"."teacher_id" IN (2))

모델폼에서 지원하지 않는 이유가 도데체 뭘까?

다소 복잡하게 해결한 것 처럼 느껴진다.
분명 더 나은 해결책이 있을 듯 싶다.
(이유를 아시거나, 더 좋은 방법을 아시는 분은 알려주세요)


References

0개의 댓글