[Django] N:1 (1)

송수빈·2026년 5월 17일

SSAFY

목록 보기
14/15
post-thumbnail

Many to one relationships

모델 관계

  • 관계
    • 데이터베이스 내 여러 테이블 간의 논리적인 연결 관계를 나타냄
  • 관계의 종류
    • 1:1 (One to One) 관계
      • 한 테이블의 레코드가 다른 테이블의 단 하나의 레코드와만 1:1로 매칭되는 관계
      • 예시) 한 명의 사용자는 오직 하나의 프로필 정보(상세정보)만 가짐
    • N:1 (Many to One) 관계
      • 여러 개의 레코드(N)가 하나의 레코드(1)에 소속되는 관계
      • 예시) 하나의 게시글(1)에는 여러 개의 댓글(N)이 달릴 수 있음
        하지만, 특정 댓글 하나가 여러 게시글에 동시에 달릴 수는 없음
    • N:M (Many to Many) 관계
      • 여러 레코드가 여러 레코드와 양방향으로 연결되는 관계
      • 두 테이블 사이에 연결을 위한 중개 테이블(Through Table)이 필요함
      • 예시) 한 명의 학생은 여러 수업을 신청할 수 있음
        동시에 하나의 수업에는 여러 명의 학생이 수강함

Many to one relationships

  • 한 테이블의 0개 이상의 레코드가 다른 테이블의 레코드 한 개와 관련된 관계
    • N:1 or 1:N
  • 댓글과 게시글의 관계
    • 0개 이상의 댓글은 1개의 게시글에 작성될 수 있다.
  • 테이블 관계 설정
    • 관계 설정을 위한 Foreign Key(외래 키, FK)를 N:1에서 1을 담당하는 테이블에 위치하면 안됨
      • Article Table에 Foreign Key 칼럼을 위치시키면 중복 데이터로 인해 낭비가 발생함
      • 댓글 생성마다 Comment의 정보와 함께 Article 정보(id, title, content, ..)가 매번 중복 저장됨
    • 관계 설정을 위한 Foreign Key(외래 키, FK)의 적절한 위치는 바로 N:1에서 N을 담당하는 테이블에 위치
      • Comment가 생성되면 Article의 정보만 저장하면 됨
      • 외래 키 칼럼에 저장되는 데이터는 참조하는 데이터를 대표하는 Primary Key(PK) 정보를 저장함

댓글 모델 정의

  • ForeignKey 필드
    • 한 모델이 다른 모델을 참조하는 관계를 설정하는 필드
      • N:1 관계 표현할 때 사용
      • 데이터베이스에서 외래 키로 구현됨
    ForeignKey(to, on_delete)
    • to 속성
      • 참조하는 모델 class 이름 (N:1에서 N이 아닌 1의 class 정보)
    • on_delete 속성
      • 외래 키가 참조하는 객체(1)가 사라졌을 때, 외래 키를 가진 객체(N)를 어떻게 처리할 지를 정의하는 설정 (데이터 무결성)
      • 데이터 무결성: 데이터가 정확하고 일관되며, 신뢰할 수 있도록 유지되는 상태
  • on_delete 속성 종류
    • CASCADE
      • 참조된 객체(부모 객체)가 삭제될 때 이를 참조하는 모든 객체도 삭제되도록 지정
      • 예) 게시글이 삭제되면 해당 게시글의 모든 댓글을 삭제
    • PROTECT
      • 삭제하려는 부모 객체에 자식 객체가 존재한다면 해당 부모 객체를 삭제하지 못하도록 지정
      • 예) 게시글을 삭제할 때 해당 게시글에 댓글이 존재하면 게시글 삭제 불가
    • SET_NULL
      • 부모 객체가 삭제되면, 해당 필드에 값이 NULL이 저장되도록 지정
      • 단, 해당 ForeignKey 필드 설정이 null=True가 설정 되어야 함
  • 댓글 모델 정의하기
    • ForeignKey 클래스의 인스턴스 이름은 참조하는 모델 클래스 이름의 단수형으로 작성하는 것을 권장
      class Comment(models.Model):
          article = models.ForeignKey(Article, on_delete=models.CASCADE)
          content = models.CharField(max_length=200)
          created_at = models.DateTimeField(auto_now_add=True)
          updated_at = models.DateTimeField(auto_now=True)
      • 단일 객체를 참조하므로, 속성명을 단수형으로 작성하면 의미가 명확해짐
    • Migration 이후 댓글 테이블 확인
      • 만들어지는 필드 이름 규칙: ‘작성한 외래 키 필드명’ + ‘_id’

      • 댓글 테이블의 article_id 외래 키 필드 확인: bigint 자료형 확인

        ⇒ 참조하는 클래스 이름의 소문자(단수형)로 작성하는 것이 권장되었던 이유

댓글 생성 연습

댓글 생성 연습 준비하기

  • 연습을 위한 shell 환경에서 진행

  • ipython 설치 필요 ($pip install ipython)

  • django shell 시작 후 댓글 작성을 위한 게시글 하나 작성

    # django shell 실행
    python manage.py shell
    
    # 게시글 생성
    Article.objects.create(title='title', content='content')
      

댓글 생성 연습

  • 댓글 생성 후 바로 저장 확인

    # Comment 클래스의 인스턴스 comment 생성
    comment = Comment()
    
    # 인스턴스 변수 저장
    comment.content = 'first comment'
    
    # DB에 댓글 저장
    comment.save()
    
    # 에러 발생
    IntegrityError: NOT NULL constraint failed: articles_comment.article_id
    => articles_comment 테이블의 ForeignKeyField, article_id 값이 저장 시 누락되었기 때문
  • 게시글 정보를 가져와 댓글 저장

    # 게시글 정보 조회하여 가져오기
    article = Article.objects.get(pk=1)
      
    # 외래 키 데이터 입력
    comment.article = article
      
    # 직접적으로 테이블의 필드에 pk를 넣어도 저장되지만 권장하지 않음
    # comment.article_id = article.pk
      
    # 댓글 저장 및 확인
    comment.save()
    • article_id에 직접 pk를 넣는 것을 권장하지 않는 이유: 직접 pk를 입력하는 경우에는 숫자를 저장하기 때문에 잘못된 객체의 pk 값이 저장될 수 있음 (잘못된 정보지만 에러 발생 안 함)
  • comment 인스턴스를 통한 article 값 참조하기

    comment.pk
    # 출력: 1
          
    comment.content
    # 출력: 'first comment'
          
    # 클래스 변수명인 article로 조회 시 해당 참조하는 게시물 객체를 조회할 수 있음
    comment.article
    # 출력: <Article: Article object (1)>
          
    # 테이블 상의 article_id 값은 해당 게시글의 Primary Key(PK)값만 확인가능
    comment.article_id
    # 출력: 1
    • article_pk는 존재하지 않는 필드이기 때문에 사용 불가
  • comment를 통한 article 객체의 데이터 참조하기

    # 1번 댓글이 작성된 게시글의 pk 조회
    comment.article.pk
    # 출력: 1
          
    # 1번 댓글이 작성된 게시글의 content 조회
    comment.article.content
    # 출력: 'content'
  • 두 번째 댓글 생성 및 데이터 확인

    commentary = Comment(content='second comment', article=article)
    commentary.save()
          
    commentary.pk
    # 출력 2
    commentary
    # 출력: <Comment: Comment object (2)>
    Commentary.article.pk
    # 출력: 1
      

관계 모델 참조

참조

  • 참조란 직접 대상의 정보를 저장하고 필요할 때 활용하는 것
    • 댓글(Comment)에는 게시글(Article) 정보를 저장하는 ForeignKey 필드인 article이 존재하며, 해당 필드를 통해 게시글 정보에 쉽게 접근할 수 있음
    • 그래서 댓글(Comment)은 게시글(Article)을 참조한다고 함
  • 특정 게시글(Article)의 댓글(Comment) 정보 조회하기
    • QuerySet API의 .all() 사용하기 → X
      • 특정 게시글(Article)의 댓글(Comment)들이 아닌 모든 댓글 정보를 가져오게 됨
        # 특정 게시물의 댓글이 아닌 모든 댓글을 조회함
        comment = Comment.objects.all()
    • QuerySet API의 .filter() 사용하기
      • 특정 게시글(Article) 정보를 조회 후 댓글(Comment)에서 filter를 활용해 댓글 조회 가능
        # 특정 게시글 정보를 가져온 후 filter를 이용할 수 있음
        article = Article.objects.get(pk=1)
        comments = Comment.objects.filter(article=article)

역참조

  • 역참조란 누가 나를 참조하는지 거꾸로 조회하는 것
    • 직접적으로 정보를 가지고 있지 않고 반대로 확인해야 하기 때문에 참조하는 것을 거꾸로 찾아야 함
      • 게시글 입장에서는 연결된 댓글 정보를 담는 필드가 존재하지 않음
      • 그래서 해당 글을 참조하고 있는 댓글들을 역으로 찾아야 하며, Django에서는 이런 관계를 자동으로 추적해서 해당 게시글에 달린 댓글을 쉽게 찾아올 수 있도록 역참조 기능을 제공함
  • 역참조 기본 구조
    article.comment_set.all()
    • article: 모델 인스턴스
      • 특정 게시글에 작성된 댓글 전체를 조회하는 요청
      • models.py에 정의된 모델 클래스로 생성된 실제 데이터를 의미
        • ‘article.title’과 같이 속성에 접근 가능하며 속성을 수정할 수 있음
      • 역참조에서는 참조 가능한 필드가 없는 모델 클래스의 인스턴스를 사용하면 됨
        • Article(1):Comment(N) → Article에 참조 필드가 없어서 Article의 인스턴스를 사용
    • comment_set: related manager (역참조 이름)
      • 특정 게시글에 작성된 댓글 전체를 조회하는 요청
      • related manager라고 부르며 N:1 혹은 N:M 관계에서 역참조 시에 사용하는 매니저를 의미
      • objects 매니저를 통해 QuerySet API를 사용했던 것처럼 related manager를 통해 QuerySet API를 사용할 수 있게 됨
    • all: QuerySet API
      • 특정 게시글에 작성된 댓글 전체를 조회하는 요청
      • 데이터를 가져오기 위한 쿼리 결과 집합을 만드는 인터페이스
      • SQL 쿼리를 직접 쓰지 않고도 DB를 사용할 수 있음
  • related manager (역참조 이름) 이름 규칙
    • ‘모델 클래스명 + _set’이 기본 값이며 Django에서 자동으로 생성해 줌
    • 관계를 직접 정의하지 않은 모델에서 연결된 객체들을 조회할 수 있게 함
    • 특정 댓글의 게시글 참조 (Comment → Article)
      • comment.article
    • 특정 게시글의 댓글 목록 역참조 (Article → Comment)
      • article.comment_set.all()
  • related manager 연습
    • django shell 실행
      python manage.py shell
    • 1번 게시글 조회
      article = Article.objects.get(pk=1)
    • 1번 게시글에 작성된 모든 댓글 조회하기 (역참조)
      article.comment_set.all()
      
      <QuerySet [<Comment: Comment object (1)>, <Comment: Comment object (2)>]>
    • 1번 게시글에 작성된 모든 댓글 내용 출력하기
      # 1번 게시글의 모든 댓글 정보들을 저장
      comments = article.comment_set.all()
      
      # 1번 게시글 댓글 정보들을 반복하여 개별 댓글 내용 출력
      for comment in comments:
          print(comment.content)

댓글 구현

댓글 CREATE

댓글 CREATE 구현

  • 사용자로부터 댓글 데이터를 입력 받기 위한 CommentForm 정의
    # articles/forms.py
    from .models import Article, Comment
    
    class CommentForm(forms.ModelForm):
        class Meta:
            model = Comment
            fields = '__all__'
  • 댓글이 작성되는 곳은 게시글 상세(detail) 페이지 하단
    • detail view 함수에서 CommentForm을 detail 페이지에서 사용할 수 있게 context로 전달
      def detail(request, pk):
          article = Article.objects.get(pk=pk)
          comment_form = CommentForm()
          context = {
      	      'article': article,
      	      'comment_form': comment_form,
      	  }
      	  return render(request, 'articles/detail.html', context)
    • detail view 함수에서 넘어오는 CommentForm을 사용하여 detail 페이지에 렌더링
      <!-- 게시글 정보 출력 코드 -->
      <hr>
      
      <form action="#" method="POST">
      	{% csrf_token %}
      	{{ comment_form }}
      	<input type="submit">
      </form>
  • Comment 클래스의 외래 키 필드 article 또한 데이터 입력이 필요한 필드이기 때문에 출력 되는 것
    • 하지만, 외래 키 필드 데이터는 사용자로부터 입력 받는 값이 아닌 view 함수 내에서 다른 방법으로 전달 받아 저장되어야 함
    • CommentForm의 출력 필드를 조정하여 외래 키(Foreign Key) 필드가 출력되지 않도록 함
    • articles/forms.py
      from .models import Article, Comment
      
      class CommentForm(forms.ModelForm):
          class Meta:
              model = Comment
              fields = ('content', )
  • 출력에서 제외된 외래 키 데이터는 어디서 받아와야 할까?
    • detail 페이지의 URL에 게시글 정보가 존재
      • path('<int:pk>/', views.detail, name='detail')
      • 해당 게시글의 pk 값이 variable routing으로 전달되고 있음
    • 댓글의 외래 키 데이터에 필요한 정보가 바로 게시글의 pk 값
      • 댓글 작성 시 해당 pk 값을 이용하여 게시글 데이터를 가져와 사용
  • 댓글 저장 로직은 detail 함수가 아닌 개별 함수로 작성
    • 댓글 저장은 게시글 상세 보기와 전혀 다른 기능이기 때문 (단일 책임 원칙)
      • 단일 책임 원칙: 함수는 하나의 기능만 수행하고, 그 기능이 변경될 이유도 하나여아 함
    • 댓글 저장 시 게시글 pk 정보를 URL로 전달하여 사용
    • articles/urls.py
      app_name = 'article'
      urlpatterns = [
          ...
          path('<int:pk>/comments/', views.comments_create, name='comments_create'),
      ]
    • articles/detail.html
      <form action="{% url 'articles:comments_create' article.pk %}" method="POST">
      	{% csrf_token %}
      	{{ comment_form }}
      	<input type="submit">
      </form>
  • comments_create 함수는 POST 작성만 진행
    • 댓글 작성 폼을 출력하는 GET 동작은 detail 함수에 이미 작성
      def comments_create(request, pk):
          article = Article.objects.get(pk=pk)
          comment_form = CommentForm(request.POST)
          if comment_form.is_valid():
      	      comment_form.save()
      	      return redirect('articles:detail', article.pk)
      	# 유효성 검사에 실패 했을 때 에러 정보 및 필요 데이터 전달
          context = {
      	    'article': article,
      	    'comment_form': comment_form,
      	}
      	return render(request, 'articles/detail.html', context)
  • URL로 전달받은 게시글의 pk를 이용해서 게시글 정보를 가져옴
    • 댓글 생성 시 가져온 article 객체를 추가해야 함
    • 가져온 article 객체는 댓글 생성 시 어떻게 저장하면 될까?
  • save 메서드의 commit 인자
    save(commit=False)
    • 기본적으로 commit 속성은 True가 기본 값
      • 설정 값이 True인 경우 인스턴스를 생성하고 반환한 다음 DB에도 저장 요청을 보냄
    • commit이 False인 경우 DB에 저장 요청을 보내지 않고 인스턴스만 반환
      • Create, but don’t save the new instance.
    • 댓글을 저장할 때 바로 DB에 저장 요청을 보내는 것이 아닌 게시글 정보를 추가한 후 저장 요청을 보내도록 로직을 구성하면 게시글 정보와 함께 댓글을 저장할 수 있게 됨
    • articles/views.py
      def comments_create(request, pk):
          article = Article.objects.get(pk=pk)
          comment_form = CommentForm(request.POST)
          if comment_form.is_valid():
      	      comment = comment_form.save(commit=False)   # 댓글을 저장하지 않고 인스턴스만 생성
      	      comment.article = article                   # 댓글에 게시글 정보 추가
      	      comment.save()                              # 댓글 정보 DB에 저장 요청
      	      return redirect('articles:detail', article.pk)
          # 유효성 검사에 실패 했을 때 에러 정보 및 필요 데이터 전달
          context = {
      	    'article': article,
      	    'comment_form': comment_form,
      	}
          return render(request, 'articles/detail.html', context)
  • 댓글 작성 후 DB에서 생성 확인
    • article_id 필드에 게시글 정보가 저장되어 있는 것을 확인

댓글 READ

댓글 READ 구현

  • 댓글이 보이는 위치는 게시글 상세 페이지 하단 위치
    • detail view 함수에서 전체 댓글 데이터를 조회해서 detail.html로 전달하여 댓글 데이터 조회
    • articles/views.py
      def detail(request, pk):
          article = Article.objects.get(pk=pk)
          comment_form = CommentForm()
          # 특정 게시글의 댓글을 역참조로 조회
          comments = article.comment_set.all()
          context = {
      	      'article': article,
      	      'comment_form': comment_form,
      	      'comments': comments,
      	  }
      	  return render(request, 'articles/detail.html', context)
    • articles/detail.html
      <a href="#">UPDATE</a>
      
      <h4>댓글 목록</h4>
      <ul>
      	{% for comment in comments %}
      		<li>{{ comment.content }}</li>
      	{% endfor %}
      </ul>
        

댓글 DELETE

  • 개별 댓글마다 삭제할 수 있도록 기능을 추가
    • 어떤 댓글을 삭제해야 하는지 삭제 대상 정보를 전달하기 위해 variable routing 이용
    • articles/urls.py
      app_name = 'articles'
      urlpatterns = [
          ...,
          path('<int:article_pk>/comments/<int:comment_pk>/delete/', views.comments_delete, name='comments_delete'),
      ]
      • article_pk 정보는 URL 패턴을 일관되게 작성하기 위함 외에도 삭제 후 detail 페이지로 돌아갈 때 사용할 수 있는 정보이기 때문에 작성
  • 댓글 삭제 view 함수 정의
    • articles/views.py
      def comments_delete(request, article_pk, comment_pk):
          comment = Comment.objects.get(pk=comment_pk)
          comment.delete()
          # 댓글 삭제 후 article 게시글 상세 페이지로 이동
          return redirect('articles:detail', article_pk)
  • 댓글 삭제 버튼 추가
    • variable routing으로 전달되는 게시글 pk, 삭제될 댓글 pk 전달
    • articles/detail.html
      <a href="#">UPDATE</a>
      
      <h4>댓글 목록</h4>
      <ul>
      	{% for comment in comments %}
      		<li>
      			{{ comment.content }}
      			<form action="{% url 'articles:comments_delete' article.pk comment.pk %}" method="POST">
      				{% crsf_token %}
      				<input type="submit" value="DELETE">
      			</form>
      		</li>
      	{% endfor %}
      </ul>
  • 댓글 삭제 버튼 출력 확인 및 삭제 테스트

참고

데이터 무결성

  • 데이터 무결성이란 데이터베이스에 저장된 데이터의 정확성, 일관성, 유효성을 유지하는 것
    • 데이터베이스에 저장된 데이터 값의 정확성을 보장하는 것
    • 중요성: 데이터의 신뢰성 확보, 시스템 안정성, 보안 강화

admin site 댓글 등록

  • admin site에 댓글 등록하기
    • 작성된 Comment 모델에 대해 Admin site에서 관리할 수 있도록 아래와 같이 등록
    • articles/admin.py
      from .models import Article, Comment
      
      admin.site.register(Article)
      admin.site.register(Comment)

댓글 추가 구현

  • 댓글이 없는 경우 대체 콘텐츠 출력
    • DTL의 ‘for empty’ 태그 활용
      • for 문에 반복할 요소가 없는 경우 empty 태그가 실행됨
      • articles/detail.html
        	{% for comment in comments %}
        		<li>
        			{{ comment.content }}
        			<form action="{% url 'articles:comments_delete' article.pk comment.pk %}" method="POST">
        				{% crsf_token %}
        				<input type="submit" value="DELETE">
        			</form>
        		</li>
        	{% empty %}
        		<p>댓글이 없어요..</p>
        	{% endfor %}
  • 댓글 개수 출력하기
    • DTL filter의 length 활용
      <!-- views.py에서 전달받은 댓글로 길이 확인-->
      {{ comments|length }}
      
      <!--역참조를 이용하여 댓글 길이 확인-->
      {{ article.comment_set.all|length }}
    • QuerySet API의 count() 메서드 활용
      <!-- 역참조로 QuerySet API 이용 댓글 길이 확인 -->
      {{ article.comment_set.count }}
profile
🌱 🐜

0개의 댓글