모델을 잘 작성하는 것도 중요하지만 실제로 모델을 우리가 짤 일은 많지 않으니 이미 작성된 모델을 잘 이해하고 ORM을 통해 가져오는 걸 잘 하는 것이 중요.
관계를 활용하여 편하게 하려고 RDB를 짜는 거니까, 관계를 잘 이해하자.
모델을 짤 때는 신중하고 견고하게 짜야 한다. 잘못 짠 모델에 Data가 들어가면 돌이킬 수 없는 상황이 되므로.
관계형으로 모델을 만들고 이에 기반하여 입력된 Relational DB에서 ORM(Object-Relational Mapping)으로 구현된 메서드를 사용하여 데이터를 데려오는 것을 해보았다.
1:N 모델을 통해 하나의 카테고리에 여러 개의 글, 하나의 반에 여러 명의 학생 등의 관계를 구현
각 유저가 무한대로 좋아요를 누를 수 있는 것도 방법이지만, 일반적인 경우 한 사람은 한 번만 좋아요를 누르거나 취소할 수 있다.
Like라는 모델을 만들어 좋아요를 누를 때 생성되게 한다.
댓글 하나에 여러 명이 좋아요를 누를 수 있으므로, 댓글 1, 좋아요 N의 관계로 foreign key 설정한다.
한 사람이 여러 개의 댓글에 좋아요를 누를 수 있으므로 Student 1, Like N의 관계로 foreign key를 설정한다.
N 쪽에서 ForeignKey 필드를 가지고 있으므로 Like에 필드를 정의한다.
class Like (models .Model ):
comment = models .ForeignKey (Comment , on_delete = models .CASCADE , related_name = 'likes' )
liked_by = models .ForeignKey (Student , on_delete = models .CASCADE , related_name = 'likes' )
created_at = models .TextField (default = time .time ())
이 경우 '어떤 댓글'과 '누가'에 대한 데이터가 좋아요 누를 때마다 인스턴스가 생겨서 1줄씩 쌓인다
한 댓글에 여러 명의 '누가'를 담는 N:N 관계로 나중에 구현해볼 수 있다.
이름과 등급, 구매한 물품을 갖는 고객 정보 테이블과, 상품 정보 테이블, 할인율과 연관된 고객 등급 테이블을 만들어 서로 관계를 설정해보자.
한 명의 고객은 한 개의 등급을 갖는 반면, 한 등급은 여러 고객을 가질 수 있으므로 등급 1, 고객 N의 1:N 관계
아직 N:N 관계를 배우지 않았으니, 한 사람은 한 개의 물건만 살 수 있다고 하자. 그러면 한 상품은 여러개 팔릴 수 있지만 한 명의 고객은 한 상품만 살 수 있으므로 상품 1, 고객 N의 1:N 관계
class Level (models .Model ):
level = models .CharField (max_length = 2 )
discount_rate = models .IntegerField (default = 0 )
class Product (models .Model ):
name = models .CharField (max_length = 16 )
price = models .IntegerField ()
created_at = models .TextField (default = time .time ())
is_deleted = models .BooleanField (default = False )
class Customer (models .Model ):
user = models .OneToOneField (User , on_delete = models .CASCADE , related_name = 'customer' )
level = models .ForeignKey (Level , on_delete = models .SET_NULL , related_name = 'customers' , null = True , blank = True )
shopped_product = models .ForeignKey (Product , on_delete = models .SET_NULL , related_name = 'purchased_by' , null = True , blank = True )
등급은 쇼핑몰 정책에서 한 번 정의되고 나면 자주 바뀌지는 않는다. 필드를 정의할 때 choices라는 옵션을 통해 이미 정해진 내용 안에서만 고를 수 있게 할 수 있다.
choices에 할당할 수 있는 것은 두 개의 요소를 가진 이터러블을 담은 sequence 객체 또는 여러 값을 인스턴스 변수로 가진 클래스 객체이다.
choices가 필드에 주어지면 기본 form 위젯은 select box 형태를 띤다.
등급코드와 등급이름을 문자열로 갖는 튜플로 이루어진 배열 등으로 choices를 넣어준다고 하자
DB에는 등급코드가 저장되며 해당 데이터 안의 인스턴스 변수처럼 참조할 수 있다.
튜플의 두번째 요소인 등급이름은 해당 필드의 값으로 저장되며, 그룹객체로도 저장할 수 있다.
class Level (models .Model ):
LEVEL_CHOICES = [
('GR' , 'Green' ),
('SV' , 'Silver' ),
('GD' , 'Gold' ),
('PT' , 'Platinum )
]
level = models .CharField (choices = LEVEL_CHOICES , default = 'GR' , max_length = 2 )
discount_rate = models .IntegerField (default = 0 )
필드에 들어갈 값이 내가 정해둔 로직(함수)을 통화해야 insert되어 실수를 예방하게끔 한다.
def validate_discount_rate (value ):
if value > 1 or value < 0 :
return 에러에러에러
class CustomerLevel (models .Model ):
name = models .CharField (max_length = 32 )
discount_rate = models .FloatField (validators = [validate_discount_rate ])
옵션의 값으로는 위와 같이 함수 이름이 담긴 배열을 넣어준다. 여러 개일 수도 있고 한개여도 배열에 넣어서 넘겨야 한다.
이는 이 필드에 입력되는 값이 해당 함수를 에러 없이 통과하는 경우에만 데이터가 생성되게 하는 것으로, 정수만 받거나 존재하는 범위 내의 값만 받는 등으로 쓸 수 있다.
장고에서 기본적으로 제공하는 validator 사용하는게 더 편하다.
쇼핑몰 유저가 생성될 때, 특정 등급 등 객체를 ForeignKey로 할 때 dafault값을 갖게끔 하려면 두 가지 방법이 있다.
테이블 생성 시점을 달리하여 Customer 테이블이 생성되기 전에 CustomerLevel 테이블을 먼저 생성하고 데이터를 만들어줘야만 한다.
default 등 옵션 안에서는 연산이 안되니 아래와 같이 함수로 빼서 넣어줘야한다. 이 때 함수는 호출을 해도 되고 이름만 넣어줘도 된다.
class Customer (models .Model ):
def initial_date ():
return Level .objects .get (name = 'Green' )
level = models .ForeignKey (Level , default = initial_date (), on_delete = models .PROTECT , related_name = 'customers' )
지금은 같은 models.py에 있지만, 앱이 다를 경우 $ python manage.py makemigrations {app name}
으로 특정 앱의 모델만 먼저 migrate할 수 있다.
default 값의 대상이 될 모델을 migrate한 후 JSON 파일로 만든 데이터를 $ python manage.py loaddata {file name}
한다. 그 다음 default를 가지고 생성될 모델을 migrate해준다.
고객 테이블, 예약자와 예약제품 등의 정보를 담은 예약 건 테이블, 상품 테이블로 만들 수 있다.
예약 건은 고객 한 명이 여러 예약을 할 수 있으므로 1:N 으로 고객을 바라보는 ForeignKey 필드의 변수를 갖는다.
한 상품이 여러 예약의 대상이 될 수 있으므로 상품과 예약도 1:N의 관계를 가지며, 예약건 하나는 상품 하나를 바라보는 ForeignKey 필드의 변수를 갖는다.
class Customer (models .Model ):
user = models .OneToOneField (User , on_delete = models .CASCADE , related_name = 'customer' )
name = models .CharField (max_length = 16 )
class Product (models .Model ):
name = models .CharField (max_length = 32 )
price = models .IntegerField ()
class Reservation (models .Model ):
customer = models .ForeignKey (Customer , on_delete = models .SET_NULL , related_name = 'reservations' , null = True , blank = True )
product = models .ForeignKey (Product , on_delete = models .SET_NULL , related_name = 'reservations' , null = True , blank = True )
appointment = models .DateTimeField ()
is_paid = models .BooleanField (default = False )
is_completed = models .BooleanField (default = False )
is_cancelled = models .BooleanField (default = False )
예약은 완료되었는지, 취소되었는지, 결제는 되었는지 등을 BooleanField를 갖게 했는데, 단순 완료/미완 상태에 대해서 하나하나씩 필드를 따로 관리하기보다는 choices를 활용해서 status로 관리하는 것도 방법이다.
그러나 결제의 경우는 나누어 관리할 필요가 있으므로(카드결제를 한 예약건을 따로 관리한다거나 등) Payment라는 모델을 따로 만들어서 method와 status를 바라보는 1:N 관계로 구현하는 것이 좋다.
학생 조회 페이지에서 팔로잉/팔로워 기능 구현하기
템플릿의 student_detail에서 팔로잉을 할 수 있도록 form과 button 태그를 만들어준다.
action의 경로에 {% url 'follow' student.pk %}
를 해주고, csrf_token과 method는 POST로 해주는 것을 잊지 말 것
urls.py에서 follow 경로를 만들어주는데, 누구를 팔로우하는지 알아야 하니까 student_pk도 endpoint에 넣어준다.
views.py에서 함수를 정의한다. follow 버튼은 무조건 POST로 오니까 method를 확인하지 않아도 되며, return은 redirect로 student_detail 페이지로 보내준다.
models.py에 Relationship이라는 모델을 다음과 같이 만들어주면서, 한 명의 학생에게 하나의 relationship 객체가 붙게 한다.
class Relationship (models .Model ):
student = models .OneToOneField (Student , on_delete = models .CASCADE , related_name = 'relationship' )
followers = models .ManyToManyField (Student , related_name = 'following' )
이렇게 하면 한 학생을 팔로우하는 다른 학생 객체들은 relationship 객체의 followers에 이터러블로 담기는 모델이 생성된다. makemigrations
와 migrate
를 해준다.
views.py에서 회원가입시 생성된 학생 객체가 relationship을 가지도록, signup 함수에 relationship을 생성하며 생성된 학생 객체를 student에 값으로 넣어준다.
follow 함수에서 들어온 student_pk를 통해 relationship 객체를 취득한다.
relationship = Relationship.objects.filter(student__pk=student_pk)
내가 팔로우 중일 때 버튼을 누르면 relationship 객체의 followers 목록에서 내가 사라지고, 팔로잉을 하고 있지 않으면 그 목록에 추가되게끔 조건식을 만든다.
def follow (request , student_pk ):
relationship = Relationship .objects .filter (student__pk = student_pk )
if request .user .student in relationship .followers .all ():
relationship .followers .remove (request .user .student )
else :
relationship .followers .add (request .user .student )
return redirect ('student_detail' , student_pk )
이 때 followers 목록이 담는 것이 user 객체인지 student 객체인지 잘 확인할 것
student 객체를 담도록 해둔 필드인데 request.user
를 담으면 대혼란 초래
팔로우를 누르면 바뀌게끔 하려면, 템플릿에서 if문으로 작업할 수도 있긴 하지만 views에서 처리해주는 게 바람직하다.
student_detail에서 위의 조건식과 동일하게 내가 follow 중이라면 is_followed라는 변수가 True값을 가지게 한 후 context에 담아서 렌더링하고, 템플릿에는 is_followed 값에 따라 다른 버튼을 마크업한다.
def student_detail (request , student_pk ):
target_student = get_object_or_404 (Student , pk = student_pk )
is_followed = False
followers = Relationship .objects .filter (owner__pk = student_pk ).first ().followers .all ()
if request .user .student in followers :
is_followed = True
context = {
'student' : target_student ,
'is_followed' : is_followed
}
return render (request , 'student_detail.html' , context )
< form action ="{% url 'follow' student.pk %} " method ='POST '>
{% csrf_token %}
{% if is_followed %}
< button type ='submit '> 팔로우 취소</ button >
{% else %}
< button type ='submit '> 팔로우</ button >
{% endif %}
</ form >
다양한 예제에서 관계형 테이블을 사용해본 만큼 실제 생활에서도 엄청난 사용양상이 있겠다는 짐작이 된다.
장고는 정말 많은 기능을 구현해놨구나...