기술정리/Django

Django ManyToManyField 커스텀 모델 이전하기

bingual 2024. 4. 8. 15:12
반응형

ManyToManyField 

ForeignKey, OneToOneField와 같이 Django 내에서는 데이터베이스 관계성을 지원하는 ManyToManyField를 지원한다.

여기서 ManyToManyField란 게시글의 좋아요, 태그 기능과 같이 두 테이블 간의 관계를 정의해야 할 때 사용할 수 있다.

 

해당 게시글에선 이미 정의된 기본 관계 테이블을 커스텀 관계 테이블로 이전하는 과정을 설명한다.

 

 

기본 관계 테이블

 

models.py

class Post(models.Model):
    tag_set = models.ManyToManyField(
        "Tag",
        related_name="blog_post_set",
        related_query_name="blog_post",
        blank=True,
    )
    # 생략


class Tag(models.Model):
    name = models.CharField(max_length=100)

    def __str__(self) -> str:
        return self.name

    class Meta:
        ordering = ["name"]
        constraints = [
            models.UniqueConstraint(Lower("name"), name="blog_tag_name_unique")
        ]
        indexes = [
            models.Index(
                fields=["name"],
                name="blog_tag_name_like",
                opclasses=["varchar_pattern_ops"],
            )
        ]

 

 

 

SQL DDL

-- auto-generated definition
create table blog_post_tag_set
(
    id      bigint generated by default as identity
        primary key,
    post_id bigint not null
        constraint blog_post_tag_set_post_id_0ecab89b_fk_blog_post_id
            references blog_post
            deferrable initially deferred,
    tag_id  bigint not null
        constraint blog_post_tag_set_tag_id_add72666_fk_blog_tag_id
            references blog_tag
            deferrable initially deferred,
    constraint blog_post_tag_set_post_id_tag_id_b9baa74f_uniq
        unique (post_id, tag_id)
);

alter table blog_post_tag_set
    owner to myuser4;

create index blog_post_tag_set_post_id_0ecab89b
    on blog_post_tag_set (post_id);

create index blog_post_tag_set_tag_id_add72666
    on blog_post_tag_set (tag_id);

 

 

models에 정의된 Post, Tag 모델을 정의하고 마이그레이션을 하면 위와 같이 blog_post_tag_set 관계 테이블이 생성된다. 

 

 

 

 

Post, Tag의 레코드는 이전에 따로 이미 생성해 두었었다. 새로 생성한 관계 테이블에 값을 삽입해 보면 위와 같이 각 테이블의 pk 칼럼의 값들이 삽입됨을 알 수 있다. 즉 ManyToManyField를 사용하여 마이그레이션을 적용하면 위와 같은 기본 관계형 테이블이 생성되기 때문에 따로 커스텀을 하기 위해서는 별도의 과정이 필요함을 알 수 있다.

 

 

커스텀 관계 테이블

 

models.py

class Post(models.Model):
    tag_set = models.ManyToManyField(
        "Tag",
        related_name="blog_post_set",
        related_query_name="blog_post",
        through="PostTagRelation",
        # ManyToManyField 필드가 정의된 모델에 대한 외래키 필드명을 먼저 지정
        through_fields=("post", "tag"),
        blank=True,
    )
    ...


class Tag(models.Model):
    ...


class PostTagRelation(models.Model):
    post = models.ForeignKey(Post, on_delete=models.CASCADE)
    tag = models.ForeignKey(Tag, on_delete=models.CASCADE)
    # 관계 모델을 통해, 관계에 대한 추가 정보를 담을 수 있다.
    created_at = models.DateTimeField(auto_now_add=True)

    class Meta:
        constraints = [
            models.UniqueConstraint(
                fields=["post", "tag"],
                name="blog_post_tag_relation_unique",
            )
        ]

 

 

  • Post Model에 through, through_fields를 지정해준다.
  • PostTagRelation Model을 새로 정의하고 created_at 필드를 추가한다.

해당 과정을 거치면 마이그레이션시 새로운 커스텀 관계 테이블이 생성된다.

 

 

migrations

def copy_relations(apps, schema_editor):
    Post = apps.get_model("blog", "Post")
    DefaultPostTagRelation = Post.tag_set.through
    PostTagRelation = apps.get_model("blog", "PostTagRelation")

    new_relation_list = []
    for relation in DefaultPostTagRelation.objects.all():
        new_relation = PostTagRelation(
            post_id=relation.post_id,
            tag_id=relation.tag_id,
        )
        new_relation_list.append(new_relation)
    PostTagRelation.objects.bulk_create(new_relation_list, batch_size=1000)

 

 

새로운 관계 테이블은 생성하였을 때 기존 기본 관계 테이블에 있는 값을 복사해 가져올 필요성이 있다.
makemigrations 명령을 수행하고 생성된 마이그레이션 파일에 해당 코드를 RunPython을 이용해 적용시켜 주도록 한다. 

 

 

 

 

blog_post_tag_set 테이블의 레코드를 그대로 복사하여 적용이 된 것을 확인할 수 있다.

 

 

models.py

# 주석 처리를 진행하여 새로운 마이그레이션 파일을 생성하고 적용하여 기존 기본 관계 테이블 삭제
class Post(models.Model):
#     tag_set = models.ManyToManyField(
#         "Tag",
#         related_name="blog_post_set",
#         related_query_name="blog_post",
#         through="PostTagRelation",
#         through_fields=("post", "tag"),
#         blank=True,
#     )
    ...


# 주석을 풀고 다시 새로운 마이그레이션 파일을 생성하고 적용하도록 한다.
class Post(models.Model):
    tag_set = models.ManyToManyField(
        "Tag",
        related_name="blog_post_set",
        related_query_name="blog_post",
        through="PostTagRelation",
        through_fields=("post", "tag"),
        blank=True,
    )
    ...

 

 

기존에 생성한 blog_post_tag_set 기본 관계 테이블은 사용하지 않을 테니 해당 테이블을 삭제하는 과정이 필요하다. 위와 같이 2개의 마이그레이션 파일을 생성해 주고 적용시켜 주게 되면 기존 기본 관계 테이블은 삭제된다.

 

 

 

 

조회 명령 또한 제대로 작동하는 것을 확인할 수 있다.