장고 form 을 사용하면서 ModelForm 을 안 쓰는 사람은 아마 없을거다.
근데 모델 폼의 인스턴스를 save() 할 때
폼에서 다루지않는 필드 모두를 기존과 같은 값으로 업데이트를 한다.
문제는 하나의 row에 대해 통째로 덮어쓰기 작업을 한다는 것.
UpdateView 등의 제네릭 뷰의 post 함수 내부 동작은 대략 이렇다.
아래 예시를 보면 모델과 모델 폼이 있다.
# 동아리 모델
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())
이 예제에서는 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))
다소 복잡하게 해결한 것 처럼 느껴진다.
분명 더 나은 해결책이 있을 듯 싶다.
(이유를 아시거나, 더 좋은 방법을 아시는 분은 알려주세요)