본문으로 바로가기

QuerySet 활용하기, related_name

category Django, Flask/🔫 Django 2020. 5. 12. 10:40
 

Making queries | Django documentation | Django

Django The web framework for perfectionists with deadlines. Overview Download Documentation News Community Code Issues About ♥ Donate

docs.djangoproject.com

 

 

admin에 list_display에 ManyToMany로 설정된 패널을 넣을 수 없어 미디어 쿼리를 이용해서 넣은 경험이 있을 것이다. 

def count_amenities(self, obj):
    print(obj.amenities.count())
    return obj.amenities.count()

 

그런데 도대체 미디어 쿼리는 무엇을 하는 것인가?

 

🚀 shell 환경에서의 접속

 

우선, 가상 환경에 접속한 후 다음 명령어로 shell에 접속한다.

python manage.py shell

 

이제 appmodelimport하여 활용할 수 있다.

users/models.py에 정의한 User 모델을 import해보았다.

from users.models import User

from users.models import User

 

 

🚀 var(), dir()

 

이제 해당 모델에 vars(User)를 찍어봅시다.

(https://www.programiz.com/python-programming/methods/built-in/vars)

 

The vars() function returns the __dict__ attribute of the given object.

즉, vars(User)는 User 모델에 정의된 __dict__ 속성 목록이 뜹니다.

 

__dict__, dir() 에 대해서는 네임스페이스와 연관해서 작성한 포스트가 있으니 참고합시다.

(https://darrengwon.tistory.com/552)

cf 1) __dict__
파이썬의 모든 것은 객체라는 것을 알고 있을겁니다. 그리고
모든 객체는 암묵적으로 __dict__ 라는 속성을 가지고 있습니다. 이 속성은 말 그대로 사전타입(dict)이며 객체가 그 자체에 가지고 있는 모든 속성의 이름과, 그 이름이 가리키는 대상을 키, 값의 쌍으로 가지고 있습니다.


cf 2) dir(User)

(https://www.programiz.com/python-programming/methods/built-in/dir)

 

The dir() method tries to return a list of valid attributes of the object.

dir(User)는 User의 유효한 모든 속성을 출렵합니다.

해당 객체의 네임스페이스 내에 정의된 모든 것을 출력한다고 보시면 됩니다.

요기를 참고합시다.

 

__dict__를 통해 해당 모델이 가지고 있는 속성들을 살펴보면 우리가 정의한 속성들을 살펴볼 수 있습니다.

{
  '__module__': 'users.models',
  '__doc__': ' Custom User Model ',
  'GENDER_MALE': 'male',
  'GENDER_FEMALE': 'female',
  'GENDER_OTHER': 'other',
  'GENDER_CHOICES': (('male', 'Male'), ('female', 'Female'), ('other', 'Other')),
  '_meta': < Options
  for User > ,
  'DoesNotExist': < class 'users.models.User.DoesNotExist' > ,
  'MultipleObjectsReturned': < class 'users.models.User.MultipleObjectsReturned' > ,
  'avatar': < django.db.models.fields.files.ImageFileDescriptor object at 0x00000189A4B694C8 > ,
}

 

🚀 Manager / QuerySet API

 

User 모델이 가지고 있는 메서드인 objects를 실행해봅시다. 상식적으로 해당 모델의 objects를 가져오는 메서드겠죠.

가져와보면 Manager란 형태입니다. Manger는 쿼리셋을 사용하는데 필요한 것입니다.

 

User.objects
<django.contrib.auth.models.UserManager object at 0x00000189A4B75688>

 

 

User.objects라는 매니저를 통해 모든 레코드을 가져와보도록하겠습니다.

# 모든 row를 가져온다.

User.objects.all() 

# 결과 : <QuerySet [<User: bossmonster>, <User: user1>]>

 

결과값은 단순한 객체, 배열을 가져오는 게 아니라 QuerySet이란 것을 가져옵니다.

 

쿼리를 직접 사용하지도 않았는데 (실은 모르는 새에 쿼리셋을 통해 사용한 것이지만) 이런 일이 가능한 것은 공식 문서를 읽어보면 모델을 생성할 경우 database-abstraction API를 자동으로 주었기 때문입니다. 이것을 통해 CRUD가 가능하다고 합니다.

 

Once you’ve created your data models, Django automatically gives you a database-abstraction API that lets you create, retrieve, update and delete objects

 

 

이 쿼리셋을 이용하여 다음과 같이 filter를 적용하여 조건에 맞는 row만 가져올 수도 있다.

all_user = User.objects.all()
all_user.filter(superhost=True)
# 결과 : <QuerySet []>
User.objects.get(username="test")

 

 

이렇게 똑똑한 방식으로 작동할 수 있는 것은 QuerySet 덕분이다.

 

 

🚀 QuerySet 메서드

 

당연히 all() 외에도 QuerySet을 제어할 수 있는 메서드로 annotate(), order_by(), reverse() 등이 존재한다.

 

예를 들어

objects.all() : 모든 쿼리셋 반환
objects.count() : 객체의 개수 반환
objects.first() : 첫 번째 객체 반환
objects.last() : 마지막 객체 반환

등등.

 

자세한 사항은 첨부한 공식 문서를 참고하자.

 

 

QuerySet API reference | Django documentation | Django

Django The web framework for perfectionists with deadlines. Overview Download Documentation News Community Code Issues About ♥ Donate

docs.djangoproject.com

 

🚀 FK 관계 설정된 모델 간의 소통 : xxx_set

 

계속해서, User.objects.all() 을 통해 User 모델에 2개의 row가 있음을 확인했습니다. 그 중 하나를 가져온 후 dir를 찍어보겠습니다.

boss = User.objects.get(username="bossmonster")

dir(boss)

 

['CURRENCY_CHOICES', 'CURRENCY_KRW', 'CURRENCY_USD', 'DoesNotExist', 'EMAIL_FIELD', 'GENDER_CHOICES', 'GENDER_FEMAIL', .... 중략 .... 'refresh_from_db', 'reservervation_set', 'review_set', 'room_set', 'save', 'save_base', 'serializable_value', 'set_password', 'set_unusable_password', 'superhost',]

 

그 결과론, User에 정의해준 속성들 뿐만 아니라 User를 가리키고 있는 관계들(1대 다)이 xxx_set의 이름으로 포함되어 있는 것을 알 수 있습니다

# reservation/models.py에 작성한 코드
guest = models.ForeignKey("users.User", on_delete=models.CASCADE)

위의 코드의 결과로 reservervation_set이 User 모델의 미디어 쿼리의 속성으로 생긴 것입니다.

 

이것들을 이용해서, 역으로 User와 연결된 room, review, reservervation 등을 가져올 수도 있습니다.

그러니까, User.objects.get(username="bossmonste")와 연결된 room 을 모두(all) 가져오라는 것입니다.

이것은, User 모델과 Room 모델이 FK 관계로 연결되었기 때문에 가능한 것입니다.

boss = User.objects.get(username="bossmonster")
boss.room_set.all()
# 결과 : <QuerySet [<Room: Tokyo Home>]>

 

🚀 related_name (xxx.set의 이름 변경하기)

 

해당 xxx_set의 이름은 연결된 ForeignKey에 related_name 속성을 줌으로써 변경할 수도 있습니다.

 

아래와 같은 코드로 인해 room_set은 rooms로 변경되었습니다. related_name을 이해할 수 있는 쉬운 방법은, 아래의 경우 이제 User에서 rooms로 해당 모델을 가져올 수 있다는 것입니다.

 

# rooms/model.py에 작성됨
# 이후 user에 연결된 room을 가져올 때 이름을 rooms로 가져올 수 있게 됩니다.
host = models.ForeignKey(
  "users.User", related_name="rooms", on_delete=models.CASCADE)
        
# 마이그레이션을 해줘야 반영됩니다.
# 마이그레이션 결과 :  Alter field host on room
# 마이그레이션 한 다음 python shell에서 나간 후 재접속해야 합니다.
 
boss = User.objects.get(username="bossmonster")
boss.rooms.all()
# 원본
boss.room_set.all()

# related_name을 rooms로 설정한 이후
boss.rooms.all()

 

습관적으로 related_name은 해당 모델의 복수형으로 적어주는 것이 좋습니다.

 

 

🚀 ManyToMany 관계에서의 소통

 

한편 ManyToMany는 xxx.set의 형태가 아닙니다. 확인해봅시다.

 

room은 amenity와 다대 다 관계에 있도록 작성되었습니다.

# rooms/models.py에서 작성된 코드

amenities = models.ManyToManyField("Amenity", blank=True)

 

shell 환경에서 sample room을 가져온 후 dir를 찍어보면, 다대 다 관계에 설정된 것들은 xxx.set의 형태가 아니라 그냥 이름이 있는 것을 볼 수 있습니다.

from rooms.models import Room
sample = Room.objects.get(name = "sample room")

dir(sample)

sample.amenities

 

위에서와 같이 shell 에서 그냥 해당 속성의 이름으로 가져올 수 있습니다만, related_name을 통해 지정하여 가져올 수도 있습니다.

from rooms.models import Room
room = Room.objects.get(id=1)
room.amenity.all()

 

manytomany로 연결된 것도, related_name을 통해 쉽게 지정할 수 있습니다. 그냥 관습적으로 related_name을 적어두면 편합니다. 저는 FK던 MTM던 related_name를 다 적어둡니다.

(https://stackoverflow.com/questions/30394225/django-using-of-related-name-in-manytomany-and-in-foreignkey)

 

amenities = models.ManyToManyField("Amenity", related_name="rooms", blank=True)

이 코드는 여러 개의 amenity가 여러 개의 room과 연관되었음을 나타냅니다. 따라서 amenity의 입장에서 room을 가져올 때 rooms의 이름으로 가져올 수 있음을 말합니다.

 

from rooms.models import Amenity
shower = Amenity.objects.get(name = "Shower")
shower.rooms

 

 

🚀 그래서 이 쿼리셋으로 뭘 하지?

 

 

@admin.register(models.RoomType, models.Facility, models.Amenity, models.Rule)
class ItemAdmin(admin.ModelAdmin):

    def used_by(self, obj):
        return obj.rooms.count()

    list_display = (
        "name",
        "used_by",
    )

 

이것이 가능한 이유는 admin에 등록된 각 모델들이 rooms의 이름으로 related_name이 설정되었기 때문입니다.

# rooms/models.py에 작성되었습니다.

room_type = models.ForeignKey(
    "RoomType", related_name="rooms", on_delete=models.SET_NULL, null=True
)
amenities = models.ManyToManyField("Amenity", related_name="rooms", blank=True)
facilities = models.ManyToManyField("Facility", related_name="rooms", blank=True)
rules = models.ManyToManyField("Rule", related_name="rooms", blank=True)

 

 

 


darren, dev blog
블로그 이미지 DarrenKwonDev 님의 블로그
VISITOR 오늘 / 전체