Computer Science/BackEnd

Django | Django ORM QuerySet 사용하기

토마토. 2022. 10. 29. 22:51

발단

Django로 작은 블로그를 만들고 있다.

User, Post, Notification 세 개의 앱으로 구성된 프로젝트다.

 

앱 별로 주요 기능은 다음과 같다. 

  • User
    • Follow/Following
    • Profile (마이페이지)
  • Post
    • Post (블로그 포스트)
    • Comment
    • Clapse (좋아요/공감 격)
    • Tag (#해시태그)
  • Notification
    • Notification (팔로우, 좋아요, 댓글 등을 알림으로 남겨줌)

기본적인 기능과 권한 설정, 페이지네이션 등은 어렵지 않게 구현하였는데, 

데이터베이스에 데이터가 별로 없어도 리퀘스트 하나에 3~5초씩 걸리는 문제가 발생했다. 

문제는 QuerySet에 있을 것이라 판단하여 QuerySet 성능 개선을 시도했다. 

 

내가 사용한 쿼리는 .filter, .create, .save, .all, .get, .delete, .add, .remove 등으로 겉보기에 간단하고 명료해보였다. 

기능을 잘 수행하는 쿼리들이기 때문에 어떻게 개선해야할지 잘 감이 오지 않아 일단 QuerySet을 공부해보기로 했다.

 

 

 


QuerySet 사용하기

[참고] Making queries | Django documentation | Django (djangoproject.com)

 

Creating objects

from blog.models import Blog
b = Blog(name='Beatles Blog', tagline='All the lastest Beatles news.')
b.save()

Django ORM에서 SQL statement를 대신 실행해준다. 

save() method는 SQL INSERT를 실행해준다. 

리턴 값은 없음. 

만들고 저장하는 걸 동시에 하려면, create() method를 이용하면 된다. 

 

Saving changes to objects

b5.name = 'New name'
b5.save()

이미 데이터베이스에 있는 object를 수정할 때에도 save() 메소드를 사용한다. 

이때, save() 매소드를 호출할 때에야 SQL UPDATE를 실행해주며 그 전까지는 데이터베이스를 건드리지 않는다. 

-> ORM laziness 찾아볼 것

 

Saving and fields ForeignKey, ManyToManyField

from blog.models import Blog, Entry
entry = Entry.objects.get(pk=1)
cheese_blog = Blog.objects.get(name="Cheddar Talk")
entry.blog = cheese_blog
entry.save()

- ForeignKey field인 entry.blog를 업데이트하기

- model.field = object 식으로 업데이트 해준 뒤에, model.save()를 해주면 된다. 

from blog.models import Author
joe = Author.objects.create(name="Joe")
entry.authors.add(joe)

- ManyToManyField인 entry.authors를 업데이트하기

- models.field.add(object)면 저장까지 완료된다. 

john = Author.objects.create(name="John")
paul = Author.objects.create(name="Paul")
george = Author.objects.create(name="George")
ringo = Author.objects.create(name="Ringo")
entry.authors.add(john, paul, george, ringo)

- ManyToManyField entry에 여러 개의 object를 더해줄 때

- model.field.add(a, b, c, d, ...)와 같은 식으로 불러줘도 된다. 

 

Retrieving objects

model class Manager를 통해서 QuerySet을 구축한다.

QuerySet은 database에서 가져온 object의 집합을 의미한다. 이때 QuerySet은 여러 개의 filter를 가질 수 있다. 

filter들은 파라미터를 통해서 조건을 준다, 그러니까 SQL 식으로 말하면 WHERE, LIMIT 같은 역할을 하는 것이다. 

 

QuerySet을 model Manager를 통해 가져올 수 있다. 모든 model은 적어도 한 개의 Manager를 갖는데, 이걸 objects라고 부른다. 

-> [Manager 문서] Managers | Django documentation | Django (djangoproject.com)

 

Blog.objects #Blog model의 Manager, 가능
b = Blog(name='Foo', tagline='Bar')
b.objects #불가능

Model Manager에는 record 단위가 아니라 Model 단위에서만 접근할 수 있다. 

Model Manager는 모델에 대한 QuerySet의 주요 출처다. 

ex) Blog.objects.all() 

 

Retrieving all objects

all_entries = Entry.objects.all()

 

Retrieving specific objects with filters

두 가지 방법이 흔히 사용된다.

  • filter() : 파라미터에 부합하는 쿼리셋을 리턴함
Entry.objects.filter(pub_date__year=2006)
Entry.objects.all().filter(pub_date__year=2006)
  • exclude() : 파라미터에 부합하는 객체를 제외한 쿼리셋을 리턴함

 

참고로 파라미터로 사용하는 요소는 다음 링크에서 찾아볼 수 있다. 

Making queries | Django documentation | Django (djangoproject.com)

 

Chaining filters

QuerySet을 필터링한 결과는 다시 QuerySet이므로, 필터링을 chain으로 엮어서 거듭 진행하는 것이 가능하다. 

Entry.objects.filter(
	headline__startswith='What'
).exclude(
	pub_date__gte=datetime.date.today()
).filter(
	pub_date__gte=datetime.date(2005, 1, 30)
)

이 쿼리 역시 쿼리셋을 리턴한다. 

 

Filtered QuerySets are unique

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

쿼리셋 q1, q2, q3는 모두 분리되어 있다.

 

QuerySets are lazy

QuerySet은 lazy하다.

QuerySet을 생성하는 것 자체만으로는 데이터베이스에 영향을 주지 않는다. 

QuerySet을 생성하고 그 다음에 QuerySet은 evaluate해야 그제야 query를 실행하고 DB에 접근하는 것이다. 

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)

위 코드에서 print(q)에 이르러서야 쿼리셋을 실행한다. 

QuerySet을 실행해달라고 할 때만 그 결과를 받을 수 있다. 

QuerySet이 실행되는 때에 관해서는 다음 문서를 참고하면 된다. 

QuerySet API reference | Django documentation | Django (djangoproject.com)

 

Retrieving a single object with get()

one_entry = Entry.objects.get(pk=1)

filter는 쿼리셋을 받아오는 반면, get은 하나의 object를 반환한다. 

따라서 get은 pk처럼 하나의 object만이 조건을 만족할 때만 사용할 수 있다. 

get은 만족하는 object가 없으면 DoesNotExist exception을, 여러 개의 object가 있으면 MultipleObjectsReturned exception을 내보낸다.

 

Other QuerySet methods

all(), get(), filter(), exclude()를 제일 많이 쓰게 될 것

다른 게 필요하다면, 다음 문서 참고하기. 

QuerySet API reference | Django documentation | Django (djangoproject.com)

 

Limiting QuerySets

Python array-slicing을 이용해서 원하는 개수의 결과를 얻을 수 있다. 

SQL LIMIT, OFFSET 구문이 이와 대응한다. 

Entry.objects.all()[:5]

== LIMIT 5

Entry.objects.all()[5:10]

== OFFSET 5 LIMIT 5

참고로, 음수 인덱싱(ex [-1])은 불가능하다. 

 

Entry.objects.order_by('headline')[0]
Entry.objects.order_by('headline')[0:1].get()

한 개의 오브젝트를 받고 싶을 때

위 코드는 IndexError를, 아래 코드는 DoesNotExist Exception을 리턴함

 

Field lookups

Field lookup은 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(Foreign model의 primary key)를 찾아볼 수 있다. 

Entry.objects.filter(blog_id=4)

QuerySet API reference | Django documentation | Django (djangoproject.com)

 

이외에도 다음의 Field lookup을 사용할 수 있다. 

# exact 
# SELECT ... WHERE headline = 'Cat bites dog';
Entry.objects.get(headline__exact="Cat bites dog")

# iexact - case insensitive
Blog.objects.get(name__iexact="beatles blog")

# contains
# SELECT ... WHERE headline LIKE '%Lennon%'
Entry.objects.get(headline__contains='Lennon')

# startswith
# endswith
# istartswith
# iendswith

 

Lookups that span relationships

Django ORM은 알아서 SQL JOIN을 처리해주어서 모델과 연관된 다른 모델에 따라갈 수 있다.

아래 예시를 살펴보자. 

Entry.objects.filter(blog__name='Beatles Blog')
Blog.objects.filter(entry__headline__contains='Lennon')
Blog.objects.filter(entry__authors__name='Lennon')
Blog.objects.filter(entry__authors__isnull=False, entry__authors__name__isnull=True)

 

Filters can reference fields on the model

field와 constant가 아니라(field='c'), field와 field를 비교하고 싶을 때를 위해 Django에는 F expression 이 있다. 

F() expression은 같은 모델의 두 필드를 비교할 때 사용된다. 

 

다음 사례를 보자. 

from django.db.models import F

# 기본 사용법
Entry.objects.filter(number_of_comments__gt=F('number_of_pingbacks'))

# 사칙연산 가능함
Entry.objects.filter(number_of_comments__gt=F('number_of_pingbacks')*2)
Entry.objects.filter(rating__lt=F('number_of_comments')+F('number_of_pingbacks'))
Entry.objects.filter(authors__name=F('blog__name'))

# 다른 상수와 조합하는 것도 가능함
from datetime import timedelta
Entry.objects.filter(mod_date__gt=F('pub_date')+timedelta(days=3))

 

Expressions can reference transforms

우선 transform은 Django generic class 중 하나로 field 형태를 바꾸도록 해준다. 

가장 많이 활용되는 예시는 __year로 DateField를 IntegerField로 변환해주는 것이 있다. 

자세한 내용은 다음 링크 참조

Lookup API reference | Django documentation | Django (djangoproject.com)

 

from django.db.models import F
Entry.objects.filter(pub_date__year=F('mod_date__year'))

from django.db.models import Min
Entry.objects.aggregate(first_published_year=Min('pub_date__year'))

from django.db.models import OuterRef, Subquery, Sum
Entry.objects.values('pub_date__year').annotage(
	top_rating=Subquery(
    	Entry.objects.filter(
        	pub_date__year=OuterRef('pub_date__year'),
        ).order_by('-rating').values('rating')[:1]
    ),
    total_comments=Sum('number_of_comments'),
)

 

 

The lookup shortcut pk

# 똑같은 값 가져오기
Blog.objects.get(id__exact=14)
Blog.objects.get(id=14)
Blog.objects.get(pk=14)

# 값 중에 하나 찾기
Blog.objects.filter(pk__in=[1, 4, 7])
Blog.objects.filter(pk__gt=14)

# Join과 결합한 사용
Entry.objects.filter(blog__id__exact=3)
Entry.objects.filter(blog__id=3)
Entry.objects.filter(blog__pk=3)

 

Escaping percent signs and underscores in LIKE statements

Entry.objects.filter(headline__contains='%')

 

Caching and QuerySets

효율적인 코드를 작성하기 위해 중요한 부분

QuerySet은 cache를 이용해서 데이터베이스 접근을 최소화한다.

 

- 1) 처음 QuerySet을 생성했을 때

=> cache 비어있음

- 2) QuerySet이 evaluated 되면, query result를 cache에 저장함

- 3) 다음 evaluation에서 QuerySet이 cache 결과를 사용함

 

cache를 잘 사용한 사례

queryset = Entry.objects.all()
print([p.headline for p in queryset]) # Evaluate
print([p.pub_date for p in queryset]) # Reuse

 

QuerySet이 cache하지 않는 경우

queryset = Entry.objects.all()
[entry for entry in queryset]
print(queryset[5])
print(queryset[5])

 

Asynchronous queries

@이미지 재출처 Django로 비동기 작업이 필요할 땐 Celery['redis'] (velog.io)

동기는 한개씩 직렬 처리를 하는 것이고, 

비동기는 쓰레드, 다중으로 작업을 한번에 처리할 수 있는 것(병렬)이다. 

동기와 비동기의 개념과 차이 (tistory.com)

 

원래 장고에서는 asynchronous query를 지원해주지 않았으나, 장고 4.1부터 asynchronous iteration을 사용할 수 있게 되었다. 

 

Deleting objects

delete() 오브젝트를 삭제하고,

삭제한 오브젝트의 수와 타입과 삭제한 타입의 수를 딕셔너리로 리턴함

>>> e.delete()
(1, {'blog.Entry':1})

 

여러 개를 동시에 삭제하는 것도 가능하다. 

>>> Entry.objects.filter(pub_date__year=2005).delete()
(5, {'webapp.Entry':5})

Delete method는 안전을 위해 Manager에서 직접 접근할 수 없다. 

즉, Model.delete() 같은 형태로 사용하는 것은 불가능하다. 

 

Copying model instances

모든 필드값이 복사된 새 인스턴스를 만들기

blog = Blog(name='My blog', tagline='Blogging is easy')
blog.save()

blog.pk = None
blog._state.adding = True
blog.save()

 

Updating multiple objects at once

Entry.objects.filter(pub_date__year=2007).update(headline='Everything is the same')

update() 메소드를 이용해서 여러 오브젝트를 바꿀 수 있다.

단, non-relation field와 ForeignKey에만 사용 가능하다.

ForeignKey update() 사용법은 다음과 같다. 

b = Blog.objects.get(pk=1)
Entry.objects.update(blog=b)

update()는 save() 함수를 호출할 필요가 없다. 

 

Related objects

ForeignKey, OneToOneField, ManyToManyField가 related object에 해당한다. Blog 모델에 User가 ForeignKey로 정의되었다고 하자. Django는 User에서 Blog에 접근할 수 있는 다른 측면의 API 접근자를 만들었다. 

  • OneToOneField
b = Blog.objects.get(id=1)
b.entry_set.all()

b.entry_set.filter(headline__contains='Lennon')
b.entry_set.count()

Blog 모델에 Entry가 ForeignKey로 정의되어있을 때

Entry object를 가져와서 필터릴할 수 있다. 

 

  • ManyToManyField

OneToOneField와 거의 유사하지만, reverse 이름에서 fieldname_set으로 사용한다. 

e = Entry.objects.get(id=3)
e.authors.all()
e.authors.count()
e.authors.filter(name__contains='John')

a = Author.objects.get(id=5)
a.entry_set.all()

Entry 모델에서 authors를 manyToManyField로 정의한 상황이다. 

이때 Author에서 Entry에 접근할 때에는 entry_set과 같은 형태로 접근해주어야 한다. 

 

 

Falling back to raw SQL

for p in Person.objects.raw('SELECT * FROM myapp_person'):
	print(p)

Performing raw SQL queries | Django documentation | Django (djangoproject.com)