백엔드 Back-end/장고 Django

Q. 장고Django에서 외래키ForeignKey 필드의 id를 얻을 때, field.id를 쓰지 말고 field_id를 써야 하는 이유는?

Tap to restart 2022. 11. 5. 21:00

A. 쓸데 없이 데이터베이스에 Select 쿼리를 날리기 때문이다. 

 

간단히 다음과 같은 장고 모델이 있다고 하자. 

전체 코드는 DRF_CRUD 예제에서 확인할 수 있다.

from django.db import models


class Category(models.Model):
    name = models.CharField(max_length=200)
    created_at = models.DateTimeField(auto_now_add=True)
    updated_at = models.DateTimeField(auto_now=True)


class Beverage(models.Model):
    name = models.CharField(max_length=200)
    category = models.ForeignKey(Category, on_delete=models.CASCADE)
    price = models.IntegerField()
    is_available = models.BooleanField(default=False)
    created_at = models.DateTimeField(auto_now_add=True)
    updated_at = models.DateTimeField(auto_now=True)

Beverage 모델의 category 필드는 외래키가 걸려 있다. bevarage.category.name 이런 식으로 해당 beverage에 연결된 category의 필드에 접근이 가능하다. 데이터를 한 두개 입력하자.

 

로그 설정에 아래처럼 추가하자. 그래야 데이터베이스 쿼리문을 확인할 수 있다. 참고: Django Logging

LOGGING = {
    'version': 1,
    'handlers': {
        'console': {
            'level': 'DEBUG',
            'class': 'logging.StreamHandler',
        }
    },
    'loggers': {
        'django.db.backends': {
            'level': 'DEBUG',
            'handlers': ['console'],
        }
    }
}

 

확인하는 방법

장고 셸에 들어가자.

아래 명령어를 입력하면 장고 셸로 들어갈 수 있다. 

$ python manage.py shell

아래 코드를 장고 셸에서 실행하자.

from app.models import Beverage
beverage_qs = Beverage.objects.all()
for beverage in beverage_qs:
    print(beverage.category_id)

for beverage in beverage_qs:
    print(beverage.category.id)

그럼 아래와 같은 결과를 얻을 수 있다.

>>> from app.models import Beverage
>>> beverage_qs = Beverage.objects.all()
>>> for beverage in beverage_qs:
...     print(beverage.category_id)
... 
(0.001) 
                SELECT VERSION(),
                       @@sql_mode,
                       @@default_storage_engine,
                       @@sql_auto_is_null,
                       @@lower_case_table_names,
                       CONVERT_TZ('2001-01-01 01:00:00', 'UTC', 'UTC') IS NOT NULL
            ; args=None
(0.001) SET SESSION TRANSACTION ISOLATION LEVEL READ COMMITTED; args=None
(0.001) SELECT `app_beverage`.`id`, `app_beverage`.`name`, `app_beverage`.`category_id`, `app_beverage`.`price`, `app_beverage`.`is_available`, `app_beverage`.`created_at`, `app_beverage`.`updated_at` FROM `app_beverage`; args=()
1
1
>>> for beverage in beverage_qs:
...     print(beverage.category.id)
... 
(0.001) SELECT `app_category`.`id`, `app_category`.`name`, `app_category`.`created_at`, `app_category`.`updated_at` FROM `app_category` WHERE `app_category`.`id` = 1 LIMIT 21; args=(1,)
1
(0.001) SELECT `app_category`.`id`, `app_category`.`name`, `app_category`.`created_at`, `app_category`.`updated_at` FROM `app_category` WHERE `app_category`.`id` = 1 LIMIT 21; args=(1,)
1

category.id로 할 때마다 SELECT가 호출되는 것을 확인할 수 있다.

 

아래처럼 확인해도 알 수 있다.

from app.models import Beverage
beverage = Beverage.objects.get(pk=1)
print(beverage.category.id)

 

실제 해보면 아래처럼 된다. SELECT 쿼리를 추가로 날려서 category의 id를 얻는 것을 확인할 수 있다.

>>> from app.models import Beverage
>>> beverage = Beverage.objects.get(pk=1)
(0.001) 
                SELECT VERSION(),
                       @@sql_mode,
                       @@default_storage_engine,
                       @@sql_auto_is_null,
                       @@lower_case_table_names,
                       CONVERT_TZ('2001-01-01 01:00:00', 'UTC', 'UTC') IS NOT NULL
            ; args=None
(0.000) SET SESSION TRANSACTION ISOLATION LEVEL READ COMMITTED; args=None
(0.001) SELECT `app_beverage`.`id`, `app_beverage`.`name`, `app_beverage`.`category_id`, `app_beverage`.`price`, `app_beverage`.`is_available`, `app_beverage`.`created_at`, `app_beverage`.`updated_at` FROM `app_beverage` WHERE `app_beverage`.`id` = 1 LIMIT 21; args=(1,)
>>> print(beverage.category.id)
(0.001) SELECT `app_category`.`id`, `app_category`.`name`, `app_category`.`created_at`, `app_category`.`updated_at` FROM `app_category` WHERE `app_category`.`id` = 1 LIMIT 21; args=(1,)
1

 

이번에는 category_id로 해보자.

from app.models import Beverage
beverage = Beverage.objects.get(pk=1)
print(beverage.category_id)
print(beverage.__dict__)

아래와 같은 결과를 얻을 수 있다.

>>> from app.models import Beverage
>>> beverage = Beverage.objects.get(pk=1)
(0.001) 
                SELECT VERSION(),
                       @@sql_mode,
                       @@default_storage_engine,
                       @@sql_auto_is_null,
                       @@lower_case_table_names,
                       CONVERT_TZ('2001-01-01 01:00:00', 'UTC', 'UTC') IS NOT NULL
            ; args=None
(0.001) SET SESSION TRANSACTION ISOLATION LEVEL READ COMMITTED; args=None
(0.001) SELECT `app_beverage`.`id`, `app_beverage`.`name`, `app_beverage`.`category_id`, `app_beverage`.`price`, `app_beverage`.`is_available`, `app_beverage`.`created_at`, `app_beverage`.`updated_at` FROM `app_beverage` WHERE `app_beverage`.`id` = 1 LIMIT 21; args=(1,)
>>> print(beverage.category_id)
1
>>> print(beverage.__dict__)
{'_state': <django.db.models.base.ModelState object at 0x7f804802a9a0>, 'id': 1, 'name': 'Americano', 'category_id': 1, 'price': 3500, 'is_available': True, 'created_at': datetime.datetime(2022, 10, 25, 14, 46, 13, 74719, tzinfo=<UTC>), 'updated_at': datetime.datetime(2022, 10, 25, 14, 46, 13, 74742, tzinfo=<UTC>)}

SELECT 쿼리가 없다. 그 이유는 beverage 객체에 beverage_id 속성이 이미 있기 때문이다. ForeignKey의 경우에는 다른 필드와 다르게 속성이 field_id로 그래서 category_id로 된 것을 확인할 수 있다.

 

결론

ForeignKey 필드는 무조건 field_id를 쓰자. field.id를 쓰면 절대 안 된다. 정말 쓸데 없이 SELECT 쿼리만 한번 더 날리기 때문이다. 

만약 위 예에서 for문을 도는 횟수가 1000번이라면 category.id 때문에 추가로 데이터베이스에 SELECT 쿼리가 1000번 날라가게 되니까.