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

이현준·2022년 4월 20일
0

Django

목록 보기
1/4
post-thumbnail

https://django-orm-cookbook-ko.readthedocs.io/en/latest/query.html

Django ORM Cookbook을 기반으로 직접 실습해보며 정리하는 글
실습용 모델은 (https://github.com/8percent/django-orm-cookbook-ko) 참고!

1. ORM이 실행하는 실제 SQL문 확인

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)도 가능

  • years_ago 필드가 5 이상인 값만 가져오기
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등 사용 가능

2. OR 연산으로 일부 조건 중 하나라도 만족하는 항목 구하기

사용자 중 이름이 '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 객체를 쓰는게 더 편하고 가독성이 좋게 느껴진다.

3. AND 연산으로 여러 조건을 만족하는 항목 구하기

사용자 중 이름이 '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')와 같이 & 연산자와 함께 사용

4. NOT 연산으로 조건 부정하기

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

5. queryset 합하기

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'

6. 필요한 열만 조회하기

이름이 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' 필드도 가져옴

7. 서브쿼리 사용하기

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"

8. 필드 값 비교하여 항목 선책

F 객체 사용하여 필드 끼리 비교('gt','lt' 등의 lookup 적용 가능)

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

9. FileField에 들어있지 않은 행 구하기

FileField, ImageField는 파일과 이미지 경로를 저장하는데, DB에서는 CharField와 동일한 문자열로 저장

from django.db.models import Q

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

10. JOIN IN Django ORM

# 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의 입장에서 사용(역방향 참조)

11. N번째 값 구하기

첫번째 항목은 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

12. 열에서 동일한 항목 구하기

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

13. 고유한 필드 값을 가진 항목 구하기

이름이 다른 사용자와 겹치지 않는 사용자 찾기

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를 이용해서 유일한 값을 구할 수 있음

14. Q 객체 활용

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

15. 집계함수

aggregateAvg, 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}

16. 무작위 뽑기

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
profile
기록, 정리하는 습관 만들기

0개의 댓글