장고 ORM 요리책

『장고 ORM 요리책(Django ORM Cookbook)』은 장고의 ORM(객체 관계 매핑) 기능과 모델 기능을 활용하는 다양한 레시피(조리법)를 담은 책입니다. 장고는 모델-템플릿-뷰(MTV) 프레임워크입니다. 이 책은 그 가운데 ‘모델’에 대해 상세히 다룹니다.

이 책은 “장고 ORM/쿼리셋/모델으로 ~을 하는 방법은 무엇인가요?”와 같은 질문 50여 개와 그 답을 담고 있습니다.

_images/BookCover.jpg

이 책의 소개

장고 ORM은 장고의 핵심 구성요소 가운데 하나로, 데이터베이스 시스템을 직접 다루지 않고도 데이터베이스를 활용할 수 있도록 하는 편리하고도 강력한 인터페이스입니다. “간단한 것을 쉽게, 어려운 것을 가능하게” 해 주는 고마운 기능이죠.

이 책은 여러 가지 예제로 문제 상황을 직접 다뤄보며 장고 ORM을 익히도록 합니다. 여러분의 장고 ORM의 이해도를 높여 줄 50여 개의 질문을 준비했습니다.

이 책을 읽는 방법

이 책의 각 장은 각각 질문을 하나씩 다룹니다. 비슷한 주제를 다루는 장들은 서로 모아 두었습니다. 이 책을 읽는 방법은 크게 두 가지가 있습니다.

  1. 특정한 질문에 관한 답을 찾는다면 그 질문을 다루는 장과 그와 연결된 장을 함께 읽으세요.
  2. 장고 ORM과 모델 계층에 대한 이해도를 높이고 싶다면 책을 처음부터 차례대로 읽으세요.

정보를 조회하고 필요한 항목을 선별하는 방법

장고 ORM이 실행하는 실제 SQL 질의문을 확인할 수 있나요?

장고 ORM이 실행하는 질의문 또는 우리가 작성한 코드에 대응하는 SQL 질의문이 무엇인지 확인하고 싶을 때가 있습니다. SQL 질의문을 구하고 싶은 queryset.querystr 을 확인하면 됩니다. 간단하죠?

Event 라는 모델이 있을 때, 이 모델의 모든 행을 데이터베이스에서 읽어오려면 Event.objects.all() 과 같은 코드를 작성하면 됩니다. 이렇게 구한 쿼리셋의 str(queryset.query) 를 확인하여 SQL 질의문을 살펴봅시다.

>>> queryset = Event.objects.all()
>>> str(queryset.query)
SELECT "events_event"."id", "events_event"."epic_id",
    "events_event"."details", "events_event"."years_ago"
    FROM "events_event"
_images/sql_query.png

두 번째 예제

>>> queryset = Event.objects.filter(years_ago__gt=5)
>>> str(queryset.query)
SELECT "events_event"."id", "events_event"."epic_id", "events_event"."details",
"events_event"."years_ago" FROM "events_event"
WHERE "events_event"."years_ago" > 5

OR 연산으로 일부 조건을 하나라도 만족하는 항목을 구하려면 어떻게 하나요?

_images/usertable.png

장고의 사용자 계정 관리 앱인 django.contrib.auth 를 사용하면 데이터베이스에 auth_user 라는 표가 생성됩니다. 이 표에는 username, first_name, last_name 등의 열이 있습니다.

OR 연산으로 여러 조건 중 하나라도 만족하는 행을 구해야 하는 경우가 많습니다. 이름이 ‘R’로 시작하거나 성이 ‘D’로 시작하는 모든 사용자를 구한다고 해 봅시다.

장고에서는 다음 두 방법으로 구할 수 있습니다.

  • queryset_1 | queryset_2
  • filter(Q(<condition_1>)|Q(<condition_2>))

질의문 살펴보기

위 조건의 SQL 질의문은 다음과 같이 생성됩니다.

SELECT username, first_name, last_name, email FROM auth_user WHERE first_name LIKE 'R%' OR last_name LIKE 'D%';
_images/sqluser_result1.png

장고 ORM 코드도 비슷합니다.

queryset = User.objects.filter(
        first_name__startswith='R'
    ) | User.objects.filter(
    last_name__startswith='D'
)
queryset
<QuerySet [<User: Ricky>, <User: Ritesh>, <User: Radha>, <User: Raghu>, <User: rishab>]>

장고 ORM이 생성하는 SQL 질의문도 한 번 확인해 봅시다.

In [5]: str(queryset.query)
Out[5]: 'SELECT "auth_user"."id", "auth_user"."password", "auth_user"."last_login",
"auth_user"."is_superuser", "auth_user"."username", "auth_user"."first_name",
"auth_user"."last_name", "auth_user"."email", "auth_user"."is_staff",
"auth_user"."is_active", "auth_user"."date_joined" FROM "auth_user"
WHERE ("auth_user"."first_name"::text LIKE R% OR "auth_user"."last_name"::text LIKE D%)'

Q 객체를 이용하는 방법도 가능합니다.

from django.db.models import Q
qs = User.objects.filter(Q(first_name__startswith='R')|Q(last_name__startswith='D'))

두 방법 모두 생성되는 SQL 질의문은 완전히 동일합니다.

In [9]: str(qs.query)
Out[9]: 'SELECT "auth_user"."id", "auth_user"."password", "auth_user"."last_login",
 "auth_user"."is_superuser", "auth_user"."username", "auth_user"."first_name",
  "auth_user"."last_name", "auth_user"."email", "auth_user"."is_staff",
  "auth_user"."is_active", "auth_user"."date_joined" FROM "auth_user"
  WHERE ("auth_user"."first_name"::text LIKE R% OR "auth_user"."last_name"::text LIKE D%)'

AND 연산으로 여러 조건을 모두 만족하는 항목을 구하려면 어떻게 하나요?

_images/usertable.png

장고의 사용자 계정 관리 앱인 django.contrib.auth 를 사용하면 데이터베이스에 auth_user 라는 표가 생성됩니다. 이 표에는 username, first_name, last_name 등의 열이 있습니다.

AND 연산으로 여러 조건을 모두 만족하는 행을 구해야 하는 경우가 많습니다. 이름이 ‘R’로 시작하고 성이 ‘D’로 시작하는 모든 사용자를 구한다고 해 봅시다.

장고에서는 다음 세 방법으로 구할 수 있습니다.

  • filter(<condition_1>, <condition_2>)
  • queryset_1 & queryset_2
  • filter(Q(<condition_1>) & Q(<condition_2>))

질의문 살펴보기

위 조건의 SQL 질의문은 다음과 같이 생성됩니다.

SELECT username, first_name, last_name, email FROM auth_user WHERE first_name LIKE 'R%' AND last_name LIKE 'D%';
_images/sqluser_result2.png

장고 쿼리셋의 filter 메서드에서 여러 조건을 결합하는 방법은 기본적으로 AND 방식입니다. 따라서 다음과 같이 조건을 그냥 나열하면 됩니다.

queryset_1 = User.objects.filter(
    first_name__startswith='R',
    last_name__startswith='D'
)

하지만 & 연산자를 사용하여 쿼리셋을 명시적으로 결합할 수도 있습니다.

queryset_2 = User.objects.filter(
    first_name__startswith='R'
) & User.objects.filter(
    last_name__startswith='D'
)

복잡한 질의를 수행할 수 있도록 도와주는 Q 객체를 이용하여 조건을 명시해도 됩니다.

queryset_3 = User.objects.filter(
    Q(first_name__startswith='R') &
    Q(last_name__startswith='D')
)


queryset_1
<QuerySet [<User: Ricky>, <User: Ritesh>, <User: rishab>]>

언제나 실제로 생성되는 SQL 질의문을 확인하여 검증하는 것이 도움이 됩니다.

In [10]: str(queryset_2.query)
Out[10]: 'SELECT "auth_user"."id", "auth_user"."password", "auth_user"."last_login", "auth_user"."is_superuser", "auth_user"."username", "auth_user"."first_name", "auth_user"."last_name", "auth_user"."email", "auth_user"."is_staff", "auth_user"."is_active", "auth_user"."date_joined" FROM "auth_user" WHERE ("auth_user"."first_name"::text LIKE R% AND "auth_user"."last_name"::text LIKE D%)'

In [11]: str(queryset_1.query) == str(queryset_2.query) == str(queryset_3.query)
Out[11]: True

NOT 연산으로 조건을 부정하려면 어떻게 하나요?

_images/usertable.png

장고의 사용자 계정 관리 앱인 django.contrib.auth 를 사용하면 데이터베이스에 auth_user 라는 표가 생성됩니다. 이 표에는 username, first_name, last_name 등의 열이 있습니다.

id < 5 라는 조건을 만족하지 않는 모든 사용자를 구해 봅시다. 이를 수행하려면 NOT 연산이 필요합니다.

장고에서는 다음 두 방법으로 구할 수 있습니다.

  • exclude(<condition>)
  • filter(~Q(<condition>))

질의문 살펴보기

위 조건의 SQL 질의문은 다음과 같이 생성됩니다.

SELECT id, username, first_name, last_name, email FROM auth_user WHERE NOT id < 5;
_images/sqluser_notquery.png

exclude 메서드를 이용하는 방법은 다음과 같습니다.

>>> queryset = User.objects.exclude(id__lt=5)
>>> queryset
<QuerySet [<User: Ritesh>, <User: Billy>, <User: Radha>, <User: sohan>, <User: Raghu>, <User: rishab>]>

Q 객체를 이용하는 방법은 다음과 같습니다.

>>> from django.db.models import Q
>>> queryset = User.objects.filter(~Q(id__lt=5))
>>> queryst
<QuerySet [<User: Ritesh>, <User: Billy>, <User: Radha>, <User: sohan>, <User: Raghu>, <User: rishab>]>

동일한 모델 또는 서로 다른 모델에서 구한 쿼리셋들을 합할 수 있나요?

SQL에서는 여러 개의 결과 집합을 합할 때 UNION 연산을 이용합니다. 장고 ORM에서 union 메서드를 이용해 쿼리셋을 합할 수 있습니다. 합하려는 쿼리셋의 모델이 서로 다른 경우, 각 쿼리셋에 포함된 필드와 데이터 유형이 서로 맞아야 합니다.

auth_user 모델에서 두 쿼리셋을 구한 뒤 합집합을 구해 봅시다.

>>> q1 = User.objects.filter(id__gte=5)
>>> q1
<QuerySet [<User: Ritesh>, <User: Billy>, <User: Radha>, <User: sohan>, <User: Raghu>, <User: rishab>]>
>>> q2 = User.objects.filter(id__lte=9)
>>> q2
<QuerySet [<User: yash>, <User: John>, <User: Ricky>, <User: sharukh>, <User: Ritesh>, <User: Billy>, <User: Radha>, <User: sohan>, <User: Raghu>]>
>>> q1.union(q2)
<QuerySet [<User: yash>, <User: John>, <User: Ricky>, <User: sharukh>, <User: Ritesh>, <User: Billy>, <User: Radha>, <User: sohan>, <User: Raghu>, <User: rishab>]>
>>> q2.union(q1)
<QuerySet [<User: yash>, <User: John>, <User: Ricky>, <User: sharukh>, <User: Ritesh>, <User: Billy>, <User: Radha>, <User: sohan>, <User: Raghu>, <User: rishab>]>

다음 코드는 실행하면 오류가 발생합니다.

>>> q3 = EventVillain.objects.all()
>>> q3
<QuerySet [<EventVillain: EventVillain object (1)>]>
>>> q1.union(q3)
django.db.utils.OperationalError: SELECTs to the left and right of UNION do not have the same number of result columns

union 메서드는 합하려는 쿼리셋의 필드와 데이터 유형이 서로 일치할 때만 실행할 수 있습니다. 그래서 마지막 명령이 실패했습니다.

Hero 모델과 Villain 모델은 둘 다 name 필드와 gender 필드를 갖고 있습니다. values_list 를 이용해 공통된 필드만 가져온 뒤 union을 수행할 수 있습니다.

Hero.objects.all().values_list(
    "name", "gender"
).union(
Villain.objects.all().values_list(
    "name", "gender"
))

위 코드를 실행하면 Hero 모델과 Villain 모델의 이름과 성별을 구할 수 있습니다.

필요한 열만 골라 조회하려면 어떻게 하나요?

_images/usertable.png

auth_user 모델에는 여러 개의 필드가 정의되어 있습니다. 그런데 이 필드가 전부 다 필요하지 않을 때도 있죠. 그럴 때 필요한 열만을 데이터베이스에서 읽어오는 방법을 알아 봅시다.

장고는 두 가지 방법을 제공합니다.

  • 쿼리셋의 values 메서드와 values_list 메서드
  • only 메서드

이름이 R로 시작하는 모든 사용자의 이름(first_name)과 성(last_name)을 구해 봅시다. 데이터베이스 시스템의 부하를 줄이기 위해 그 외의 열은 가져오지 않겠습니다.

>>> User.objects.filter(
    first_name__startswith='R'
).values('first_name', 'last_name')
<QuerySet [{'first_name': 'Ricky', 'last_name': 'Dayal'}, {'first_name': 'Ritesh', 'last_name': 'Deshmukh'}, {'first_name': 'Radha', 'last_name': 'George'}, {'first_name': 'Raghu', 'last_name': 'Khan'}, {'first_name': 'Rishabh', 'last_name': 'Deol'}]

str(queryset.query) 으로 실제로 실행되는 SQL 질의문을 확인할 수 있습니다. 다음과 같은 질의문이 실행되었습니다.

SELECT "auth_user"."first_name", "auth_user"."last_name"
FROM "auth_user" WHERE "auth_user"."first_name"::text LIKE R%

실행 결과는 사전의 리스트입니다.

한편, only 메서드를 사용할 수도 있습니다.

>> queryset = User.objects.filter(
    first_name__startswith='R'
).only("first_name", "last_name")

str(queryset.query) 로 SQL 질의문을 확인해 봅시다.

SELECT "auth_user"."id", "auth_user"."first_name", "auth_user"."last_name"
FROM "auth_user" WHERE "auth_user"."first_name"::text LIKE R%

only 메서드가 values 메서드와 다른 점은 id 필드를 함께 가져온다는 점 뿐입니다.

장고에서 서브쿼리 식을 사용할 수 있나요?

장고에서 SQL 서브쿼리(subquery, 질의문 내의 하위 질의) 식을 사용할 수 있습니다. 간단한 것부터 시작해 봅시다. auth_user 모델과 일 대 일(OneToOne) 관계로 연결된 UserParent 모델이 있다고 합시다. 아래 코드로 UserParent 모델에서 auth_user 를 가진 행을 모두 구할 수 있습니다.

>>> from django.db.models import Subquery
>>> users = User.objects.all()
>>> UserParent.objects.filter(user_id__in=Subquery(users.values('id')))
<QuerySet [<UserParent: UserParent object (2)>, <UserParent: UserParent object (5)>, <UserParent: UserParent object (8)>]>

조금 더 까다로운 예제를 살펴봅시다. Category 모델의 각 행 별로, 가장 선한 Hero 행을 구해 봅시다.

모델은 다음과 같이 준비합니다.

class Category(models.Model):
    name = models.CharField(max_length=100)


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

    benevolence_factor = models.PositiveSmallIntegerField(
        help_text="How benevolent this hero is?",
        default=50
    )

이 모델에서 가장 선한 영웅을 구하려면 다음 코드를 실행합니다.

hero_qs = Hero.objects.filter(
    category=OuterRef("pk")
).order_by("-benevolence_factor")
Category.objects.all().annotate(
    most_benevolent_hero=Subquery(
        hero_qs.values('name')[:1]
    )
)

이 코드가 실행하는 SQL 질의문은 다음과 같습니다.

SELECT "entities_category"."id",
       "entities_category"."name",

  (SELECT U0."name"
   FROM "entities_hero" U0
   WHERE U0."category_id" = ("entities_category"."id")
   ORDER BY U0."benevolence_factor" DESC
   LIMIT 1) AS "most_benevolent_hero"
FROM "entities_category"

질의문을 한 단계씩 나누어 살펴봅시다. 다음 코드가 첫 번째 단계입니다.

hero_qs = Hero.objects.filter(
    category=OuterRef("pk")
).order_by("-benevolence_factor")

Hero 모델의 항목들을 선함(benevolence_factor)에 따라 내림차순으로 정렬하여 선택합니다. 그리고 category=OuterRef("pk") 를 이용해 이 선택이 서브쿼리로 사용될 수 있도록 준비합니다.

그 뒤 most_benevolent_hero=Subquery(hero_qs.values('name')[:1]) 로 서브쿼리에 별칭을 붙여 Category 쿼리셋 안에서 사용합니다. 이 때, hero_qs.values('name')[:1] 는 서브쿼리에서 첫 번째 행의 name 필드를 구하는 코드입니다.

필드의 값을 서로 비교하여 항목을 선택할 수 있나요?

장고 ORM에서 필드를 고정 값과 비교하여 항목을 선택하는 것은 간단합니다. 예를 들어, 이름(first_name) 이 'R' 로 시작하는 User 모델의 행을 구하려면 User.objects.filter(first_name__startswith='R') 와 같이 코드를 작성하면 됩니다.

그런데 필드와 필드를 서로 비교할 수도 있을까요? 예를 들어, 이름(first_name) 을 성(last_name) 과 비교하여 선택하는 것이죠. 이럴 때 F 객체를 사용합니다.

실습을 위해 User 모델의 항목을 몇 개 생성합시다.

In [27]: User.objects.create_user(email="shabda@example.com", username="shabda", first_name="Shabda", last_name="Raaj")
Out[27]: <User: shabda>

In [28]: User.objects.create_user(email="guido@example.com", username="Guido", first_name="Guido", last_name="Guido")
Out[28]: <User: Guido>

실습 데이터가 준비되었으면, 다음 코드로 이름과 성이 동일한 사용자를 구해 봅시다.

In [29]: User.objects.filter(last_name=F("first_name"))
Out[29]: <QuerySet [<User: Guido>]>

F 객체는 annotate 메서드로 계산해 둔 필드를 가리킬 때도 사용할 수 있습니다. 예를 들어, 이름의 첫 글자와 성의 첫 글자가 동일한 사용자를 구하고 싶다면 Substr("first_name", 1, 1) 를 사용할 수 있습니다.

In [41]: User.objects.create_user(email="guido@example.com", username="Tim", first_name="Tim", last_name="Teters")
Out[41]: <User: Tim>
#...
In [46]: User.objects.annotate(first=Substr("first_name", 1, 1), last=Substr("last_name", 1, 1)).filter(first=F("last"))
Out[46]: <QuerySet [<User: Guido>, <User: Tim>]>

F 객체에 __gt, __lt 등의 룩업(lookup)을 적용하는 것 또한 가능합니다.

FileField에 파일이 들어있지 않은 행은 어떻게 구할 수 있나요?

장고의 FileFieldImageField 는 파일과 이미지 파일의 경로를 저장합니다. 이것은 응용 수준에서의 구별이고, 데이터베이스 수준에서는 모두 CharField 와 동일한 방식으로 저장됩니다. 파일이 없는 행을 구하려면 다음 코드를 실행하면 됩니다.

no_files_objects = MyModel.objects.filter(
    Q(file='')|Q(file=None)
)

두 모델을 결합(JOIN)하려면 어떻게 하나요?

SQL에서는 JOIN 문을 이용해 동일한 값을 가진 열을 기준으로 두 표를 결합할 수 있습니다. 결합 연산은 여러 가지 방법으로 수행할 수 있습니다. 다음은 장고에서 이를 수행하는 몇 가지 예입니다.

>>> a1 = Article.objects.select_related('reporter') // Using select_related
>>> a1
<QuerySet [<Article: International News>, <Article: Local News>, <Article: Morning news>, <Article: Prime time>, <Article: Test Article>, <Article: Weather Report>]>
>>> print(a1.query)
SELECT "events_article"."id", "events_article"."headline", "events_article"."pub_date", "events_article"."reporter_id", "events_article"."slug", "auth_user"."id", "auth_user"."password", "auth_user"."last_login", "auth_user"."is_superuser", "auth_user"."username", "auth_user"."first_name", "auth_user"."last_name", "auth_user"."email", "auth_user"."is_staff", "auth_user"."is_active", "auth_user"."date_joined" FROM "events_article" INNER JOIN "auth_user" ON ("events_article"."reporter_id" = "auth_user"."id") ORDER BY "events_article"."headline" ASC
>>> a2 = Article.objects.filter(reporter__username='John')
>>> a2
<QuerySet [<Article: International News>, <Article: Local News>, <Article: Prime time>, <Article: Test Article>, <Article: Weather Report>]>
>>> print(a2.query)
SELECT "events_article"."id", "events_article"."headline", "events_article"."pub_date", "events_article"."reporter_id", "events_article"."slug" FROM "events_article" INNER JOIN "auth_user" ON ("events_article"."reporter_id" = "auth_user"."id") WHERE "auth_user"."username" = John ORDER BY "events_article"."headline" ASC

두번째로 큰 항목은 어떻게 구하죠?

어떤 필드를 기준으로 데이터를 정렬했을 때, 두 번째 항목을 구해야 하는 경우가 있습니다. 예컨대, 나이·급여 등이 두번째로 많은 사용자를 찾아야 하는 경우가 있겠죠.

장고 ORM에서 첫번째 항목은 first() 메서드로, 마지막 항목은 last() 메서드로 구할 수 있습니다. 하지만 N번째 항목을 구하는 메서드는 제공되지 않습니다. 그 대신, 파이썬의 인덱싱 연산을 이용할 수 있습니다.

_images/usertable.png

아래 코드와 같이, 인덱싱 연산으로 정렬된 데이터의 N번째 항목을 구할 수 있습니다.

>>> user = User.objects.order_by('-last_login')[1] // Second Highest record w.r.t 'last_login'
>>> user.first_name
'Raghu'
>>> user = User.objects.order_by('-last_login')[2] // Third Highest record w.r.t 'last_login'
>>> user.first_name
'Sohan'

User.objects.order_by('-last_login')[2] 와 같이 쿼리셋에 인덱스 연산을 지시할 때, 장고 ORM은 데이터베이스에서 전체 데이터를 가져온 뒤 인덱싱하는 것이 아니라, LIMIT ... OFFSET SQL 구문을 이용해 필요한 데이터만 읽어 옵니다. 실제로 생성되는 SQL 질의문을 살펴봅시다.

SELECT "auth_user"."id",
       "auth_user"."password",
       "auth_user"."last_login",
       "auth_user"."is_superuser",
       "auth_user"."username",
       "auth_user"."first_name",
       "auth_user"."last_name",
       "auth_user"."email",
       "auth_user"."is_staff",
       "auth_user"."is_active",
       "auth_user"."date_joined"
FROM "auth_user"
ORDER BY "auth_user"."last_login" DESC
LIMIT 1
OFFSET 2

특정 열의 값이 동일한 항목은 어떻게 찾나요?

_images/usertable2.png

first_name 이 서로 동일한 사용자들을 구한다고 합시다. 특정 열에서 중복된 값을 찾을 때는 아래와 같이 Count 를 구한 뒤 중복 수를 기준으로 골라내면 됩니다.

>>> duplicates = User.objects.values(
    'first_name'
    ).annotate(name_count=Count('first_name')).filter(name_count__gt=1)
>>> duplicates
<QuerySet [{'first_name': 'John', 'name_count': 3}]>

위와 같이 중복 값을 구했으면, 이 값을 가진 항목을 아래와 같이 구할 수 있습니다.

>>> records = User.objects.filter(first_name__in=[item['first_name'] for item in duplicates])
>>> print([item.id for item in records])
[2, 11, 13]

쿼리셋에서 고유한 필드 값을 가진 항목은 어떻게 구하나요?

_images/usertable2.png

이름이 다른 사용자와 겹치지 않은 사용자를 찾는다고 합시다. 다음과 같이 구할 수 있습니다.

distinct = User.objects.values(
    'first_name'
).annotate(
    name_count=Count('first_name')
).filter(name_count=1)
records = User.objects.filter(first_name__in=[item['first_name'] for item in distinct])

한편, User.objects.distinct("first_name").all() 와 같은 코드는 고유한 first_name 을 가진 사용자별로 첫번째 사용자를 구하는 코드입니다. 위 코드와는 실행 결과가 다릅니다.

Q 객체를 이용해 복잡한 질의를 수행하는 방법은 무엇인가요?

앞선 몇 개의 장에서 Q 객체를 이용해 OR 연산, AND 연산, NOT 연산을 수행해 보았습니다. Q 객체를 이용하면 SQL 질의문의 WHERE 절에 해당하는 기능을 온전히 활용할 수 있습니다.

조건식에서 OR 연산을 수행하려면 다음과 같이 합니다.

>>> from django.db.models import Q
>>> queryset = User.objects.filter(
    Q(first_name__startswith='R') | Q(last_name__startswith='D')
)
>>> queryset
<QuerySet [<User: Ricky>, <User: Ritesh>, <User: Radha>, <User: Raghu>, <User: rishab>]>

조건식에서 AND 연산을 수행하려면 다음과 같이 합니다.

>>> queryset = User.objects.filter(
    Q(first_name__startswith='R') & Q(last_name__startswith='D')
)
>>> queryset
<QuerySet [<User: Ricky>, <User: Ritesh>, <User: rishab>]>

이름(first_name)이 ‘R’로 시작하되, 성(last_name)에 ‘Z’가 포함되지 않은 사용자를 모두 구하려면 다음과 같이 조건을 작성하면 됩니다.

>>> queryset = User.objects.filter(
    Q(first_name__startswith='R') & ~Q(last_name__startswith='Z')
)

위 코드로 생성되는 질의문은 다음과 같습니다.

SELECT "auth_user"."id",
       "auth_user"."password",
       "auth_user"."last_login",
       "auth_user"."is_superuser",
       "auth_user"."username",
       "auth_user"."first_name",
       "auth_user"."last_name",
       "auth_user"."email",
       "auth_user"."is_staff",
       "auth_user"."is_active",
       "auth_user"."date_joined"
FROM "auth_user"
WHERE ("auth_user"."first_name"::text LIKE R%
       AND NOT ("auth_user"."last_name"::text LIKE Z%))

Q 객체를 이용하면 이보다 더 복잡한 조건의 질의도 문제 없이 지시할 수 있습니다.

기록된 항목의 집계를 구할 수 있나요?

장고 ORM을 이용해 항목을 생성·조회·갱신·삭제할 수 있지만, 때로는 항목들의 집계값을 구하고 싶을 때가 있습니다. 장고 ORM에는 SQL의 일반적인 집계 기능을 수행하는 Max, Min, Avg, Sum 등의 함수가 있습니다. 다음은 이 집계 함수를 이용하는 예입니다.

>>> from django.db.models import Avg, Max, Min, Sum, Count
>>> User.objects.all().aggregate(Avg('id'))
{'id__avg': 7.571428571428571}
>>> User.objects.all().aggregate(Max('id'))
{'id__max': 15}
>>> User.objects.all().aggregate(Min('id'))
{'id__min': 1}
>>> User.objects.all().aggregate(Sum('id'))
{'id__sum': 106}

항목을 무작위로 뽑고 싶습니다. 효율적인 방법이 있을까요?

Category 모델을 아래와 같이 정의했다고 합시다.

class Category(models.Model):
    name = models.CharField(max_length=100)

    class Meta:
        verbose_name_plural = "Categories"

    def __str__(self):
        return self.name

저장된 Category 항목 가운데 하나를 무작위로 구해야 합니다. 두 가지 방법을 살펴보겠습니다.

먼저 살펴볼 방법은 정직하고 이해하기 쉽습니다. order_by 메서드로 항목들을 정렬할 때, 정렬 기준을 ‘무작위’로 지정하는 것입니다. 데이터를 무작위로 정렬하여 첫 번째 항목을 가져오면 무작위 항목을 구할 수 있습니다. 코드로 작성해 봅시다.

def get_random():
    return Category.objects.order_by("?").first()

주의: 사용하는 데이터베이스 시스템에 따라 order_by('?') 의 실행 비용이 비싸고 성능이 느릴 수 있습니다. 뒤이어 살펴볼 다른 방법과의 비교를 위해 Category 표에 1백만 개의 항목을 추가해 두겠습니다. 명령행 인터페이스에서 python manage.py dbshell 를 실행하여 데이터베이스 셸을 열고, 아래 질의문을 실행하시면 실습에 필요한 항목을 준비할 수 있습니다.

INSERT INTO entities_category
            (name)
(SELECT Md5(Random() :: text) AS descr
 FROM   generate_series(1, 1000000));

위 SQL 질의문을 자세히 이해할 필요는 없습니다. (1부터 1백만까지의 수열을 생성하고 난수에 MD5 해시를 적용한 값을 생성하여 데이터베이스에 저장합니다.)

두 번째 방법은 전체 표를 정렬하는 대신 저장된 항목의 마지막 ID를 이용하는 것입니다. 표에서 ID의 최대값을 구하고, 1과 마지막 ID 사이의 난수를 하나 생성합니다. ID가 이 난수와 동일한 항목을 구하면 됩니다.

In [1]: from django.db.models import Max

In [2]: from entities.models import Category

In [3]: import random

In [4]: def get_random2():
   ...:     max_id = Category.objects.all().aggregate(max_id=Max("id"))['max_id']
   ...:     pk = random.randint(1, max_id)
   ...:     return Category.objects.get(pk=pk)
   ...:

In [5]: get_random2()
Out[5]: <Category: e2c3a10d3e9c46788833c4ece2a418e2>

In [6]: get_random2()
Out[6]: <Category: f164ad0c5bc8300b469d1c428a514cc1>

이 방법은 항목을 삭제하거나 해서 ID가 중간에 비어있는 경우에는 쓸 수 없습니다. 그런 경우에는 유효한 값이 나올 때까지 반복하도록 하면 됩니다. 다음은 그 방식으로 위의 함수를 수정한 것입니다.

In [8]: def get_random3():
   ...:     max_id = Category.objects.all().aggregate(max_id=Max("id"))['max_id']
   ...:     while True:
   ...:         pk = random.randint(1, max_id)
   ...:         category = Category.objects.filter(pk=pk).first()
   ...:         if category:
   ...:             return category
   ...:

In [9]: get_random3()
Out[9]: <Category: 334aa9926bd65dc0f9dd4fc86ce42e75>

In [10]: get_random3()
Out[10]: <Category: 4092762909c2c034e90c3d2eb5a73447>

삭제된 항목이 많지 않다면 위의 무한반복 구문 while True: 는 금방 종료될 것입니다. 그러면 파이썬의 timeit 을 이용해 두 방법의 성능 차이를 확인해 봅시다.

In [14]: timeit.timeit(get_random3, number=100)
Out[14]: 0.20055226399563253

In [15]: timeit.timeit(get_random, number=100)
Out[15]: 56.92513192095794

get_random3get_random 보다 283배 빠르게 실행되었습니다. 단, get_random 은 언제나 이용할 수 있는 반면에, get_random3 의 방법은 장고의 기본 ID 생성 방식(auto increment, 자동 증가)을 재정의한 경우나 삭제된 항목이 너무 많을 때에는 사용하기가 어려울 수 있습니다.

장고가 지원하지 않는 데이터베이스 함수를 사용할 수 있나요?

장고에는 Lower, Coalesce, Max 등의 데이터베이스 함수가 포함되어 있습니다. 하지만 장고가 데이터베이스가 지원하는 모든 함수를 제공하는 것은 아닙니다. 특히, 특정 데이터베이스 시스템의 전용 함수들은 제공되지 않습니다.

장고가 제공하지 않는 데이터베이스 함수를 실행하기 위해서는 장고의 Func 객체를 사용하면 됩니다.

PostgreSQL에는 fuzzystrmatch 확장 기능이 있습니다. 이 확장에는 텍스트 데이터의 유사도를 측정하기 위한 함수가 여러 가지 포함되어 있습니다. PostgreSQL 데이터베이스 셸에서 create extension fuzzystrmatch 를 실행하여 이 확장을 설치하고 아래의 실습을 진행해 주세요.

레벤슈타인 치환 거리 알고리즘을 구현한 levenshtein 함수를 이용해 보겠습니다. 실습에 사용할 Hero 모델의 항목을 여러 개 생성합시다.

Hero.objects.create(name="Zeus", description="A greek God", benevolence_factor=80, category_id=12, origin_id=1)
Hero.objects.create(name="ZeuX", description="A greek God", benevolence_factor=80, category_id=12, origin_id=1)
Hero.objects.create(name="Xeus", description="A greek God", benevolence_factor=80, category_id=12, origin_id=1)
Hero.objects.create(name="Poseidon", description="A greek God", benevolence_factor=80, category_id=12, origin_id=1)

이제 name 이 ‘Zeus’ 와 비슷한 Hero 항목들을 구해 봅시다.

from django.db.models import Func, F
Hero.objects.annotate(like_zeus=Func(F('name'), function='levenshtein', template="%(function)s(%(expressions)s, 'Zeus')"))

like_zeus=Func(F('name'), function='levenshtein', template="%(function)s(%(expressions)s, 'Zeus')") 코드에서 Func 객체를 세 개의 인자로 초기화하였습니다. 첫 번째 인자는 함수에 적용할 열, 두 번째 인자는 데이터베이스에서 실행할 함수의 이름, 세 번째 인자는 함수를 실행할 SQL 질의문의 템플릿입니다. 이 함수를 여러 번 재사용할 계획이라면 다음과 같이 클래스를 확장하여 정의해 두면 편리합니다.

class LevenshteinLikeZeus(Func):
    function='levenshtein'
    template="%(function)s(%(expressions)s, 'Zeus')"

이제 Hero.objects.annotate(like_zeus=LevenshteinLikeZeus(F("name"))) 와 같이 클래스를 이용할 수 있습니다.

이렇게 구한 레벤슈타인 거리를 기준으로 이름이 비슷한 항목을 선별할 수 있습니다.

In [16]: Hero.objects.annotate(
    ...:         like_zeus=LevenshteinLikeZeus(F("name"))
    ...:     ).filter(
    ...:         like_zeus__lt=2
    ...:     )
    ...:
Out[16]: <QuerySet [<Hero: Zeus>, <Hero: ZeuX>, <Hero: Xeus>]>

항목을 생성·갱신·삭제하는 방법

여러 개의 행을 한번에 생성하는 방법이 있나요?

여러 개의 신규 객체를 한꺼번에 저장하고 싶은 경우가 있습니다. 예를 들어, 여러 개의 분류 항목을 단번에 생성하되, 데이터베이스에 질의를 여러 번 수행하지 않아야 한다고 합시다. bulk_create 메서드를 이용하면 여러 개의 신규 객체를 한 번에 저장할 수 있습니다.

다음 예를 살펴보세요.

>>> Category.objects.all().count()
2
>>> Category.objects.bulk_create(
    [Category(name="God"),
     Category(name="Demi God"),
     Category(name="Mortal")]
)
[<Category: God>, <Category: Demi God>, <Category: Mortal>]
>>> Category.objects.all().count()
5

bulk_create 메서드는 저장되지 않은 객체들을 담은 리스트를 인자로 전달받습니다.

기존에 저장된 행을 복사해 새로 저장하는 방법은 무엇인가요?

장고 ORM에는 모델 인스턴스를 복사하는 내장 메서드가 없습니다. 하지만 모든 필드의 값을 복사하여 새 인스턴스를 만들고 새로 저장하는 것은 어렵지 않습니다.

모델 인스턴스를 저장할 때, pk 필드 값이 None 으로 지정되어 있으면 데이터베이스에 새 행으로 저장됩니다. pk 외의 모든 필드 값은 그대로 복제됩니다.

In [2]: Hero.objects.all().count()
Out[2]: 4

In [3]: hero = Hero.objects.first()

In [4]: hero.pk = None

In [5]: hero.save()

In [6]: Hero.objects.all().count()
Out[6]: 5

특정 모델의 항목이 하나만 생성되도록 강제하는 방법이 있나요?

특정 모델의 항목이 단 하나만 생성되도록 강제하고 싶을 때가 있습니다. 프로그램의 환경 설정 기록, 공유 자원에 대한 잠금 제어 등을 예로 들 수 있습니다.

다음은 Origin 이라는 모델을 싱글턴(단일개체)으로 만드는 기법입니다.

class Origin(models.Model):
    name = models.CharField(max_length=100)

    def save(self, *args, **kwargs):
        if self.__class__.objects.count():
            self.pk = self.__class__.objects.first().pk
        super().save(*args, **kwargs)

위 코드는 save 메서드를 재정의하여 pk 필드를 이미 존재하는 값으로 지정하도록 강제합니다. 이로써 객체가 이미 존재할 때 create 메서드를 호출하는 경우 IntegrityError 예외가 발생하도록 합니다.

모델 인스턴스를 저장할 때, 다른 모델에 반정규화된 필드를 함께 갱신하는 방법이 있나요?

모델을 다음과 같이 구성했다고 합시다.

class Category(models.Model):
    name = models.CharField(max_length=100)
    hero_count = models.PositiveIntegerField()
    villain_count = models.PositiveIntegerField()

    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)
    # ...

Hero 모델과 Villain 모델의 항목을 새로 저장할 때, Category 모델의 hero_count 필드와 villain_count 필드를 갱신해야 합니다.

다음과 같이 Hero 모델과 Villain 모델의 save 메서드를 재정의하면 됩니다.

class Hero(models.Model):
    # ...

    def save(self, *args, **kwargs):
        if not self.pk:
            Category.objects.filter(pk=self.category_id).update(hero_count=F('hero_count')+1)
        super().save(*args, **kwargs)


class Villain(models.Model):
    # ...

    def save(self, *args, **kwargs):
        if not self.pk:
            Category.objects.filter(pk=self.category_id).update(villain_count=F('villain_count')+1)
        super().save(*args, **kwargs)

위 코드에서 self.category.hero_count += 1 과 같이 인스턴스의 값을 수정하는 것이 아니라, update 메서드로 데이터베이스의 갱신을 수행하도록 한 것을 확인하시기 바랍니다.

또 다른 방법으로, ‘시그널(신호)’이라는 기능을 이용하는 방법이 있습니다. 시그널을 이용하는 예를 살펴봅시다.

from django.db.models.signals import pre_save
from django.dispatch import receiver

@receiver(pre_save, sender=Hero, dispatch_uid="update_hero_count")
def update_hero_count(sender, **kwargs):
    hero = kwargs['instance']
    if hero.pk:
        Category.objects.filter(pk=hero.category_id).update(hero_count=F('hero_count')+1)

@receiver(pre_save, sender=Villain, dispatch_uid="update_villain_count")
def update_villain_count(sender, **kwargs):
    villain = kwargs['instance']
    if villain.pk:
        Category.objects.filter(pk=villain.category_id).update(villain_count=F('villain_count')+1)

save 메서드 재정의 방법과 시그널의 비교

save 메서드를 재정의하는 방법과 시그널을 이용하는 방법 모두 사용할 수 있습니다. 어느 것을 사용하는 것이 좋을까요? 다음 규칙을 권해 드립니다.

  • 반정규화 필드에 영향을 끼치는 모델을 여러분이 통제할 수 있다면 save 메서드를 재정의합니다.
  • 반정규화 필드에 영향을 끼치는 모델을 여러분이 통제할 수 없다면(그 영향이 라이브러리 등에서 이루어진다면) 시그널을 이용합니다.

TRUNCATE 문을 수행하는 방법이 있나요?

SQL의 TRUNCATE 문은 표에 저장된 모든 항목을 제거하는 명령입니다. 장고는 TRUNCATE 문을 실행하는 명령을 제공하지 않습니다. 하지만 delete 메서드를 이용해 비슷한 결과를 얻을 수 있습니다.

다음 예를 보십시오.

>>> Category.objects.all().count()
7
>>> Category.objects.all().delete()
(7, {'entity.Category': 7})
>>> Category.objects.all().count()
0

위 코드는 잘 동작합니다. 하지만 TRUNCATE 문이 아니라 DELETE FROM ... 과 같은 SQL 질의를 수행합니다. 삭제해야 하는 항목의 수가 매우 많은 경우 처리 속도가 느릴 수 있습니다. truncate 명령이 필요하다면 다음과 같이 Category 모델에 classmethod 로 추가하면 됩니다.

class Category(models.Model):
    # ...

    @classmethod
    def truncate(cls):
        with connection.cursor() as cursor:
            cursor.execute('TRUNCATE TABLE "{0}" CASCADE'.format(cls._meta.db_table))

이렇게 메서드를 정의해 두면 Category.truncate() 를 실행하여 정말로 데이터베이스 시스템에 TRUNCATE 문을 질의할 수 있습니다.

모델 인스턴스가 생성·갱신될 때 발생하는 시그널에는 어떤 것이 있나요?

장고의 시그널을 이용하면 모델 인스턴스의 생명주기에 따라 특정 코드가 실행되도록 예약해 둘 수 있습니다. 장고가 제공하는 시그널의 종류는 다음과 같습니다.

  • pre_init
  • post_init
  • pre_save
  • post_save
  • pre_delete
  • post_delete

이 가운데 pre_savepost_save 가 가장 많이 사용됩니다. 이 두 시그널을 좀 더 자세히 살펴봅시다.

시그널과 save 메서드 재정의 비교

시그널을 이용하면 save 메서드를 재정의하는 것과 비슷한 효과를 누릴 수 있습니다. 그래서 어느 것을 사용해야 할지 헷갈릴 수도 있는데요, 다음 규칙에 따르시기 바랍니다.

  • 다른 사람(외부 라이브러리 등)이 여러분 앱의 save 메서드를 재정의·커스터마이즈하도록 허용하려면 직접 시그널을 발생시켜야 합니다.
  • 여러분이 통제할 수 없는 앱의 save 메서드가 호출될 때 원하는 코드가 실행되도록 하려면 post_save 시그널 또는 pre_save 시그널을 이용해야 합니다.
  • 여러분이 통제할 수 있는 앱의 저장 방식을 손 볼 때는 save 메서드를 재정의해야 합니다.

다음은 UserToken 모델의 예입니다. 이 모델은 사용자 인증 정보를 제공하는 역할을 하며, User 가 생성될 때 함께 생성됩니다.

class UserToken(models.Model):
    token = models.CharField(max_length=64)

    # ...

시간 정보를 다른 양식으로 변환하여 데이터베이스에 저장하려면 어떻게 해야 하나요?

장고에서 시간을 나타내는 텍스트를 다른 양식의 텍스트로 변환하여 데이터베이스에 저장하는 방법은 여러 가지가 있습니다. 몇 가지만 소개하겠습니다.

“2018-03-11”이라는 시간 텍스트가 있는데, 이 양식으로는 데이터베이스에 저장할 수 없다고 가정합시다. 아래와 같이 장고의 dateparser 모듈이나 파이썬 표준 라이브러리를 이용하여 날짜 양식을 변환할 수 있습니다.

>>> user = User.objects.get(id=1)
>>> date_str = "2018-03-11"
>>> from django.utils.dateparse import parse_date // Way 1
>>> temp_date = parse_date(date_str)
>>> a1 = Article(headline="String converted to date", pub_date=temp_date, reporter=user)
>>> a1.save()
>>> a1.pub_date
datetime.date(2018, 3, 11)
>>> from datetime import datetime // Way 2
>>> temp_date = datetime.strptime(date_str, "%Y-%m-%d").date()
>>> a2 = Article(headline="String converted to date way 2", pub_date=temp_date, reporter=user)
>>> a2.save()
>>> a2.pub_date
datetime.date(2018, 3, 11)

조회 결과를 정렬하는 방법

쿼리셋을 오름차순/내림차순으로 정렬할 수 있나요?

order_by 메서드로 쿼리셋을 정렬할 수 있습니다. 기준 필드를 지정해 오름차순 혹은 내림차순으로 정렬할 수 있습니다. 다음 코드를 살펴보세요.

>>> User.objects.all().order_by('date_joined')  # 오름차순
<QuerySet [<User: yash>, <User: John>, <User: Ricky>, <User: sharukh>, <User: Ritesh>, <User: Billy>, <User: Radha>, <User: Raghu>, <User: rishab>, <User: johny>, <User: paul>, <User: johny1>, <User: alien>]>
>>> User.objects.all().order_by('-date_joined')  # 내림차순
<QuerySet [<User: alien>, <User: johny1>, <User: paul>, <User: johny>, <User: rishab>, <User: Raghu>, <User: Radha>, <User: Billy>, <User: Ritesh>, <User: sharukh>, <User: Ricky>, <User: John>, <User: yash>]>

기준 필드를 여러 개 지정할 수도 있습니다.

User.objects.all().order_by('date_joined', '-last_login')

SQL 질의문은 다음과 같습니다.

SELECT "auth_user"."id",
       -- More fields
       "auth_user"."date_joined"
FROM "auth_user"
ORDER BY "auth_user"."date_joined" ASC,
         "auth_user"."last_login" DESC

대문자·소문자를 구별하지 않고 정렬하려면 어떻게 하나요?

_images/usertable2.png

order_by 메서드로 쿼리셋을 정렬할 때, 텍스트 필드를 기준으로 하면 알파벳의 대문자·소문자를 구분하여 정렬이 수행됩니다. 다음 예에서 보듯, 대문자에 소문자보다 높은 우선순위가 부여됩니다.

>>> User.objects.all().order_by('username').values_list('username', flat=True)
<QuerySet ['Billy', 'John', 'Radha', 'Raghu', 'Ricky', 'Ritesh', 'johny', 'johny1', 'paul', 'rishab', 'sharukh', 'sohan', 'yash']>

텍스트 필드에서 대문자·소문자를 구별하지 않고 정렬하려면 다음과 같이 Lower 를 사용하면 됩니다.

>>> from django.db.models.functions import Lower
>>> User.objects.all().order_by(Lower('username')).values_list('username', flat=True)
<QuerySet ['Billy', 'John', 'johny', 'johny1', 'paul', 'Radha', 'Raghu', 'Ricky', 'rishab', 'Ritesh', 'sharukh', 'sohan', 'yash']>

annotate 메서드로 Lower 를 적용한 열을 준비하고, 그 열을 기준으로 정렬하는 방법도 가능합니다.

User.objects.annotate(
    lower_name=Lower('username')
).order_by('lower_name').values_list('username', flat=True)

여러 개의 필드를 기준으로 정렬하는 방법이 있나요?

쿼리셋의 order_by 메서드에 여러 개의 정렬 기준 필드를 인자로 전달할 수 있습니다. 그러면 여러 개의 필드를 기준으로 정렬이 수행됩니다.

In [5]: from django.contrib.auth.models import User

In [6]: User.objects.all().order_by("is_active", "-last_login", "first_name")
Out[6]: <QuerySet [<User: Guido>, <User: shabda>, <User: Tim>]>

외래 키로 연결된 다른 표의 열을 기준으로 정렬할 수 있나요?

Category 모델과 Hero 모델이 다음과 같이 외래 키(ForeignKey)로 연결되어 있습니다.

class Category(models.Model):
    name = models.CharField(max_length=100)


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

아래 코드는 Hero 모델의 쿼리셋을 category 필드 순으로 정렬하되, category가 같은 항목은 (Hero 의) name 필드 순으로 정렬합니다.

Hero.objects.all().order_by(
    'category__name', 'name'
)

'category__name' 인자에 이중 밑줄 기호(__ )를 사용한 것을 봐 주세요. 이중 밑줄 기호로 연결된 모델의 필드를 가리킬 수 있습니다.

SQL 질의문은 다음과 같이 생성됩니다.

SELECT "entities_hero"."id",
       "entities_hero"."name",
       -- more fields
FROM "entities_hero"
INNER JOIN "entities_category" ON ("entities_hero"."category_id" = "entities_category"."id")
ORDER BY "entities_category"."name" ASC,
         "entities_hero"."name" ASC

계산 필드를 기준으로 정렬할 수 있나요?

Category 모델과 Hero 모델이 있습니다.

class Category(models.Model):
    name = models.CharField(max_length=100)


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

Category 항목들을 각 Category 항목에 속한 Hero 항목의 개수에 따라 정렬하고 싶다면, 다음과 같이 annotate 메서드로 계산 필드를 준비하여 기준으로 삼을 수 있습니다.

Category.objects.annotate(
    hero_count=Count("hero")
).order_by(
    "-hero_count"
)

모델을 정의하는 방법

일대일 관계는 어떻게 나타내나요?

일대일 관계란 두 표에서 각 항목이 서로 다른 표의 항목 단 하나와 연결되는 관계입니다. 우리가 쉽게 이해할 수 있는 예를 들자면, 우리들 각자는 생부와 생모를 각각 하나씩만 가질 수 있습니다.

다음 예는 장고의 사용자 인증 모델에 UserParent 모델을 일대일 관계로 연결해서 각 사용자마다 사용자의 부모를 기록할 수 있도록 합니다.

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 (하위 삭제)는 연결된 항목이 삭제될 때 해당 항목을 함께 삭제하도록 합니다. 따라서, 아래의 코드를 실행하면 User 모델의 항목(u2) 뿐 아니라 UserParent 의 항목(p2)도 함께 삭제됩니다.

>>> u2.delete()

일대다 관계는 어떻게 나타내나요?

일대다 관계란 한 표의 상위 항목이 다른 표의 여러 하위 항목에서 참조되는 관계입니다. 일대다 관계에서 상위 항목이 반드시 하위 항목을 가진다는 보장은 없습니다. 상위 항목은 하위 항목을 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 필드만 있으면 간단한 트위터 앱을 만들 수 있습니다.

트위터의 핵심 기능으로 ‘트윗’, ‘팔로우’, ‘마음에 들어요’가 있습니다. 아래의 두 모델로 그 핵심 기능을 모두 구현할 수 있습니다. User 모델은 장고의 사용자 인증 모델을 확장하여 정의했습니다.

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

이 모델로 할 수 있는 일은 다음과 같습니다.

1) 사용자가 다른 사용자를 '팔로우'  취소할  있습니다.
2) 사용자가 팔로우하는 다른 사용자가 작성한 트윗을   있습니다.
3) 사용자가 트윗에 '마음에 들어요'  취소할  있습니다.

ManyToMany 필드로 수행할 수 있는 연산을 몇 가지 살펴볼 텐데, 그 전에 항목을 몇 개 생성해 둡시다.

>>> 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()

생성한 항목들을 ManyToMany 필드로 연결해 봅시다.

>>> 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()

완전히 동작하는 예제는 다음 저장소에서 확인해 주세요.

https://github.com/yashrastogi16/simpletwitter

모델에 자기 참조 외래 키를 정의할 수 있나요?

자기 참조 외래 키를 이용하여 중첩 관계·재귀 관계를 표현할 수 있습니다. 일대다 관계와 유사하지만, 이름에서 알 수 있듯이 모델이 자기 자신을 참조한다는 특징이 있습니다.

자기 참조 외래 키는 아래의 두 가지 방법으로 작성할 수 있습니다.

class Employee(models.Model):
    manager = models.ForeignKey('self', on_delete=models.CASCADE)

# 또는

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)한 정보를 보여줄 수도 있고, 한 표의 부분 집합만을 보여줄 수도 있습니다. 이를 활용하면 복잡한 질의문을 감추고 필요한 정보를 쉽게 조회하는 인터페이스를 만들 수 있습니다.

다음 스크린샷은 데이터베이스를 SQLiteStudio라는 프로그램으로 열어 본 모습입니다. 표가 26개 있고, 뷰는 없습니다.

_images/before_view.png

SQL 질의문을 실행하여 간단한 뷰를 생성하겠습니다.

create view temp_user as
select id, first_name
from auth_user;

뷰가 생성되어 표 26개와 뷰 1개가 있습니다.

_images/after_view.png

장고 앱에서는 모델을 정의할 때 메타(Meta) 클래스에 managed = False, db_table="temp_user" 와 같이 옵션을 설정하여 뷰를 가리키는 모델로 사용할 수 있습니다.

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

union 연산이 있는 뷰는 아래 주소의 문서(Django Admin Cookbook)를 참고하세요.

http://books.agiliq.com/projects/django-admin-cookbook/en/latest/database_view.html?highlight=view

분류·댓글처럼 아무 모델이나 가리킬 수 있는 범용 모델을 정의할 수 있나요?

다음 모델을 봐 주세요.

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)를 추가했습니다.

FlexCategory 모델의 데이터베이스 스키마는 다음과 같이 정의됩니다.

Hero 모델의 항목을 분류할 때는 다음과 같이 합니다.

hero = Hero.objects.create(name='Hades')
FlexCategory.objects.create(content_object=hero, name="mythic")

‘ghost’로 분류된 Hero 를 구하려면 다음과 같이 조회합니다.

Hero.objects.filter(flex_category__name='ghost')

위의 ORM 코드가 생성하는 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

모델에 연결된 표의 이름을 지정할 수 있나요?

여러분이 모델에 연결된 데이터베이스 표의 이름을 직접 지정하지 않으면 장고가 자동으로 표의 이름을 지어 줍니다. 자동으로 붙는 데이터베이스 표의 이름은 “앱의 레이블”(manage.py startapp 명령에서 지은 이름)과 모델 클래스의 이름을 밑줄 기호로 연결한 것이 됩니다.

이 책의 예제에서는 entities 앱과 events 앱을 사용했으므로 모든 모델의 표 이름이 entities_ 또는 events_ 로 시작합니다.

_images/db_table.png

이 이름을 직접 붙이시려면 모델의 Meta 클래스에 db_table 값을 설정하면 됩니다.

class TempUser(models.Model):
    first_name = models.CharField(max_length=100)
    . . .
    class Meta:
        db_table = "temp_user"

모델 필드의 데이터베이스 열 이름을 지정할 수 있나요?

모델 필드가 가리키는 데이터베이스의 열 이름을 지정하려면 필드 인스턴스의 초기화 매개변수 db_column 에 원하는 이름을 전달하면 됩니다. 이 매개변수에 인자를 전달하지 않으면 필드 이름과 동일한 이름이 사용됩니다.

class ColumnName(models.Model):
    a = models.CharField(max_length=40,db_column='column1')
    column2 = models.CharField(max_length=50)

    def __str__(self):
        return self.a
_images/db_column.png

위 예에서 보듯, db_column 으로 지정한 이름이 필드 이름보다 우선순위가 높습니다. 첫 번째 열의 이름이 a가 아니라 column1로 지어졌습니다.

null=Trueblank=True 의 차이가 무엇인가요?

nullblank 는 둘 다 기본값이 False 입니다. 이 두 설정은 모두 필드(열) 수준에서 동작합니다. 즉, 필드(열)를 비워두는 것을 허용할 것인지를 설정합니다.

null=True 는 필드의 값이 NULL(정보 없음)로 저장되는 것을 허용합니다. 결국 데이터베이스 열에 관한 설정입니다.

date = models.DateTimeField(null=True)

blank=True 는 필드가 폼(입력 양식)에서 빈 채로 저장되는 것을 허용합니다. 장고 관리자(admin) 및 직접 정의한 폼에도 반영됩니다.

title = models.CharField(blank=True)  # 폼에서 비워둘 수 있음. 데이터베이스에는 ''이 저장됨.

null=Trueblank=True 를 모두 지정하면 어떤 조건으로든 값을 비워둘 수 있음을 의미합니다.

epic = models.ForeignKey(null=True, blank=True)
# 단, CharFields()와 TextFields()에서는 예외입니다.
# 장고는 이 경우 NULL을 저장하지 않으며, 빈 값을 빈 문자열('')로 저장합니다.

또 하나 예외적인 경우가 있습니다. 불리언 필드(BooleanField)에 NULL을 입력할 수 있도록 하려면 null=True 를 설정하는 것이 아니라, 널 불리언 필드(NullBooleanField)를 사용해야 합니다.

기본 키(PK)로 ID 대신 UUID를 사용할 수 있나요?

장고에서 모델을 생성하면 ID 필드가 기본 키로 생성됩니다. ID 필드의 기본 데이터 유형은 양의 정수입니다.

양의 정수가 아니라 UUID를 기본 키로 사용하고 싶다면 장고 1.8 버전에서 추가된 UUIDField 를 사용하면 됩니다.

import uuid
from django.db import models

class Event(models.Model):
    id = models.UUIDField(primary_key=True, default=uuid.uuid4, editable=False)
    details = models.TextField()
    years_ago = models.PositiveIntegerField()

>>> eventobject = Event.objects.all()
>>> eventobject.first().id
'3cd2b4b0c36f43488a93b3bb72029f46'

슬러그 필드를 사용할 수 있나요?

슬러그(slug)는 URL의 구성요소로 웹사이트의 특정 페이지를 가리키는 사람이 읽기 쉬운 형식의 식별자입니다. 장고에서는 슬러그 필드(SlugField)로 슬러그를 지원합니다. 다음 예에서 사용법을 확인하실 수 있습니다. 앞서 살펴보았던 Article 모델에 슬러그 필드를 추가해 가독성을 높여 보았습니다.

from django.utils.text import slugify
class Article(models.Model):
    headline = models.CharField(max_length=100)
    . . .
    slug = models.SlugField(unique=True)

    def save(self, *args, **kwargs):
        self.slug = slugify(self.headline)
        super(Article, self).save(*args, **kwargs)
    . . .

>>> u1 = User.objects.get(id=1)
>>> from datetime import date
>>> a1 = Article.objects.create(headline="todays market report", pub_date=date(2018, 3, 6), reporter=u1)
>>> a1.save()
# 슬러그는 자동으로 생성됩니다. create 메서드를 따로 정의한 게 아닙니다.
>>> a1.slug
'todays-market-report'
슬러그의 장점:
사람이 이해하기 좋다. (/1/ 보다 /blog/ 가 좋다)
제목과 URL을 동일하게 맞춰 검색엔진 최적화(SEO)에 도움이 된다.

장고 프로젝트 하나에서 여러 개의 데이터베이스를 사용할 수 있나요?

데이터베이스의 접속에 관련된 설정은 대부분 settings.py 파일에서 이루어집니다. 장고 프로젝트에 여러 개의 데이터베이스를 추가하려면 해당 파일의 DATABASES 사전에 등록하면 됩니다.

DATABASE_ROUTERS = ['path.to.DemoRouter']
DATABASE_APPS_MAPPING = {'user_data': 'users_db',
                        'customer_data':'customers_db'}

DATABASES = {
    'default': {
        'ENGINE': 'django.db.backends.sqlite3',
        'NAME': os.path.join(BASE_DIR, 'db.sqlite3'),
    },
    'users_db': {
        'NAME': 'user_data',
        'ENGINE': 'django.db.backends.postgresql',
        'USER': 'postgres_user',
        'PASSWORD': 'password'
    },
    'customers_db': {
        'NAME': 'customer_data',
        'ENGINE': 'django.db.backends.mysql',
        'USER': 'mysql_cust',
        'PASSWORD': 'root'
    }
}

여러 개의 데이터베이스를 함께 사용하려면 데이터베이스 중계기(database router)에 대해 알아야 합니다. 장고의 기본 중계 설정은 데이터베이스를 특정하지 않은 경우 기본(default) 데이터베이스로 중계하는 것입니다. DATABASE_ROUTERS 설정의 기본값은 [] 입니다. 중계기는 다음과 같이 정의할 수 있습니다.

class DemoRouter:
    """
    user_data 앱의 모델에서 수행되는 모든 데이터베이스 연산을 제어하는 중계기
    """
    def db_for_read(self, model, **hints):
        """
        user_data 앱의 모델을 조회하는 경우 users_db로 중계한다.
        """
        if model._meta.app_label == 'user_data':
            return 'users_db'
        return None

    def db_for_write(self, model, **hints):
        """
        user_data 앱의 모델을 기록하는 경우 users_db로 중계한다.
        """
        if model._meta.app_label == 'user_data':
            return 'users_db'
        return None

    def allow_relation(self, obj1, obj2, **hints):
        """
        user_data 앱의 모델과 관련된 관계 접근을 허용한다.
        """
        if obj1._meta.app_label == 'user_data' or \
           obj2._meta.app_label == 'user_data':
           return True
        return None

    def allow_migrate(self, db, app_label, model_name=None, **hints):
        """
        user_data 앱의 모델에 대응하는 표가 users_db 데이터베이스에만 생성되도록 한다.
        """
        if app_label == 'user_data':
            return db == 'users_db'
        return None

중계기를 위와 같이 설정해 두었으면, 모델이 서로 다른 데이터베이스를 사용하도록 다음과 같이 정의할 수 있습니다.

class User(models.Model):
    username = models.Charfield(ax_length=100)
    . . .
        class Meta:
        app_label = 'user_data'

class Customer(models.Model):
    name = models.TextField(max_length=100)
    . . .
        class Meta:
        app_label = 'customer_data'

여러 개의 데이터베이스를 관리할 때 사용하는 마이그레이션 명령도 알아두세요.

$ ./manage.py migrate --database=users_db

장고 ORM 코드를 테스트하는 방법

질의 횟수가 고정된 횟수만큼만 일어나는지 확인할 수 있을까요?

장고 단위 테스트 클래스의 assertNumQueries() 메서드를 사용하여 데이터베이스에 발생하는 질의 횟수를 검증할 수 있습니다.

def test_number_of_queries(self):
    User.objects.create(username='testuser1', first_name='Test', last_name='user1')
    # 위 ORM 명령으로 질의 횟수가 1 번 일어나야 한다.
    self.assertNumQueries(1)
    User.objects.filter(username='testuser').update(username='test1user')
    # 질의 횟수가 한 번 증가해야 한다.
    self.assertNumQueries(2)

데이터베이스를 재사용하여 테스트 실행 속도를 높일 수 있나요?

python manage.py test 명령을 실행할 때마다 데이터베이스가 새로 생성됩니다. 이것은 마이그레이션이 많지 않을 때는 문제가 되지 않습니다. 하지만 마이그레이션이 많아질수록 테스트 실행 시 데이터베이스 재생성에 많은 시간을 소요하게 됩니다. 이런 상황을 피하기 위해 이전에 생성된 데이터베이스를 재사용할 수 있습니다.

테스트 명령에 --keepdb 플래그를 추가하여 데이터베이스가 삭제되는 것을 방지하고 테스트 실행 간 데이터베이스를 유지할 수 있습니다. 데이터베이스가 존재하지 않으면 데이터베이스를 새로 생성합니다. 마지막 테스트 실행 이후 마이그레이션이 추가되었으면 최신 상태를 유지하기 위해 마이그레이션을 수행합니다.

$ python manage.py test --keepdb

모델 객체를 데이터베이스에서 다시 읽어들일 수 있나요?

refresh_from_db() 메서드를 사용하여 데이터베이스에서 모델을 다시 읽어들일 수 있습니다. 값을 갱신하는 테스트를 작성할 때 유용한 기능입니다. 다음 예를 살펴보세요.

class TestORM(TestCase):
    def test_update_result(self):
        userobject = User.objects.create(username='testuser', first_name='Test', last_name='user')
        User.objects.filter(username='testuser').update(username='test1user')
        # 이 때, userobject 인스턴스의 username은 'testuser' 입니다.
        # 그러나 데이터베이스에서는 'test1user'로 수정되었습니다.
        # 모델 인스턴스의 속성이 데이터베이스와 맞지 않으므로 다시 읽어들입니다.
        userobject.refresh_from_db()
        self.assertEqual(userobject.username, 'test1user')

찾아보기 / 표