admin에 list_display에 ManyToMany로 설정된 패널을 넣을 수 없어 미디어 쿼리를 이용해서 넣은 경험이 있을 것이다.
def count_amenities(self, obj):
print(obj.amenities.count())
return obj.amenities.count()
그런데 도대체 미디어 쿼리는 무엇을 하는 것인가?
🚀 shell 환경에서의 접속
우선, 가상 환경에 접속한 후 다음 명령어로 shell에 접속한다.
python manage.py shell
이제 app의 model을 import하여 활용할 수 있다.
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() : 마지막 객체 반환
등등.
자세한 사항은 첨부한 공식 문서를 참고하자.
🚀 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를 다 적어둡니다.
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)
'Django, Flask > 🔫 Django' 카테고리의 다른 글
업로드 다루기: Meida와 static의 차이, mark_safe (0) | 2020.05.13 |
---|---|
모델/어드민 내에서 로직 설계, 어드민 패널에 반영하기 (0) | 2020.05.12 |
Admin panel Customizing (further) (0) | 2020.05.12 |
ForeignKey, ManyToManyField로 모델 연결하기 (0) | 2020.05.12 |
core를 이용하여 반복되는 Abstract model 설계 및 활용 (0) | 2020.05.12 |