Django Models 8 ManyToMany 비대징 재귀참조 중개모델

대칭 관계가 아닌데 중개 모델을 사용하는 경우

자기 자신을 포함하는 ManyToManyField가 존재 하고 친구의 관계가 대칭이 아닐경우 그리고 이때 관계 테이블에서 block기능이 있을 경우에 대해 알아 보자

일단 self로 자신모델을 다 대다 필드로 가져야 하며 symmentrical=False로 비대칭을 적용한다. 그리고 서로 관계에 대해서 block할수 잇는 추가 적인 필드를 사용해야 하기 때문에 through=로 중개 모델을 사용한다. 그리고 중개 모델은 유저의 외래키를 가지며 현재 유저간의 관계유형을 표현할수 있는 필드를 하나 만든다

하지만 같은 모델에 대해 ForeignKey를 두개 쓰고 있기 때문에 역 참조시 어떤 모델을 참조해야 하는지에대한 부분이 불분명 해진다. 그래서 마이그레이션을 할 수 없다. 이 것을 해결하기 위해 Relation모델에 있는 각 ForeignKey들에 related_name을 지정 해 주어야 한다.

from django.db import models


class TwitterUser(models.Model):
    """
    내가 A를 follow 함
        나는 A의 follower
        A는 나의 followee
    A와 내가 서로 follow함
        나와 A는 friend
    Block기능이 있어야 함
    """
    name = models.CharField(max_length=50)

    relations = models.ManyToManyField(
        'self',
        # 비대칭 적용
        symmetrical=False,
        # 중개 모델 적용
        through='Relation',
        # 역참조를 없앰
        related_name='+'
    )


class Relation(models.Model):
    """
    유저간의 관계를 정의 하는 중개 모델
    """
    RELATION_TYPE_FOLLOWING = 'f'
    RELATION_TYPE_BLOCK = 'b'
    CHOICES_TYPE = (
        (RELATION_TYPE_FOLLOWING, '팔로잉'),
        (RELATION_TYPE_BLOCK, '차단'),
    )
    from_user = models.ForeignKey(
        TwitterUser,
        on_delete=models.CASCADE,
        # 자신이 from_user인 경우의 Relation목록을 가져오고 싶은 경우
        related_name='relations_by_from_user',
    )
    to_user = models.ForeignKey(
        TwitterUser,
        on_delete=models.CASCADE,
        # 자신이 to_user인 경우의 Relation목록을 가져오고 싶은 경우
        related_name='relations_by_to_user',
    )
    # 서로의 관계를 표현하기 위한 필드
    type = models.CharField(max_length=1, choices=CHOICES_TYPE)

이제 서로 관계를 생성해보자

u1, u2, u3, u4 = [TwitterUser.objects.create(name=name) for name in ['장동건', '손지창', '기무라', '타쿠야']]
# u1.relations.add(u2) # X 이건 실행 안됨.
# 중개 모델에서는 중개 테이블의 인스턴스를 직접 만들어야 함.

# u1이 u2를 follow한다.
 Relation.objects.create(from_user=u1, to_user=u2, type='f') 
 
# 혹은 from_user의 인스턴스에서 Relation을 역참조하여 바로 만들수 있다.
u1.relations_by_from_user.create(to_user=u3, type='f')

u1이 팔로우 한 유저 목록을 받아옴

u1.relations.all()

그런데 문제가 하나 있다. 이렇게 가져오면 u1과 관계된 유저 목록에서 상대방을 follow했는지 block했는지 구분 할 수가 없다. 그럴 경우 TwiiterUser모델에서 필터를 걸수 없다. 그래서 Relations모델에서 인스턴스를 가져와 필터링을 해야 한다

u1이 from_user인 Relations를 모두 가져옴

u1.relations_by_from_user.all()

그중에 follow인 Relations와 block인 Relations를 가져옴

# follow
u1.relations_by_from_user.filter(type='f')
# block
u1.relations_by_from_user.filter(type='b')

u1이 follow하고 있는 유저 pk리스트

u1.relations_by_from_user.filter(type='f').values_list('to_user', flat=True)

이렇게도 사용할 수 있다.

Relation.objects.filter(from_user=u1, type='f').values_list('to_user', flat=True)

이렇게 u1이 팔로우 하고 있는 유저 pk리스트를 가져 와서 유저를 쿼리필터를 통해 찾아 낼 수 있다.

u1_following_pk_list = u1.relations_by_from_user.filter(type='f').values_list('to_user', flat=True)
# <손지창>
TwitterUser.objects.filter(pk__in=u1_following_pk_list)

values는 해당 쿼리셋에서 각각을 키/값 딕셔너리로 바꾸어 반환한다. 그리고 values_list는 딕셔너리가 아닌 리스트형태로 값들을 튜플로 만들어 리스트에 담아 반환한다. 여기에 원하는 필드만 지정하여 values_list('pk') 가져 올 수 있다. 그리고 flat은 가져오는 필드들을 주어진 요소가 하나라는 가정하에 튜플을 리스트에 담는것이 아닌 하나의 리스트에 요소들을 그냥 담아 주는 것이다.

# <Query Set[{'id':1, 'name':'장동건'}, {'id':2 ...
TwitterUser.objects.values()

# <Query Set[(1, '장동건'), (2, '손지창'), ...
TwitterUser.objects.values_list()

# <Query Set[(1, ), (2, ), ...
TwitterUser.objects.values_list('pk')

# <Query Set[1, 2, 3, 4]>
TwitterUser.objects.values_list('pk', flat=True)

이렇게 만들어진 쿼리문들을 이용하여 필요한 정보를 가져오는 프로퍼티를 만들 수 있다

class TwitterUser(models.Model):
    ....
    @property
    def following(self):
        """
        내가 follow하고 있는 TwitterUser목록을 가져옴
        """
        following_relations = self.relations_by_from_user.filter(
            type=Relation.RELATION_TYPE_FOLLOWING,
        )
        # 위에서 정제한 쿼리셋에서 'pk'값만 리스트로 가져옴( 내가 팔로잉하는 유저의 pk리스트)
        following_pk_list = following_relations.values_list('to_user', flat=True)

        # TwitterUser테이블에서 pk가 following_pk_list에 포함되는 User목록을
        # following_users변수로 할당
        following_users = TwitterUser.objects.filter(pk__in=following_pk_list)
        return following_users

이렇게 만들어진 프로퍼티를 사용하면 내가 팔로잉 하고 있는 유저의 목록을 가져 올 수 있다.

u1 = TwitterUser.objects.get(pk=1)
# 손지창 기무라
u1.following

나를 follow 하고 있는 유저 목록을 가져 오는 것

class TwitterUser(models.Model):
    ....
    @property
    def followers(self):
        follower_pk_list = self.relations_by_to_user.filter(
            type=Relation.RELATION_TYPE_FOLLOWING).values_list('from_user', flat=True)
        return TwitterUser.objects.filter(pk__in=follower_pk_list)

그리고 유저가 to_user를 팔로우 하는 함수를 만들어 보자

class TwitterUser(models.Model):
    ....
    def follow(self, to_user):
        """
        to_user 에 주어진 TwitterUser를 follow함
        """
        self.relations_by_from_user.create(
            to_user=to_user,
            type=Relation.RELATION_TYPE_FOLLOWING,
        )

사용은

u1.follow(to_user=u3)

유저를 block하고 싶다면

class TwitterUser(models.Model):
    ....
    def block(self, to_user):
        """
        to_user 에 주어진 TwitterUser를 block함
        """
        self.relations_by_from_user.create(
            to_user=to_user,
            type=Relation.RELATION_TYPE_BLOCK,
        )
u1.block(uo_user=u4)

유저가 block한 다른 유저 목록을 가져옴

class TwitterUser(models.Model):
    ....
    @property
    def block_users(self):
        """
        내가 block하고 있는 TwitterUser목록을 가져옴
        """
        blocking_pk_list = self.relations_by_from_user.filter(
            type=Relation.RELATION_TYPE_BLOCK).values_list('to_user', flat=True)
        return TwitterUser.objects.filter(pk__in=blocking_pk_list)

이렇게 하면 하나의 문제가 생기는데 내가 follow한 사람을 block시키면 두가지 상태를 모두 가지는 유저가 생기게 된다. block에 유저가 들어가면 follow관계가 없어져야 한다. 이것을 데이터 베이스 차원에서 제한을 걸어 줄 수 있다. 그리고 해당 관계의 생성 시간을 넣어 보자

class Relation(models.Model):
    ....
    created_date = models.DateTImeField(auto_now_add=True)
    class Meta:
        # from_user와 to_user의 값이 이미 있을 경우
        # DB에 중복 데이터 저장을 막음
        # ex) from_user가 1, to_user가 3인 데이터가 이미 있다면
        #       두 항목의 값이 모두 같은 또 다른 데이터가 존재 할 수 없음.
        unique_together = (
            ('from_user', 'to_user'),
        )

unique_together로 관계되는 컬럼의 중복을 막을수 있다. 그리고 makemigrations를 하면 무언가를 물어 볼텐데 이 것은 created_date가 새로 만들어 지면서 기존에 있던 row들에 새로만드는 필드에 대한 데이터를 어떻게 넣을 것인지 물어 보는 것이다. 기본적으로 created_date에는 default값이 없기 때문에 그렇다. 질문의 1번은 현재 넣을 값을 사용자가 직접 지정 하는 것이고 2번은 필드 속성에 default값을 넣어서 다시 makemigrations을 하는 것이다. 지금 별 데이터가 없으니 1번을 선택 하자.

그러면 또 질문이 나오는데 이것은 기존 row에 대해 어떤 값을 넣을지 물어 보는데 default값은 timezone.now이다. 다른 것을 넣고 싶으면 datetime도 사용 가능하다. 딱히 변할게 없으니 그냥 엔터를 쳐서 기본값으로 세팅 하자.

이후 migrate를 하면 오류가 날 수도 있는데 이는 이미 Relations에 중복으로 맺어진 관계가 있기 때문이다. ORM으로 모든 Relations를 지우고 다시 migrate해보자. 그러면 잘 될 것이다.

그리고 같은 유저에 대해 followblock을 하려면 에러가 날 것이다.

auto_now_add, auto_now

# 처음으로 객체가 만들어 지는 순간에만 현재 시간을 기록
created_date = models.DateTImeField(auto_now_add=True)
# 객체가 업데이트 될 때마다 시간을 재 기
modified_date = models.DateTImeField(auto_now=True)

마지막으로 내가 임의의 유저를 follow하고 있는지 임의의 유저가 나를 follow하고 있는지 확인하는 함수를 만들어 보자

class Relations:
    ....
    def is_followee(self, to_user):
        """
        내가 to_user를 follow하고 있는지 여부를 True/False로 리턴
        """
        return self.following.filter(pk=to_user.pk).exists()

    def is_follower(self, from_user):
        """
        from_user가 나를 follow하고 있는지 여부를 True/False로 리턴
        """
        return self.followers.filter(pk=from_user).exists()

add(), create(), set(), clear()

add()MTM필드에 내용을 추가 하는 것이고 create()는 생성하는 것. set()은 모두 지우고 다시 넣는것 위 세개는 다 대다 중개 모델을 사용할 경우 사용 할 수 없는 것들이다.

clear()는 모두 지우는 것 이것은 사용할 수 있다.

Comments