[CEOS] 2주차 미션: 에브리타임 DB 모델링 및 Django ORM

HAEN·2023년 4월 6일
0

2주차 미션은 MySQL을 Django와 연동 및 에브리타임 DB 모델링, 및 Django ORM 사용해보기이다! 이번에도 과정을 적어보겠다


1. MySQL 세팅 및 Django와 연동하기

MySQL과 Workbench는 이미 깔려있었기에 설치 과정은 따로 적지 않겠다
미션을 위한 데이터베이스 'ceos_everytime'를 생성해준다

지난 미션 때 코드리뷰에서 Django의 SECRET_KEY를 깃허브에 그대로 노출시킨 것을 지적받았는데,
이번에는 .env 파일을 통해 환경 변수를 설정하고 gitignore를 통해 개인 정보들을 안전하게 처리해보겠다

manage.py 파일이 있는 디렉토리에 .env 파일을 만들고, 아래와 같이 환경변수들을 추가해준다

DJANGO_ALLOWED_HOSTS=localhost,127.0.0.1,[::1]
DJANGO_SECRET_KEY={프로젝트의 SECRET_KEY, 'Django secret key generator'라는 사이트에서 새로 발급받을 수 있음}

DATABASE_NAME=ceos_everytime
DATABASE_USER=root
DATABASE_PASSWORD={MySQL password}
DATABASE_HOST=localhost
DATABASE_PORT=3306

.env 파일을 다 만들었다면

python manage.py migrate

migrate 명령어를 사용해 DB에 Django 관련 테이블들을 생성해주자

제대로 생성되었다면 다음과 같은 화면과 함께

생성한 DB에 테이블들이 생긴 것을 확인할 수 있다


2. 에브리타임 ERD 작성

모델링에서 가장 중요한 것은 ERD를 제대로 짜는 것이라고 배웠기에 열심히 짜려고 했으나, 에브리타임에 이렇게 많은 기능이 있었다니... 생각할 수록 고려할 것들이 너무 많았다 ㅠ
제한 된 시간 안에 완성해야 하는 과제이니 기능들을 세세하게 고려하지 못 하였지만 최대한 열심히 작성해보았다 아쉽 🥲

  • 유저는 회원가입시 학교를 선택할 수 있다
  • 학교 별로 게시판들이 따로 존재하며 각 게시판에 게시글을 작성할 수 있다
  • 게시글에 댓글, 대댓글도 작성 및 공감 할 수 있다
  • 필요한 게시글을 스크랩하여 확인할 수 있다
  • 시간표를 만들 수 있으며 친구의 시간표도 확인할 수 있다

가장 헷갈렸던 것이 Course 테이블을 짜는 것이었다
강의평 기능과 프론트와 정보를 어떻게 주고받을 것인가에 대해 고민하다가
일단 Course 테이블에 강의의 기본 공통 정보를 저장하고, CourseDetail에 강의 세부사항인 요일, 시간, 교시 등을 저장하였다
좋은 방식인지는 모르겠지만, 일단 우리 학교의 시간표를 생각했을 때는 이렇게 하는 것이 효율적일 것 같았다 실제 에브리타임에서는 어떤 방식으로 저장하는지 궁금하다...!


3. Model 작성하기

작성한 ERD를 바탕으로 model들을 구현하였다

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

# Create your models here.


class BaseTimeModel(models.Model):
    created_at = models.DateTimeField(auto_now_add=True)
    modified_at = models.DateTimeField(auto_now=True)

    class Meta:
        abstract = True


class School(BaseTimeModel):
    name = models.CharField(max_length=20)

    def __str__(self):
        return self.name


class Profile(models.Model):
    user = models.OneToOneField(User, on_delete=models.CASCADE)
    school = models.ForeignKey(School, on_delete=models.CASCADE, related_name='users')
    nickname = models.CharField(max_length=10, unique=True)

    def __str__(self):
        return self.user.username


class Board(BaseTimeModel):
    school = models.ForeignKey(School, on_delete=models.CASCADE, related_name='boards')
    name = models.CharField(max_length=20)
    description = models.CharField(max_length=200)
    allow_anony = models.BooleanField(default=True)

    def __str__(self):
        return self.name


class Post(BaseTimeModel):
    user = models.ForeignKey(Profile, on_delete=models.CASCADE, related_name='posts')
    board = models.ForeignKey(Board, on_delete=models.CASCADE, related_name='posts')
    title = models.CharField(max_length=30)
    content = models.TextField()
    like_number = models.PositiveIntegerField(default=0)

    def __str__(self):
        return self.title


class Comment(BaseTimeModel):
    user = models.ForeignKey(Profile, on_delete=models.CASCADE, related_name='comments')
    post = models.ForeignKey(Post, on_delete=models.CASCADE, related_name='comments')
    content = models.TextField()
    is_delete = models.BooleanField(default=False)  # 댓글이 지워져도 대댓글들은 보이게
    like_number = models.PositiveIntegerField(default=0)

    def __str__(self):
        return self.content


class CommentReply(BaseTimeModel):
    user = models.ForeignKey(Profile, on_delete=models.CASCADE, related_name='comment_replies')
    comment = models.ForeignKey(Comment, on_delete=models.CASCADE, related_name='comment_replies')
    content = models.TextField()
    like_number = models.PositiveIntegerField(default=0)

    def __str__(self):
        return self.content


class PostLike(BaseTimeModel):
    user = models.ForeignKey(Profile, on_delete=models.CASCADE, related_name='post_likes')
    post = models.ForeignKey(Post, on_delete=models.CASCADE, related_name='post_likes')

    def __str__(self):
        return '{}번 게시글에 {}님의 공감'.format(self.post.id, self.user.user.username)


class CommentLike(BaseTimeModel):
    user = models.ForeignKey(Profile, on_delete=models.CASCADE, related_name='comment_likes')
    comment = models.ForeignKey(Comment, on_delete=models.CASCADE, related_name='comment_likes')

    def __str__(self):
        return '{}번 댓글에 {}님의 공감'.format(self.comment.id, self.user.user.username)


class CommentReplyLike(BaseTimeModel):
    user = models.ForeignKey(Profile, on_delete=models.CASCADE, related_name='comment_reply_likes')
    comment_reply = models.ForeignKey(CommentReply, on_delete=models.CASCADE, related_name='comment_reply_likes')

    def __str__(self):
        return '{}번 대댓글에 {}님의 공감'.format(self.comment_reply.id, self.user.user.username)


class Scrap(BaseTimeModel):
    user = models.ForeignKey(Profile, on_delete=models.CASCADE, related_name='scraps')
    post = models.ForeignKey(Post, on_delete=models.CASCADE, related_name='scraps')

    def __str__(self):
        return '{} 유저가 {}번 게시글 스크랩'.format(self.user.username, self.post.id)


class Timetable(BaseTimeModel):
    user = models.ForeignKey(Profile, on_delete=models.CASCADE, related_name='timetables')
    name = models.CharField(max_length=10)
    course = models.ManyToManyField('CourseDetail')

    def __str__(self):
        return '{}님의 시간표:{}'.format(self.user.nickname, self.name)


class Course(BaseTimeModel):
    title = models.CharField(max_length=20)
    professor = models.CharField(max_length=20)
    credit = models.IntegerField()
    hours = models.IntegerField()

    def __str__(self):
        return '{}-{}'.format(self.title, self.professor)


class CourseDetail(BaseTimeModel):

    DAY_CHOICES = [
        ('MON', '월'),
        ('TUE', '화'),
        ('WED', '수'),
        ('THU', '목'),
        ('FRI', '금'),
        ('SAT', '토'),
        ('SUN', '일')
    ]

    # 1~15교시 선택
    TIME_CHOICES = [
        (1, 1),
        (2, 2),
        (3, 3),
        (4, 4),
        (5, 5),
        (6, 6),
        (7, 7),
        (8, 8),
        (9, 9),
        (10, 10),
        (11, 11),
        (12, 12),
        (13, 13),
        (14, 14),
        (15, 15)
    ]

    course = models.ForeignKey(Course, on_delete=models.CASCADE, related_name='course_details')
    course_code = models.CharField(max_length=10)
    day = models.CharField(max_length=3, choices=DAY_CHOICES)
    time = models.IntegerField(choices=TIME_CHOICES)
    hours = models.IntegerField(default=1)
    classroom = models.CharField(max_length=10)

    def __str__(self):
        return self.course_code


class Friend(BaseTimeModel):
    from_user_id = models.ForeignKey(Profile, on_delete=models.CASCADE, related_name='friend')
    to_user_id = models.ForeignKey(Profile, on_delete=models.CASCADE, related_name='friend_list')
    is_accepted = models.BooleanField(default=False)

    def __str__(self):
        return 'from user: {}, to user: {}'.format(self.from_user_id.user.username, self.to_user_id.user.username)

주요 모델들을 살펴보겠다

  • BaseTimeModel
    테이블마다 기본적으로 존재하는 정보인 수정 시간, 생성 시간을 저장한 모델이다 추상 클래스로 선언하여 다른 모델들이 상속할 수 있게 설정하였다

  • Profile
    Django에서 기본으로 제공하는 User 모델을 확장하기 위한 방법은 3가지가 있다

    - 기본 User 모델을 OneToOneField 방식으로 연결하여 확장
     - AbstractUser 상속
     - AbstractBaseUser 상속

    AbstractBaseUser를 상속받는 것이 가장 User 모델 필드를 자유롭게 구성할 수 있지만, 이 미션에서는 간단하게 OneToOneField 방식이나 AbstractUser를 상속 받으려고 하였다
    찾아보니 OneToOneField 방식이 가장 간단하지만 추가 쿼리를 발생시키는 문제점이 있다고 하여 AbstractUser를 상속하는 User 모델을 작성하였다
    하지만 여러 에러들을 만나 결국 OneToOneField 확장 방법으로 User 모델을 구현하였다 에러는 깃허브 README.md에 정리해놓았따
    코드 리뷰를 통해 에러를 해결하는 방법을 배워서 새로운 과제에서 수정해볼 생각이다!

  • Comment, CommentReply
    에브리타임은 댓글에 대댓글을 달 수 있다 그래서 단순하게 생각했을 때 Comment와 CommentReply 모델을 따로 만들고 일대다 관계로 연결하면 될 것 같았다
    코드리뷰에서 Comment 모델에 parent_id를 따로 저장하여 받는 방법을 알게 되었고, DB 설계에 정답은 없지만 좋은 방법인 것 같아서 수정해보려고 한다 유익한 CEOS 코드리뷰...❤️

  • CourseDetail
    강의 세부 정보 모델이다
    강의 요일은 월~일, 강의 시간은 1~15교시까지 정해져 있으므로 데이터를 넣을 때 선택할 수 있었으면 좋겠다는 생각에 choices를 사용해보았다!

구현하면서 가장 편리했던 것은 ManyToManyField를 사용해서 다대다 관계를 설정하면, Django가 알아서 중간 테이블을 만들어 다대다 관계를 풀어준다...!


3. Django ORM 사용해보기

ORM(Object-Relation Mapping)이란?
객체와 관계형 데이터베이스의 데이터를 자동으로 매핑해주는 것
즉, DB를 다루기 위해 필요한 SQL 없이도 데이터베이스에 접근하여 CRUD가 가능하도록 만들어준다

Django는 Python 코드를 통해 데이터베이스에 접근, 조작할 수 있도록 하는 ORM 쿼리를 제공한다!

우선 아래 명령어로 쉘에 접속한다

python manage.py shell

(1) 데이터베이스에 해당 모델 객체 3개 이상 넣기

나는 유저와 다대일 관계로 연결된 timetable 테이블에 데이터를 넣었다

우선 from import를 통해 작성한 모델들을 import 한 후,
objects.get 쿼리를 통해 user 데이터를 가져온다(미리 admin 페이지에서 생성해놓음)

그리고 timetable 객체를 만들어 user를 연결해주고 save()로 저장해준다

(2) 삽입한 객체들을 쿼리셋으로 조회해보기 (단, 객체들이 객체의 특성을 나타내는 구분가능한 이름으로 보여야 함)

Timetable.objects.all() 쿼리를 사용하면 해당 모델에 만들어진 객체를 보여준다
보여주는 형식은 모델 내의 __str__함수를 통해 설정할 수 있다

(3) filter 함수 사용해보기

Timetable.objects.filter(user__nickname) 쿼리를 통해 특정 닉네임을 가진 유저의 시간표를 조회해보았다
특이하게 언더바 두개를 통해 외래키로 연결된 모델의 필드를 가져올 수 있었다


여기까지 Django의 ORM 쿼리를 사용해서 데이터베이스를 조작해보았는데, 역시 admin 페이지로 하는 것이 더 편하다 ㅎㅎㅎ

4. 회고

Django로 처음 직접 에브리타임 모델링을 해 보았다
평소에는 당연하게 생각했던 기능들인데, 실제로 모델링을 하려니 굉장히 많은 기능들이 있고 복잡하다는 것을 깨달았다
시간적 여유가 부족하여 구체적으로 구현하지는 못 한 게 아쉽다
이번 미션을 진행하면서 조금이나마 Django에 익숙해질 수 있었다


profile
핸수

0개의 댓글