장고 ORM에서 query의 결과값으로 queryset 의 instance로 준다. 여기서 살펴봐야 할 "정참조, 역참조"를 기반으로 select_related, prefetch_related, related_names 에 대해서 자세하게 살펴보자.
queryset은 django ORM에서 사용하는 자료구조 형으로 DB에서 전달 받은 결과값들을 ORM에 맞는 object (class - instance)의 목록이 된다. django ORM 강력함은 모두 이 queryset으로 부터 출발한다.
여러기지 상황을 살펴보기 위해 사전에 테스트 전용 모델을 몇가지 만들어 보자!, Profile, User, Product 2가지를 사용하자!
class Profile(models.Model):
nick_name = models.CharField(max_length=20)
created_at = models.DateTimeField(auto_now_add=True)
class User(models.Model):
USER_TYPE = (
('S', 'Seller'),
('B', 'Buyer'),
)
name = models.CharField(max_length=20)
user_type = models.CharField(max_length=2, choices=USER_TYPE, default='B')
profile = models.ManyToManyField(Profile)
class Product(models.Model):
name = models.CharField(max_length=20)
description = models.CharField(max_length=200)
seller = models.ForeignKey(User, on_delete=models.CASCADE)
buyer = models.ForeignKey(User, on_delete=models.CASCADE)
seller = models.ForeignKey(User, on_delete=models.CASCADE, related_name="product_seller")
buyer = models.ForeignKey(User, on_delete=models.CASCADE, related_name="product_buyer")
정참조와 역참조는 "참조 기준이 되는 A와 참조하는 B의 관계"가 어떻게 되는지 표현하는 것이다.
위 테스트 모델에서는 Product는 User를 FK로 가지고 있다. Product는 User를 정참조로 User를 접근할 수 있다.
User는 Product의 FK가 없지만, Product에 의해서 참조 당하고 있다. User를 통해 Product를 가져오는 역참조를 할 수 있다.
Product 모델에서 User를 참조하는 seller와 buyer 컬럼이 2가지다.
User Model을 기준으로, user를 통해 Product를 가져오고 싶다.(역참조) 즉, 1번 유저를 통해 product를 가져오고 싶은데, 이 사람이 seller인지 buyer인지 user 기준으로는 도저히 알 길이 없는 것이다. (물론 위 모델에서는 편의를 위해 USER_TYPE으로 셀러와 유저로만 구분해 두었다.)
그래서 Product에서 User를 다른 필드이름으로 참조하려면 seller와 buyer의 related_names를 꼭 정해줘야 한다. 아래 User, Product tabel의 데이터가 있다고 생각하자.
Profile
id | nick_name |
---|---|
1 | nuung |
2 | test_only |
user_profile ( Many-to-Many 라 자동생성되는 테이블 )
id | user_id | profile_id |
---|---|---|
1 | 1 | 1 |
2 | 2 | 2 |
2 | 2 | 3 |
3 | 3 | 2 |
User
id | name | user_type |
---|---|---|
1 | test1 | B |
2 | test2 | B |
3 | test3 | S |
Product
name | desc | seller_id | buyer_id |
---|---|---|---|
product_test1 | product_test1 description | 3 | 1 |
product_test2 | product_test2 description | 3 | 2 |
3번 유저는 seller다. 3번 유저를 통해 판매중인 product를 다 가져오고 싶다. 그리고 우린 Product에서 seller의 related_names "product_seller" 라고 정의했다.
기본적으로 역참조는 [model classname]_set 이라는 속성을 사용해 접근한다. 3번 셀러가 판매중은 product를 모두 가져오는 코드는 아래와 같다.
user = User.objects.get(id=3)
products = user.product_set.all()
하지만 AttributeError: 'User' object has no attribute 'product_set'
라는 에러를 뱉는다. 이미 우리가 related_names를 정의 했기 때문이다.
🔥그래서 user.product_seller.all()
로 가져와야 한다.🔥
SQL 쿼리로 보면 아래와 같다. 컬럼은 * 로 대체 했다. where 조건에 user id가 붙는 것이다.
SELECT * FROM `product` WHERE `product`.`seller_id` = 3
_set
을 붙이는 경우가 더 많았고, 그에 따라 일종의 코드 규칙처럼 정해진 느낌도 있다. 하지만 위 테이블 예시와 같이 🔥특정 모델에서 서로 다른 두 컬럼(속성, 필드)이 같은 테이블(모델)를 참조하는 경우🔥 는 필수로 써야한다. # shell 에서 러닝
sell_products = user.product_seller.all()
>>> sell_products
<QuerySet [<Product: Product object (1)>, <Product: Product object (2)>]>
>>> sell_products[0].seller
<User: User object (3)>
>>> sell_products[0].buyer
<User: User object (1)>
위와 같은 과정을 거친다. 사실 django는 sell_products[0].seller
에서 SELECT * FROM User WHERE id=3;
이라는 쿼리를 실행시킨다. (더 정확하겐 캐시에 없으면 DB에 다시 접근해서 가져온다 - 즉 user 3번 정보가 캐시에 없으면 쿼리를 날릴 수 밖에 없다.)
사실 django에서, ORM이 사용하기 편하니까 아무 생각없이 queryset남발하게 되면서 굉장히 많은 query가 날라가는 경우가 많다.
그래서 "최적화적 관점" 에서 select_related, prefetch_related를 사용한다. JOIN으로 product를 가져올때 user까지 다 가져오는 것이다.
객체가 역참조하는 single object(1:1 or N:1)이거나, 또는 정참조 FK 일 때 사용한다. DB 단에서 INNER JOIN 으로 쿼리를 수행한다.
user로 부터 판매하는 product를 가져오고, 구매자의 정보를 한꺼번에 가져와 보자.
buyer_from_seller = user.product_seller.all().select_related("buyer")
>>> buyer_from_seller
<QuerySet [<Product: Product object (1)>, <Product: Product object (2)>]>
# 직접 아래 코드를 통해 어떤 query를 날리는지 보자
>>> print(buyer_from_seller.query)
SELECT `product`.`id`, `product`.`name`, `product`.`description`, `product`.`seller_id`, `product`.`buyer_id`, T3.`id`, T3.`name`, T3.`user_type`
FROM `product` INNER JOIN `user` T3
ON (`product`.`buyer_id` = T3.`id`)
WHERE `product`.`seller_id` = 3
selected_related() 의 파라미터는 여러가지 값이 들어갈 수 있다. 기본적으로 참조하려는 모델(class)의 이름을 소문자로 쓰고 싱글 또는 더블쿼테이션으로 감싸면 된다. 즉 INNER JOIN하는 테이블이 여러개이고 컬럼이 여러개면 파라미터로 소문자 형태로 넘겨주면 된다.
하지만 related_names를 지정한 경우 related_names을 사용해 줘야 한다. 그래서 buyer를 파라미터로 넘겨준 것이다.
객체가 정참조 multiple objects(N:N or 1:N)이거나, 또는 역참조 FK일때 사용한다. 즉 모든 relationships에서 사용이 가능하다. selected_related와 달리, prefetch_related는 SQL의 JOIN을 실행하지 않고, python에서 joining을 실행한다.
즉 Selected_related는 하나의 Query로 related Objects들을 불러오지만, Prefetch_related는 main query가 실행이 된 후 별도의 query가 실행이 된다! -> 가급적 selected_related를 사용하는 것이 쿼리 사용량, 리소스 소모를 줄일 수 있다.
아래 2가지의 차이점일 살펴보자.
# X prefetch_related
User.objects.get(id=2).profile.all()
# 위는 사실 아래와 같이 동작한다.
a = User.objects.get(id=2)
b = a.profile.all()
# O prefetch_related
User.objects.prefetch_related('profile').get(id=2).profile.all()
이렇게 좋은 콘텐츠를 만들어 주셔서 감사합니다.^^