https://django-orm-cookbook-ko.readthedocs.io/en/latest/query.html
Django ORM Cookbook을 기반으로 직접 실습해보며 정리하는 글
실습용 모델은 (https://github.com/8percent/django-orm-cookbook-ko) 참고!
queryset = Event.objects.all()
print(queryset.query)
SELECT "events_event"."id", "events_event"."epic_id",
"events_event"."details", "events_event"."years_ago"
FROM "events_event"
all()
메서드로 Event 테이블의 모든 내용을 읽어온다. print 대신 str(queryset.query)
도 가능
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
'__' (더블 언더바) 와 gt,lt,gte,lte등 사용 가능
사용자 중 이름이 'R'로 시작하거나, 'D'로 시작하는 사용자만 찾기
queryset_1 | queryset_2
queryset = User.objects.filter(
first_name__startwith='R'
) |
User.objects.filter(
last_name__startwith='D'
)
# SQL문
str(queryset.query)
'SELECT "auth_user"."id", "auth_user"."password", ...
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
queryset = User.objects.filter(
Q(first_name__startwith='R') | Q(last_name__startwith='D')
)
두 방법 모두 실제 생성되는 SQL문은 동일하지만, 나는 Q 객체를 쓰는게 더 편하고 가독성이 좋게 느껴진다.
사용자 중 이름이 'R'로 시작하고, 'D'로 시작하는 사용자만 찾기
queryset = User.objects.filter(
first_name__startswith='R',
last_name__startswith='D'
)
filter()
메서드는 기본적으로 여러 조건을 결합할 때 AND 방식을 따르기 때문에 그냥 나열하면 구현 가능!
만약 Q
객체를 이용한다면 Q(first_name__startswith='R') & Q(last_name__startswith='D')
와 같이 &
연산자와 함께 사용
id < 5
를 만족하지 않는 모든 사용자 구하기
exdlude()
queryset = User.objects.exclude(id__lt = 5)
str(queryset.query)
SELECT id, username, first_name, last_name, email
FROM auth_user
WHERE NOT id < 5
Q
객체 이용queryset = User.objects.filter(~Q(id__lt=5))
SQL에서 UNION 처럼, django orm에서도 union()
메서드를 사용
만약 합하려는 쿼리셋이 서로 다른 모델에서 가져온 쿼리셋일 경우 포함된 필드와 데이터 유형이 서로 맞아야 가능
queryset_1 - User.objects.filter(id__gte=5)
queryset_2 = User.objects.filter(id__lte=8)
queryset_3 = queryset_1.union(queryset_2)
str(queryset_3.query)
'SELECT "events_event"."id", "events_event"."epic_id", "events_event"."details", "events_event"."years_ago"
FROM "events_event"
WHERE "events_event"."id" >= 00000000000000000000000000000005
UNION SELECT "events_event"."id", "events_event"."epic_id", "events_event"."details", "events_event"."years_ago"
FROM "events_event"
WHERE "events_event"."id" <= 00000000000000000000000000000008'
이름이 R로 시작하는 모든 사용자들의 first_name과 last_name 구하기
queryset = User.objects.filter(
first_name__startswith='R'
).values('first_name', 'last_name')
str(queryset.query)
SELECT "auth_user"."first_name", "auth_user"."last_name"
FROM "auth_user" WHERE "auth_user"."first_name"::text LIKE R%
values()
: 딕셔너리 타입으로 결과 반환values_list()
: 튜플 타입으로 결과 반환only()
메서드는 values()
와 비슷하지만, 'id' 필드도 가져옴
auth_user
모델과 일대일 관계로 설정된 UserParent
모델이 있을 때, UserParent
모델에서 auth_user
의 모든 행 구하기
from django.db.models import Subquery
users = User.objects.all()
UserParent.onjects.filter(user_id__in=Subquery(users.values('id')))
아래와 같은 모델일 때, Hero 중 benevolence_factor가 가장 높은 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_queryset = Hero.objects.filter(
caregory = OuterRef("pk")
).order_by("-benevolence_factor")
order_by("-benevolence_factor")
으로 benevolencefactor를 내림차순으로 정렬OuterRef()
: 외부 테이블의 데이터에 접근, 외부 테이블의 'pk'를 빅해서 같을 때만 반환이후 메인 쿼리 정의
Category.objects.all().annotate(
most_benevolent_hero=Subquery(
hero_queryset.values('name')[:1}
)
)
)
Hero의 name 항목만 가져오고 [:1] 로 최상위에 있는 영웅만 조인
최종 queryset과 SQL문 확인
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"
F
객체 사용하여 필드 끼리 비교('gt','lt' 등의 lookup 적용 가능)
first_name
과 last_name
비교하여 선택
# 이름과 성이 동일한 사용자 검색
User.objects.filter(last_name=F("first_name"))
# 생성 시각과 수정 시각이 같은 게시글 검색
Post.objects.filter(created_at=F("updated_at"))
F
객체는 데이터베이스에 저장되어 있는 값을 메모리에 저장한 후 연산하는 번거로운 과정 없이, django가 만든 SQL 쿼리를 이용해 데이터베이스 레벨에서 바로 연산이 가능
하게 해준다.
추후 F
객체 참고(https://brownbears.tistory.com/367)
FileField
, ImageField
는 파일과 이미지 경로를 저장하는데, DB에서는 CharField
와 동일한 문자열로 저장
from django.db.models import Q
no_files_objects = MyModel.objects.filter(
Q(file='')|Q(file=None)
)
# select_related
Article.objects.select_related('reporter')
# lookup
Article.objects.filter(reporter__username='Lee')
select_releated
, prefetch_realted
는 하나의 QuerySet을 가져올 때, 미리 related objects들 까지 다 불러와서 캐시에 저장해주는 함수
select_releated
: 1:1, 1:N 관계에서 N의 입장에서 사용 가능(정방향 참조)prefetch_related
: 1:N관계에서 1의 입장에서 사용(역방향 참조)첫번째 항목은 first()
, 마지막 항목은 last()
메서드를 사용
N번째 항목은 인덱싱 연산 이용
User.objects.order_by('-created_at')[0]
이 때, Django ORM은 데이터베이스에서 전체 데이터를 불러온 후 인덱싱 하지 않고, LIMIT
,OFFSET
구문으로 필요한 데이터만 읽어옴
# User.objects.order_by('-last_login')[2]
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
LIMIT, OFFSET
LIMIT
: 출력 갯수 제한OFFSET
: 시작 위치 지정ex) 11번째 부터 10개의 POST 가져오기
SELECT *
FROM POST
LIMIT 10
OFFSET 10
first_name
이 서로 동일한 사용자들을 찾아야 할 때, Count
를 구한 뒤 중복 수를 기준으로 골라내면 됨
from django.db.models import Count
queryset = User.objects.values('first_name').annotate(name_count = Count('first_name')).filter(name_count__gt=1)
queryset
<QuerySet [{'first_name': 'John', 'name_count': 3}]>
'''
SELECT `accounts_user`.`first_name`, COUNT(`accounts_user`.`first_name`) AS `name_count`
FROM `accounts_user`
GROUP BY `accounts_user`.`first_name`
HAVING COUNT(`accounts_user`.`first_name`) > 1 ORDER BY NULL
'''
records = User.objects.filter(first_name__in=[item['first_name'] for item in queryset])
'''
SELECT `accounts_user`.`id`, `accounts_user`.`password`, `accounts_user`.`email`, `accounts_user`.`first_name`
FROM `accounts_user`
WHERE `accounts_user`.`first_name` IN ('John')
'''
이름이 다른 사용자와 겹치지 않는 사용자 찾기
unique = 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 unique])
이전과 같은 방법으로 Count
를 이용해서 유일한 값을 구할 수 있음
Q
객체를 &
, |
, ~
등과 함께 사용하면 SQL의 WHERE 절에 해당하는 기능을 활용 가능
ex)OR
, 이름이 R로 시작하거나 성이 D로 시작하는 사용자
from django.db.models import Q
queryset = User.objects.filter(
Q(first_name__startswith='R'
) | Q(last_name__startswith='D')
)
ex2) 이름이 R로 시작하고 성에 Z가 포함되지 않는 사용자
queryset = User.objects.filter(
Q(first_name__startswith='R'
) & ~Q(last_name__startswith='Z')
)
aggregate
와 Avg, Max, Min, 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}
class Category(models.Model):
name = models.CharField(max_length=100)
class Meta:
verbose_name_plural = "Categories"
def __str__(self):
return self.name
위와 같은 모델에서 무작위로 하나를 뽑아야 할 때 가능한 방법
1) order_by
메서드로 항목을 정렬할 때 정렬기준을 무작위로 지정한 후 첫번째 항목 가져오기
def get_random():
return Category.objects.order_by("?").first()
2) 표에서 ID의 최대값을 구하고, 1과 ID최대값 사이의 난수를 생성해서 가져오기
from django.db.models import Max
from entities.models import Category
import random
def get_random2():
# ID의 최대값
max_id = Category.objects.all().aggregate(max_id=Max("id"))['max_id']
# 1과 최대값 사이의 난수 생성
pk = random.randint(1, max_id)
return Category.objects.get(pk=pk)
이 방식은 어떤 항목을 삭제한 적이 있다면, 중간에 비어있는 ID가 있기 때문에 사용할 수가 없기 때문에 while문 활용
from django.db.models import Max
from entities.models import Category
import random
def get_random():
# ID의 최대값
max_id = Category.objects.all().aggregate(max_id=Max("id"))['max_id']
while True:
# 1과 최대값 사이의 난수 생성
pk = random.randint(1, max_id)
return Category.objects.get(pk=pk)
if category:
return category