시작하기 전에


안녕하세요 똑똑한 개발자에서 백엔드 개발을 하고 있는 jujun입니다.

django orm 은 디폴트로 해당 모델의 모든 컬럼을 불러옵니다.

하지만 only 기능을 사용한다면 필요한 항목만 불러올 수 있게 됩니다.

이는 불필요한 데이터를 불러오지 않게하여 컴퓨터 자원을 보다 효율적으로 사용할수 있습니다.

하지만 잘못 사용하면 lazy lading 에 의해 불필요한 쿼리를 발생시킵니다.

자세히 알아보겠습니다.

.

본론


예제코드

아래와 같은 모델이 있습니다.

# api.raw.models

class Party(models.Model):
    country = models.ForeignKey("raw.Country", verbose_name="국가", on_delete=models.SET_NULL ,null=True)
    owner = models.ForeignKey("user.User", verbose_name="소유자", on_delete=models.SET_NULL ,null=True)
    name = models.CharField("정당 이름", max_length=50)
    description = models.TextField("설명", blank = True, null = True)
    image = models.ImageField("사진", upload_to='raw/party/', blank = True, null = True)
    color = models.CharField("정당 색", max_length=50)

    class Meta:
        verbose_name = '정당'
        verbose_name_plural = verbose_name
        ordering = ['country', 'name']

    def __str__(self):
        return self.name

only() 기능

django orm 은 기본적으로 모델에 있는 모든 항목들을 DB로부터 불러옵니다.

>>> from api.raw.models import Party
>>> p_default = Party.objects.get(id=1)
(0.001) SELECT "raw_party"."id", "raw_party"."country_id", "raw_party"."owner_id", "raw_party"."name", "raw_party"."description", "raw_party"."image", "raw_party"."color" FROM "raw_party" WHERE "raw_party"."id" = 1 LIMIT 21; args=(1,)
>>> p_default.name
'국민'

django shell 에서 Party 모델에서 id 값이 1인 인스턴스를 불러왔습니다. 0.001초가 걸렸고 Party 에 있는 id, country_id, 등등 모든 항목들을 가져왔습니다.

only를 통해 name 만 가져오겠습니다.

>>> from api.raw.models import Party
>>> p_only = Party.objects.only('name').get(id=2)
(0.001) SELECT "raw_party"."id", "raw_party"."name" FROM "raw_party" WHERE "raw_party"."id" = 2 LIMIT 21; args=(2,)
>>> p_only.name
'더불어'

0.001가 걸렸고 모든 컬럼이 아닌 id와 name 항목만 가져왔습니다.


update

해당 인스턴스를 업데이트 하겠습니다.


>>> p_default.name ='국민의힘'
>>> p_default.save()
(0.009) UPDATE "raw_party" SET "country_id" = 1, "owner_id" = 1, "name" = '국민의힘', "description" = '', "image" = '', "color" = 'red' WHERE "raw_party"."id" = 1; args=(1, 1, '국민의힘', '', '', 'red', 1)

기본 orm 으로 불러왔던 모든 데이터가 DB에 반영되었습니다. 실행 시간은 0.009초입니다.

>>> p_only.name = '더불어민주당'
>>> p_only.save()
(0.003) UPDATE "raw_party" SET "name" = '더불어민주당' WHERE "raw_party"."id" = 2; args=('더불어민주당', 2)
>>> 

반면 only를 통해 불러온 인스턴스는 name 만 반영하였습니다. 실행시간은 0.003 초입니다.

Filter 에서 사용

>>> for party in Party.objects.filter(country_id=1).only('name'):
...     party.name
... 
(0.001) SELECT "raw_party"."id", "raw_party"."name" FROM "raw_party" WHERE "raw_party"."country_id" = 1 ORDER BY "raw_party"."country_id" ASC, "raw_party"."name" ASC; args=(1,)
'국민의힘'
'더불어민주당'
>>> q = Party.objects.only('name','owner__email', 'country__name').select_related('owner', 'country').filter(country_id=1)
>>> for party in q:
...     party.name
...     party.country.name
...     party.owner.email
... 
(0.001) SELECT "raw_party"."id", "raw_party"."country_id", "raw_party"."owner_id", "raw_party"."name", "raw_country"."id", "raw_country"."name", "user_user"."id", "user_user"."email" FROM "raw_party" INNER JOIN "raw_country" ON ("raw_party"."country_id" = "raw_country"."id") LEFT OUTER JOIN "user_user" ON ("raw_party"."owner_id" = "user_user"."id") WHERE "raw_party"."country_id" = 1 ORDER BY "raw_party"."country_id" ASC, "raw_party"."name" ASC; args=(1,)
'국민의힘'
'Korea'
'admin@admin.com'
'더불어민주당'
'Korea'
'user1@user.com'
>>> 

Object로 검색하기

>>> u = User.objects.only('nickname', 'email').get(id=1)
(0.021) SELECT "user_user"."id", "user_user"."email", "user_user"."nickname" FROM "user_user" WHERE "user_user"."id" = 1 LIMIT 21; args=(1,)
>>> Party.objects.get(owner=u)
(0.005) SELECT "raw_party"."id", "raw_party"."country_id", "raw_party"."owner_id", "raw_party"."name", "raw_party"."description", "raw_party"."image", "raw_party"."color" FROM "raw_party" WHERE "raw_party"."owner_id" = 1 LIMIT 21; args=(1,)
<Party: 국민의힘>
>>> 

해당 모델 인스턴스는 생기지만 DB에서 데이터를 가져오지 않는 것으로 보입니다.

get_or_create

>>> party = Party.objects.get(id = 1) # 필요한 데이터를 불러오는 중
(0.001) SELECT "raw_party"."id", "raw_party"."country_id", "raw_party"."owner_id", "raw_party"."name", "raw_party"."description", "raw_party"."image", "raw_party"."color" FROM "raw_party" WHERE "raw_party"."id" = 1 LIMIT 21; args=(1,)
>>> upp = RegionUpp.objects.get(id = 1)
(0.005) SELECT "raw_regionupp"."id", "raw_regionupp"."name", "raw_regionupp"."country_id" FROM "raw_regionupp" WHERE "raw_regionupp"."id" = 1 LIMIT 21; args=(1,)
>>> party_usa = Party.objects.get(id=3)
(0.001) SELECT "raw_party"."id", "raw_party"."country_id", "raw_party"."owner_id", "raw_party"."name", "raw_party"."description", "raw_party"."image", "raw_party"."color" FROM "raw_party" WHERE "raw_party"."id" = 3 LIMIT 21; args=(3,)
>>> 
>>> upp
<RegionUpp: 텍사스>
>>> party
<Party: 국민의힘>
>>> party_usa
<Party: Republican>
>>> 
# get 으로 사용해봄. 
>>> result_get , created = ResultRegionUpp.objects.only('total').get_or_create(region_upp = upp, party=party_usa) 
(0.005) SELECT "result_resultregionupp"."id", "result_resultregionupp"."total" FROM "result_resultregionupp" WHERE ("result_resultregionupp"."party_id" = 3 AND "result_resultregionupp"."region_upp_id" = 1) LIMIT 21; args=(3, 1)
>>> created
False
>>> 
>>> result_get.total
0
# only 외의 데이터를 호출하면 쿼리 발생
>>> result_get.party
(0.001) SELECT "result_resultregionupp"."id", "result_resultregionupp"."party_id" FROM "result_resultregionupp" WHERE "result_resultregionupp"."id" = 7 LIMIT 21; args=(7,)
(0.001) SELECT "raw_party"."id", "raw_party"."country_id", "raw_party"."owner_id", "raw_party"."name", "raw_party"."description", "raw_party"."image", "raw_party"."color" FROM "raw_party" WHERE "raw_party"."id" = 3 LIMIT 21; args=(3,)
<Party: Republican>
>>> 
# create 로 사용해봄. 
>>> result_create, created = ResultRegionUpp.objects.only('total').get_or_create(region_upp=upp, party=party)
(0.003) SELECT "result_resultregionupp"."id", "result_resultregionupp"."total" FROM "result_resultregionupp" WHERE ("result_resultregionupp"."party_id" = 1 AND "result_resultregionupp"."region_upp_id" = 1) LIMIT 21; args=(1, 1)
(0.002) INSERT INTO "result_resultregionupp" ("region_upp_id", "party_id", "total") VALUES (1, 1, 0) RETURNING "result_resultregionupp"."id"; args=(1, 1, 0)
>>> created
True
>>> result_create.total
0
>>> result_create.party
<Party: 국민의힘>

get_or_create에서 사용해 보았습니다. get되는 경우는 only 가 잘 작동하지만 create된 경우에는 모든 항목이 불러옵니다. 아래는 ResultRegionUpp모델입니다. 혹시나 궁금할실까봐 가져왔습니다.


class ResultRegionUpp(models.Model):
    
    region_upp = models.ForeignKey("raw.RegionUpp", verbose_name="상위지역", on_delete=models.SET_NULL,null=True, blank=True)
    party = models.ForeignKey("raw.party", verbose_name="정당", on_delete=models.SET_NULL,null=True, blank=True)
    total = models.PositiveIntegerField("물감수", default= 0)
    

    class Meta:
        verbose_name = "상위 지역별 물감 현황"
        verbose_name_plural = verbose_name
        ordering= ['region_upp', '-total']

    def __str__(self):
        return f'{self.region_upp}__{self.party}'

단점

>>> p_default = Party.objects.get(id=1)
(0.001) SELECT "raw_party"."id", "raw_party"."country_id", "raw_party"."owner_id", "raw_party"."name", "raw_party"."description", "raw_party"."image", "raw_party"."color" FROM "raw_party" WHERE "raw_party"."id" = 1 LIMIT 21; args=(1,)
>>> p_default.color
'red'
>>> 
>>> p_only = Party.objects.only('name').get(id=2)
(0.001) SELECT "raw_party"."id", "raw_party"."name" FROM "raw_party" WHERE "raw_party"."id" = 2 LIMIT 21; args=(2,)
>>> p_only.color
(0.001) SELECT "raw_party"."id", "raw_party"."color" FROM "raw_party" WHERE "raw_party"."id" = 2 LIMIT 21; args=(2,)
'blue'

미리 불러오지 않은 데이터를 호출하면 그제서야 DB에 가서 필요한 데이터를 가져옵니다. 이것은 불필요한 작업입니다.

마치는 말


모델이 커지면 커질 수록 데이터를 불러올 때 불필요한 데이터도 많아집니다. 그렇다고 여러 모델을 만들어 FK 연결하는 것은 어떤 데이터가 어디에 있는지 등 여러모로 머리를 복잡하게 만듭니다. only를 사용하여 보다 간단한 데이터 구조를 만드시길 바랍니다.

jujun's profile image

jujun

2021-07-27 10:35

Read more posts by this author