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

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” 라는 데이터베이스를 만들고 있었는데, addressphone number 같이 매우 보편적인 것들을 데이터베이스에 만드려고 하는 상황이 올지 모릅니다.


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


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

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


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


restaurant 모델을 만들어 줄때에 addressphone 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>]>

데이터베이스 리뷰