본문 바로가기
WebFramework/Python Django

[Python Django] 17. Django ORM 테이블 사이 1:N 맺기 - ForeignKey

by Rosmary 2024. 4. 29.
728x90
반응형

 

 

 

 

이번 포스팅부터는 Django의 ORM에서 제공하는 테이블 사이 관계를 설정하는 방법에 대해 정리해보려한다. 사실 DB의 테이블도 각각의 데이터가 클래스 객체처럼 동작을 한다고 생각을 한다면, 이 데이터들이 다른 테이블의 데이터와도 상호작용을 할 수 있도록 만들 수 있을 것이다.

 

예를 들어서, 어떤 포스팅(Post)에 댓글(Comment)을 작성한다고 하면, 작성 글을 정보를 담는 Post라는 테이블과, 댓글 정보를 담는 Comment라는 테이블 사이에 연결 고리가 있어야 할 것이다. 위의 예시에서는 하나의 Post에 여러 개의 댓글이 달리기 때문에, Comment 테이블의 각 데이터는 댓글이 작성된 본체(?) 포스팅을 구별할 수 있는 데이터(보통 Primary Key 값을 많이 사용한다)가 테이블 내에 들어가야 할 것이다.

 

다른 예시로는 필자가 예전에 축구할 때 사용해봤던, 팀원끼리 경기 평점을 주는 어플리케이션을 예로 들 수 있는데, 팀원 1이 여러 명의 다른 팀원에게 별점을 부여하고, 다른 팀원들 역시 팀원 1에게 별점을 부여하는 방식이다. 이러한 정보 역시 DB에서 별도의 테이블을 만들어 팀원 간 별점 정보를 저장하도록 한다.

 

Django ORM은 테이블 사이 관계에 따라 테이블 컬럼에 다른 테이블 정보의 PK 값을 Foreign Key로 설정해준다던가, 별도의 테이블을 생성하는 등의 기능을 제공한다. 

 

Django ORM을 통해 구현할 수 있는 Database에서 테이블 사이 관계는 크게 아래와 같이 구분할 수 있다.

 

-  1:1 관계 (One to One)

-  1:N 관계 (One to N or One to Many)

-  N:N 관계 (N to N or Many to Many)

 

이번 포스팅에서는 1:N관계에 대해 먼저 정리해보려한다. 

 

 

 

1. 테스트 환경 설정

 

DB 관계 구현 테스트를 위해, 아래와 같은 프로젝트를 생성하려 한다.

 

[ App 설정 ]

-  members  : 사용자(회원) 기능

-  articles      : 게시글, 댓글 및 좋아요 기능

 

[ members.model 설정 - Members ]

-  username  : email로 설정

-  password  : Django 제공 필드 사용

-  nickname  : 웹 상에 표시할 사용자 이름

-  red_dt       : 가입일

-  update_dt : 회원 정보 수정일

-  last_login_dt: 마지막 접속일

 

[ articles.model 설정 - Articles ]

-  member:  게시글 작성자 정보 저장  (DB 쿼리로는 member_id로 컬럼명을 설정하며, Foreign Key 속성까지 추가한다)

-  title        : 게시글 제목 작성

-  content  :  게시글 본문 작성

-  create_dt: 게시글 작성일

-  update_dt: 게시글 수정일

 

[ articles.model 설정 - Comments ]

-  member     : 댓글 작성자 정보 저장

-  article        :  댓글이 달리는 게시글 정보 저장

-  content      : 댓글 내용 작성

-  created_dt : 게시글 작성일

-  update_dt  : 게시글 수정일

 

 

views.py와 forms.py는 여기서는 중요한 내용이 아니라, 필자가 지금까지의 포스팅에서 해 왔던 대로 진행하려 한다.

대략적인 그림은 아래와 같이 나타난다.

 

 

참고로, settings.py의 LANGUAGE_CODE를 변경했더니 기본 form의 label, help_text와 error_messages가 모두 한글로 표시된다.

 

 

 

 

1. models.OneToMany()로 1:N 관계 구현

 

게시글과 댓글의 관계를 생각해보자. 게시글은 보통 웹 계정이 있는 사용자에 의해 생성이 된다. 한 명의 사용자가 게시글을 아예 생성하지 않을 수도 있고, 반대로 여러 개의 게시글을 생성하는 것도 가능하다. 하지만, 게시글이 여러 명의 사용자 정보를 가지는 것은 불가능하다. 반드시 사용자에 의해 생성되기 때문이다.

 

이렇게 사용자(1):게시글(N)의 관계가 나타나는 경우, 사용자(Members) 테이블과 게시글(Articles) 테이블 사이 1:N 관계를 이루고 있다고 말한다. 이 경우, Members와 Articles의 Model은 아래와 같은 형태를 띄게 된다.

 

 

 

Members class의 경우, 이전의 포스팅에서 작성했던 내용에서 크게 벗어나지 않는다. 다만, Articles는 조금 새로운 것이 보이는데, Members와의 관계를 설정하기 위해 models 모듈 내에 존재하는 ForeignKey라는 클래스를 사용했다.

 

이 ForeignKey는 테이블 내 Field에서 다른 테이블의 PK 값을 지정해야하는 경우, 즉, 다른 테이블의 정보를 참조해야하는 경우에 사용한다. 위의 예시에서는 Article은 반드시 작성자 정보가 필요하며, 작성자는 회원(Members) 테이블에 지정되어 있기 때문에 Members 테이블을 참조하기 위해 ForeignKey 클래스로 연결한 것이라 보면 된다. 인자로 to를 사용하여, 연결하고자 하는 Model의 클래스를 지정한다.

 

 이 ForeignKey는 참조하고자 하는 테이블이 있다면 사용할 수 있기 때문에, 하나 이상의 필드에서 여러 번 사용하여도 문제는 발생하지 않는다. 현재의 테스트 환경 내에서 댓글 테이블인 Comments를 보면,

 

 

 

댓글 작성자에 대한 정보를 지정하기 위해 Members 테이블을 참조하고, 동시에 댓글이 연결된 게시글 정보를 지정하기 위해 Articles를 참조하도록 만들었음에도 동작에 문제는 없는 것이 확인된다.

 

 

 

이제 개별 사용자가 작성한 글 목록만 별도의 페이지에서 볼 수 있도록 제작한다고 가정해보자. 그럼, Article 테이블 내에 존재하는 정보 중, members 필드의 값이 로그인 상태의 User 정보를 담는 Member 객체인 정보만 추출하면 된다. Article 테이블의 members 필드는, Members 클래스의 인스턴스를 담고 있기 때문에, 

 

 

의 형태로 filter 조건에 직접 Members 인스턴스를 지정해주어도 되지만, ForeignKey가 설정되는 즉시, 참조하는 테이블의 ID 값을 직접 바라볼 수 있도록 <FK필드명>_id라는 필드가 DB에 추가되기 때문에 아래와 같이 코드를 작성하여 개별 사용자가 작성한 모든 게시글을 호출하는 것도 가능하다.

 

 

 

결과는 두 방식 모두 아래와 같이 나타난다.

 

 

 

실제 DB에서도 FK 설정된 fields는 <FK필드명>_id 의 필드가 생성되어 있는 것이 확인된다.

 

 

 

혹은 아래와 같이, Members 클래스에서도 직접 articles 정보를 호출하는 것도 가능하다.

 

 

 

"실제 Members 클래스는 article과 관련된 어떠한 필드도 존재하지 않는데, 어떻게 저게 가능한가요?"

 

두 테이블 사이 ForeignKey로 관계가 설정되는 경우, ForeignKey에 의해 참조되는 테이블(Members)은 참조하는 테이블(Articles) 내에 필드가 존재하기에 Articles에서는 Members 정보를 호출할 수 있지만, 반대로 Members는 Articles과 관련된 어떠한 필드도 보이지 않기에 당연히 궁금할 수 밖에 없는 사항이다. Django는 ForeignKey로 설정된 두 테이블 중, 1:N 관계에서 1에 해당하는 테이블에도 N을 참조할 수 있도록 내부적으로 기능을 제공한다. 이를 Django에서는 Backward(역참조)라는 용어로 정의하고 있는데, ForeignKey를 설정한 테이블에 대한 접근을 <lowcase_table_name>_set 형태로 호출할 수 있게 된다.

 

 

즉, 원래는 아래와 같이 articles_set으로 Members 클래스에서 게시글 정보를 조회하는 것이 정석이나,

 

 

ForeignKey 설정 시, related_name을 지정해줌으로써 기본 포맷이 아닌 다른 이름으로도 역참조가 가능하다는 것이다.

 

 

 

 

ForeignKey 클래스를 사용함에 있어서 반드시 필요한 매개변수 중에 on_delete라는 변수가 있다. 이 변수는 테이블이 ForeignKey로 참조하고 있는 정보가 사라지게 되면, 현재 테이블의 데이터에 대한 처리 방식을 지정하는 변수다.

 

예를 들어, 현재 testuser3은 article id 1과 6을 작성했는데, testuser3가 회원 탈퇴를 하게 되면,

 

-  testuser3이 작성한 모든 글을 DB에서 삭제할지: models.CASCADE

-  ForeignKey로 설정된 데이터의 삭제를 불허할지: models.PROTECT

-  testuser3이 작성한 글이 존재한다면 testuser3 계정의 삭제를 불허할 지: models.RESTRICT

-  testuser3이 작성한 모든 글을 Null로 전환할 지: models.SET_NULL

-  testuser3이 작성한 모든 글을 기본값으로 전환할 지: models.SET_DEFAULT

-  아무것도 하지 않을지: models.DO_NOTHING

 

등의 로직을 on_delete 값을 통해 지원한다.

 

https://docs.djangoproject.com/en/5.0/ref/models/fields/#django.db.models.ForeignKey

 

Model field reference | Django documentation

The web framework for perfectionists with deadlines.

docs.djangoproject.com

 

 

일반적으로 CASCADE가 가장 많이 사용되며, RESTRICT와 PROTECT가 그 다음으로 많이 사용되는 듯 한데, 이들의 동작을 직접 테스트해보려한다.

 

 

 

Article이 참조하는 Members의 on_delete 설정을 models.PROTECT로 설정한 뒤, DB를 다시 migration 하고 서버를 재실행했다. 현재 testuser1이 한 개의 글을 작성한 상태인데, 

 

 

 

이 상태에서 testuser1이 회원 탈퇴를 하려고하면, 아래와 같이 회원 탈퇴를 실행하지 못한다는 오류가 발생하게 된다.

 

Article 외에도 Comment 역시 PROTECT가 설정되어 있어 관련 내용이 에러로 나타난다.

 

 

이번에는 ForeignKey의 on_delete 옵션을 모두 RESTRICT로 변경해서 테스트를 진행해보자. 이를 위해 게시글을 작성하지 않은 새 유저를 testuser5로 생성하고, on_delete 값을 수정한 뒤 다시 migration과 서버 재기동을 실행한다.

 

 

 

아무런 게시글이 없는 testuser5는 바로 회원 탈퇴가 가능하지만, 게시글이 존재하는 testuser1 계정은 탈퇴 시도 시, 다시 오류가 발생한다. 다만, 오류는 앞서 보았던 것과 달리 RestrictedError로 표시된다.

 

 

 

마지막으로 CASCADE 설정 후, Article도 함께 삭제되는지 확인해보자.

 

 

 

실제 DB에서 testuser1의 회원 정보와, testuser1이 작성한 모든 게시글이 사라졌으며,

 

웹 페이지에서도 testuser1이 작성한 article id 3번이 사라진 것이 확인된다.

 

 

 

댓글 역시 member 필드의 ForeignKey on_delete 설정에 대해 models.CASCADE로 지정되어 있기 때문에 testuser1이 작성한 모든 댓글 역시 사라지게 된다. 실제로, 댓글을 "ㅋㅋㅋ"로 달아놓았던 게시글의 상세 목록을 보면 testuser1이 작성한 내역이 사라졌음을 확인할 수 있다.

 

testuser1은 testuser4 이전에 댓글이 존재했었다.

 


 

다음 포스팅에서는 N:N 관계를 구현하는 ManyToMany에 대해 정리해보려한다.

 

* 추가:  지금보니, 테이블 생성하고 매개변수에 따른 동작만 설명했지, 정작 데이터를 추가/삭제 방법은 깜빡하고 작성을 못했다... 다음 포스팅에서 이와 관련된 내용을 포스팅하니, 필요하신 분들은 이곳을 눌러 확인하시면 된다.

 

 

End.

 

 

반응형

댓글