RelatedManager란?::Queryset메소드들>filter()/exclude()/annotate()/aggregate()/count()/distinct()

권수민·2023년 9월 25일
1

RelatedManager

RelatedManager는 FK나 ManyToManyField, OneToOneField와 같은 관계의 필드에서 반대쪽 모델로의 역참조를 관리할 때 사용되는 매니저이다.

이 메니저는 Queryset메서드를 지원하여 데이터베이스 쿼리를 작성할때 유용하게 사용된다.

대표적인 메서드를 예를 들어 설명하겠다!

이전 역참조와 정참조에서 사용했던 그 예시로 같지만 간단하게 모델을 재정의 하도록 하겠다.

<모델>

from django.db import models

class User(models.Model):
    username = models.CharField(max_length=20)

class Post(models.Model):
    title = models.CharField(max_length=200)
    author = models.ForeignKey(User, related_name="posts", on_delete=models.CASCADE)
	publish_date = models.DateField()
    comments = models.ManyToManyField("users.User", related_name="comment", through="Comments",verbose_name="댓글")
    category = models.CharField(max_length=200)  # 카테고리를 문자열 필드로 표현
    length = models.PositiveIntegerField(default=0) # 포스트의 내용의 길이를 정수 필드로 표현, 예를 들어, 문자의 수 => 현재는 content라는 필드가 있으면 그것의 길이, 없으니 일반적으로 그냥 본문의 길이. 같은의미다

만약 length에 관련해서 조건을 넣어주고 싶다면,
모델에는 length필드를 제거하고, 밑에 함수를 추가하여 그 값을 가져올수 있지만

필드로서 length는 명시되지 않았기 떄문에 데이터베이스에 저장되지는 않는다.
따라서 데이터베이스에는 이에 해당하는 컬럼이 존재하지 않을 것이다.

예) content 필드가 있으면 content 문자의 수 , 없으면 title문자의수 만

 @property
    def length(self):
        if self.content:
            return len(self.content)
        else:
            return len(self.title)

@property를 사용하면, 클래스 내의 함수를 해당 클래스의 인스턴스를 통해 "함수 호출"(() 사용) 없이 접근할 수 있게 됩니다.

원래

  post = Post(필드값....) # 모델 인스턴스 생성
  print(post.length()) #이렇게는 못하니까
  length = post.length()
  print(length) #이렇게 해줘야한다

그게 번거롭기때문에 @property를 사용하면
=> 메서드를 마치 인스턴스 변수/속성처럼 (즉, 함수 호출 () 없이) 접근할 수 있게되는것

  post = Post(필드값....) 
  print(post.length)

이렇게 바로 접근이 가능하기 때문에 데이터베이스에는 저장되지않아도 값을 api에서는 불러와서 사용할수는 있다.

모델 필드에도 따로 저장하고 조건 또한 넣으려고 한다면, 추가적인 로직이 필요하다.

1. 저장 메서드 오버라이드:

	class Post(models.Model):
    # ... 기타 필드 ...
    content_length = models.PositiveIntegerField(default=0)
    
	def save(self, *args, **kwargs):
    	self.content_length = len(self.content) if self.content else len(self.title)
    	# super(Post, self).save(*args, **kwargs) #파이썬2
        #현재는 python3활용하므로 밑으로쓰기
        super().save(*args, **kwargs) # 파이썬3

Post 클래스의 경우, 직접적으로 보이지는 않지만 models.Model을 상속받고 있습니다.

view.py

post = Post.objects.get(id=post_id)
post.save() 

post.save()불러오게되면, 우리가 기존에 데이터베이스에 저장하는 메소드인 models.Model의 save 메서드를 호출하는 것이 아니라, Post 클래스 내에서 새로히 만든 save 메서드를 불러오게 되는것으로, 기존 save()에 오버라이드하여 추가적인 작업을 수행할 수 있도록 한 뒤에, super(Post, self).save(*args, **kwargs)호출하며 원래 models.Model의 save 메서드를 호출하여 실제 저장 작업을 수행하게 만든것입니다.

그럼 저 super(Post, self).save(*args, **kwargs)는 ?

super에 관련한 설명은 이전 포스팅에서도 했지만 좀더 간단하게 설명하는 겸, 추가적인 설명을 더 들어주겠다.

일단 여기서 super(class,self)의 인자는 python2에서 쓰던 방식이고 현재는 python3로 많이 사용되기에 이제는 인자 값이 필요없는 super()해주면된다.

super().save(*args, **kwargs) # 파이썬3

<그럼 여기서 잠깐!>

super()라는 함수는 파이썬에 내장된 함수로 상속된 상위 클래스의 메서드를 호출하기 위해 사용하는데. 여기서 간혹 상속인자를 여러개 주게 되는 경우시에 이게 충돌이 일어날 수 있기 때문에 MRO(Method Resolution Order)를 기반으로 다음 클래스의 메서드를 호출한다.

예를 들어줄게요:

class A:
    def hello(self):
        print("Hello from A class")

class B(A):
    def hello(self):
        super().hello()
        print("Hello from B class")

class C(A):
    def hello(self):
        super().hello()
        print("Hello from C class")

class D(B, C):
    def hello(self):
        super().hello()
        print("Hello from D class")

d = D()
d.hello()

그럼 내가 d의 hello함수를 호출하게 됐는데?

이것 값은 ??

Hello from A class
Hello from C class
Hello from B class
Hello from D class

이렇게 나오는데 왜 이렇게 나오지 > 라는 생각을 하게된다.

먼저 b다 끝내고 c다끝내고 A로 돌아와야하는게 아닌가?

여기서 문제점은 우리가 생각하는 방식으로 MRO짜져 있지 않다는것이다.

클래스의 MRO를 확인하려면 클래스명.mro() 함수를 사용하면 되는데,

D.mro()

=> [D, B, C, object]와 같은 출력 결과

D에서 hello 호출을 시작으로 super().hello() 상위 hello()가 호출되는데, 우리는 모델에서 받은 인자 A를 기준으로 생각해 그곳으로 들어가서 결과값이 반환된다고 생각하는데 다중으로 받았을때는 A가 아니라 그 다음에 주었던 인자값이 상위 클라스로 인지되어 올라가진다고 보면 된다.

B.hello()의 super().hell0()는 C.hello()고
C.hello()의 super().hell0()는 A클라스로 A클라스는 상위로 갈게 없어 상위클라스의 destination찍고,
이게 끝이아니라 다시 올라온순서대로 꺼꾸로 가서 마지막 프린트 할것 나 나가야한다.

그래서

Hello from A class -> 정점찍고
Hello from C class -> c로 꺼구로 돌아와서 print()실행
Hello from B class
Hello from D class

이러한 값이 나오는것.

<여기서 잠깐 *args, **kwargs의 인자>

전에 언패킹에 관에 설명하면서 * => 리스트, ** =>딕셔너리를 위해 사용한다고 말했던것 기억나나?

이때 args는 arguments로 인자들이라는 의미를 가지고있고,
kwargs도 마찬가지로 key arguments라는 의미로 딕셔너리형태의 인자를 말하는것이다.

그래서 인자들안에 그러한 두 형태를 가지고 있을 수 있다는 의미로 args,**kwags를 넣는것. 순서는 그냥 보편적으로 args 다음 **kwargs순으로 쓴다.

주로 args인자에 값을 넣는 경우는 거의 없다.

2.(선택적) Django 시그널 pre_save사용:

from django.db.models.signals import pre_save
from django.dispatch import receiver

@receiver(pre_save, sender=Post)
def update_content_length(sender, instance, **kwargs):
  instance.content_length = len(instance.content) if instance.content else len(instance.title)

Django의 pre_save 시그널을 사용하여 Post 객체가 저장되기 전에 content_length 값을 업데이트할 수도 있는 방법이 바로 위에 있는 방법이데, 이 방법은 1번에서 보여준 메서드 오버라이딩 대신 모델의 저장 로직 외부에서 추가 로직을 처리할 수 있게 해줍니다.

pre_save

장고모델의 save()메서드가 호출 되기 직전에 발생하는 신호로 데이터베이스 저장전 객체 필드를 수정하거나 유효성 검사를 수행하는데 사용된다.

@receiver

이 함수 데코레이터는 신호와 연결될 함수를 정의하고 그 신호를 처리할 함수를 연결하는 역할을 함으로써 연결된 함수가 신호 발생시마다 호출된다.

Post 모델의 객체가 데이터베이스에 저장되기 전에 pre_save의 신호를 호출하여 def update_content_length()함수를 호출해 실행한다. 그 후에 알아서 저장된다.

따라서 이 함수는 model.py안에 해달 클라스 모델안에 넣어주거나, 많은 신호관련 로직이 있다면 signal.py를 따로 생성해 모아주는게 답이지만, 현재로서는 큰 프로젝트를 진행하는것이 아니니 model.py 해당 클래스안에 적어주는게 옳다고 본다.

드디어! 이제 RelatedManger 주요 메서드를 설명해주겠다!

  1. filter(): 여러개의 데이터를 뽑아오고 싶을때 쓴다

특정 조건에 맞는 포스트들만 필터링합니다.

user = User.objects.get(id=1)
posts_from_2023 = user.posts.filter(publish_date__year=2023)

여기서 쿼리체이닝이라는것을 필터 매니저로 보여주면 :

# 2023년에 발행된 포스트 중에서, "Django"라는 단어를 제목에 포함하는 포스트만 필터링
specific_posts = Post.objects.filter(published_date__year=2023).filter(title__contains="Django")
  1. exclude():
    특정 조건을 제외한 포스트들만 필터링할 때 사용한다.
user = User.objects.get(id=1)
posts_not_from_2023 = user.posts.exclude(publish_date__year=2023)
  1. annotate():
    각 포스트에 대한 추가 정보를 주기 위해 주석을 달 때 사용
    즉, 주석이라해서 내 주석이아닌 장고 ORM의 주석이라보면된다.

각 레코드에 대한 추가정보를 "주석"으로 추가하는 경우에 사용되는것이다.

즉, 추가로 필드를 넣어주는 거라고 보면된다.
그렇지만 이 것이 실제로 모델에 영구적으로 추가된다거나 데이터베이스에 반영되는 것은아니다.
그저 임시적으로 추가정보를 제공하기위해 QuertSet에 메타 데이터를 첨부하는것이다.
QuerySet의 생애 주기 동안만 존재하고, 평가된 후에는 정보는 사라진다.

여기서 num_comments 라는 임시 필드를 주석으로 처리하고, Count()라는 집계함수를 넣어 해당 모델의 comments 필드의 갯수를 센것!

from django.db.models import Count
user = User.objects.get(id=1)
posts_with_comment_count = user.posts.annotate(num_comments=Count('comments'))

결과값 출력:

for post in posts_with_comment_count:
    print(post.title, post.num_comments)

여기서 잠시 메타 데이터는 뭔가?

"메타 데이터" ==> "데이터에 대한 데이터"라는 의미
기본 데이터의 구조, 성격, 특성, 방식 등에 대한 정보를 제공하는 데이터

예) 디지털 사진 파일을 생각해보면:

기본 데이터: 사진의 픽셀, 색상, 이미지 자체의 내용
메타 데이터: 촬영 날짜, 카메라 모델, GPS 위치, ISO 설정, 셔터 속도 등

즉, 메타 데이터는 주 데이터의 컨텍스트나 특성을 설명하는 추가 정보를 제공하는 역할을 한다.

모델에서도 많이 사용하지 > class Meta :

4.aggregate():
포스트의 그룹에 대한 정보를 집계해준다.

밑의 함수는 현재 특정사용자의 집합을 모아서 length(문자수)라는 post모델의 필드를 가르키고 집계함수의 Avg()를 불러와 평균값을 계산해주고있다.

from django.db.models import Avg
user = User.objects.get(id=1)
average_post_length = user.posts.aggregate(Avg('length'))

출력값을 가지고 나오려할때 여기서 lengthavg는 키다.
자동적으로 원래필드이름과 집계함수의 이름(소문자)과 연결하여 생성된다.
Sum('length')라면 length
sum이 될것.

avg_length = average_post_length['length__avg']
print(avg_length)

5.count():
포스트의 수를 카운트! > 이게 바로 전에 템플릿에서 우리가 사용했던 하나의 매니저다!

user = User.objects.get(id=1)
number_of_posts = user.posts.count()
  1. distinct():
    중복되지 않은 값을 가진 포스트만 반환한다.

다시말하자면, 만약 한 사용자가 여러 포스트를 다양한 카테고리에 작성시,
같은 카테고리에 여러 포스팅을 했을 수 있잖아?
그럼 같은 카테고리가 같은게 여러게 나올꺼아니야?
중복값을 없애고 카테고리를 단 한번씩만 나타내는 리스트를 반환하는것을 말하는것!

user = User.objects.get(id=1)
unique_post_categories = user.posts.values('category').distinct()

여기서 year는 뭐냐하면 바로 lookuptype이라고 하는 장고ORM에서 사용하는 기능이다.
이것은 데이터베이스 쿼리 작성시 필드값에 대한 특정조건을 나타내기 위해서 사용되어지는데 : Lookup type은 필드 이름과 이중 밑줄(__
) 뒤에 나오며, 사용하려는 조건에 따라 다양한 lookup type이 있다.

자주사용하는 것을 예시로 들어주겠다.

  1. exact:
    정확하게 일치하는 값을 찾습니다.
entries = Entry.objects.filter(title__exact="Hello World")
  1. iexact:
    대소문자를 구분하지 않고 정확하게 일치하는 값을 찾습니다.
entries = Entry.objects.filter(title__exact="Hello World")
  1. contains:
    필드 값 내에 지정된 부분 문자열이 포함되어 있는지 찾습니다.
entries = Entry.objects.filter(title__contains="Hello")

4.icontains:
대소문자를 구분하지 않고 필드 값 내에 지정된 부분 문자열이 포함되어 있는지 찾습니다.

entries = Entry.objects.filter(title__icontains="hello")

5.gt, gte, lt, lte:
필드 값이 지정된 값보다 큰, 크거나 같은, 작은, 작거나 같은지를 찾습니다.

gt (Greater Than):필드의 값이 지정한 값보다 큰 레코드를 필터링
gte (Greater Than or Equal to): 필드의 값이 지정한 값보다 크거나 같은 레코드를 필터링
lt (Less Than): 필드의 값이 지정한 값보다 작은 레코드를 필터링
lte (Less Than or Equal to): 필드의 값이 지정한 값보다 작거나 같은 레코드를 필터링

entries_from_past = Entry.objects.filter(pub_date__lte=date.today())
#date.today()값보다 작거나 같은 레코드를 불러온다.

6.year, month, day:
DateField 또는 DateTimeField의 연도, 월, 일을 기준으로 필터링합니다.

entries_from_2023 = Entry.objects.filter(pub_date__year=2023)
profile
초보개발자

0개의 댓글