본문 바로가기

문돌이 존버/Django 스터디

장고(Django) 관계(relationship)를 표현하는 모델 필드, ForeignKey

반응형
본 글은 Holix의 "리액트와 함께 장고 시작하기 Complete" 강의를 듣고 작성한 일지입니다.

이번 시간엔 관계(relationship)를 표현하는 모델 필드에 대해 알아보려고 합니다. 사실상 장고 이전에 DB에 대한 개념이 어느정도 있어야 하지만, DB 관련 내용은 크게 다루지 않겠습니다.

간단하게 RDMBS에서의 관계 예시를 짚고 넘어가겠습니다.

1:N 관계 -> models.ForeignKey 로 표현
- 1명의 유저(User)가 쓰는 다수의 포스팅(Post)
- 1명의 유저(User)가 쓰는 다수의 댓글(Comment)
- 1개의 포스팅(Post)에 다수의 댓글(Comment)

1:1 관계 -> models.OneToOneField 로 표현
- 1명의 유저(User)는 1개의 프로필(Profile)

M:N 관계 -> models.ManyToManyField 로 표현
- 1개의 포스팅(Post)에는 다수의 태그(Tag)
  (1개의 태그(Tag)에는 다수의 포스팅(Post))

장고에서 ForeignKey(to, on_delete) 는 1:N 관계에서 N측에 명시를 합니다.

to: 대상모델
- 클래스를 직접 지정하거나, 클래스명을 문자열로 지정
- 자기 참조는 "self" 지정

on_delete: Record 삭제 시 규칙
- CASCADE: FK로 참조하는 다른 모델의 Record도 삭제
- PROTECT: ProtectedError를 발생시키며 삭제 방지
- SET_NULL: null로 대체. 필드에 null=True 옵션 필수
- SET: 대체할 값이나 함수 지정. 함수의 경우 호출하여 리턴값 사용
- DO_NOTHING: 어떠한 액션 X, 단 DB에 따라 오류 발생 가능성 존재
# models.py
class Comment(models.Model):
    post = models.ForeignKey(Post, on_delete=models.CASCADE) # 'POST'라고 문자열 형태도 가능
    message = models.TextField()
    created_at = models.DateTimeField(auto_now_add=True)
    updated_at = models.DateTimeField(auto_now=True)

ForeignKey 를 지정하면 N측에서 참조하는 대상의 pk(=id) 값을 가져옵니다. 일단 데이터베이스 스키마를 업데이트하고 어떻게 표현되는지 살펴보시죠.

python manage.py makemigrations blog1
python manage.py sqlmigrate blog1 0004

위에서 0004는 makemigrations 이후 나타나는 파일명을 보고 정하셔야 합니다.

참조하는 대상의 pk 값을 가져오면 디폴트로 "post_id"라고 표현이 됩니다.


ForeignKey 가 걸린 모델을 다룰 때 일부 데이터를 추출하시려면 기억하셔야 할 문법들이 있습니다.

python manage.py shell

먼저 댓글이 어느 포스팅에 달려 있는지 살펴보기 위해서 아래와 같은 방법을 사용합니다.

from blog1.models import Post, Comment
comment = Comment.objects.first()

# 1번 방법
Post.objects.get(pk=comment.post_id)
# 2번 방법
comment.post

반대로 포스팅에 어떤 댓글이 달려 있는지 살펴보기 위해선 아래 방법들을 사용할 수 있습니다.

comment.post_id # 2
post = Post.objects.get(pk=2)

# 1번 방법
Comment.objects.filter(post_id=2)

# 2번 방법
Comment.objects.filter(post__id=2) # Post 모델의 id를 직접 참조

# 3번 방법
Comment.objects.filter(post=post)

# 4번 방법(추천)
post.comment_set.all()

위의 4번과 같은 방법을 reverse_name 이라고 표현합니다. reverse_name 이란 참조 대상이 되는 모델에서 본인을 참조하는 모델의 데이터에 접근할 때 사용하는 형식입니다. 디폴트는 "모델명소문자_set" 입니다.

하지만 reverse_name 디폴트 명은 앱 이름을 고려하지 않고 모델명만 고려하기 때문에 충돌 가능성이 존재합니다. 이렇게 되면 makemigrations 명령이 실패하기 때문에 충돌을 피해야 하는데요. 이러한 이름 충돌을 피하는 방법은 2가지가 있습니다.

1. 어느 한쪽의 FK에 대해, reverse_name을 포기 -> related_name='+'

2. 어느 한쪽의 (혹은 모두) FK의 reverse_name을 변경
ex) FK(User, ..., related_name='blog_post_set')
     FK(User, ..., related_name='shop_post_set')

ForeignKey 에는 limit_choices_to 라는 옵션이 또 있습니다(ManyToManyField 에서도 지원). Form을 통한 Choice 위젯에서 선택항목을 제한하는 것이 가능한 기능인데, 이해를 돕기 위해 아래 페이지를 살펴보시죠.

댓글을 작성할 때 선택할 수 있는 포스팅은 총 2개입니다. 아무런 제한조건이 없기 때문에 모든 포스팅에 댓글을 달 수 있는 상황입니다. 여기서 제한조건을 추가하여 선택항목을 좁히는 것이 limit_choices_to 라는 파라미터의 역할입니다.

dict/Q 객체를 통한 지정: 일괄 지정
dict/Q 객체를 리턴하는 함수 지정: 매번 다른 조건 지정 가능
# models.py
class Comment(models.Model):
    post = models.ForeignKey(Post, on_delete=models.CASCADE,
                            limit_choices_to={'is_public': True})
    message = models.TextField()
    created_at = models.DateTimeField(auto_now_add=True)
    updated_at = models.DateTimeField(auto_now=True)

728x90
반응형