Cracking Django ORM

서재환·2022년 9월 20일
0

Django

목록 보기
21/40

ORM

Queryset vs Model Instance

queryset

데이터베이스에서 전달 받은 객체 목록이다. 데이터 타입은 파이썬에서 제공되는 기본형이 아니고 복잡한 자료구조로 전달된다. 그래서 queryset이라고 명명하게 되었다. 후에 해당 자료구조는 JSON이나 dictionary 형태로 가공되어 제공되는 것이 일반적이다.

queryset은 한개의 쿼리와 N개의 추가 쿼리셋으로 구성되어있다.

Model Instance

Model이 DB에서의 테이블이라면 Model Instance테이블에서 셀렉트 한 하나의 레코드를 일컫는다.

참고자료

Lazy Loading

queryset의 특징이 있다. queryset은 불필요한 query(select문)를 날리지 않는다는 점이다. 예를 들어 아래와 같은 코드가 있을 때, 1번 시점은 아직 query를 날리기 전이다. users가 의미하는 것은 모델(테이블)에서 전달받은 객체 목록이다. 아직 해당 테이블에 query를 날리기 전이다.

2번 시점에서 ORM이 query를 날린다. 3번 지점에서 또 한번 query를 날리게 된다. 이렇게 ORM이 쿼리를 2번 날리는 이유는 2번 시점에서 데이터베이스에 히트를 칠 때 가지고 온 값이 하나의 User 모델의 인스턴스만 가지고 오기 때문이다(ORM의 lazyloading).

ex1)

users = User.objects.all() #1
User = users[0] 		   #2
user_list = list(users)    #3

자 이번에는 한문장 한문장은 위와 같지만 순서가 다르다. 하지만 이 차이점으로 인해 총 한번의 쿼리를 수행한다. 왜냐하면 2번 시점에서 디비에 접근할 때 가지고 온 값이 3번을 포함하고 있어 다시 쿼리를 날리지 않아도 되기 때문이다.

ex2)

users = User.objects.all() #1
user_list = list(users)    #2
User = users[0] 		   #3

ORM의 Lazy Loading이라는 특징 그리고 캐싱이라는 특징을 잘 숙지하고 ORM을 작성 할 필요성이 있다.

Lazy Loading으로 인한 N+1 문제

User 테이블에서 Post(게시물)에 대한 객체를 왜래키로 들고 있다고 했을 때 아래와 같이 코드를 작성하게 될 경우 총 1+N의 쿼리를 발생시키게 된다. 왜냐하면 ORM은 필요 한 만큼만 query를 날리기 때문이다.

users = User.objects.all()

for user in users:
	user.userinfo
    
equals to
#1
select * from User;

#2
select * from UserInfo
where UserInfo.id = 1;

#3
select * from UserInfo
where UserInfo.id = 2;

#4
select * from UserInfo
where UserInfo.id = 3;

		...
		...
		...

이에 N+1문제를 해결하기 위해서 Django ORM 에선prefetch_related('UserInfo')를 통해 필요한 항목을 한번의 쿼리로 한 번에 가지고 올 수 있다. #1 == #2

#1
User.objects.prefetch_related('UserInfo')
#2
select * from UserInfo where UserInfo.id in [1,2,3 ... n]

class Queryset

Eager Loading

ORM에서 한 번에 많은 데이터를 가지고 오고 싶을 때 이를 Eager Loading 이라고 합니다. Django ORM에선 prefetch_related select_related 메서드를 제공한다.

_result_cache

_result_cache는 Django에서 구현된 Queryset 객체 내에 있는 멤버 변수이다. Queryset 재호출시 해당 변수에 저장된 값이 없으면 query를 수행한다.

join을 통해서 데이터를 로딩한다.
역방향참조모델은 select_related에 줄 수 없다.

select_related('정방향_참조_필드')

해당 메서드의 경우 역방향참조필드를 줄 수 없다.

예시를 통해 살펴보자. 아래 Django ORM은 아래 SQL문과 그 의미가 같다. OrderProduct라는 테이블이 두개의 테이블을 정방향 참조하고 있다고 가정했을 때 select_related 사용이 가능하다.

SQL문을 보면 알 수 있겠지만 OrderProduct 테이블과 공통된 필드가 있는 부분을 RelatedOrder 테이블과 RelatedProduct 테이블에서 가지고 온 후 그 중 OrderProduct의 아이디가 4인 값을 가지고 와서 전체를 보여주는 SQL 구문이다.

#예시 
OrderProduct.select_related('related_order','related_product')
.filter(related_order=4)

equals to
select * from OrderProduct OP
	inner join RelatedOrder on (OP.related_order_id = RelatedOrder.id)
    inner join RelatedProduct on (OP.related_order_id = RelatedProduct.id)
    where OP.related_order_id=4;

Inner Outer 여부는
QuerySet조건절 변경에 따라 JOIN 옵션이 변할수있다
하지만 일반적으로 ForiegnKey(null=True) 이면 OUTER JOIN

select * from model m
	inner OR outer join '정방향참조필드' r on (m.r_id = r.id)
    where '조건절';

추가쿼리를 사용해 데이터를 가지고 온다.
역방향참조필드 정방향참조필드 모두 사용 가능하나 역방향참조필드를 사용 할 것을 권장함.

쿼리셋은 한개의 쿼리와 N개의 추가쿼리로 이루어져있다. 무슨 말인가 하면 prefetch_related 라는 ORM을 사용했을 때 prefetch의 대상이 되는 모델에 대해서 쿼리를 한번 날리고 prefetch_related('A') 처럼 그 안에 해당되는 모델(A)에 대해서 추가쿼리를 수행하기 때문에 한개의 쿼리와 N개의 추가쿼리로 이루어졌다는 말이 등장하게 된다.

#1
prefetch_related('역방향_참조_필드')

prefetch_related('A', 'B', 'C')

>>> select * from A where id in = [...]
>>> select * from B where id in = [...]
>>> select * from C where id in = [...]
#2
AModel.objects.prefetch_related("b_model_set", "c_models")

equals to

AModel.objects.prefetch_related(
	Prefetch(to_attr="b_model_set"), queryset=BModel.objects.all())
    Prefetch(to_attr="c_models"), CModel.objects.all())
)

equals to

select * from A;
select * from B where B.id in [...];
select * from C where C.id in [...];

위에서 `queryset=BModel.objects.all()`이라는 부문은
queryset=BModel.objects.filter(is_delete=False) 와 같이 customizing 할 수 있다.

equals to

select * from A;
select * from B where B.id in [...] and B.is_delete=False;
select * from C where C.id in [...];
Model.objects.filter(조건절).select_related('정방향_참조_필드')
	.prefetch_related('역방향_참조_필드')

select * from Model m
	(inner or left outer) join '정방향_참조_필드' r on m.r_id = r.id where = '조건절';
    
select * from '역방향_참조_필드' where id in ('첫번 째 결과 id 리스트');
Model.objects.filter(조건절).select_related('정방향_참조_필드')
	.prefetch_related('역방향_참조_필드를 가지고 있는 참조필드')

select * from Model m
	(inner or left outer) join '정방향_참조_필드' r on m.r_id = r.id where = '조건절';
    
select * from '참조필드' a
	(inner or left outer) join '참조필드가 참조하고 있는 역뱡향 참조필드' b on (a.b_id = b.id)
where id in ('첫번 째 결과 id 리스트');

filter().prefetch_related(Prefetch(model, queryset=model.select_related(model))

#3
OrderProduct.objects.filter(product_cnt__lt=30, related_order__descrption='*')
	.prefetch_related(
    	Prefetch('related_order', 
        	queryset=Order.objects.select_related('mileage').all() )
        )
    )

*** equals to ***

select * from OrderProduct OP 
	inner join RelatedOrder RO on (OP.related_order_id = RO.id)
	where OP.product_cnt < 30;

select * from RelatedOrder RO
	left outer join Mileage M on (RO.id = M.related_order_id)
	where RO.id in = [위 query id 결과]

참고자료

annotate

annotate 용례

###
User.objects.annotate(first=Substr("first_name", 1, 1),
	last=Substr("last_name", 1, 1)).filter(first=F("last"))

values()

queryset = Post.objects.values()
print(str(queryset.query))

SELECT "blog_post"."id", "blog_post"."author_id", 
	"blog_post"."title", "blog_post"."text",
	"blog_post"."created_date", "blog_post"."published_date"
	FROM "blog_post"

별칭

#1
queryset = Post.objects.annotate(ti=F('title'), te=F('text'))
print(str(queryset.query))

SELECT "blog_post"."id", "blog_post"."author_id",
	"blog_post"."title", "blog_post"."text",
	"blog_post"."created_date", "blog_post"."published_date",
	"blog_post"."title" AS "ti", "blog_post"."text" AS "te" FROM "blog_post"
#2
queryset = Post.objects.annotate(ti=F('title'), te=F('text')).values('ti','title')
print(str(queryset.query))


SELECT "blog_post"."title" AS "ti", "blog_post"."text" AS "te" FROM "blog_post"

queryset 합치기

참고자료

aggregate vs annotate

aggregate

디비 내부 특정 모델에 대한 쿼리셋에 대해 계산을 한다.

annotate

디비 내부 특정 모델에 안의 레코드에 대해 계산한다.
참고자료

0개의 댓글