Django_1:N

박상욱·2022년 3월 3일
0

1:N 관계(One to Many Relationship)

django 모델 간의 관계를 맺는 방법.

우리는 Post 모델을 정의해서 게시물 데이터를 관리할 수 있고 회원 인증을 통한 User 모델을 통해서 유저들을 관리할 수 있게 되었습니다. 우리가 인스타그램을 사용할 때 로그인한 다음에 본문만 작성해서 게시물을 올리죠.

우리가 지금까지 Post에 게시물 데이터를 추가할 때에는 author를 매번 지정해주어야만 했는데, 이제 이것을 User 모델을 활용해서 개선해봅시다.

author를 직접 지정하는 것이 아니라 글 쓰는 시점에 로그인한 유저의 username을 활용하면 좋겠죠?RDB에서는 테이블과 테이블 간의 관계를 맺어줄 수 있는데, 모든 게시물은 반드시 로그인한 유저가 작성한다고 했을 때 한 명의 유저는 여러 개의 게시물을 작성할 수 있겠죠?

하지만 하나의 게시물에는 여러 명의 저자가 할당될 수는 없습니다. 이러한 관계를 1:N 관계라고 합니다.

이렇게 관계를 맺게 되면 이제 author와 같은 정보를 User 테이블에 저장하게 되고 Post에서는 해당 게시물을 작성한 유저가 몇 번 id를 가진 유저인지만 저장하면 됩니다.
교안을 보면,


user_id필드 처럼 다른 테이블의 row를 식별할 수 있게끔 해주는 필드를 foreign key라고 합니다. 지금 까지 RDB 이론이었습니다.

Django에서도 이 개념 그대로 모델과 모델간의 관계를 맺을 수 있는 기능이 있습니다. 일단 저희는 이제 Post 테이블에서 author라는 필드를 제거하고 user_id와 같이 해당 게시물을 누가 작성했는지를 나타내는 User 모델의 PK값만 들고 있께끔 변경하면 좋을텐데요. 기존에 작성했던 Post 모델을 다음과 같이 수정하면 됩니다.

models.py

from django.db import models
from django.contrib.auth.models import User

# Create your models here.

class Post(models.Model):

    user = models.ForeignKey(User, on_delete=models.CASCADE)
    body = models.TextField()
    created_at = models.DateTimeField()

    def __str__(self):
        return f'{self.user.get_username()}: {self.body}'

django User 모델을 사용하기 위해서 User 모델을 import 해주시고 user라는 어트리뷰트에 models.ForeignKey() 함수를 사용합니다. 이를 통해서 Post 모델과 User 모델을 연동하기 위해 첫 번째 인자로 User 모델 클래스를 적어주겠습니다. on_delete 옵션을 CASCADE로 설정하면 연동시킨 유저가 삭제되었을 때 해당 유저가 작성한 모든 게시물도 동시에 지워줍니다.

django 모델에 어트리뷰트를 작성할 때 주의하실 점은 우리가 지금 user, body, creatd_at 이라는 어트리뷰트를 추가했는데 여기에서 작성해준 변수의 이름이 우리가 실제 python 코드로 django 모델을 활용할 떄의 변수명이 됩니다. 즉, post.user post.body post.created_at과 같이 사용할 수 있게 되는 거에요.

그리고 django는 이 모델 클래스의 모습을 해석해서 어떻게 데이터베이스에 저장해야 되는지를 분석하는데 이 과정에서 실제 DB 내에 있는 테이블의 각각의 필드 이름을 자동 생성하게 됩니다.

~~이때 우리가 모델 클래스에서는 user, body, created_at 이라고 작성했지만 실제 데이터베이스에 저장될 떄에는 body나 created_at이라는 이름과 동일한 이름을 가진 column이 생성될 수 있고 이 user의 경우에는 실제 데이터베이스에 user라는 이름을 가진 column이 생성되는게 아니라 user_id같은 이름이 바뀐 형태로 column이 생성될 수도 있습니다. 그런데 이건 어차피 자동 생성되는 것이라 우리가 신경 쓸 필요는 없구요, ~~

우리는 django 모델을 사용하는 이상 python 코드로만 데이터를 조작하죠. django의 Foreing key 함수를 통해 User 모델과 Post 모델을 연동하뎐 post 테이블에 User_id 라는 새로운 column이 형성되고 여기에는 연동된 유저의 PK값만 저장이 되지만, 실제 우리가 Python 코드로 활용 될 에는 우리가 어트리뷰트로 추가했던 user라는 이름을 활용해서 post.user의 형태로 사용합니다.

이 post.user라고 사용할 경우에 user_id만 담겨 있는게 아니라 user_id에 해당하는 user 인스턴스 자체, 유저에 대한 모든 정보가 담겨있다고 생각하면 됩니다.

user = User.objects.get(username='_heeham')
post.user

그러니까 예를 들면, 이런 식으로 User.object.get 함수를 이용해 우리가 특정 유저를 찾아내면 user 라는 이 변수는 해당 유저에 대한 모든 정보를 담고 있는 user 인스턴스가 저장이 되죠. 마찬가지로 post.user의 형태로 사용하게 되면 해당 게시물에 연동된 유저에 대한 정보를 다 담고 있는 user 인스턴스를 바로 활용할 수 있게 되는 겁니다.

str 메소드에서 author가 아닌 user 어트리뷰트를 통해서 get_username()을 호출하는 것으로 username을 이용할 수 있게끔 str 메소드를 변경하겠습니다.

python manage.py makemigrations posts를 통해서 migration 파일을 만들어봅시다. 명령어를 입력하면 아래와 같은 안내 메세지가 나옵니다.

'null이 되면 안되는 user 라는 필드를 post에 추가하고자 하는데, 기본값(defalut)을 지정해주지 않아서 우리가 migration 할 수 없다.'

우선 기존의 Post 테이블에서 author라는 필드가 필요 없어졌으니 이걸 지워주게 되는데 여기에 새로운 필드인 user를 추가하게 되면, DB에는 user_id로 추가되지만, 각 게시물을 누가 작성했는지에 대한 정보 user의 PK 값이 자동으로 채워지지는 않습니다. 생각해보세요. 여기 왼쪽 테이블만 보면 author 정보가 없으니 익명 게시물에 해당하는데, 이 상태에서 우리가 유저와 연동하면서 모든 게시물에 반드시 작성자가 있어야 한다고 말을 했어요. 그런데 정작 각 게시물이 누구에 의해 작성되었는지 정보를 채울 방법을 알려주지 않아서 여기서 문제가 생긴다는 거죠. 그래서 django가 말하는 겁니다.

'Post 모델에 따르면 user_id 필드에는 반드시 값을 채워넣야 하는데 디폴트 값을 주지 않아서 내가 값을 채워 넣을 방법을 모르겠어 어떻게 할래?'

그래서 django는 두 가지 옵션을 제시하고 있습니다. 기본적으로 지정될 default 값을 하나 지정해주는 방식 또는 끝내고 models.py에 default을 작성해주는 방식입니다.

아무 기본값이나 정해주는 것은 원래 서로 다른 여러 명이 작성한 게시물들일텐데 하나의 특정 유저가 작성한 것처럼 전부 다 채워넣는다는 건 말이 안되겠죠?? 그래서 우선 user 필드를 nullable로 바꿔서 migration을 진행한 다음, 기존에 있었던 post 데이터에 각각 유저 정보를 적당히 채워주고 다시 non-nullable(유저 정보가 있을 때에만 게시물을 작성할 수 있게끔)로 변경하도록 하겠습니다. 그래서 ForeignKey의 세번째 인자로, null=True 넣어줍니다.

from django.db import models
from django.contrib.auth.models import User

# Create your models here.

class Post(models.Model):
    user = models.ForeignKey(User, on_delete=models.CASCADE, null=True)
    body = models.TextField()
    created_at = models.DateTimeField()

    def __str__(self):
        if self.user:
            return f'{self.user.get_username()}: {self.body}'
        else:
            return f'{self.body}'

ForeignKey 함수의 세 번째 인자로 null=True라는 값을 주어 해당 게시물이 작성될 때 user 데이터가 채워지지 않더라도 정상적으로 게시물을 만들 수 있도록 하겠습니다. str 메소드도 유저가 있을 때와 유저가 없을 때를 구분해서 사용하게끔 변경하겠습니다. 다시 migration 파일을 만들고 해당 파일을 확인 해 보도록 하겠습니다. 실제로 새롭게 생성된 migration 파일을 살펴봐도 operations에 RemoveField라고 해서 author 필드를 삭제하고 있고 AddField라고 해서 user 필드를 추가하고 있습니다. 하지만 여기선 기존 필드를 삭제하고 새로운 필드를 추가한 것 뿐이지, 어떻게 데이터를 채워나가야 하는지에 대한 코드는 없습니다.

admin 페이지에 들어가 Posts의 내용을 확인해봅시다. author 필드를 삭제한 다음, 이에 따라 str 메소드도 변경해주었기 때문에 게시물 목록에 보이는 author가 다 사라졌습니다. 게시물 상세로 들어가면 다음과 같이 유저를 지정해줄 수 있습니다. 저는 유저가 몇 명 없지만, 회원가입을 통해서 유저를 직접 여러 개 생성하신 다음에 각각의 게시물에 서로 다른 유저를 지정한 다음 저장해주세요. 이제 모든 게시물에 유저가 지정되었고 그 결과 게시물 목록에서 username을 활용해 작성자 정보가 출력되는 것을 확인할 수 있습니다.

이제 유저가 연동되지 않은 게시물은 단 한 개도 존재하지 않기 떄문에 Post 모델로 가셔서, null 옵션을 False로 변경한 다음 makemigrations posts를 통해서 새롭게 migration 파일을 만들어줍니다.

우리가 이전 migration에서 nullable로 user 필드를 변경한 다음 직접 데이터 보정을 통해서 user가 null인 게시물이 없게끔 만들어주었기 때문에 문제가 없으니까 그대로 migration 파일을 생성하겠다는 의미가 됩니다. 2번을 입력하고 엔터를 눌러서 migration 파일을 만들어준 다음 migrate posts를 통해서 migration까지 진행해주도록 하겠습니다.
이제 모든 게시물은 user 데이터를 연동해야만 작성할 수 있게끔 변경되었습니다. 지금까지 작성해왔던 코드를 글쓰기와 글 수정, 삭제하는 부분을 모두 인증된 사용자만 볼 수 있고 자신이 쓴 게시물만 수정, 삭제가 가능하도록 하나씩 수정해 보도록 하겠습니다.

먼저 post 안에 있는 author라는 필드를 활용해 작성자 정보를 출력하는 부분을 post.user.get_username을 통해서 계정의 이름, username을 보여주게끔 변경해주세요.

posts/templates/posts/index.html

<tbody>
    {% for post in posts %}
    <tr>
        <td>{{ post.id }}</td>
        <td>{{ post.user.get_username }}</td>
        <td>{{ post.body }}</td>
        <td>{{ post.created_at }}</td>
        <td><a href="{% url 'posts:detail' post.id %}">보기</a></td>
    </tr>
    {% endfor %}
</tbody>

그리고 로그인을 한 상태에서만 새로운 글을 쓸 수 있게끔 변경해봅시다. django가 제공하는 로그인 여부를 검증하는 코드를 아래처럼 넣어주세요. 이제 현재 사용자가 로그인되어 있지 않다면 로그인 페이지로 redirect 시켜버립니다. 그리고 모델에서 이미 삭제한 author 관련 내용을 수정하도록 하겠습니다. 현재 로그인 한 user 정보를 user라는 변수에 담도록 하겠습니다.

posts/views.py

def new(request):
    if not request.user.is_authenticated:
        return redirect('accounts:login')

    return render(request, 'posts/new.html')

def create(request):
    if not request.user.is_authenticated:
        return redirect('accounts:login')

    user = request.user
    body = request.POST.get('body')

    post = Post(user=user, body=body, created_at=timezone.now())
    post.save()

    return redirect('posts:detail', post_id=post.id)

정보를 담을 template도 수정 해 주도록 하겠습니다. new template으로 가서 author 부분을 삭제 해 주도록 하겠습니다.

posts/templates/posts/new.html

{% extends 'base.html' %}

{% load static %}

{% block content %}
<div class='container'>
    <h1>New Post</h1>
    <form method="POST" action="{% url 'posts:create'%}">
        {% csrf_token %}
        <div class='mb-3 col-md-6'>
            <label class="form-label" for="body">Body</label>
            <textarea class="form-control" name="body" id="body" cols="30" rows="10"></textarea>
        </div>
        <button type="submit" class="btn btn-primary">작성하기</button>
    </form>
    <a href="{% url 'posts:index' %}">목록</a>
</div>
{% endblock %}

이제 로그인을 한 상태에서만 글쓰기 페이지에 접근할 수 있고 해당 form에서 별도로 author를 지정해주지 않아도 username이 정상적으로 출력되는 것을 확인할 수 있습니다.

이제 detail template으로 가셔서 이전에 author를 사용하고 있던 부분을 아래처럼 변경해주면 작성한 user의 username이 출력되는 것을 확인할 수 있습니다.

profile
기획,디자이너,개발 찍먹파입니다.

0개의 댓글