당신은 멋쟁이, 우리는 장고쟁이~

0%

Django Models 9편 - One-to-One

Models - Relationships (One-to-One)


one-to-one relationships


one-to-one 관계, 즉 1대 1 관계를 정의하기 위해서는,


OneToOneField를 사용해 줍니다. 다른 필드 타입을 사용하는것과 비슷하게, 모델 클래스 안에 클래스 속성으로 정의해 줍니다.


OneToOneField 는 위치 인자를 필요로 합니다.


OneToOneField 는 연결된 모델 클래스를, 필수 위치 인자로 받습니다


예를들어, “places” 라는 데이터베이스를 만들고 있었는데, address 나 phone number 같이 매우 보편적인 것들을 데이터베이스에 만드려고 하는 상황이 올지 모릅니다.


places 라는 장소위에 restaurant 이라는 데이터베이스를 만들고 싶으면, Place 를 만들때 포함하였던


데이터베이스 필드들을 Restaurant 모델에 반복하여 생성해 주기 보다는,

Restaurant 이 OneToOneField 를 Place 에 갖게 하면 됩니다.


이게 무슨 이야기냐면,,, 하나의 장소 (Place)는 address 나 phone number 같은 보편적인 정보를 가지고 있을지 모릅니다. 그런데 만약, 하나의 장소 위에 식당 (restaurant) 을 만들고 싶다면?


restaurant 모델을 만들어 줄때에 address 나 phone number 같은 보편적인 필드들을 다시 정의하지 않고,


Restaurant 이 OneToOneField 를 사용하여 Place 모델과 연결되면 됩니다.



사실, Restaurant 도 하나의 Place 이기 때문에, 이런 상황을 해결 하기 위해서는 보통 상속 개념을 사용합니다.

클래스를 상속 하면, 상속받는 클래스의 필드와 메서드들을 사용할수 있다는것을 숙지해둡시다.


데이터베이스 상속은 암묵적으로, one-to-one 관계를 형성합니다.


OneToOneField 예시


1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
from django.db import models 

# Create your models here


class Place(models.Model):
name = models.CharField(max_length=50)
address = models.CharField(max_length=80)

def __str__(self):
return "%s the place" % self.name


class Restaurant(models.Model):
# OneToOneField 의 첫번째 인자로, Place 클래스를 받음
place = models.OneToOneField(Place, on_delete=models.CASCADE, primary_key=True)
serves_hot_dogs = models.BooleanField(default=False)
serves_pizza = models.BooleanField(default=False)

def __str__(self):
return "%s the restaurant" % self.place.name

class Waiter(models.Model):
restaurant = models.ForeignKey(Restaurant, on_delete=models.CASCADE)
name = models.CharField(max_length=50)

def __str__(self):
return "%s the waiter at %s" % (self.name, self.restaurant)



위의 예시 모델에서, 하나의 장소 Place 에는 하나의 Restaurant 이 존재하고, 이는 OneToOneField 로 연결되어 있습니다. 하나의 Restaurant 에는 ForeignKey 로 연결되어 있는, Waiter 라는 모델이 존재합니다.


이는, 하나의 Restaurant 에 일하는 웨이터가 여러명 있을수 있는 관계를 모델링 한것입니다.

OneToOneField 는 선택 인자로 parent_link 를 받습니다.


Python API 사용해보기


장소 (Place) 2 곳을 생성해 줍니다.


1
2
3
4
5
6
7
In [1]: p1 = Place(name="명랑핫도그", address="성남시 분당구 정자1동")                                                                                                    

In [2]: p1.save()

In [3]: p2 = Place(name="피자헛", address="성남시 분당구 금곡동 불정로 87")

In [4]: p2.save()

식당을 생성해 주고, 부모 객체를 해당 객체의 primary key 로 전달해줍니다.


1
2
3
In [5]: r = Restaurant(place=p1, serves_hot_dogs=True, serves_pizza=False)                                                                                                

In [6]: r.save()

r 은 Restaurant 의 인스턴스 이니까, 하나의 Restaurant 인스턴스는, 해당 장소에 접근할수 있습니다.


1
2
In [7]: r.place                                                                                                                                                           
Out[7]: <Place: 명랑핫도그 the place>

반대로, 하나의 Place 는 restaurant 에 접근할수 있습니다.


1
2
In [8]: p1.restaurant                                                                                                                                                     
Out[8]: <Restaurant: 명랑핫도그 the restaurant>

p2 는 연결된 restaurant 이 아직 없습니다.

따라서, 아래 코드를 shell 에서 실행 하면, p2 에는 restaurant 이 없다고 나옵니다


1
2
3
4
5
6
7
8
9
10
In [9]: from django.core.exceptions import ObjectDoesNotExist                                                                                                             

In [10]: try:
...: p2.restaurant
...: except ObjectDoesNotExist:
...: print("There is no restaurant here.")
...:
# Shift + Enter 를 실행하면, 실행됨.

There is no restaurant here.

hasattr 메서드를 사용하여, 예외처리를 하지 않아도 됩니다.

  • hasattr 는 파이썬 내장함수 입니다.
  • 객체가 주어진 이름의 어트리뷰트를 가지고 있을때 True 를 반환하고, 그렇지 않을때에 False 를 반환합니다.
  • hasatt(object, name)

1
2
In [11]: hasattr(p2, 'restaurant')                                                                                                                                        
Out[11]: False

할당 기호 (=) 를 사용하여, place 를 설정해 줍니다. place 는 Restaurant 의 primary key 이므로, save 는 새로운 restaurant 을 생성합니다


1
2
3
4
5
6
7
8
9
10
11
12
# 위에서 r = Restaurant(place=p1, serves_hot_dogs=True, serves_pizza=False)
# r.place = p2 로 바꿔줍니다.


In [12]: r.place = p2
In [13]: r.save()

In [14]: p2.restaurant
Out[14]: <Restaurant: 피자헛 the restaurant>

In [15]: r.place
Out[15]: <Place: 피자헛 the place>

Place 를 다시 역으로 바꿔줍니다.


1
2
3
4
In [16]: p1.restaurant = r                                                                                                                                                

In [17]: p1.restaurant
Out[17]: <Restaurant: 명랑핫도그 the restaurant>

One-To-One 관계가 할당 되기 이전에

객체가 저장 되어 있어야 ValueError 가 나질 않습니다.


1
2
3
4
5
6
7
In [18]: p3 = Place(name='Daemon Dogs', address='944 W. Fullerton')                                                                                                       

In [19]: Restaurant.objects.create(place=p3, serves_hot_dogs=True, serves_pizza=False)
---------------------------------------------------------------------------
ValueError Traceback (most recent call last)
<ipython-input-19-18b4cdec5239> in <module>
----> 1 Restaurant.objects.create(place=p3, serves_hot_dogs=True, serves_pizza=False)

Restaurant.objects.all() 은 Restaurants 만 반환합니다. Place 는 반환하지 않습니다.

Place.objects.all() 은 Places 들을 반환합니다. Restaurant을 가지고 있던 안있던, 결과를 반환합니다.


1
2
3
4
5
In [20]: Restaurant.objects.all()                                                                                                                                         
Out[20]: <QuerySet [<Restaurant: 명랑핫도그 the restaurant>, <Restaurant: 피자헛 the restaurant>]>

In [21]: Place.objects.order_by('name')
Out[21]: <QuerySet [<Place: 명랑핫도그 the place>, <Place: 피자헛 the place>]>

관계간 룩업들을 사용하여, 해당 모델에 쿼리문을 작성할수 있습니다.


1
2
3
4
5
6
7
8
9
10
11
12
13
In [22]: Restaurant.objects.get(place=p1)                                                                                                                                 
Out[22]: <Restaurant: 명랑핫도그 the restaurant>

In [23]: Restaurant.objects.get(place__pk=1)
Out[23]: <Restaurant: 명랑핫도그 the restaurant>

In [24]: Restaurant.objects.filter(place__name__startswith="명")
Out[24]: <QuerySet [<Restaurant: 명랑핫도그 the restaurant>]>

In [25]: Restaurant.objects.exclude(place__address__contains="헛")
Out[25]: <QuerySet [<Restaurant: 명랑핫도그 the restaurant>, <Restaurant: 피자헛 the restaurant>]>

# 한글은 조회 필터가 안걸리는지, exclude 를 했는데도. 조회가 되서 나옵니다;;;;

물론 거꾸로도 작동 합니다.


1
2
3
4
5
6
7
8
9
10
11
In [27]: Place.objects.get(pk=1)                                                                                                                                          
Out[27]: <Place: 명랑핫도그 the place>

In [28]: Place.objects.get(restaurant__place=p1)
Out[28]: <Place: 명랑핫도그 the place>

In [29]: Place.objects.get(restaurant=r)
Out[29]: <Place: 명랑핫도그 the place>

In [30]: Place.objects.get(restaurant__place__name__startswith="명")
Out[30]: <Place: 명랑핫도그 the place>


Waiter 를 Restaurant 에 추가해 줍니다.

Waiter는 Restaurant 에 ForeignKey 로 등록 되어 있습니다.


1
2
3
4
In [31]: w = r.waiter_set.create(name="장고쟁이")                                                                                                                         

In [32]: w
Out[32]: <Waiter: 장고쟁이 the waiter at 명랑핫도그 the restaurant>

웨이터들을 쿼리를 통해 조회합니다.


1
2
3
4
5
In [33]: Waiter.objects.filter(restaurant__place=p1)                                                                                                                      
Out[33]: <QuerySet [<Waiter: 장고쟁이 the waiter at 명랑핫도그 the restaurant>]>

In [34]: Waiter.objects.filter(restaurant__place__name__startswith="명")
Out[34]: <QuerySet [<Waiter: 장고쟁이 the waiter at 명랑핫도그 the restaurant>]>

데이터베이스 리뷰