수학과의 좌충우돌 프로그래밍

[Django] ORM Cookbook, 정보를 조회하고 필요한 항목을 선별하는 방법(1) 본문

웹프로그래밍/Django

[Django] ORM Cookbook, 정보를 조회하고 필요한 항목을 선별하는 방법(1)

ssung.k 2020. 8. 27. 19:30

해당 포스팅은 Django ORM CookBook 을 공부하며 새로 알게된 사실이나 더 나아가 추가적으로 같이 알면 좋을 내용을 정리하고 있습니다.

 

1. 장고 ORM이 실행하는 실제 SQL 질의문 확인

ORM에 대응되는 SQL 질의문을 확인하기 위해서는 다음과 같이 확인할 수 있습니다.

queryset = Post.objects.all()
str(queryset.query)
'''
'SELECT `post_post`.`id`, `post_post`.`user_id`, `post_post`.`title`, `post_post`.`content`, `post_post`.`kind`, `post_post`.`image`, `post_post`.`created_at`, `post_post`.`updated_at` 
FROM `post_post`'
'''

 

필요한 순간에 쿼리를 확인해볼 수 있지만 개인적으로 logging을 통해서 모든 쿼리를 직접 확인하여 있어 위와 같은 방법은 많이 사용하지 않습니다.

Django-Logging-설정-및-SQL-쿼리-확인

 

2. OR 연산

쿼리를 날리다보면 OR 연산이 필요한 경우가 많이 있습니다.

ORM에서는 이를 두 가지 방법으로 구현이 가능합니다.

이름이 첫번째가 R로 시작하거나 D로 끝나는 유저들을 조회해봅시다.

  • queryset1 | queryset2

    queryset = User.objects.filter(
            first_name__startswith='R'
        ) | User.objects.filter(
        last_name__startswith='D'
    )
    

     

  • Q 객체 이용하기

    from django.db.models import Q
    queryset = User.objects.filter(Q(first_name__startswith='R')|Q(last_name__startswith='D'))
    

     

이와 매칭되는 SQL 질의문은 다음과 같습니다.

'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"
WHERE ("auth_user"."first_name"::text LIKE R% OR "auth_user"."last_name"::text LIKE D%)'

 

3. AND 연산

OR 연산과 마찬가지로 AND 연산도 많이 사용됩니다.

하지만 AND 연산은 filter에 조건을 여러 개 넘겨줌으로서 쉽게 구현할 수 있습니다.

  • filter(condition1, condition2)
  • queryset1 & queryset2
  • Q 객체 이용하기

 

 

4. NOT 연산

NOT 연산도 아래와 같이 두 가지 방법으로 구현이 가능합니다.

id < 5 라는 조건을 만족하지 않는 모든 사용자를 구해봅시다.

  • exclude(conditions)

    queryset = User.objects.exclude(id__lt=5)
    

     

  • filter(~Q(conditions))

    from django.db.models import Q
    queryset = User.objects.filter(~Q(id__lt=5))
    

     

 

5. UNION 연산

SQL에서는 여러 결과를 UNION으로 묶어 줄 수 있습니다.

장고 ORM에서도 union 메서드를 이용해 쿼리셋을 합칠 수 있습니다.

단 합치기 위해서는 각 쿼리셋의 필드의 수와 데이터 유형이 일치해야합니다.

q1 = User.objects.filter(id__gte=5)
q2 = User.objects.filter(id__lte=9)
q1.union(q2)

 

다른 모델이더라도 동일한 필드만 가져와 union을 해줄 수 있습니다.

post = Post.objects.all().values_list('title', 'content')
comment = Comment.objects.all().values_list('title', 'content')
post.union(comment)
SELECT `post_post`.`title`,  `post_post`.`content`
FROM `post_post` 
UNION 
SELECT `post_comment`.`title`  ,`post_comment`.`content` 
FROM `post_comment`

 

 

6. SELECT

ORM을 통해 일반적으로 쿼리를 날리면 모든 필드에 대해서 데이터를 가져옵니다.

하지만 필드가 전부 필요하지 않은 경우에 이는 불필요한 연산입니다.

ORM에서도 특정 필드만을 읽어오는 기능을 제공해줍니다.

  • values

    post = Post.objects.all().values('title', 'content')
    # <QuerySet [{'title': '첫번째 Post', 'content': '첫번째 내용입니다.'}, {'title': '두번째 Post', 'content': '두번째 내용입니다.'}]>
    

    반환되는 결과는 딕셔너리들의 배열입니다.

  • values_list

    post = Post.objects.all().values_list('title', 'content')
    # <QuerySet [('첫번째 Post', '첫번째 내용입니다.'), ('두번째 Post', '두번째 내용입니다.')]>
    

    반환되는 결과는 튜플의 배열입니다.

  • only

    post = Post.objects.all().only('title', 'content')
    # <QuerySet [<Post: arkss의 첫번째 Post>, <Post: arkss의 두번째 Post>]>
    

    반환되는 결과는 모델 객체의 배열입니다.

    위와 다른 점은 SQL에서 id도 함께 가져옵니다.

    SELECT `post_post`.`id`, `post_post`.`title`, `post_post`.`content` FROM `post_post`
    

 

 

7. 서브쿼리

ORM에서 서브쿼리를 사용 할 수 있습니다.

서브쿼리를 위해 아래와 같은 모델을 정의하였습니다.

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 중에 benevolence_factor가 가장 높은 Hero를 구해봅시다.

우선 서브쿼리를 먼저 정의합니다.

benevolence_factor 에 대해 내림차순으로 정렬하여 높은 값이 가장 상위에 위치하도록 하고 OuterRef 를 통해 외부 테이블의 pk와 비교하여 같을 때만 반환할 수 있도록 합니다.

hero_qs = Hero.objects.filter(
    category=OuterRef("pk")
).order_by("-benevolence_factor")

 

이제 본 쿼리를 정의합니다.

영웅의 name 만 필요하므로 values로 가져오고 [:1] 를 통해서 제일 상위의 영웅만 조인합니다.

Category.objects.all().annotate(
    most_benevolent_hero=Subquery(
        hero_qs.values('name')[:1]
    )
)

 

최종적으로 다음과 같습니다.

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. 필드값 비교하여 SELECT

상황에 따라서 필드 값을 비교하여 쿼리를 날릴 수 있습니다.

(맨날 날린다고만 표현해서 블로그에 쓰기에 적합한 용어는 아니라고 생각하지만 적절한 단어를 못 찾겠어요..)

이 때는 F 객체를 사용합니다.

 

다음과 같은 상황을 고려해봅시다.

Post 모델 중 생성된 시각과 최근 수정 시각이 같은 데이터를 가져오기 위해서는 다음과 구할 수 있습니다.

from django.db.models import F

post = Post.objects.filter(created_at=F("updated_at"))

 

또한 F 객체에 __gt, __lt 등의 룩업을 적용하는 것 또한 가능합니다.

 

 

9. FileField에 파일이 들어있지 않은 행

FileFieldImageField 모두 해당 파일의 경로를 문자열로 저장합니다.

따라서 파일이 없는 행을 아래와 같이 구할 수 있습니다.

from django.db.models import Q

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

 

Comments