[Django] Django ORM Cookbook : 6

GreenBean·2021년 9월 30일
0

Django ORM Cookbook

목록 보기
6/8
post-thumbnail

Django ORM

Django ORM Cookbook


일대일 관계

  • 일대일 관계란 두 표에서 각 항목이 서로 다른 표의 항목 단 하나와 연결되는 관계
    • 쉽게 이해할 수 있는 예를 들자면, 각자는 생부와 생모를 각각 하나씩만 가질 수 있음
from django.contrib.auth.models import User

class UserParent(models.Model):
    user = models.OneToOneField(
        User,
        on_delete=models.CASCADE,
        primary_key=True,
    )
    father_name = models.CharField(max_length=100)
    mother_name = models.CharField(max_length=100)
>>> u1 = User.objects.get(first_name='Ritesh', last_name='Deshmukh')
>>> u2 = User.objects.get(first_name='Sohan', last_name='Upadhyay')

>>> p1 = UserParent(user=u1, father_name='Vilasrao Deshmukh', mother_name='Vaishali Deshmukh')
>>> p1.save()

>>> p1.user.first_name

'Ritesh'

>>> p2 = UserParent(user=u2, father_name='Mr R S Upadhyay', mother_name='Mrs S K Upadhyay')
>>> p2.save()
>>> p2.user.last_name

'Upadhyay'
  • on_delete 메서드는 그 필드에 연결된 항목이 삭제될 때 그 항목을 가리키는 항목들을 어떻게 처리해야 할지 설정
    • on_delete=models.CASCADE(하위 삭제)는 연결된 항목이 삭제될 때 해당 항목을 함께 삭제
    • u2.delete()를 실행하면 User 모델의 항목(u2) 뿐 아니라 UserParent 의 항목(p2)도 함께 삭제됨

일대다 관계

  • 일대다 관계란 한 표의 상위 항목이 다른 표의 여러 하위 항목에서 참조되는 관계
  • 일대다 관계에서 상위 항목이 반드시 하위 항목을 가진다는 보장은 없음
    • 상위 항목은 하위 항목을 0개, 1개, 여러 개 가질 수 있음
  • 장고 모델에서 일대다 관계를 정의할 때는 ForeignKey 필드를 사용
class Article(models.Model):
    headline = models.CharField(max_length=100)
    pub_date = models.DateField()
    reporter = models.ForeignKey(User, on_delete=models.CASCADE, related_name='reporter')

    def __str__(self):
        return self.headline

    class Meta:
        ordering = ('headline',)
>>> u1 = User(username='johny1', first_name='Johny', last_name='Smith', email='johny@example.com')
>>> u1.save()

>>> u2 = User(username='alien', first_name='Alien', last_name='Mars', email='alien@example.com')
>>> u2.save()


>>> from datetime import date
>>> a1 = Article(headline="This is a test", pub_date=date(2018, 3, 6), reporter=u1)
>>> a1.save()
>>> a1.reporter.id

13

>>> a1.reporter

<User: johny1>
  • 상위 객체를 데이터베이스에 저장하지 않은 채로 하위 객체에 할당하려 하면 ValueError 예외가 발생
>>> u3 = User(username='someuser', first_name='Some', last_name='User', email='some@example.com')
>>> Article.objects.create(headline="This is a test", pub_date=date(2018, 3, 7), reporter=u3)

Traceback (most recent call last):
...
ValueError: save() prohibited to prevent data loss due to unsaved related object 'reporter'.


>>> Article.objects.create(headline="This is a test", pub_date=date(2018, 3, 7), reporter=u1)
>>> Article.objects.filter(reporter=u1)

<QuerySet [<Article: This is a test>, <Article: This is a test>]>
  • u1 하나에 여러 개의 Article이 연결되어 있음(일대다 관계)을 확인할 수 있음

다대다 관계

  • 다대다 관계란 한 표의 항목이 다른 표의 항목 여러 개를 가리킬 수 있고, 반대로 다른 표의 항목이 그 표의 항목을 여러 개 가리킬 수도 있는 관계
    • 실제로 실행 가능한 예로, 트위터 앱을 다뤄 볼 예정
      • 필드 몇 개와 ManyToMany 필드만 있으면 간단한 트위터 앱을 만들 수 있음
      • 트위터의 핵심 기능으로 ‘트윗’, ‘팔로우’, ‘마음에 들어요’가 있음
class User(AbstractUser):
    tweet = models.ManyToManyField(Tweet, blank=True)
    follower = models.ManyToManyField(settings.AUTH_USER_MODEL, blank=True)
    pass

class Tweet(models.Model):
    tweet = models.TextField()
    favorite = models.ManyToManyField(settings.AUTH_USER_MODEL, blank=True, related_name='user_favorite')

    def __unicode__(self):
        return self.tweet
  • 이 모델로 할 수 있는 일
    • 사용자가 다른 사용자를 '팔로우' 및 취소할 수 있음
    • 사용자가 팔로우하는 다른 사용자가 작성한 트윗을 볼 수 있음
    • 사용자가 트윗에 '마음에 들어요' 및 취소할 수 있음
>>> t1 = Tweet(tweet="I am happy today")
>>> t1.save()
>>> t2 = Tweet(tweet="This is my second Tweet")
>>> t2.save()

>>> u1 = User(username='johny1', first_name='Johny', last_name='Smith', email='johny@example.com')
>>> u1.save()
>>> u2 = User(username='johny1', first_name='Johny', last_name='Smith', email='johny@example.com')
>>> u2.save()
>>> u3 = User(username='someuser', first_name='Some', last_name='User', email='some@example.com')
>>> u3.save()
>>> u2.tweet.add(t1)
>>> u2.save()
>>> u2.tweet.add(t2)
>>> u2.save()

>>> # 사용자가 다른 사용자를 '팔로우' 할 수 있음
>>> u2.follow.add(u1)
>>> u2.save()

>>> # 트윗이 사용자에 연결되어 있음
>>> # 사용자들이 트윗에 '마음에 들어요' 및 취소를 할 수 있음
>>> t1.favorite.add(u1)
>>> t1.save()
>>> t1.favorite.add(u3)
>>> t1.save()

>>> # '마음에 들어요' 취소
>>> t1.favorite.remove(u1)
>>> t1.save()

자기 참조 외래 키 정의

  • 자기 참조 외래 키를 이용하여 중첩 관계·재귀 관계를 표현할 수 있음
  • 일대다 관계와 유사하지만, 이름에서 알 수 있듯이 모델이 자기 자신을 참조한다는 특징이 있음
# 방법 1
class Employee(models.Model):
    manager = models.ForeignKey('self', on_delete=models.CASCADE)

# 방법 2
class Employee(models.Model):
    manager = models.ForeignKey("app.Employee", on_delete=models.CASCADE)

기존 데이터베이스를 장고 모델로 옮기기

  • 장고에는 기존 데이터베이스를 분석하여 그에 맞는 모델을 생성해주는 inspectdb 명령이 있음
  • 셸에서 다음 명령을 실행하여 결과를 확인할 수 있음
  • $ python manage.py inspectdb
    • 이 명령을 실행하려면 먼저 settings.py 파일에 분석하려는 데이터베이스의 접속 정보를 설정해 두어야 함
    • 출력 결과는 생성된 모델의 파이썬 코드
    • 코드를 파이썬 모듈 파일로 저장하려면 셸의 스트림 리디렉션 기능을 이용
  • $ python manage.py inspectdb > models.py
    • 위 명령을 실행하면 분석된 모델이 파이썬 모듈 파일로 현재 디렉토리에 저장됨
    • 이 파일을 앱의 올바른 위치로 옮긴 뒤, 적절히 수정하여 사용하면 됨

데이터베이스 뷰에 대응하는 모델 정의

  • 데이터베이스 뷰는 데이터베이스 내에서 조회할 수 있도록 질의문으로 정의된 객체
    • 뷰가 데이터를 물리적으로 저장하는 것은 아니지만, 실제 표와 같이 조회할 수 있기 때문에 ‘가상 표’라고 불리기도 함
  • 뷰는 여러 표를 결합(JOIN)한 정보를 보여줄 수도 있고, 한 표의 부분 집합만을 보여줄 수도 있음
  • 이를 활용하면 복잡한 질의문을 감추고 필요한 정보를 쉽게 조회하는 인터페이스를 만들 수 있음
class TempUser(models.Model):
    first_name = models.CharField(max_length=100)

    class Meta:
        managed = False
        db_table = "temp_user"


>>> # 실제 표와 마찬가지로 뷰를 조회할 수 있음
>>> TempUser.objects.all().values()

<QuerySet [{'first_name': 'Yash', 'id': 1}, {'first_name': 'John', 'id': 2}, {'first_name': 'Ricky', 'id': 3}, {'first_name': 'Sharukh', 'id': 4}, {'first_name': 'Ritesh', 'id': 5}, {'first_name': 'Billy', 'id': 6}, {'first_name': 'Radha', 'id': 7}, {'first_name': 'Raghu', 'id': 9}, {'first_name': 'Rishabh', 'id': 10}, {'first_name': 'John', 'id': 11}, {'first_name': 'Paul', 'id': 12}, {'first_name': 'Johny', 'id': 13}, {'first_name': 'Alien', 'id': 14}]>

>>> # 그러나 뷰에 기록은 하지 못함
>>> TempUser.objects.create(first_name='Radhika', id=15)

Traceback (most recent call last):
...
django.db.utils.OperationalError: cannot modify temp_user because it is a view
  • 장고 앱에서는 모델을 정의할 때 메타(Meta) 클래스에 managed = False, db_table="temp_user" 와 같이 옵션을 설정하여 뷰를 가리키는 모델로 사용할 수 있음

범용 모델 정의

  • 아무 모델이나 가리킬 수 있는 범용 모델을 정의하는 방법
class Category(models.Model):
    name = models.CharField(max_length=100)

    class Meta:
        verbose_name_plural = "Categories"

class Hero(models.Model):
    name = models.CharField(max_length=100)
    category = models.ForeignKey(Category, on_delete=models.CASCADE)

class Villain(models.Model):
    name = models.CharField(max_length=100)
    category = models.ForeignKey(Category, on_delete=models.CASCADE)


# Category 모델을 범용 모델로 변경
from django.contrib.contenttypes.fields import GenericForeignKey
from django.contrib.contenttypes.models import ContentType

class FlexCategory(models.Model):
    name = models.SlugField()
    content_type = models.ForeignKey(ContentType, on_delete=models.CASCADE)
    object_id = models.PositiveIntegerField()
    content_object = GenericForeignKey('content_type', 'object_id')

class Hero(models.Model):
    name = models.CharField(max_length=100)
    flex_category = GenericRelation(FlexCategory, related_query_name='flex_category')

class Villain(models.Model):
    name = models.CharField(max_length=100)
    flex_category = GenericRelation(FlexCategory, related_query_name='flex_category')
  • 수정한 코드에서는 FlexCategory 모델에 외래 키 필드(ForeignKey) 하나와 양의 정수 필드(PositiveIntegerField) 하나를 정의하여 범용 외래 키 필드(GenericForeignKey)를 사용할 수 있도록 함
    • 분류를 이용할 모델에 범용 관계 필드(GenericRelation)를 추가
# Hero 모델의 항목을 분류할 때
>>> hero = Hero.objects.create(name='Hades')
>>> FlexCategory.objects.create(content_object=hero, name="mythic")


# 'ghost'로 분류된 Hero를 구할 때
>>> Hero.objects.filter(flex_category__name='ghost')

# SQL 질의문
SELECT "entities_hero"."name"
FROM "entities_hero"
INNER JOIN "entities_flexcategory" ON ("entities_hero"."id" = "entities_flexcategory"."object_id"
                                       AND ("entities_flexcategory"."content_type_id" = 8))
WHERE "entities_flexcategory"."name" = ghost
profile
🌱 Backend-Dev | hwaya2828@gmail.com

0개의 댓글