Django queryset 정참조, 역참조, related_names, select_related, prefetch_related

정현우·2022년 3월 17일
6

Django Basic to Advanced

목록 보기
6/40
post-thumbnail

Django queryset

장고 ORM에서 query의 결과값으로 queryset 의 instance로 준다. 여기서 살펴봐야 할 "정참조, 역참조"를 기반으로 select_related, prefetch_related, related_names 에 대해서 자세하게 살펴보자.

Django 테스트

  • 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, buyer를 아래와 같이 바꿔주자! -> related_name를 추가했다. 이유는 계속 살펴보자.
    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

    idnick_name
    1nuung
    2test_only
  • user_profile ( Many-to-Many 라 자동생성되는 테이블 )

    iduser_idprofile_id
    111
    222
    223
    332
  • User

    idnameuser_type
    1test1B
    2test2B
    3test3S
  • Product

    namedescseller_idbuyer_id
    product_test1product_test1 description31
    product_test2product_test2 description32
  • 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
  • FK에 related_names를 무조건 지정할 필요는 없다. django의 syntax를 보면 _set 을 붙이는 경우가 더 많았고, 그에 따라 일종의 코드 규칙처럼 정해진 느낌도 있다. 하지만 위 테이블 예시와 같이 🔥특정 모델에서 서로 다른 두 컬럼(속성, 필드)이 같은 테이블(모델)를 참조하는 경우🔥 는 필수로 써야한다.

  • 우린 위에서 user부터 product를 가져오는 역참조를 했다. 하지만 user로 부터 product를 가져오면서 product만 가져오는게 아니라 user를 같이 가져오고 싶다면?
# 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()
  • prefetch_related 함수를 사용하지 않을 시, 매번 User 모델(클래스)에서 Profile 모델(클래스) many to many로 참조할떄마다 두번 씩 쿼리를 수행한다는 것을 알 수 있음
  • prefetch_related 함수를 사용하면 N:N(many-to-many) 관계를 대상으로 참조할때 Data를 참조 대상까지 Cache에 저장하기 때문에 해당 함수를 사용하길 권장한다.
profile
도메인 중심의 개발, 깊이의 가치를 이해하고 “문제 해결” 에 몰두하는 개발자가 되고싶습니다. 그러기 위해 항상 새로운 것에 도전하고 노력하는 개발자가 되고 싶습니다!

2개의 댓글

comment-user-thumbnail
2023년 2월 3일

이렇게 좋은 콘텐츠를 만들어 주셔서 감사합니다.^^

1개의 답글