본문 바로가기

Frame Work/Django

[Django] OneToOne Relation

728x90
반응형

OneToOne Relation

OneToOne은 1:1 관계를 뜻한다. 예를 들어 "하나의 사용자는 하나의 프로필만 가질 수 있다"라는 상황에서 사용할 수 있다. OneToOne Relation을 경우 하나의 객체만을 반환한다. 아래 예제를 통해 이해해보자.

 

Author와 AuthorProfile Model

아래 모델은 Author(작가)와 이 Author에 대한 Profile 정보를 저장하는 간단한 모델이다.
class BaseModel(models.Model):
    date_of_create = models.DateTimeField(auto_now_add=True)
    date_of_update = models.DateTimeField(auto_now=True)

    class Meta:
        abstract = True

class Author(BaseModel):
    first_name = models.CharField(max_length=10)
    last_name = models.CharField(max_length=10)
    email = models.EmailField(blank=True, verbose_name='e-mail')

    objects = models.Manager()

    class Meta:
        db_table = "author"

    def __str__(self):
        return u"%s %s" % (self.first_name, self.last_name)


class AuthorProfile(BaseModel):
    description = models.CharField(max_length=15, null=True)
    author = models.OneToOneField(Author, on_delete=models.CASCADE)

    class Meta:
        db_table = "author_profile"

    def __str__(self):
        return u"[ %s ]: Profile" % (self.author.name)
AuthorProfile 모델에서는 author를 OneToOne Relation을 통해 참조하고 있다. 앞서 설명했듯 위와 같은 구조에서는 Author는 하나의 AuthorProfile만 가질 수 있으며 이를  Django ORM을 통해 살펴보면 다음과 같이 데이터를 저장할 수 있다.
>>> author = Author.objects.get(id=1)
(0.001) | SELECT version(), @@sql_mode, @@default_storage_engine, @@sql_auto_is_null, @@lower_case_table_names,
	|                                                                               convert_tz('2001-01-01 01:00:00', 'UTC', 'UTC') IS NOT NULL

(0.001) | SET SESSION TRANSACTION ISOLATION LEVEL READ COMMITTED

(0.002) | SELECT `author`.`id`,
	|        `author`.`date_of_create`,
	|        `author`.`date_of_update`,
	|        `author`.`first_name`,
	|        `author`.`last_name`,
	|        `author`.`email`
	| FROM `author`
	| WHERE `author`.`id` = 1
	| LIMIT 21

>>> author_profile = AuthorProfile.objects.create(description='first author', author=author)
(0.002) | INSERT INTO `author_profile` (`date_of_create`, `date_of_update`, `description`, `author_id`)
	| VALUES ('2022-06-06 11:37:36.998191', '2022-06-06 11:37:36.998259', 'first author', 1)
이 상태에서 author에 추가로 profile을 생성해서 저장하려고 하면 어떻게 될까? 결과는 다음과 같다.
>>> AuthorProfile.objects.create(description='second profile', author=author)
Traceback (most recent call last):
  	...
    _mysql.connection.query(self, query)
django.db.utils.IntegrityError: (1062, "Duplicate entry '1' for key 'author_id'")
OneToOne Relation의 특성으로 인해  IntegrityError가 발생하는 것을 알 수 있다.
 

OneToOneField, Read

위 예제를 통해 AuthorProfile Model에 데이터가 생성됬다. 추가로 데이터 하나만 더 삽입하고 Django ORM을 통해 이 Model에서 데이터를 읽어들일 때 SQL이 어떤식으로 날라가는지 확인해보자. 
일단 AuthorProfile Model의 데이터를 읽어보자.
>>> AuthorProfile.objects.all()
(0.001) | SELECT `author_profile`.`id`,
	|        `author_profile`.`date_of_create`,
	|        `author_profile`.`date_of_update`,
	|        `author_profile`.`description`,
	|        `author_profile`.`author_id`
	| FROM `author_profile`
	| LIMIT 21

(0.001) | SELECT `author`.`id`,
	|        `author`.`date_of_create`,
	|        `author`.`date_of_update`,
	|        `author`.`first_name`,
	|        `author`.`last_name`,
	|        `author`.`email`
	| FROM `author`
	| WHERE `author`.`id` = 1
	| LIMIT 21

(0.001) | SELECT `author`.`id`,
	|        `author`.`date_of_create`,
	|        `author`.`date_of_update`,
	|        `author`.`first_name`,
	|        `author`.`last_name`,
	|        `author`.`email`
	| FROM `author`
	| WHERE `author`.`id` = 2
	| LIMIT 21

<QuerySet [<AuthorProfile: [ minjune kim ]: Profile>, <AuthorProfile: [ seojune choi ]: Profile>]>

추가로 삽입했던 데이터까지 합쳐서 제대로 읽어 들이지만 불필요한 쿼리를 발생시키는 것 같다(N+1). AuthorProfile Model에서 데이터를 읽어 들이는 케이스이므로 정방향 참조인 Select Related를 통해서 데이터를 읽어보자.

>>> AuthorProfile.objects.select_related('author')
(0.001) | SELECT `author_profile`.`id`,
	|        `author_profile`.`date_of_create`,
	|        `author_profile`.`date_of_update`,
	|        `author_profile`.`description`,
	|        `author_profile`.`author_id`,
	|        `author`.`id`,
	|        `author`.`date_of_create`,
	|        `author`.`date_of_update`,
	|        `author`.`first_name`,
	|        `author`.`last_name`,
	|        `author`.`email`
	| FROM `author_profile`
	| INNER JOIN `author` ON (`author_profile`.`author_id` = `author`.`id`)
	| LIMIT 21

<QuerySet [<AuthorProfile: [ minjune kim ]: Profile>, <AuthorProfile: [ seojune choi ]: Profile>]>

AuthorProfile Model의 author를 가져올 때는 INNER JOIN을 발생시킨다는 것을 알 수 있다.

 

OneToOneRel이 Null=True 이면?

해당 author 속성을 NULL이 가능하게 설정 후 데이터를 읽었을 경우 어떻게 동작하는지 확인해보자. AuthorProfile Model을 다음과 같이 변경 하자.

class AuthorProfile(BaseModel):
    ...
    author = models.OneToOneField(Author, on_delete=models.SET_NULL, null=True)
	...

이후 데이터를 삽입하자.

>>> AuthorProfile.objects.create(description='Null Author', author=None)
(0.003) | INSERT INTO `author_profile` (`date_of_create`, `date_of_update`, `description`, `author_id`)
	| VALUES ('2022-06-06 11:53:07.068142', '2022-06-06 11:53:07.068212', 'Null Author', NULL)

그리고 다시 Select Related 이용해서 읽어보자.

>>> AuthorProfile.objects.select_related('author')
(0.001) | SELECT version(), @@sql_mode, @@default_storage_engine, @@sql_auto_is_null, @@lower_case_table_names,
	|                                                                               convert_tz('2001-01-01 01:00:00', 'UTC', 'UTC') IS NOT NULL

(0.001) | SET SESSION TRANSACTION ISOLATION LEVEL READ COMMITTED

(0.001) | SELECT `author_profile`.`id`,
	|        `author_profile`.`date_of_create`,
	|        `author_profile`.`date_of_update`,
	|        `author_profile`.`description`,
	|        `author_profile`.`author_id`,
	|        `author_profile`.`manager_id`,
	|        `author`.`id`,
	|        `author`.`date_of_create`,
	|        `author`.`date_of_update`,
	|        `author`.`first_name`,
	|        `author`.`last_name`,
	|        `author`.`email`
	| FROM `author_profile`
	| LEFT OUTER JOIN `author` ON (`author_profile`.`author_id` = `author`.`id`)
	| LIMIT 21

<QuerySet [<AuthorProfile: [ minjune kim ]: Profile>]>

on_delete 속성과 null 속성이 변경되었으므로 쿼리도 LEFT OUTER JOIN으로 변경되었다.

 
 

OneToOneRel이 N개 일 때

OneToOneRel 속성을 하나 더 추가한 뒤 Select Related를 걸면 어떻게 될지 문득 궁금했다. 추가한 모델은 다음과 같다.

class AuthorManager(BaseModel):
    manager_name = models.CharField(max_length=15, null=True)    

    class Meta:
        db_table = "author_manager"

    def __str__(self):
        return u"[ %s ]" % (self.manager_name)

이후 AuthorProfile은 다음과 같이 변경했다.

class AuthorProfile(BaseModel):
    description = models.CharField(max_length=15, null=True)
    author = models.OneToOneField(Author, on_delete=models.CASCADE)
    manager = models.OneToOneField(AuthorManager, on_delete=models.CASCADE)

    class Meta:
        db_table = "author_profile"

    def __str__(self):
        return u"[ %s ]: Profile" % (self.author)

Author를 관리하는 Manager는 한 명이고 이는 AuthorProfile Model에 저장하자 라는 의도이다. 이후 AuthorProfile에 AuthorManager 정보를 저장하자.

>>> author_manager = AuthorManager.objects.create(manager_name='first_manager')

>>> first_author_profile = AuthorProfile.objects.get(id=1)
>>> first_author_profile.manager = author_manager
>>> first_author_profile.save()

이후 같은 방법으로 Select Related를 이용해 읽었을 경우 결과는 다음과 같다.

>>> AuthorProfile.objects.select_related('author', 'manager')
(0.001) | SELECT version(), @@sql_mode, @@default_storage_engine, @@sql_auto_is_null, @@lower_case_table_names,
	|                                                                               convert_tz('2001-01-01 01:00:00', 'UTC', 'UTC') IS NOT NULL

(0.001) | SET SESSION TRANSACTION ISOLATION LEVEL READ COMMITTED

(0.001) | SELECT `author_profile`.`id`,
	|        `author_profile`.`date_of_create`,
	|        `author_profile`.`date_of_update`,
	|        `author_profile`.`description`,
	|        `author_profile`.`author_id`,
	|        `author_profile`.`manager_id`,
	|        `author`.`id`,
	|        `author`.`date_of_create`,
	|        `author`.`date_of_update`,
	|        `author`.`first_name`,
	|        `author`.`last_name`,
	|        `author`.`email`,
	|        `author_manager`.`id`,
	|        `author_manager`.`date_of_create`,
	|        `author_manager`.`date_of_update`,
	|        `author_manager`.`manager_name`
	| FROM `author_profile`
	| INNER JOIN `author` ON (`author_profile`.`author_id` = `author`.`id`)
	| INNER JOIN `author_manager` ON (`author_profile`.`manager_id` = `author_manager`.`id`)
	| LIMIT 21
728x90
반응형