쿼리 작성하기
쿼리 작성하기 (Making queries)
모델을 정의하고 나면 Django가 데이터베이스에 접근할 수 있는 추상화 API를 자동으로 만들어 줘요. 이 문서는 그 API로 객체를 생성하고, 조회하고, 수정하고, 삭제하는 흐름을 하나씩 짚어 주는 입문서예요. SQL을 직접 짜지 않아도 되지만, 뒤에서 어떤 SQL이 실행되는지 함께 이해하면 훨씬 든든해져요.
객체 만들기
모델 클래스는 하나의 데이터베이스 테이블을, 그리고 그 클래스의 인스턴스는 테이블의 한 레코드를 나타내요. 객체를 만들려면 모델 클래스에 키워드 인자로 값을 넣어 인스턴스를 만든 뒤 save()를 호출하면 돼요.
>>> from blog.models import Blog
>>> b = Blog(name="Beatles Blog", tagline="All the latest Beatles news.")
>>> b.save()
이때 뒤에서는 INSERT SQL이 실행돼요. 주의할 점이 하나 있어요. Django는 save()를 명시적으로 호출하기 전까지는 데이터베이스를 건드리지 않아요. 그리고 save()는 반환값이 없어요. 이미 저장된 객체의 값을 바꾸고 싶을 때도 속성을 바꾼 뒤 save()를 다시 호출하면 UPDATE SQL이 실행돼요.
>>> b5.name = "New name"
>>> b5.save()
ForeignKey와 ManyToManyField 저장하기
ForeignKey 필드는 보통 필드와 똑같이 처리하면 돼요. 알맞은 타입의 객체를 필드에 할당하고 save()를 호출하면 그만이에요.
>>> entry = Entry.objects.get(pk=1)
>>> cheese_blog = Blog.objects.get(name="Cheddar Talk")
>>> entry.blog = cheese_blog
>>> entry.save()
ManyToManyField는 조금 달라요. 관계에 레코드를 추가할 때는 필드의 add() 메서드를 사용해요.
>>> joe = Author.objects.create(name="Joe")
>>> entry.authors.add(joe)
여러 레코드를 한 번에 추가하고 싶으면 add()에 인자를 여러 개 넘기면 돼요.
>>> entry.authors.add(john, paul, george, ringo)
잘못된 타입의 객체를 할당하거나 추가하려 하면 Django가 오류를 냅니다.
객체 조회하기
객체를 조회할 때는 모델 클래스의 Manager를 통해 QuerySet을 만들게 돼요. QuerySet은 데이터베이스에서 가져온 객체들의 집합이고, 필터를 하나 이상 붙여 결과를 좁힐 수 있어요. SQL로 치면 QuerySet은 SELECT 문, 필터는 WHERE나 LIMIT 같은 제한 절에 해당해요.
각 모델에는 기본으로 objects라는 Manager가 있어요. 그런데 이 Manager는 모델 클래스를 통해서만 접근할 수 있어요. 인스턴스에서는 접근할 수 없어요. 이렇게 막아 두는 이유는 "테이블 전체에서 하는 작업"과 "레코드 하나에 하는 작업"을 확실히 구분하기 위해서예요.
>>> Blog.objects
<django.db.models.manager.Manager object at ...>
>>> b = Blog(name="Foo", tagline="Bar")
>>> b.objects
Traceback:
...
AttributeError: "Manager isn't accessible via Blog instances."
모든 객체 가져오기
Manager.all()로 테이블의 전체 객체를 담은 QuerySet을 얻어요.
>>> all_entries = Entry.objects.all()
필터로 원하는 객체만 가져오기
보통은 전체가 아니라 일부만 필요하죠. 그럴 때 QuerySet을 정제해서 필터 조건을 붙여요. 가장 흔한 두 가지 방법은 filter(**kwargs)와 exclude(**kwargs)예요. filter는 조건에 맞는 객체만, exclude는 조건에 맞지 않는 객체만 담은 새로운 QuerySet을 돌려줘요.
>>> Entry.objects.filter(pub_date__year=2006)
QuerySet을 정제한 결과는 다시 QuerySet이므로, 필터를 계속 이어 붙일 수 있어요.
>>> Entry.objects.filter(
... headline__startswith="What"
... ).exclude(
... pub_date__gte=datetime.date.today()
... ).filter(
... pub_date__gte=datetime.date(2005, 1, 30)
... )
이 예시는 headline이 "What"으로 시작하면서, 2005년 1월 30일부터 오늘 사이에 발행된 글을 골라내는 쿼리예요.
정제된 QuerySet은 매번 새로워요
필터를 붙일 때마다 이전 QuerySet과 전혀 얽히지 않는 새로운 QuerySet이 만들어져요. 정제를 여러 번 하더라도 처음의 기준 QuerySet은 그대로 남아요.
>>> q1 = Entry.objects.filter(headline__startswith="What")
>>> q2 = q1.exclude(pub_date__gte=datetime.date.today())
>>> q3 = q1.filter(pub_date__gte=datetime.date.today())
q2와 q3는 둘 다 q1에서 출발하지만 서로 다른 QuerySet이고, q1은 정제 과정의 영향을 전혀 받지 않아요.
QuerySet은 게으르다 (lazy)
QuerySet은 게을러요. QuerySet을 만드는 순간에는 데이터베이스 작업이 전혀 일어나지 않아요. 필터를 아무리 많이 쌓아도, 실제로 그 결과가 필요한 순간(평가되는 순간)에야 비로소 쿼리가 실행돼요. 다음 코드가 세 번 데이터베이스를 건드릴 것처럼 보이지만, 실제로는 마지막 줄의 print(q)에서 딱 한 번만 실행돼요.
>>> q = Entry.objects.filter(headline__startswith="What")
>>> q = q.filter(pub_date__lte=datetime.date.today())
>>> q = q.exclude(body_text__icontains="food")
>>> print(q)
get()으로 단일 객체 가져오기
결과가 정확히 하나뿐이라는 걸 알고 있다면 Manager.get()으로 객체를 직접 받아올 수 있어요. filter()는 항상 QuerySet을 주지만, get()은 객체 자체를 돌려줘요.
>>> one_entry = Entry.objects.get(pk=1)
단, 일치하는 결과가 없으면 DoesNotExist 예외가 나요. 이 예외는 쿼리를 수행하는 모델 클래스의 속성으로 붙어 있어서, 위 코드에서는 Entry.DoesNotExist가 돼요. 반대로 일치하는 객체가 두 개 이상이면 MultipleObjectsReturned 예외가 나요.
QuerySet 개수 제한하기
파이썬의 슬라이스 문법을 활용하면 SQL의 LIMIT와 OFFSET에 해당하는 동작을 할 수 있어요. 처음 5개면 [:5], 6번째부터 10번째까지면 [5:10]이에요.
>>> Entry.objects.all()[:5] # LIMIT 5
>>> Entry.objects.all()[5:10] # OFFSET 5 LIMIT 5
음수 인덱스(Entry.objects.all()[-1])는 지원하지 않아요. 슬라이스는 보통 새로운 QuerySet을 만들 뿐 쿼리를 실행하지 않지만, 슬라이스의 step 파라미터([:10:2]처럼)를 쓰면 실제로 쿼리가 실행돼요.
필드 조회 (Field lookups)
필드 조회는 SQL WHERE 절의 핵심을 지정하는 방법이에요. filter(), exclude(), get()의 키워드 인자로 field__lookuptype=value 형태를 사용해요. 여기서 __는 밑줄 두 개예요.
>>> Entry.objects.filter(pub_date__lte="2006-01-01")
이 코드는 대략 아래 SQL로 번역돼요.
SELECT * FROM blog_entry WHERE pub_date <= '2006-01-01';
ForeignKey 필드는 기본 규칙에 한 가지 예외가 있어요. 필드 이름 뒤에 _id를 붙이면 관련 모델의 기본키 원본 값을 직접 넘길 수 있어요.
>>> Entry.objects.filter(blog_id=4)
자주 쓰는 조회 형식을 몇 가지 볼게요.
exact— 정확히 일치. 조회 형식을 생략하면exact가 기본이에요. 그래서id__exact=14와id=14는 같아요.iexact— 대소문자를 무시한 일치.name__iexact="beatles blog"는 "Beatles Blog", "beatlesblog", "BeAtlESblOG" 모두와 매치돼요.contains— 대소문자를 구분하는 포함 검사.headline__contains="Lennon"은 SQL의LIKE '%Lennon%'에 해당해요. 대소문자 무시 버전은icontains예요.startswith,endswith— 각각 시작·끝 검색. 대소문자 무시 버전은istartswith,iendswith예요.
관계를 가로지르는 조회
Django는 관계를 따라가며 조회하는 강력하고 직관적인 방법을 제공해요. 뒤에서 JOIN을 자동으로 처리해 주니까, 관련 모델의 필드 이름을 밑줄 두 개로 이어 주면 돼요. Blog의 name이 "Beatles Blog"인 모든 Entry를 찾으려면 이렇게 써요.
>>> Entry.objects.filter(blog__name="Beatles Blog")
관계를 따라가는 것은 원하는 만큼 깊어질 수 있고, 역방향(Blog에서 Entry로)도 가능해요. 기본적으로 역방향 관계는 관련 모델의 소문자 이름으로 참조해요.
>>> Blog.objects.filter(entry__headline__contains="Lennon")
여러 값 관계 가로지르기
ManyToManyField나 역방향 ForeignKey를 가로질러 여러 속성을 필터링할 때는, 조건들이 같은 관련 객체에서 만족되어야 하는지가 관건이 돼요. 2008년 글 중 headline에 "Lennon"이 있는 블로그를 찾고 싶을 때, 두 조건을 한 filter()에 넣으면 같은 entry 하나가 두 조건을 모두 만족해야 해요.
>>> Blog.objects.filter(entry__headline__contains="Lennon", entry__pub_date__year=2008)
반면에 "Lennon"이 있는 글도, 2008년 글도 각각 아무거나 가진 블로그를 찾고 싶다면 filter()를 이어 붙여요. 이렇게 하면 여러 JOIN이 생겨 중복 결과가 나올 수 있어요.
>>> Blog.objects.filter(entry__headline__contains="Lennon").filter(entry__pub_date__year=2008)
정리하면, 각 조건이 같은 관련 객체에서 만족되어야 하면 한 filter() 호출 안에 넣고, 그렇지 않으면 filter()를 나눠서 쓰면 돼요.
Q 객체로 복잡한 조회하기
키워드 인자로 넣는 조건들은 모두 AND로 묶여요. OR 같은 더 복잡한 조건이 필요할 때는 Q 객체(django.db.models.Q)를 사용해요. Q 객체는 키워드 인자들의 모음을 감싸는 객체이고, &, |, ^ 연산자로 결합할 수 있어요. ~ 연산자로 부정(NOT)할 수도 있어요.
from django.db.models import Q
# OR
Q(question__startswith="Who") | Q(question__startswith="What")
# NOT과의 결합
Q(question__startswith="Who") | ~Q(pub_date__year=2005)
filter() 같은 조회 함수에 Q 객체를 위치 인자로 여러 개 넘기면 그들은 AND로 묶여요.
>>> Poll.objects.get(
... Q(question__startswith="Who"),
... Q(pub_date=date(2005, 5, 2)) | Q(pub_date=date(2005, 5, 6)),
... )
이 쿼리는 대략 다음 SQL로 번역돼요.
SELECT * from polls WHERE question LIKE 'Who%'
AND (pub_date = '2005-05-02' OR pub_date = '2005-05-06');
조회 함수는 Q 객체와 키워드 인자를 함께 쓸 수도 있어요.
더 알아보기 (Learn more)
- Django 모델과 필드 — 모델 정의, 필드 타입, 메타 옵션
- QuerySet API 레퍼런스 — 모든
QuerySet메서드와 필드 조회의 완전한 목록 - 데이터 모델 레퍼런스 — 각종 모델 옵션 상세
- 관계 기반 객체 — OneToOne, ManyToMany 등 관계 조회 예제 실행