발단
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
동기는 한개씩 직렬 처리를 하는 것이고,
비동기는 쓰레드, 다중으로 작업을 한번에 처리할 수 있는 것(병렬)이다.
원래 장고에서는 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)
'Computer Science > BackEnd' 카테고리의 다른 글
장고 배포하기(WSGI, Gunicorn, ASGI) | 장고 공식 문서 번역 (0) | 2022.11.25 |
---|---|
Django | SQL 쿼리 로그로 Django QuerySet 이해하기 (0) | 2022.10.29 |
Django for APIs | #6 Permissions (0) | 2022.10.01 |
Django for APIs | #5 blog API (0) | 2022.09.30 |
Django for APIs | #4 Todo API (0) | 2022.09.27 |