Django Model

지니🧸·2022년 10월 16일
0

Django

목록 보기
4/9

Django Model

1. 데이터베이스와 장고 모델

Model: 장고에서 데이터 구조를 정의하고 데이터를 활용하는 부분

  1. 데이터베이스 기본 개념+장고 모델과의 연관성
  2. 장고 migration
  3. 데이터 간의 관계를 모델로 표현하는 방법
  4. 모델을 통해 데이터 간의 관계를 활용하는 방법

1-2. 데이터베이스란?

데이터베이스: 컴퓨터에 체계적으로 저장되는 데이터의 모음

  • Relational Database: 데이터를 테이블 형태로 저장
    • Relation = 테이블
  • Non-Relational Database: 데이터를 테이블 형태로 저장하지 않음
    • 데이터를 다양하게 저장할 수 있지만 주로 문서(document) 형태로 데이터 저장
      • document-oriented database: 데이터를 문서 형태로 저장하는 데이터베이스
      • JSON 형식이 가장 보편적
    • 데이터를 문서로 저장하면 필요한 정보를 한 문서에서 바로 가져올 수 있어서 필요한 정보를 더 빨리 가져올 수 있지만 데이터가 중복됨

Database Management System (DBMS): 데이터베이스를 관리해주는 프로그램

  • (ex) SQLite (장고가 사용), MySQL, PostgreSQL

Structured Query Language (SQL): 데이터베이스와 소통할 때 쓰는 언어

1-3. 데이터베이스 테이블

  • 각 row: 테이터 하나
  • 각 column: 속성 (attribute, field) 하나
    • 데이터 타입이 정해져 있음
    • 고유값, 빈 값 가능 여부 등
  • id 속성: 각 row를 식별하는데 쓰이는 고유값
    • 장고는 id 필드(column)을 자동으로 만들어줌
    • id 값은 데이터베이스가 알아서 채워줌
  • primary key: 사용자가 지정한 row를 식별할 수 있는 column 또는 column의 조합
    • 대부분의 경우 primary key는 id로 설정

1-4. 데이터베이스 테이블 간의 관계

Foreign Key: 테이블과 테이블 간의 관계를 나타낼 때

  • 다른 테이블의 Primary Key를 참조함

관계의 종류

  1. 일대일 (1:1)
    1. (ex) 한 명의 유저는 하나의 프로필을 가질 수 있고, 하나의 프로필은 한 유저에게 속해있음
  2. 일대다 (1:N)
    1. (ex) 한 명의 유저는 여러 리뷰를 작성할 수 있지만, 하나의 리뷰는 작성자가 한명
  3. 다대다 (M:N)
    1. (ex) 한 명의 유저는 여러 리뷰를 좋아할 수 있고, 하나의 리뷰는 여러 유저가 좋아할 수 있음

1-5. 장고 모델과 ORM

Object-Relational Mapper (ORM): 파이썬의 object를 데이터베이스의 테이블 형태로 맵핑

  • django를 sql로 바꿔줌
  • django를 적고 migration 후 sql로

2. Migration

2-3. Migration 원리

python manage.py makemigrations
python manage.py migrate

Migration: 장고 모델에 정의된 내용을 데이터베이스 테이블로 옮겨주는 과정 (2단계 프로세스)

  1. 마이그레이션 파일 만들기 (python manage.py makemigrations)
    • 이 단계를 실행하면 장고는 models.py를 보고 모델에 대한 변경점을 마이그레이션 파일에 기록
    • app 안의 migrations 폴더 안에 생성
      • 파일 이름: {migration 번호}_{migration 내용}.py
        • migration 번호: 항상 네자리 숫자 (0001, 0002, 0003 …)
        • (예) 0003_auto.py
  2. 마이그레이션 파일 적용하기 (python manage.py migrate)
    • 커맨드를 실행하면 orm이 migration 파일의 내용을 sql로 변환해서 실행
    • 모델에 대한 변화가 데이터베이스에 반영

마이그레이션은 App 단위로 관리

프로젝트의 앱마다 앱에 있는 모델들에 대해서 마이그레이션 파일을 관리

2-4. 마이그레이션 사용해보기

어떤 마이그레이션이 적용됐는지 확인하고 싶을 때

python manage.py showmigrations

특정 앱의 마이그레이션 상태 확인하기

python manage.py showmigrations {app 이름}
python manage.py showmigrations coplate

특정 앱의 특정 마이그레이션 실행하기

python manage.py migrate {app 이름} {migration 파일 #}
python manage.py migrate coplate 0001

마이그레이션 파일 직접 만들기

python manage.py makemigrations --name "{파일 이름}"
python manage.py makemigrations --name "review_alter_title_max_length"
  • name을 따로 지정하지 않으면 장고가 자동적으로 이름을 생성함

fixture(데이터가 장고가 알아볼 수 있는 형태로 저장)를 데이터베이스에 넣기

python manage.py loaddata {fixture_name}
python manage.py loaddata data.json

2-6. 데이터베이스 직접 둘러보기

  1. SQLite를 사용해 db.sqlite3의 테이블 확인하기

  2. 장고의 데이터베이스 쉘

    python manage.py dbshell

    데이터베이스에 있는 모든 테이블 리스트하기

    .tables

    데이터를 조회할 때 column에 대한 정보도 조회하겠다

    .headers on

    테이블의 column 자체에 대한 정보 보기

    PRAGMA table_info('coplate_review');

    테이블 데이터 조회
    유저 테이블에 있는 모든 데이터 조회

    SELECT * FROM coplate_user;

    유저 테이블의 특정 column들만 조회

    SELECT email, nickname FROM coplate_user;

    데이터 필터하기

    SELECT email, nickname FROM coplate_user WHERE id=1;

    dbshell 종료

    .exit

2-7. 이전 마이그레이션으로 돌아가기

migration 6번까지 적용된 상태에서 migration 5번까지로 돌아가기

python manage.py migrate coplate 0005

migration 6번 파일 삭제하기: 파일 삭제

특정 앱의 모든 migration 취소하기

python manage.py migrate coplate zero

Migration Dependency

: 특정 migration이 다른 migration에게 의존

Untitled

  • migrate coplate 0001을 실행하면 coplate 0001의 디펜던시를 다 적용한 후 coplate 0001 적용
  • migrate coplate zero를 실행하면 coplate 001에 의존하는 coplate 0002~0005와 account 0001~0002를 먼저 취소하고 coplate 0001을 취소함

2-10. Migration 정리

makemigrations python manage.py makemigrations [app_label]

  • migration 파일을 만들어주는 커맨드
  • 지금까지의 마이그레이션을 모두 적용했을 때의 모델 상태와 현재 모델 상태를 비교해서, 변경점을 파일에 기록해줌
  • —name 옵션 python manage.py makemigrations --name "custom_name"
    • --name 옵션을 사용하지 않으면 장고가 알아서 이름을 정해주는데 가끔 auto로 시작하는 의미 없는 이름을 사용

migrate python manage.py migrate [app_label] [migration_name]

  • migration 파일 적용해줌
    • 장고 ORM이 migration 파일에 있는 내용을 SQL로 변환한 다음, SQL 코드를 실행해서 데이터베이스에 변화를 줌
python manage.py migrate {app_name}
python manage.py migrate {app_name} {migration_number}
python manage.py migrate {app_name} {prev_migration_number} #migration 되돌리기
python manage.py migrate zero #앱의 모든 migration 취소

showmigrations python manage.py showmigrations [app_label]

  • 어떤 migration 파일이 적용된 상태인지 확인

Migration Dependency: 특정 migration이 다른 migration에 의존

  • 다른 마이그레이션이 적용된 상태여야 이번 마이그레이션을 적용할 수 있음
  • 장고는 마이그레이션을 적용할 때 알아서 디펜던시를 먼저 적용
    • 마이그레이션을 취소할 때는 취소하는 마이그레이션에 대해 디펜던시가 있는 애들을 먼저 취소
  • 디펜던시 그래프: migration 간의 디펜던시를 그림으로 그린 것

2-11. Migration시 주의사항

취소한 migration을 다시 migrate한다고 삭제된 데이터가 다시 생성되지는 않음

테이블에 새로운 필드 추가하기

  • 새로운 필드를 추가하면 기존 데이터에도 값을 넣어줘야 함
    • 2가지 옵션: default 옵션을 수동 입력 OR 모델에 직접 default 값 입력
    • 주로 null=True, unique=True 등으로 설정

2-13, 14. 데이터 Migration

데이터 마이그레이션: 데이터를 다루는 마이그레이션 (데이터 삽입, 수정 등)

  • 언제 필요할까?
    • 데이터를 테이블의 구조를 따라 옮겨야 할 때
      • (예) last_name과 first_name 컬럼을 합쳐 full_name 컬럼을 생성하고 채우는 마이그레이션
  1. 모델에 필드(email_domain)를 추가한 후 makemigration
python manage.py makemigrations --name "user_email_domain"
  1. email_domain 필드를 채워넣는 data migration 만들기
#빈 migration 파일 만들기
python manage.py makemigrations **--empty** coplate --name "populate_email_domain"
  1. 0007_populate_email_domain.py의 operations 부분에 데이터를 옮기는 코드 작성
# Generated by Django 4.1 on 2022-08-24 08:09

from django.db import migrations

def save_email_domain(apps, schema_editor):
    User = apps.get_model('coplate', 'User')
    for user in User.objects.all():
        user.email_domain = user.email.split('@')[1]
        user.save()

class Migration(migrations.Migration):

    dependencies = [
        ("coplate", "0006_user_email_domain"),
    ]

    operations = [
        migrations.RunPython(save_email_domain),
    ]
  • save_email_domain(apps, schema_editor)
    • migration 함수는 parameter로 apps와 schema_editor를 받음
  • apps.get_model(’{app_name}’, ‘{model_name}’)
    • migrations.py에서는 모델을 import해서 쓰지 않고 apps를 통해 받아야 함
    • models.py에서 import할 수 없는 이유: models.py의 모델들은 지금 이 migration 파일이 예상하는 것보다 더 최신 버전일 수도 있기 때문

2-16. 데이터 마이그레이션 되돌리기

  • 프로그래머가 정의한 migration은 장고가 되돌리는 방법을 모를 수도 있음
    • RunPython의 두번째 파라미터로 마이그레이션을 되돌릴 때 실행할 함수를 넣으면 됨
# Generated by Django 4.1 on 2022-08-24 08:09

from django.db import migrations

def save_email_domain(apps, schema_editor):
    User = apps.get_model('coplate', 'User')
    for user in User.objects.all():
        user.email_domain = user.email.split('@')[1]
        user.save()

class Migration(migrations.Migration):

    dependencies = [
        ("coplate", "0006_user_email_domain"),
    ]

    operations = [
        migrations.RunPython(save_email_domain, **migrations.RunPython.noop**),
    ]
  • migrations.RunPython.noop
    • 이 migration을 되돌릴 때는 아무것도 실행할 필요 없음

2-17. Data Migration

  1. model에 새로운 필드 추가 (models.py)
class User(AbstractUser):
    ...

    email_domain = models.CharField(max_length=30, null=True)
  1. 비어있는 마이그레이션 파일 생성
python manage.py makemigrations --empty coplate --name "populate_email_domain"
  1. 생성한 마이그레이션 파일 내용 채우기
from django.db import migrations

def save_email_domain(apps, schema_editor):
    User = apps.get_model('coplate', 'User')
    for user in User.objects.all():
        user.email_domain = user.email.split('@')[1]
        user.save()

class Migration(migrations.Migration):

    dependencies = [
        ('coplate', '0006_user_email_domain'),
    ]

    operations = [
        migrations.RunPython(save_email_domain, migrations.RunPython.noop),
    ]
  1. migrate하기
python manage.py migrate

3. 데이터 모델링과 모델 구현하기

3-1. 데이터 모델링 I: 개체 모델링하기

데이터 모델링: 서비스의 요구 사항에 맞게 데이터의 구조와 형식을 정하는 것

  • 요구 사항 (명사=개체/모델, 동사=관계)
    • 한 유저는 여러 댓글을 달 수 있고, 댓글 하나는 한명의 작성자를 가짐
    • 리뷰 하나에는 여러 댓글이 있을 수 있고, 댓글 하나는 하나의 리뷰에 속함
    • 한 유저는 여러 리뷰에 좋아요를 누를 수 있고, 좋아요 하나는 한 유저의 것
    • 리뷰 하나에는 좋아요 여러 개가 있을 수 있고, 좋아요 하나는 하나의 리뷰에 속함
    • 한 유저는 여러 유저를 팔로우할 수 있고, 한 유저는 여러 유저한테 팔로우 받을 수 있음
  • Comment 모델
    • field: content, dt_created, dt_updated
  • 동사인지 명사인지 애매하면:
    • 따로 기록하고 싶은 정보가 있으면 → 객체
    • 아니면 → 관계
  • 좋아요 → 개체
    • 좋아요 누른 리뷰 모아보기
    • 가장 최근에 좋아한 리뷰부터
    • 좋아요를 언제 눌렀는지 (생성 시간) 기록해야 함
    • 필드: dt_created
  • 팔로우 → 관계
    • 팔로우 하는 유저들의 리뷰 모아보기
    • 가장 최신 리뷰부터 (언제 팔로우했는지는 관련X)
    • 팔로우의 생성 시간은 기록해 놓을 필요 X

3-2. 데이터 모델링 II: 관계 모델링하기

관계 파악하기

다이어그램:

Untitled

    • 인 쪽이 다(N)

3-3. 일대다 관계 구현하기

1:N(일대다) 관계: ForeignKey 사용

  • models.ForeignKey(<to_model>)
    • ForeignKey가 to_model의 primary key 저장
    • 1:N 관계에서 N쪽에 해당하는 모델에 정의
  • 예시
    #models.py
    class Review(models.Model):
    		...
    		dt_created = models.DateTimeField(auto_now_add=True)
    		dt_updated = models.DateTiemField(auto_now=True)
    		**author = models.ForeignKey(User, on_delete=models.CASCADE)**
    		...
    • author는 user id 값을 저장
    • on_delete: ForeignKey가 참조하는 데이터가 삭제되면 어떤 액션을 취할 것인지
      • on_delete = models.CASCADE
        • 특정 유저가 삭제되면 그 유저를 참조하고 있는 리뷰도 모두 삭제
      • on_delete = models.PROTECT
        • 유저를 참조하고 있는 리뷰가 하나라도 있으면 유저를 삭제하지 못함
      • on_delete = models.SET_NULL
        • 특정 유저를 삭제하면 그 유저를 참조하고 있던 리뷰들의 author 값이 모두 Null로 설정
    • ForeignKey
      • 일반 필드처럼 null, blank, default 같은 옵션 사용 가능
      • null=True 여야지만 models.SET_NULL 사용 가능

댓글 Model 정의하기

#models.py
class Comment(models.Model):
    content = models.TextField(max_length=500, blank=False)

    dt_created = models.DateTimeField(auto_now_add=True)

    dt_updated = models.DateTimeField(auto_now=True)

    author = models.ForeignKey(User, on_delete=models.CASCADE)

    review = models.ForeignKey(Review, on_delete=models.CASCADE)

    def __str__(self):
        return self.content[:30]

3-5. 일대일(1:1) 관계 구현하기

유저 한 명은 프로필 하나를 가지고 프로필 하나는 유저 한 명에게 속함

models.OneToOneField(<to_model>, on_delete=…)

  • <to_model>의 primary key 저장
  • <to_model>의 instance들이 이미 저장돼있어야 함
  • 어느 모델이 어느 모델에 속해있는가?
    • 프로필이 유저에 속해있음
      • 속해있는 모델에 OneToOneField 정의
      • 프로필에 유저와의 관계 정의
#models.py
class User(AbstractUser):
		def __str__(self):
				return self.email

class Profile(models.Model):
		nickname = models.CharField(
				max_length=15, 
				unique=True, 
				null=True, 
				validators=[validate_no_special_characters], 
				error_messages={'unique':'이미 사용중인 닉네임입니다.'},
		)
		profile_pic = models.ImageField(default='default_profile_pic.jpg', upload_to='profile_pics')
		intro = models.CharField(max_length=60, blank=True)
		user = models.OneToOneField(User, on_delete=models.CASCADE)

3-6. 유저 모델 나누기

User 모델을 User 모델과 Profile 모델로 나눌 때 데이터를 잃지 않으려면 데이터 마이그레이션 해야 함

Untitled

  • 프로필 모델로 데이터를 옮긴 후, 유저 모델에서 프로필 필드를 지워줘야 함
  1. 프로필 모델 생성하기
#models.py
class User(AbstractUser):
    nickname = models.CharField(
        max_length=15, 
        unique=True, 
        null=True,
        validators=[validate_no_special_characters],
        error_messages={'unique': '이미 사용중인 닉네임입니다.'},
    )

    profile_pic = models.ImageField(default='default_profile_pic.jpg', upload_to='profile_pics')

    intro = models.CharField(max_length=60, blank=True)

    def __str__(self):
        return self.email

class Profile(models.Model):
    nickname = models.CharField(
        max_length=15, 
        unique=True, 
        null=True,
        validators=[validate_no_special_characters],
        error_messages={'unique': '이미 사용중인 닉네임입니다.'},
    )

    profile_pic = models.ImageField(default='default_profile_pic.jpg', upload_to='profile_pics')

    intro = models.CharField(max_length=60, blank=True)

    user = models.OneToOneField(User, on_delete=models.CASCADE)
  • Profile 모델 정의하고 User 모델에 있는 프로필 필드 복사한 다음, OneToOneField로 일대일 관계 형성
  • 유저 모델에 있는 프로필 필드는 남겨줌
  • makemigrations & migrate
python manage.py makemigrations --name "profile"
python manage.py migrate
  1. 프로필 데이터 옮기기
python manage.py makemigrations --empty coplate --name "migrate_profile_data"
  • migration 파일 채우기
#0008_migrate_profile_data.py
from django.db import migrations

def user_to_profile(apps, schema_editor):
    User = apps.get_model('coplate', 'User')
    Profile = apps.get_model('coplate', 'Profile')
    for user in User.objects.all():
        Profile.objects.create(
            nickname = user.nickname,
            profile_pic = user.profile_pic,
            intro = user.intro,
            user = user,
        )

class Migration(migrations.Migration):

    dependencies = [
        ('coplate', '0007_profile'),
    ]

    operations = [
        migrations.RunPython(user_to_profile),
    ]
  • migration 적용하기
python manage.py migrate
  1. 유저 모델에서 프로필 필드 지우기
  • 유저 모델에서 프로필 필드를 다 지우고 AbstractUser에 해당하는 필드만 남겨둠
#models.py
class User(AbstractUser):
		def __str__(self):
				return self.email
  • ProfileForm에서도 필드 삭제
#forms.py
class ProfileForm(forms.ModelForm):
    class Meta:
        model = User
        fields = [
            # 'nickname',
            # 'profile_pic',
            # 'intro',
        ]
        widgets = {
            # 'intro': forms.Textarea,
        }
  • migration
python manage.py makemigrations --name "delete_user_profile_fields"
python manage.py migrate
  1. 다시 유저 모델 하나로 돌아가기
  • migration 파일에 migration 되돌리기 함수 추가
#0008_migrate_profile_data.py
def user_to_profile(apps, schema_editor):
    User = apps.get_model('coplate', 'User')
    Profile = apps.get_model('coplate', 'Profile')
    for user in User.objects.all():
        Profile.objects.create(
            nickname = user.nickname,
            profile_pic = user.profile_pic,
            intro = user.intro,
            user = user,
        )
        

**def profile_to_user(apps, schema_editor):
    User = apps.get_model('coplate', 'User')
    Profile = apps.get_model('coplate', 'Profile')
    for profile in Profile.objects.all():
        user = profile.user
        user.nickname = profile.nickname
        user.profile_pic = profile.profile_pic
        user.intro = profile.intro
        user.save()**

class Migration(migrations.Migration):

    dependencies = [
        ('coplate', '0007_profile'),
    ]

    operations = [
        migrations.RunPython(user_to_profile, **profile_to_user**),
    ]
  • migrations
python manage.py migrate coplate 0006
  • 모델과 폼 모두 코드 되돌려놓기
  • 사용하지 않는 migration 파일 삭제하기

3-7. 다대다(M:N) 관계 구현하기

models.ManyToManyField(<to_model>)

  • 두 모델 중 어느 모델에 넣든 상관없음
  • on_delete 옵션이 없음
  • 자기 자신과 연결할 때는 <to_model>에 ‘self’
    • models.ManyToManyField(’self’)
  • 관계가 대칭/비대칭인지
    • symmetrical 옵션
    • models.ManyToManyField(’self’, symmetrical=False)
  • null 옵션 X

3-8. Generic 관계란?

좋아요와 아무 모델을 연결할 수 있는 일반적인(generic) 관계 만들기

contenttypes: 장고 어플리케이션에 사용되는 모든 모델에 대한 정보를 관리하는 앱

  • 설치 필요 X
  • ContentType + GenericForeignKey → Generic 관계 형성
    • ContentType: 장고 어플리케이션에 사용되는 모든 모델에 대한 정보 저장
      • 포함내용: app_label, model
        • app_label: 모델이 속해있는 앱
        • model: 모델 클래스 이름
  • content_type과 object_id 모두 기록 → 어떤 오브젝트와도 관계 형성 가능
#models.py
class Like(models.Model):
		...
		content_type = models.ForeignKey(ContentType, on_delete=models.CASCADE)
		object_id = models.PositiveIntegerField()
		**liked_object = GenericForeignKey('content_type', 'object_id')**
  • 연결되어있는 object에 접근하기
#liked_object를 사용하지 않으면

#먼저 모델 클래스를 가져오기
model_cls = like.content_type.model_class()
#object_id로 필터
model_cls.objects.get(id=object_id)

#liked_object를 사용하면
**like.liked_object**

Generic 관계 구현하기

#models.py
...
from django.contrib.contenttype.models import ContentType
from django.contrib.contenttype.fields import GenericForeignKey
...
class Like(models.Model):
    dt_created = models.DateTimeField(auto_now_add=True)

    user = models.ForeignKey(User, on_delete=models.CASCADE)

    content_type = models.ForeignKey(ContentType, on_delete=models.CASCADE)

    object_id = models.PositiveIntegerField()

    liked_object = GenericForeignKey()

		def __str__(self):
        return f"({self.user}, {self.liked_object})"
  • GenericForeignKey
    • on_delete 설정 못함
    • 단순 관계만 보여주기 때문
  • def str
    • liked_object가 user면 user의 str 메소드 호출
    • liked_object가 review면 review의 str 메소드 호출

3-12. 장고에서 관계 구현하기 정리

일대다 관계 (1:N)

class MyModel(models.Model):
    ...    
    field_name = models.**ForeignKey**(<to_model>, on_delete=<option>, ...)
  • ForeignKey 사용
  • ‘N’에 해당하는 쪽에 ForeignKey 필드 추가
  • 자주 사용하는 on_delete 옵션 3가지
    1. models.CASCADE: 특정 유저가 삭제되면 그 유저를 참조하고 있는 리뷰도 모두 삭제
    2. models.PROTECT: 유저를 참조하고 있는 리뷰가 하나라도 있으면 유저를 삭제하지 못하게 함
    3. models.SET_NULL: 특정 유저를 삭제하면, 그 유저를 참조하고 있던 리뷰들의 author 값이 모두 NULL로 설정. (사용하려면 ForeignKey 필드에 null=True로 설정해 줘야 )

일대일 관계 (1:1)

class MyModel(models.Model):
    ...
    field_name = models.OneToOneField(<to_model>, on_delete=<option>, ...)
  • OneToOneField 사용
  • 한 모델이 다른 모델에 ‘속해'있는 경우가 많음
    • 속해있는 쪽에 OneToOneField 추가

다대다 관계 (M:N)

  • ManyToManyField 사용
    • on_delete 옵션 없음
      • ‘참조하고 있는 오브젝트'를 정의할 수 없음
    • 어느 모델에 추가하든 상관 없음
    • null 옵션 없음

자신과 관계를 맺을 때

  • 경우
    • 유저가 유저를 팔로우 하는 경우 (다대다)
    • 두 유저가 친구가 되는 경우 (다대다)
    • 댓글에 댓글을 다는 경우 (일대다)
  • <to_model>에 ‘self’를 넣어줌
  • 대칭 여부는 symmetrical 옵션을 통해 전달
# 팔로우
class User(AbstractUser):
    ...
    following = models.ManyToManyField('self', symmetrical=False)

# 친구
class User(AbstractUser):
    ...
    friends = models.ManyToManyField('self', symmetrical=True)

# 댓글
class Comment(models.Model):
    ...
    parent_comment = models.ForeignKey('self') # symmetrical은 다대다 관계에서만 사용합니다.

제네릭 관계

from django.contrib.contenttypes.models import ContentType
from django.contrib.contenttypes.fields import GenericForeignKey
    

class MyModel(models.Model):
    ...
    content_type = models.ForeignKey(ContentType, on_delete=models.CASCADE)

    object_id = models.PositiveIntegerField()

    content_object = GenericForeignKey('content_type', 'object_id')

3-13. 모델 메타 옵션

메타 옵션, Meta Option: 자기 자신에 대한 설정

class Review(models.Model):
    ...

    class Meta:
        ordering = ['-dt_created'] #views.py의 ordering 모두 삭제 가능

모델 메타 옵션

모델에 ordering을 정의하면 views.py의 order_by, ordering 등은 모두 삭제해도 됨.

3-15. Admin 사이트: ModelAdmin

  • 새로 정의한 model은 migration 적용도 하고 admin에 등록도 해야함
#admin.py
from django.contrib import admin
from django.contrib.auth.admin import UserAdmin

from .models import User, Review, **Comment, Like**

UserAdmin.fieldsets += ('Custom fields', 
			{'fields': ('nickname', 'profile_pic', 'intro',**'following'**)}),

admin.site.register(User, UserAdmin)

admin.site.register(Review)

**admin.site.register(Comment)

admin.site.register(Like)**
  • UserAdmin
    • ModelAdmin을 상속받아서 장고가 제공하는 유저 모델에 맞게 설정해둔 것
  • fieldsets (UserAdmin의 속성)
    • admin 페이지에서 보여줄 필드 정함
  • admin.site.register(User, UserAdmin)
    • 모델을 등록할 때 UserAdmin을 사용해서 등록하겠다
    • 여기서 UserAdmin을 따로 써주지 않으면 ModelAdmin이 default로 들어감

ModelAdmin 클래스

  • 특정 모델을 admin 페이지에서 어떻게 보여줄지 결정
    • 모델 필드들 중에 어떤 필드를 보여줄지
    • 각 필드를 어떤 형태로 보여줄지
  • 기본 설정 그대로
    from django.contrib import admin
    
    admin.ModelAdmin
  • Customize해서 사용
    from django.contrib import admin
    
    class CustomAdmin(admin.ModelAdmin):
    		...

3-16. Admin 사이트: Inline 사용하기

Inline: 같은 줄에서

  • admin 사이트의 리뷰 수정 페이지에서 댓글/좋아요를 추가할 수 있게 만들고 싶음
    • 리뷰를 가르키는 오브젝트들도 페이지 내에서 다룰 수 있게
from django.contrib import admin
from django.contrib.auth.admin import UserAdmin
**from django.contrib.contenttypes.admin import GenericStackedInline**
from .models import User, Review, Comment, Like

class CommentInline(admin.StackedInline):
    model = Comment

class LikeInline(**GenericStackedInline**):
    model = Like

UserAdmin.fieldsets += ('Custom fields', {'fields': ('nickname', 'profile_pic', 'intro','following')}),

class ReviewAdmin(admin.ModelAdmin):
    inlines= (
        CommentInline, #ReviewAdmin에서 CommentInline을 사용하겠다는 뜻
        LikeInline,
    )

class CommentAdmin(admin.ModelAdmin):
    inlnes= (
        LikeInline,
    )
    

admin.site.register(User, UserAdmin)

admin.site.register(Review, ReviewAdmin)

admin.site.register(Comment, CommentAdmin)

admin.site.register(Like)
  • GenericForeignKey를 사용하는 모델 (예: Like)는 admin.StackedInline 대신에 GenericStackedInline을 사용해야 함

3-17. Admin 사이트: ManyToManyField와 Inline

  • User Model의 following 필드에 blank=True 추가
  • class UserInline 정의
#admin.py
...
class UserInline(admin.StackedInline):
    model = User.following.through
    fk_name = 'to_user'
    verbose_name = 'Follower'
    verbose_name_plural='Followers'
...
UserAdmin.inlines = (UserInline,)
...
  • model = User.following.through
    • 다대다 관계로 연결된 모델을 inline으로 수정하려면 이렇게 설정해야 함
  • fk_name = ‘to_user’
    • self 관계인 경우에는 fk_name (ForeignKey name)도 설정해줘야 함
    • 다대다 관계가 가르키는 오브젝트를 인라인으로 수정하고 싶으면:
      • fk_name = ‘from_user’
    • 다대다에 대한 역관계를 인라인으로 수정하고 싶으면:
      • fk_name = ‘to_user’
    • 팔로워를 수정하고 싶은 거니까 ‘to_user’
    • self 관계가 아니면 foreignKey name 명시 X
  • verbose_name
    • admin 페이지에서 inline을 보여줄 때 조금 더 의미있는 이름을 사용하도록 설정
    • 다대다 관계가 아니면 이름이 직관적이니 명시할 필요 X

4. 데이터 다루기

4-1. CRUD 연산 복습

  • Model: Skill
  • Fields: title, summary, dt_created

.all(): 모델에 해당하는 모든 오브젝트 가져오기

  • 항상 쿼리셋 리턴
Skill.objects.all()
  • QuerySet: 여러 오브젝트의 집합/모음
    • 쿼리셋에는 추가적인 CRUD 연산 가능

.filter(): 필터 조건에 해당하는 오브젝트들을 가져옴

  • 항상 쿼리셋 리턴
Skill.objects.filter(id=3)
Skill.objects.filter(title__startswith='프로그래밍 기초')
Skill.objects.filter(summary__contains='웹 개발')
Skill.objects.filter(title__startswith='프로그래밍 기초').filter(title__contains='Python')
Skill.objects.filter(title__startswith='프로그래밍 기초')**.count()**

.get(): 조건에 해당하는 오브젝트를 가져옴

  • .get()은 .filter()와 다르게 오브젝트를 리턴함
skill = Skill.objects.get(id=1)
print(skill.title)

.order_by(): 쿼리셋을 정렬해 줌

  • 파라미터로 정렬 기준 명시
    • 내림차순으로 정렬하려면 앞에 -기호를 붙이면 됨
Skill.objects.order_by('dt_created')
Skill.objects.filter(summary__contains='웹 개발').order_by('dt_created')

.exists(): 쿼리셋에 오브젝트가 하나라도 있는지 확인해줌

  • 쿼리셋이 비어있는지 판단하기 좋음
Skill.objects.filter(title__startswith='프로그래밍 기초').exists()
Skill.objects.filter(title__contains='C++').exists()

.count(): 쿼리셋에 있는 오브젝트 개수를 세줌

Skill.objects.count()
Skill.objects.filter(title__startswith='프로그래밍 기초').count()

데이터 생성하기 .create()

  • 파라미터로 생성할 오브젝트의 필드값을 넘겨주면 됨
  • 필수가 아닌 필드나 자동으로 채워지는 필드 (예: id)는 값을 주지 않아도 됨
Skill.objects.create(
			title='웹 퍼블리싱', 
			summary='웹 페이지의 가장 기본이라 할 수 있는 HTML과 CSS를 통해 내 머릿속의 아이디어를 실현해 보세요!'
)

데이터 수정하기

  • 먼저 수정할 데이터를 가져온 다음, 필드를 수정하고 저장하면 됨
skill = Skill.objects.get(id=6)
skill.title = 'Web 퍼블리싱'
skill**.save()**

데이터 삭제하기 .delete()

  • 먼저 삭제할 데이터를 가져와서 .delete()메소드를 호출해 주면 됨
skill = Skill.objects.get(id=6)
skill.delete()

Generic View와 CRUD 연산

  • 제네릭 뷰를 사용하면 뷰가 알아서 CRUD 연ㅅ산을 해줌
    • 제네릭 뷰를 사용하면 장고가 알아서 데이터를 생성하고, 수정해 주기 때문

Template과 CRUD 연산

  • 템플릿 파일에서 제한적으로 CRUD 연산을 할 수 있음
  • <Model>.objects로 시작하는 연산을 사용할 수 없음
  • 파라미터가 들어가는 메소드도 사용할 수 없음
  • 이미 쿼리셋이 템플릿으로 전달되고 있는 경우, 쿼리셋을 반복문으로 돌 수 있음
    {% for skill in skills %}
  • 파라미터가 들어가지 않는 .count() 같은 메소드는 호출할 수 있음
    • 템플릿에서 메소드를 호출할 때는 괄호 ()를 쓰지 않음

      {{ skills.count }}

4-2. 매니저와 역관계

<Model>.objects.all()
<Model>.objects.get(...)
<Model>.objects.filter(...)
<Model>.objects.count()

Manager, 매니저: 장고 모델과 데이터베이스를 연결해 주는 인터페이스

  • (예) .objects
  • 매니저를 통해 모델에 대한 ORM 연산 수행 가능
  • 베이스 또는 기본 쿼리셋이 숨어 있음
    • (예) .objects → Model에 해당하는 모든 오브젝트들
    • 하지만 숨겨진 쿼리셋을 일반 쿼리셋처럼 사용할 수 없음
    • 매니저에 CRUD 연산을 하면 숨겨진 쿼리셋이 사용됨
      <Model>.objects.order_by(...) #정렬된 쿼리셋 리턴

Reverse Relationship, 역관계

  • Comment 모델에는 author와 review 필드가 정의되어 있으니 comment.author, comment.review로 부를 수 있음
    • Review 모델에는 comment 필드가 없지만 쉽게 comment을 접근할 수 없을까?
      • 장고에서는 가능! 한쪽으로 ForeignKey를 만들면 역관계도 정의해줌
        • .<Other_Model>_set
        • (ex) review.comment_set
          • 이것도 매니저
            • 숨겨져 있는 쿼리셋: 이 리뷰의 코멘트
for comment in review.comment_set.all():
		print(comment)
review.comment_set.count()
review.comment_set.filter(content__contains="안녕")
#html
{% for comment in review.comment_set.all %}
	...
{% endfor %}

4-3. 역관계 자세히 알아보기

OneToOneField의 역관계

  • 유저와 프로필의 관계: 프로필이 유저에 속함 → 프로필 모델에 유저 필드 정의
  • profile.user : 프로필을 가지고 있는 유저
  • 유저의 프로필은 어떻게 접근할까?
    • user.profile
      • 프로필이 속한 유저는 하나기 때문에 이렇게 간단히 사용 가능
      • user.profile은 매니저가 아닌 실제 프로필 오브젝트
    • user.profile.nickname 가능
    • user.profile.intro 가능

ManyToManyField

  • user들 간의 following 관계
  • user.following: 유저의 팔로잉 유저들
    • 매니저임
    • 숨겨진 쿼리셋: user가 팔로우하는 유저들 (팔로잉)
      • 그대로 사용 X, CRUD 연산 사용해야 함
  • 유저를 팔로우하고 있는 유저들은?
    • user.user_set
      • 매니저임
      • 숨겨진 쿼리셋: 유저를 팔로우하는 유저들 (팔로워)
        • 그대로 사용 X, CRUD 연산 사용해야 함
  • 역관계의 이름 바꾸기 (user.user_set → user.followers)
    • following 필드에 related_name으로 정의

      #models.py
      class User(AbstractUser):
          ...
          following = models.ManyToManyField(
      														'self', 
      														symmetrical=False, 
      														blank=True, 
      														**related_name="followers"**
      												)
      		...
      class Review(models.Model):
          ...
          author = models.ForeignKey(
      														User, 
      														on_delete=models.CASCADE, 
      														related_name="reviews"
      										)
          ...
      
      class Comment(models.Model):
          ...
          author = models.ForeignKey(
      														User, 
      														on_delete=models.CASCADE, 
      														related_name="comments"
      										)
      
          review = models.ForeignKey(
      														Review, 
      														on_delete=models.CASCADE, 
      														related_name="comments"
      										)
      		...
      
      class Like(models.Model):
      		...
          user = models.ForeignKey(
      														User, 
      														on_delete=models.CASCADE, 
      														related_name="likes"
      										)
      		...
    • 아예 역관계가 필요 없을 때에는 related_name=”+”

      • 역관계가 아예 생성되지 않음
  • GenericForeignKey는 자동으로 역관계를 생성하지 않음
    • GenericRelation을 import해서 역관계를 추가하고 싶은 모델에 GenericRelation 필드를 추가해주면 됨
      - 리뷰, 코멘트 모델에 likes GenericRelation 추가

      from django.contrib.contenttypes.fields import GenericForeignKey, **GenericRelation
      
      ...**
      class Review(models.Model):
      		...
      		likes = GenericRelation("Like") #Review 모델이 Like 모델보다 먼저 정의돼서 가능 
      		...
      
      class Comment(models.Model):
      		...
      		likes = GenericRelation("Like")
      		...
    • review.likes, comment.likes로 부를 수 있음

      • 둘다 매니저임
    • GenericForeignKey는 on_delete 옵션이 없어서 리뷰/코멘트가 삭제되도 거기에 달린 좋아요는 삭제되지 않음

      • 남아있는 좋아요의 liked_object는 None이 됨
      • 하지만 GenericForeignKey가 가르키는 모델에 GenericRelation을 정의하면 이게 on_delete 기능까지 수행
        • 댓글/리뷰가 지워지면 좋아요도 지워짐

Untitled

4-6. 매니저와 역관계 정리

매니저란?

  • 매니저: .objects의 .objects 부분
  • 장고 모델과 데이터베이스를 연결해주는 인터페이스
  • 매니저를 통해 모델에 대한 ORM 연산 수행 가능
  • 기본 쿼리셋이 숨어 있음
    • .objects의 기본 쿼리셋: 오브젝트 전부
    • objects.filter(…)의 기본 쿼리셋: 오브젝트 중 필터 조건에 알맞은 것
  • 매니저는 일반 쿼리셋처럼 쓸 수 없고 꼭 CRUD 연산을 함께 사용해야 함
    • for review in Review.objects (X)
    • for review in Review.objects.all() (O)

역관계, Reverse Relationship란?

  • 관계를 정의하는 필드를 통해 연결된 오브젝트에 접근 가능
  • 장고는 역관계를 만들어줌 (예) user.comment_set
  • 관계나 역관계가 여러 오브젝트를 가리킬 경우 (user.comments, user.following 등) 속성은 매니저

제네릭 관계는?

  • GenericForeignKey는 자동으로 역관계를 만들어 주지 않기 때문에 직접 역관계를 만들어 줘야 함
  • 연결된 모델에 GenericRelation 필드 추가
    • variable 이름으로 접근 가능 (예) likes = GenericRelation(”Like”) → comment.likes
  • GenericForeignKey는 on_delete 옵션이 없어서 제네릭 관계가 참조하는 오브젝트가 삭제되면 GenericForeignKey 필드는 그냥 None이 됨
    • 반대쪽 (제네릭 관계가 참조하는) 모델에 GenericRelation 필드를 정의하면 CASCADE 효과를 얻을 수 있음
    • 제네릭 관계가 참조하는 오브젝트가 삭제되면 참조하던 오브젝트도 같이 삭제됨

4-7. 필터 조건에 관계 활용하기

ForeignKey와 같이 관계를 형성하는 필드도 필터 조건으로 사용 가능

  • Review 모델에는 author라는 ForeignKey 필드가 존재
    • Review.objects.filter(author=some_user)
    • Review.objects.filter(author__nickname=”jonghoon”)
      • “__(더블 언더바)”를 사용하면 관계와 필드를 계속 타고 들어갈 수 있음
    • Review.objects.filter(authoremailendswith=”gmail.com”)
      • 이메일이 “gmail.com”으로 끝나는 유저가 작성한 리뷰
    • Comment.objects.filter(reviewauthoremail__endswith=”gmail.com”)
      • 이메일이 “gmail.com”으로 끝나는 유저가 작성한 리뷰에 달려있는 코멘트들

ManyToManyField의 필터

  • User.objects.filter(following__id=1)
    • following에 있는 유저들 중 id가 1인 유저가 하나라도 있다
    • id 1을 가진 유저를 팔로우하는 유저들 필터
  • 조건식의 왼쪽이 여러 값을 나타낸 다면 여러 값 중 하나라도 오른쪽과 매칭이 되면 됨

필터에 역관계도 활용 가능

  • User.objects.filter(reviews__restaurant_name=’코스버거')
    • 코스버거에 리뷰를 하나라도 남긴 유저 필터

GenericForeignKey로는 필터 불가

  • GenericForeignKey는 다양한 종류의 오브젝트를 가르킬 수 있기 때문에 liked_object를 호출하면 liked_object가 어떤 종류의 오브젝트인지 알 수 없음
  • GenericRelation 필드 활용
    • related_query_name 옵션을 정의하면 필터에 사용 가능
      - 필터 조건을 작성할 때만 유효
      - 이걸 통해 리뷰/댓글 오브젝트에 접근은 불가
      - 리뷰/댓글 오브젝트에 접근하려면 like.liked_object 해야 함

      class Review(models.Model):
      		...
      		likes = GenericRelation("Like", related_query_name="review")
    • Like.objects.filter(review=review)

    • Like.objects.filter(comment_id=1)

4-9. 관계와 생성, 수정, 삭제 연산

  • 생성, 수정, 삭제 연산은 주로 제네릭 뷰가 알아서 처리해주지만 제네릭 뷰를 쓰기에 적합하지 않은 상황들도 있음

생산과 수정: ForeignKey와 OneToOneField

class Comment(models.Model):
    content = models.TextField(max_length=500, blank=False)

    dt_created = models.DateTimeField(auto_now_add=True) #자동 생성

    dt_updated = models.DateTimeField(auto_now=True) #자동 생성

    author = models.ForeignKey(User, on_delete=models.CASCADE, related_name="comments")

    review = models.ForeignKey(Review, on_delete=models.CASCADE, related_name="comments")

    likes = GenericRelation("Like", related_query_name="comment") #역관계 생성
		...
  • 생산
    comment = Comment.objects.***create***(content=”안녕하세요", author=user1, review=review1)
    comment = Comment.objects.***create***(content=”안녕하세요", author_id=1, review_id=1)
  • 수정: 재할당 및 save()
    comment.author = user2
    comment.author_id = 2
    comment***.save()***
  • OneToOneField의 생산 & 수정도 같음
    class Profile(models.Model):
    		...
    		user = models.OneToOneField(User, on_delete=models.CASCADE)
    profile = Profile.objects.create(user=user1, ...)
    profile = Profile.objects.create(user_id=1, ...)
    
    profile.user=user2
    profile.user_id=2
    profile.save()
  • GenericForeignKey의 생산 & 수정
    class Like(models.Model):
        dt_created = models.DateTimeField(auto_now_add=True)
    
        user = models.ForeignKey(User, on_delete=models.CASCADE, related_name="likes")
    
        content_type = models.ForeignKey(ContentType, on_delete=models.CASCADE)
    
        object_id = models.PositiveIntegerField()
    
        liked_object = GenericForeignKey()
    		...
    like = Like.objects.create(user=user1, content_type=ct1, object_id=1)
    like = Like.objects.create(user=user1, content_type_id=1, object_id=1)
    
    like.content_type_id=2
    like.object_id=2
    like.save()
    
    #liked_object 사용 (id 전달 불가: content type 파악 불가해서)
    like = Like.objects.create(user1, liked_object=obj1) #obj1: 어떤 리뷰/코멘트 오브젝트
    like.liked_object=obj2
    like.save()
  • ManyToManyField의 생산 & 수정
    • ManyToManyField는 항상 비어있을 수 있음

    • 오브젝트 생성 시에는 ManyToManyField에 값을 전달할 필요 없고 이후에 필드에 오브젝트를 추가하거나 제거

    • .add()와 .remove() 메소드 활용

      class User(AbstractUser):
          nickname = models.CharField(
              ...
          )
      
          profile_pic = models.ImageField(default='default_profile_pic.jpg', upload_to='profile_pics')
      
          intro = models.CharField(max_length=60, blank=True)
      
          following = models.ManyToManyField('self', symmetrical=False, blank=True, related_name="followers")
      
          ...
    • user1의 following 필드에 user2, user3, user4 추가

      user1.following.add(user2, user3, user4) #user 오브젝트들 넘겨주기
      user1.following.add(2, 3, 4) #id들 넘겨주기
      
      user1.following.remove(user2, user3, user5)
      user1.following.remove(2, 3, 5) #following 필드에 없는 유저가 파라미터로 전달되면 그 유저는 그냥 무시함
      
      #ManyToMany 필드의 역관계도 똑같음
      user1.followers.add(user2, user4)
      user1.followers.remove(2)
      

삭제: .delete()

user.delete() #유저 삭제 -> 리뷰, 댓글, 좋아요 오브젝트들도 삭제 
reivew.delete() #리뷰 삭제 -> 댓글, 좋아요 오브젝트들도 삭제 
comment.delete() #댓글 삭제 -> 좋아요 오브젝트들도 삭제
  • object가 삭제되면, 그 object를 참조하고 있는 object들도 삭제됨
    • ForeignKey: on_delete=models.CASCADE
    • GenericForeignKey: 가리키는 모델에 GenericRelation 필드 정의

4-11. 관계와 CRUD 연산 정리

필터에 관계/역관계 활용하기

# 1. user가 작성한 리뷰들 필터
Review.objects.filter(author=user)

# 2. id 1을 가지고 있는 유저가 작성한 리뷰들 필터
Review.objects.filter(author__id=1)

# 3. jonghoon이라는 닉네임을 가지고 있는 유저가 작성한 리뷰들 필터
Review.objects.filter(author__nickname="jonghoon")

# 4. 이메일이 codeit.com으로 끝나는 유저들이 작성한 리뷰들 필터
Review.objects.filter(author__email__endswith='codeit.com')

# 5. jonghoon이라는 유저가 작성한 리뷰에 달린 코멘트들 필터
Comment.objects.filter(review__author__nickname='jonghoon')
  • 오브젝트 자체 비교 가능
  • 오브젝트의 필드값 비교 가능
  • 필드값을 비교할 때 연산자 사용 가능
  • 오브젝트의 필드를 접근하거나 필드에 연산자를 붙일 때 모두 더블 언더바(__) 사용
#역관계 활용

# '코스버거' 레스토랑에 대한 리뷰를 작성한 유저들 필터
User.objects.filter(**reviews**__restaurant_name='코스버거')

GenericForeignKey는?

  • GenericForeignKey는 필터 불가
    • (ex) filter에 liked_object 불가
  • 제네릭 관계가 가리키는 오브젝트(우리 예시의 경우 리뷰나 댓글)를 필터 조건에 사용하려면 해당 모델의 GenericRelation필드에 related_query_name을 설정해야 함
# 특정 리뷰에 속해있는 좋아요들 필터
Like.objects.filter(review=review)
Like.objects.filter(review__id=1)

# 특정 댓글에 속해있는 좋아요들 필터 
Like.objects.filter(comment=comment)
Like.objects.filter(comment__id=1)

관계 필드가 있는 오브젝트 생성/수정

  • ForeignKey
    • 생성

      comment = Comment.objects.create(content="안녕하세요", author=user1, review=review1)
      # 또는
      comment = Comment.objects.create(content="안녕하세요", author_id=1, review_id=1)
    • 수정

      # author 필드 수정
      comment.author = user2
      # 또는
      comment.author_id = 2
      
      # review 필드 수정
      comment.review = review2
      # 또는
      comment.review_id = 2
      
      # 오브젝트 저장
      comment.save()
  • OneToOneField
    • 생성

      profile = Profile.objects.create(nickname="testuser", user=user1)
      # 또는
      profile = Profile.objects.create(nickname="testuser", user_id=1)
    • 수정

      profile.user = user2
      # 또는
      profile.user_id = 2
      
      profile.save()
  • ManyToManyField
    • 항상 비어있을 수 없음

    • 오브젝트가 생성된 후에 값을 추가

      • 다른 필드와 다르게 오브젝트를 생성할 때는 값을 줄 수 없음
    • 값을 추가/삭제할 때: .add(), .remove()

    • 추가

      user1.following.add(user2, user3, user4) # 유저 오브젝트들 넘겨주기
      user1.following.add(2, 3, 4) # id들 넘겨주기
    • 삭제

      user1.following.remove(user2, user3, user5) 
      user1.following.remove(2, 3, 5)
    • 역관계

      # followers 추가
      user1.followers.add(user2, user4)
      user1.followers.add(2, 4)
      
      # followers 제거
      user1.followers.remove(user2) 
      user1.followers.remove(2)

GenericForeignKey 오브젝트

  • 생성
    • ContentType과 오브젝트 id

      # ContentType과 오브젝트 id 넘겨주기
      like = Like.objects.create(user=user, content_type=ct, object_id=1)
      like = Like.objects.create(user=user, content_type_id=1, object_id=1)
    • OR GenericForeignKey

      # GenericForeignKey 오브젝트 넘겨주기
      like = Like.objects.create(user=user, liked_object=obj1)
  • 수정
    like.content_type_id = 2
    like.object_id = 2
    like.save()
    
    like.liked_object = obj2
    like.save()

관계 필드가 있는 오브젝트 삭제: .delete()

  • on_delete 옵션 등에 따라 관련된 오브젝트도 같이 삭제될 수도

4-13. 댓글, 좋아요 섹션 구현하기

<div class="like-comment-block">
      <div class="like-comment-header">
        <button class="like-button">
          <img width="21px" src="{% static 'coplate/icons/ic-heart.svg' %}" alt="like icon">
          <span> 좋아요 {{ review.likes.count }}</span>
        </button>
        <div class="comment-info">
          <img src="{% static 'coplate/icons/ic-comment.svg' %}" alt="comment icon">
          <span> 댓글 {{ review.comments.count }}</span>
        </div>
      </div>
      <div class="comment-list">
        {% for comment in review.comments.all %}
          <div class="comment">
            <div class="comment-header">
              <a href="{% url 'profile' comment.author.id %}">
                <div class="author">
                  <div class="cp-avatar" style="background-image: url('{{ comment.author.profile_pic.url }}')"></div>
                  <span>{{ comment.author.nickname }}</span>
                </div>
              </a>
              {% if user == comment.author %}
                <div class="buttons">
                  <a href="#">삭제</a>
                  <span> | </span>
                  <a href="#">수정</a>
                </div>
              {% endif %}
            </div>
            <div class="comment-content">
              {{ comment.content|linebreaksbr }}
            </div>
            <div class="comment-footer">
              <div class="comment-date">
                {{ comment.dt_created|date:"Y년 n월 j일" }}
              </div>
              <button class="like-button">
                <img width="16px" src="{% static 'coplate/icons/ic-heart.svg' %}" alt="like icon">
                <span> 좋아요 {{ comment.likes.count }}</span>
              </button>
            </div>
          </div>
        {% endfor %}
      </div>
    </div>

댓글 작성 폼 구현하기

  1. CommentForm 만들기
#forms.py
...
from .models import User, Review, **Comment
...**
class CommentForm(forms.ModelForm):
    class Meta:
        model = Comment
        fields = [
            'comment',
        ]
        widgets = {
            'comment': forms.Textarea,
        }
  1. CommentForm이 리뷰 상세 페이지에서 렌더되게 하기
#views.py
...
from .forms import ReviewForm, ProfileForm, **CommentForm**
...
class ReviewDetailView(DetailView):
    ...

    def get_context_data(self, **kwargs):
        context = super().get_context_data(**kwargs)
        context['form'] = CommentForm()
        return context
  1. 변화에 알맞게 review_detail.html 수정하기
#review_detail.html
...
{% load widget_tweaks %}
...

	<form class="comment-create-form" action="#" method="post">
      {% csrf_token %}
      {% if user.is_authenticated %}
        {{ form.content|attr:"placeholder:댓글 내용을 입력해주세요."|add_class:"cp-input" }}
         <button class="cp-button small" type="submit">등록</button>
      {% else %}
        <a href="{% url 'account_login' %}**?next=**{% url 'review-detail' review.id %}"> 
          {{ form.content|attr:"placeholder:댓글을 작성하려면 로그인이 필요합니다."|add_class:"cp-input"|**attr:"disabled"** }}
        </a>
        <button class="cp-button small **secondary**" type="submit" **disabled**>등록</button>

      {% endif %}
  </form>
...
  • ?next=
    • 로그인 성공시 리디렉트

4-**17. 댓글 폼 데이터 처리하기**

  • 생성 로직을 알아서 처리해주는 제네릭 뷰 사용 선호
  1. 댓글 생성 urls.py에 연결
#urls.py
...
urlpatterns = [
    ...
    # comment
    path('reviews/<int:review_id>/comments/create/', views.CommentCreateView.as_view(), name='comment-create'),
]
  1. review_detail.html의 form의 action에 연결
...
<form class="comment-create-form" **action="{% url 'comment-create' review.id %}"** method="post">
	...
</form>
...
  1. views.py에 CommentCreateView 정의
#views.py
...
class CommentCreateView(LoginRequiredMixin, UserPassesTestMixin, CreateView):
    http_method_names = ['post']
    
    model = Comment
    form_class = CommentForm

    #접근 제어
    redirect_unauthenticated_users = True
    raise_exception = confirmation_required_redirect

    def form_valid(self, form):
        form.instance.author = self.request.user
        form.instance.review = Review.objects.get(id=self.kwargs.get('review_id'))
        return super().form_valid(form)

    def get_success_url(self):
        return reverse('review-detail', kwargs={'review_id': self.kwargs.get('review_id')})

    def test_func(self, user):
        return EmailAddress.objects.filter(user=user, verified=True).exists()
  • http_method_names = [’post’]
    • post request만 허용하겠다

4-**19. 접근 제어 코드 리팩터링하기**

만드는 view마다 접근제어 (mixin) 코드를 쓰는 대신에 mixins.py에 custom mixin 클래스 정의

  • Refactoring, 리팩터링: 외부 동작을 바꾸지 않으면서 코드의 구조 개선하기

3가지 접근 제어 조건

  • 로그인만 필요 → LoginRequiredMixin
  • 로그인 + 이메일 인증 필요: 리뷰 작성, 댓글 작성, 좋아요, 팔로우
  • 로그인 + 오브젝트 소유 필요: 리뷰 수정/삭제, 댓글 수정/삭제
  1. 로그인 + 이메일 인증 필요 (해당 뷰에 적용 후 뷰의 접근 제어 코드 삭제)
#mixins.py
from braces.views import LoginRequiredMixin, UserPassesTestMixin
from allauth.account.models import EmailAddress
from .functions import confirmation_required_redirect

class LoginAndVerificationRequiredMixin(LoginRequiredMixin, UserPassesTestMixin):
    redirect_unauthenticated_users = True
    raise_exception = confirmation_required_redirect

    ef test_func(self, user):
        return EmailAddress.objects.filter(user=user, verified=True).exists()
#views.py
...
class ReviewCreateView(LoginAndVerificationRequiredMixin, CreateView):
    model = Review
    form_class = ReviewForm
    template_name = 'coplate/review_form.html'
    
    def form_valid(self, form):
        form.instance.author = self.request.user
        return super().form_valid(form)

    def get_success_url(self):
        return reverse('review-detail', kwargs={'review_id': self.object.id})
  1. 로그인 + 오브젝트 소유 필요 (해당 뷰에 적용 후 뷰의 접근 제어 코드 삭제)
class LoginAndOwnershipRequiredMixin(LoginRequiredMixin, UserPassesTestMixin)

    redirect_unauthenticated_users = False
    raise_exception = True

    def test_func(self, user):
        obj = self.get_object()
        return obj.author == user

4-**21. 댓글 수정, 삭제 기능 추가하기**

  1. urls.py에 수정/삭제 페이지 연결
#urls.py
...
urlpatterns = [
		...
		path('comments/<int:comment_id>/edit/', views.CommentUpdateView.as_view(), name='comment-update'),
		path('comments/<int:comment_id>/delete/', views.CommentDeleteView.as_view(), name='comment-delete'),
]
  1. views.py에 CommentUpdateView와 CommentDeleteView 정의
#views.py
class CommentUpdateView(LoginAndOwnershipRequiredMixin, UpdateView):
    model = Comment
    form_class = CommentForm
    template_name = 'coplate/comment_update_form.html'
    pk_url_kwarg = 'comment_id'

    def get_success_url(self):
        return reverse('review-detail', kwargs={'review_id': self.object.review.id})

class CommentDeleteView(LoginAndOwnershipRequiredMixin, DeleteView):
    model = Comment
    template_name = 'coplate/comment_confirm_delete.html'
    pk_url_kwarg = 'comment_id'

    def get_success_url(self):
        return reverse('review-detail', kwargs={'review_id': self.object.review.id})
  1. review_detail.html에 댓글 삭제/수정 버튼에 연결
#review_detail.html
...
							{% if user == comment.author %}
                <div class="buttons">
                  <a href="**{% url 'comment-delete' comment.id %}**">삭제</a>
                  <span> | </span>
                  <a href="**{% url 'comment-update' comment.id %}**">수정</a>
                </div>
              {% endif %}
...
  1. comment_confirm_delete.html & comment_update_form.html 구현

4-**23. 좋아요 데이터 처리하기 I**

[리뷰(본문)의 좋아요]

  1. 좋아요 버튼 셋팅하기
#review_detail.html
...
				**<form action="#" method="post">**
          **{% csrf_token %}**
          <button class="like-button" **type="submit"**>
            <img width="21px" src="{% static 'coplate/icons/ic-heart.svg' %}" alt="like icon">
            <span> 좋아요 {{ review.likes.count }}</span>
          </button>
        **</form>**
...
  1. url을 통해 content_type_id와 object_id 전달
#urls.py
...
urlpatterns = [
		...
		path('like/<int:content_type_id>/<int:object_id>/', views.ProcessLikeView.as_view(), name='process-like'),
]
  1. review 모델의 content type id를 뷰를 통해 넘겨 받기
#views.py
from django.contrib.contenttypes.models import ContentType
...
class ReviewDetailView(DetailView):
    ...

    def get_context_data(self, **kwargs):
        context = super().get_context_data(**kwargs)
        context['form'] = CommentForm()
        **context['review_ctype_id'] = ContentType.objects.get(model='review').id**
        return context
  1. 페이지의 좋아요 버튼 action 채워넣기
#review_detail.html
...
			<div class="like-comment-header">
        {% if user.is_authenticated %}
        <form action="{% url 'process-like' review_ctype_id review.id%}" method="post">
          {% csrf_token %}
          <button class="like-button" type="submit">
            <img width="21px" src="{% static 'coplate/icons/ic-heart.svg' %}" alt="like icon">
            <span> 좋아요 {{ review.likes.count }}</span>
          </button>
        </form>
        {% else %}
          <a class = "like-button" href="{% url 'account_login' %}?next={% url 'review_detail' review.id %}">
            <img width="21px" src="{% static 'coplate/icons/ic-heart.svg' %}" alt="like icon">
            <span> 좋아요 {{ review.likes.count }}</span>
          </a>
        {% endif %}
        <div class="comment-info">
          <img src="{% static 'coplate/icons/ic-comment.svg' %}" alt="comment icon">
          <span> 댓글 {{ review.comments.count }}</span>
        </div>
      </div>
...
  • process-like 페이지는 content_type_id와 object_id를 넘겨줘야 함
    • content_type_id로 review_ctype_id를 넘기고, object_id로 review.id를 넘김

4-**25. View 클래스 복습**

request 파라미터

  • 우리가 주로 사용하는 request 오브젝트
  • (예) user = request.user #현재 유저

url 파라미터

  • url 파라미터는 kwargs로 전달됨
  • (예) review_id = kwargs.get(’review_id’) # <review_id> URL 파라미터

request & kwargs 둘다 self를 통해 접근 가능

def get(self, request, *args, **kwargs):
        user = request.user
        review_id = kwargs.get('review_id')

        # 또는
        user = self.request.user
        review_id = self.kwargs.get('review_id')

4-**26. 좋아요 데이터 처리하기 II**

ProcessLikeView 정의하기

#views.py

class ProcessLikeView(LoginAndVerificationRequiredMixin, View):
    http_method_names = ['post']

    def post(self, request, *args, **kwargs):
        like, created = Like.objects.get_or_create(
            user = self.request.user,
            content_type_id=self.kwargs.get('content_type_id'),
            object_id=self.kwargs.get('object_id')
        )
        if not created:
            like.delete()

				return redirect(self.request.META['HTTP_REFEREER'])
  • get_or_create
    • 조건에 해당하는 오브젝트가 있으면 그걸 get해서 like에 저장, created=False
    • 조건에 해당하는 오브젝트가 없으면 생성해서 like에 저장, created=True
  • return redirect(self.request.META[’HTTP_REFERER’])
    • 요청이 들어왔던 페이지로 redirect

4-**28. 리뷰 좋아요 눌렀는지 판단하기**

유저가 리뷰에 좋아요를 눌렀는지 확인하는 방법

  • Like.objects.filter(user=user, review=review).exists()
    • like 오브젝트 중에 특정 유저와 특정 리뷰를 가리키는 오브젝트가 있는지
  • user.likes.filter(review=review).exists()
    • user의 like 중 review를 가리키는 걸 찾기
  • review.likes.filter(user=user).exists()
    • review의 like 중 user를 가리키는 걸 찾기
  1. ReviewDetailView에서 좋아요 여부 받아오기
class ReviewDetailView(DetailView):
    model = Review
    template_name = 'coplate/review_detail.html'
    pk_url_kwarg = 'review_id'

    def get_context_data(self, **kwargs):
        context = super().get_context_data(**kwargs)
        context['form'] = CommentForm()
        context['review_ctype_id'] = ContentType.objects.get(model='review').id
        context['comment_ctype_id'] = ContentType.objects.get(model='comment').id

        **user = self.request.user
        if user.is_authenticated:
            review = self.object
            context['likes_review'] = Like.objects.filter(user=user, review=review).exists()**

        return context
  1. review_detail.html에 반영하기
{% if likes_review %}
   <img width="21px" src="{% static 'coplate/icons/ic-heart-orange.svg' %}" alt="filled like icon">
{% else %}
   <img width="21px" src="{% static 'coplate/icons/ic-heart.svg' %}" alt="like icon">
{% endif %}
<span> 좋아요 {{ review.likes.count }}</span>

4-**29. 댓글 좋아요 눌렀는지 판단하기**

리뷰에 있는 코멘트 중에 유저가 좋아하는 애들만 필터해서 템플릿으로 넘겨줌

  1. 리뷰에 있는 코멘트 필터
  2. 현재 유저가 좋아하는 코멘트 필터
Comment.objects.filter(review=review).filter(likes__user=user)

views.py의 ReviewDetailView에 넘겨줌

#views.py
class ReviewDetailView(DetailView):
    model = Review
    template_name = 'coplate/review_detail.html'
    pk_url_kwarg = 'review_id'

    def get_context_data(self, **kwargs):
        context = super().get_context_data(**kwargs)
        context['form'] = CommentForm()
        context['review_ctype_id'] = ContentType.objects.get(model='review').id
        context['comment_ctype_id'] = ContentType.objects.get(model='comment').id

        user = self.request.user
        if user.is_authenticated:
            review = self.object
            context['likes_review'] = Like.objects.filter(user=user, review=review).exists()
            **context['liked_comments'] = Comment.objects.filter(review=review).filter(likes__user=user)**

        return context

4-**31. unique_together를 사용한 유효성 검사**

  • Like 모델에 좋아요 데이터가 중복돼서 저장됨 (웹사이트 내에서는 발생하지 않지만 admin페이지에서 발생)
  • 핵심 문제: 똑같은 tuple (user, content_type, object_id) 값이 여러번 저장될 수 있어서
    • 해결: tuple에 대한 고유(unique) 제약 조건을 추가하면 됨
      • unique_together (모델 메타 옵션)
class Like(models.Model):
    ...
        
    class Meta:
        unique_together = ['user', 'content_type', 'object_id']
  • unique_together = [’user’, ‘content_type’, ‘object_id’]
    • 같은 조합의 tuple은 저장 못함

특정 필드 값이 중복되면 안 되는 경우 unique=True옵션

class MyModel(models.Model):
    my_unique_field = models.CharField(unique=True)

여러 필드의 조합이 중복되면 안 되는 경우 unique_together 메타 옵션

class MyModel(models.Model):
    my_field_a = models.CharField()

    my_field_b = models.CharField()

    class Meta:
        unique_together = ['my_field_a', 'my_field_b']

4-**32. 팔로우 섹션 구현하기**

#profile.html
			<div class="follow-section">
        <a href="#">
          팔로워 {{ profile_user.followers.count }}
        </a>
        <span class="vert-divider">|</span>
        <a href="#">
          팔로잉 {{ profile_user.following.count }}
        </a>
      </div>
      {% if user.is_authenticated and user != profile_user %}
        <form class="follow-button" action="#" method="post">
          {% csrf_token %}
          <button class="cp-button small" type="submit">
            팔로우
          </button>
        </form>
      {% endif %}

4-**33. 팔로우 데이터 처리하기**

  1. 팔로우 정보 저장하기
#팔로우
user.following.add(profile_user)
user.following.add(profile_user_id)

#언팔로우
user.following.remove(profile_user)
user.following.remove(profile_user_id)
  1. Follow 로직 처리할 url 만들기
#urls.py
...
urlpatterns = [
		path('users/<int:user_id>/follow/', views.ProcessFollowView.as_view(), name='process-follow'),
]
  1. Follow 로직을 처리할 ProcessFollowView 정의하기
class ProcessFollowView(LoginAndVerificationRequiredMixin, View):
    http_method_names = ['post']

    def post(self, request, *args, **kwargs):
        user = self.request.user
        profile_user_id = self.kwargs.get('user_id')
        if user.following.filter(id=profile_user_id).exists(): #현재 유저의 팔로잉 목록에 프로필 유저가 있는지
            user.following.remove(profile_user_id)
        else:
            user.following.add(profile_user_id)
        
        return redirect('profile', user_id=profile_user_id)
  1. Follow 버튼 수정을 위해 ProfileView에도 추가
class ProfileView(DetailView):
    model = User
    template_name = 'coplate/profile.html'
    pk_url_kwarg = 'user_id'
    context_object_name = 'profile_user'

    def get_context_data(self, **kwargs):
        context = super().get_context_data(**kwargs)
        **user = self.request.user
        profile_user_id = self.kwargs.get('user_id')
        if user.is_authenticated:
            context['is_following'] = user.following.filter(id=profile_user_id).exists()**
        context['user_reviews'] = Review.objects.filter(author__id=profile_user_id)[:4]
        return context
  1. Follow 버튼 수정하기
#profile.html
					{% if is_following %}
            <button class="cp-button small secondary" type="submit">
              언팔로우
            </button>
          {% else %}
            <button class="cp-button small" type="submit">
              팔로우
            </button>
          {% endif %}

4-**35. 팔로워, 팔로잉 페이지 추가하기**

  1. urls.py에 연결
#urls.py
...
urlpatterns = [
		path('users/<int:user_id>/following/', views.FollowingListView.as_view(), name='following-list'),
		path('users/<int:user_id>/followers/', views.FollowerListView.as_view(), name='follower-list'),
]
  1. FollowingListView & FollowerListView 정의하기
class FollowingListView(ListView):
    model = User
    template_name = 'coplate/following_list.html'
    context_object_name = 'following'
    paginate_by = 10

    def get_queryset(self): #프로필유저가 팔로잉하는 유저 목록
        profile_user = get_object_or_404(User, pk=self.kwargs.get('user_id'))
        return profile_user.following.all()

    def get_context_data(self, **kwargs):
        context = super().get_context_data(**kwargs)
        context['profile_user_id'] = self.kwargs.get('user_id')
        return context

4-**36. 템플릿 코드 리팩터링: include 태그**

반복적으로 사용되는 템플릿 코드는 component html 파일로 만들어서 호출

  1. templates/components/에 component으로 사용할 html 파일 만들기 (예) index.html의 review_list 섹션
  2. review list 섹션 대신에 include 태그로 review_list component 불러오기
{% include 'components/review_list.html' with reviews=latest_reviews empty_message="아직 리뷰가 없어요 :(" %}
  1. pagination 섹션 대신에 include 태그로 pagination component 불러오기
{% include 'components/pagination.html' with page_obj=page_obj %}

4-**38. 팔로잉 유저 리뷰 모아보기**

  1. IndexView 수정
class IndexView(View):
    def get(self, request, *args, **kwargs):
        context = {}
        context['latest_reviews'] = Review.objects.all()[:4]
        **user = self.request.user
        if user.is_authenticated:
            context['latest_following_reviews'] = Review.objects.filter(author__followers=user)[:4]**
        return render(request, 'coplate/index.html', context)
  1. index.html에 팔로잉 유저 리뷰 섹션 추가
		{% if user.is_authenticated %}
      <div class="header">
        <h2>팔로잉 유저들의 리뷰</h2>
      </div>
      {% include 'components/review_list.html' with reviews=latest_following_reviews empty_message="아직 리뷰가 없어요 :(" %}
      {% if latest_following_reviews %}
        <a class="cp-button" href="{% url 'following-review-list' %}">팔로잉 유저 리뷰 모아보기</a>
      {% endif %}
    {% endif %}
  1. following-review-list url 생성
#urls.py
...
urlpatterns = [
		...
		path('reviews/following/', views.FollowingReviewListView.as_view(), name='following-review-list'),
]
  1. FollowingReviewListView 정의
class FollowingReviewListView(LoginRequiredMixin, ListView):
    model = Review
    context_object_name = 'following_reviews'
    template_name = 'coplate/following_review_list.html'
    paginate_by = 8

    def get_queryset(self):
        return Review.objects.filter(author__followers=self.request.user)
  1. following_review_list.html 추가
{% extends "coplate_base/base_with_navbar.html" %}

{% block title %}팔로잉 유저들의 리뷰 ({{ paginator.count }}) | Coplate{% endblock title %}

{% block content %}
<main class="site-body">
  <div class="content-list max-content-width">
    <div class="header">
      <h2>팔로잉 유저들의 리뷰 ({{ paginator.count }})</h2>
    </div>
    {% include 'components/review_list.html' with reviews=following_reviews empty_message="아직 리뷰가 없어요 :(" %}
    {% if is_paginated %}
      {% include 'components/pagination.html' with page_obj=page_obj %}
    {% endif %}
  </div>
</main>
{% endblock content %}

4-**41. Q 오브젝트란?**

AND

<Model>.objects.filter(field1=val1).filter(field2=val2)
<Model>.objects.filter(field1=val1, filed2=val2)

OR: Q object 사용

  • Q object: 어떤 조건을 저장하는 오브젝트
from django.db.models import Q

q1 = Q(field1=val1)
q2 = Q(field2=val2)

<Model>.objects.filter(q1 | q2)
<Model>.objects.filter(Q(field1=val1) | Q(field2=val2))

리뷰 검색

  • 리뷰 제목에 검색어가 들어감 OR 레스토랑 이름에 검색어가 들어감 OR 리뷰 내용에 검색어가 들어감
Review.objects.filter(
		Q(title__icontains=word)
		| Q(restaurant_name__icontains=word)
		| Q(content_icontains=word)
}

4-**42. 검색 기능 구현하기 I: 검색어 전달하기**

  1. index.html에 검색 기능 추가하기
<div class="header-search">
  <form class="search-form" action="#" method="get">
    <input class="search-input" **name="query"** type="text" placeholder="식당, 음식 등을 검색해보세요" required>
    <button class="cp-button search-button" type="submit">검색</button>
  </form>
</div>
  • name=”query”
    • 검색 버튼을 눌러 전송하면 input이 name속성의 query로 전달됨
  1. urls.py에 search 연결
#urls.py
...
urlpatterns=[
		...
		path('search/', views.search_view, name='search'),
		...
]
  1. index.html의 폼의 action 수정
				<form class="search-form" **action="{% url 'search' %}"** method="get">
          <input class="search-input" name="query" type="text" placeholder="식당, 음식 등을 검색해보세요" required>
          <button class="cp-button search-button" type="submit">검색</button>
        </form>
  1. views.py에 search_view 정의
from django.http import HttpResponse
...
def search_view(request):
    query = request.GET.get('query', '')
    return HttpResponse(f"검색어: {query}")
  • request.GET
    • 쿼리 스트링 데이터가 담긴 딕셔너리
    • 딕셔너리에서 query key를 사용해서 검색어를 가져오는 것

4-**44. 검색 기능 구현하기 II: 검색어 활용하기**

뷰를 클래스형 뷰로 전환

class SearchView(ListView):
    model = Review
    context_object_name = 'search_results'
    template_name = 'coplate/search_results.html'
    paginate_by = 8

		def get_query(self):
				query = request.GET.get('query', '')
				return Review.objects.filter(
            Q(title__icontains=query)
            | Q(restaurant_name__icontains=query)
            | Q(content__icontains=query)
        )
		
		def get_context_data(self, **kwargs):
        context = super().get_context_data(**kwargs)
        context['query'] = self.request.GET.get('query', '')
        return context

참고: 코드잇 장고 강의

profile
우당탕탕

0개의 댓글

Powered by GraphCDN, the GraphQL CDN