Models


장고의 models.Model 클래스를 활용하면 직접 SQL을 사용하지 않고도 파이썬에서 객체 타입을 선언하고 DB와 연결시킬 수 있다. models.Model을 상속한 모델 클래스를 만들면 장고가 제공하는 ORM(object-relational mapping)을 이용할 수 있다.
models.Model을 상속한 클래스 내부에는 필드를 정의할 수 있고, 이 필드들도 models 라이브러리의 세부 속성으로 선언할 수 있다.
DB와 연결되었을 때 하나의 클래스는 하나의 테이블이 되고, 클래스 내부의 필드들은 각 테이블 내부의 컬럼들이 된다.

다음은 장고 모델 클래스의 예시이다.

from django.db import models

class User(models.Model):
    id = models.AutoField()
    name = models.CharField()
    age = models.IntegerField()


User 클래스는 장고와 연결된 DB에서 user 테이블이 되고, id, name, age 속성들은 user 테이블 내부의 id, name, age라는 이름의 컬럼들이 된다. 참고로 실제 테이블 이름은 user이 아닐 수 있다.
장고에서는 테이블 이름을 지을 때 기본으로 사용되는 규칙이 있어서 별도의 이름을 지정하지 않으면 그 규칙대로 이름이 지정되고, 원하는 이름이 있으면 커스텀 속성으로 지정할 수 있다.

보통은 'app이름_model이름' 으로 테이블 이름이 지정된다. 만약 User 클래스가 account라는 app 내부에서 정의되었다면, 기본적으로 해당 테이블은 account_user이 된다. 직접 테이블 이름을 지정할 수도 있다. 각 모델 클래스의 내부에는 Meta 클래스를 선언하여 테이블 자체와 관련된 정보를 설정할 수 있다.
db_table 속성에다가 지정하고 싶은 테이블 이름을 입력하면 된다. 아래와 같은 경우 DB에 저장되는 테이블 이름도 User가 된다.

from django.db import models

class User(models.Model):
    id = models.AutoField()
    name = models.CharField()
    age = models.IntegerField()
    
    class Meta:
        db_table = "User"


장고에서 모델 클래스(models.Model을 상속한 클래스)를 정의하면 기본적으로는 DB에 테이블이 생기고, 변경되는 점들이 있으면 마이그레이션(migration)을 통해 연결되는 것이 대부분이다. 그러나 Meta 클래스에서 다른 옵션을 선택하면 이 규칙들을 바꿀 수 있다.


1) abstract = True

장고에는 추상 모델(abstract model)이 있다. 이렇게 선언하면 장고 내부에서는 해당 클래스를 모델로 인식하지만, 연동된 데이터베이스에 테이블이 생기지는 않는다.
보통은 추상 모델 클래스를 만들어 놓고, 다른 모델에서 해당 클래스를 상속하는 방식으로 사용한다. 한 모델 클래스를 상속받는 다른 모델 클래스는 그 모델 클래스에 정의된 필드를 사용할 수 있다.


2) managed = False

장고에서 모델 클래스에 변경사항이 있으면 마이그레이션 커맨드(makemigrations, migrate 등)를 통해 반영할 수 있는 이유는 테이블에서는 이 옵션이 기본으로 managed=True로 되어 있기 때문이다. 그러나 이 옵션을 False로 저장하면 해당 모델을 추가하거나 내부 필드를 변경해도 장고와 연결된 DB에 변경사항이 반영되지 않는다. 변경사항을 반영하려면 직접 해당 DB에 SQL문을 입력해야 한다.
이 옵션은 주로 해당 모델이 장고 프로젝트 내에서만 사용되는 모델이 아닐 때 등에 사용된다.


3) proxy = True

이 속성을 지정한 모델은 기존에 있던 다른 모델의 프록시 모델이 된다. 즉 다른 모델과 똑같이 작용한다고 보면 된다.

from django.db import models

class User(models.Model):
    # contents
    
class Person(User):
    # contents
    
    class Meta:
        proxy = True


모델 Person이 모델 User의 프록시 모델이라고 해 보자. User.objects.all()과 Person.objects.all()은 모두 같은 결과를 리턴한다.
프록시 모델을 선언하는 이유 중 하나는 DB와 장고 모델과의 연결은 그대로 두고, User 모델이 장고 내부에서 작동하는 방식이나 메소드를 바꾸고 싶을 때이다. 기본 모델인 User와 달리 Person에서는 정렬(ordering) 방식을 다르게 한다던가, 내부 메소드를 추가로 정의하고 싶을 때 프록시 모델을 사용한다.

 

Managers

 

쿼리 등으로 장고의 모델과 데이터베이스 사이에 통신이 일어날 때, 모델과 데이터베이스가 직접적으로 통신하지 않는다. 그 사이에는 Manager 클래스가 있다.
이 manager 클래스는 한 모델마다 최소 1개 이상이 있다. 즉 1:M의 관계다. 기본적으로는 1개이지만 원한다면 하나의 모델에 여러 개의 매니저 클래스를 둘 수 있다.
우리가 알고 있는 기본 매니저 클래스가 바로 objects이다. 항상 쿼리를 날릴 때 Model.objects.all() 이라고 썼었는데, 그 objects가 바로 매니저 클래스였던 것이다.

매니저 클래스는 쿼리셋을 만드는 방식이나 쿼리셋의 결과를 다르게 하고 싶을 때에도 사용한다.
예를 들면 Person 모델에는 employee_type이 crew, chef, administrator인 세 종류의 직원이 있다고 해 보자. 만약 employee_type이 crew인 직원들 안에서만 쿼리를 수행하고 싶다면 매번 filter() 메소드를 거는 방법도 있지만, 아예 crew_members 라는 매니저 클래스를 생성하고 필터링된 쿼리를 적용한다면 더 편하고 직관적으로 쿼리를 작성할 수도 있다.

# models.py
from django.db import models

class Person(models.Model):
    # contents
    objects = models.Manager()
    crew_objects = PersonCrewManager()
    administrator_objects = PersonAdministratorManager()
    chef_objects = PersonChefManager()
    
class PersonCrewManager(models.Manager):
    def get_queryset(self):
        return super().get_queryset().filter(employee_type="crew")
    
class PersonAdministratorManager(models.Manager):
    def get_queryset(self):
        return super().get_queryset().filter(employee_type="administrator")
        
class PersonChefManager(models.Manager):
    def get_queryset(self):
        return super().get_queryset().filter(employee_type="chef")
# example.py
from models import Person

Person.objects.all()
Person.crew_objects.all()
Person.administrator_objects.all()
Person.chef_objects.all()

 

 

QuerySet

 

장고 모델에서 DB에 연결하여 정보를 가져오기 위해서 사용하는 것이 쿼리셋이다. 장고 문법으로 쿼리를 작성하면 해당 쿼리셋은 DB의 SQL로 변환되어 DB에 전달되고, DB에서 나온 데이터는 다시 장고에서 쿼리셋으로 변환된다. 

 

예를 들어서 다음과 같은 쿼리를 작성하면, DB에는 이에 해당하는 SQL문이 입력된다. 

Person.objects.get(id=1)
SELECT * FROM myapp_person WHERE id = 1;

 

사실 쿼리셋은 코드가 실행되자마자 SQL에서 데이터를 가져오지는 않는다. 즉 쿼리셋은 작성되는 시점과 데이터를 가져오는 시점(evaluate 되는 시점)이 다르다. 그래서 'querysets are lazy'라는 말이 있다. 

변수에 쿼리셋을 할당할 때는 DB를 거치지 않고, 결과물을 콘솔 등으로 print()해서 보여줄 때 DB를 거쳐서 결과를 가져오게 된다. 

 

또한 쿼리는 캐싱을 사용한다. 한 번 evaluate 된 결과를 다음에 사용할 때는 또 데이터베이스를 거치지 않고(no hit), 캐싱해둔 데이터를 사용한다는 것이다. 그런데 그 결과값을 변수에 할당해 두지 않으면 같은 쿼리 결과를 사용할 때도 데이터베이스를 계속 hit 하게 된다. 

 

또한 한 모델 테이블과 연결된 여러 모델의 데이터도 select_related()prefetch_related()를 통해서 한 번에 불러올 수 있다. 이는 특히 for loop 등으로 반복적인 작업을 계속할 때 효율적이다. 

해당 모델이 참조하는 모델 데이터를 불러올 때는 select_related(), 해당 모델을 참조하는 모델 데이터를 불러올 때는 prefetch_related()를 불러온다. many-to-many 관계의 모델 데이터도 prefetch_related()로 불러올 수 있다. 

 

캐싱이 사용되는 경우 -> 효율적

people = Person.objects.select_related("company")		# Person, Company 데이터를 모두 가져옴
for person in people:
    person.company.is_valid = True		# 여기서는 database hit 발생 X

캐싱이 사용되지 않는 경우 -> 비효율적

people = Person.objects.all()		# 여기서 Person 모델 데이터만 가져옴
for person in people:
    person.company.is_valid = True		# 한 번 실행될 때마다 database hit

 

 

복잡한 쿼리셋

 

쿼리셋에서 filter(), exclude() 등으로 여러 개의 조건을 걸 수 있는데, 간혹 복잡하거나 구체적인 조건도 설정할 수 있다. 

 

1) field lookups

말 그대로 필드의 값과 동일한 값만 찾는 게 아니라, 숫자일 경우 gt, gte, lt, lte 등으로 작거나 큰 값을 조회할 수 있다. 문자열인 경우는 contains, iexact 등으로 특정 문자열을 포함하거나 대소문자를 구분하지 않은 결과들도 조회할 수 있다. 이런 field lookup등의 경우 필드 이름 뒤에 '__'을 붙인다. 

더 복잡한 field lookup도 있다. '__'을 붙이면 해당 모델과 관계 있는 다른 모델에서의 검색도 가능하다. 이 FK 검색은 연쇄적으로 계속 일어날 수 있는데, 실제로 이렇게 쿼리를 작성할 경우 SQL문에서는 JOIN 문을 사용해서 결과를 가져온다고 한다. 

# Person의 employee_type 필드값에 'c'를 포함한 모든 레코드 조회
Person.objects.filter(employee_type__contains="c")

# Person의 foreignkey인 Company 모델의 name 필드값에 'a'를 포함한 모든 레코드 조회
Person.objects.filter(company__name__contains="a")

 

2) Q()

Q() 안에는 filter(), exclude()에 사용하는 것처럼 조건문이 온다. 다만 Q()를 여러 개 사용해서 AND, OR, XOR 등 다양한 조건이 교차하는 필터링을 할 수 있다. 기존의 chaining 방법으로는 모든 조건들을 전부 만족하는 쿼리만 불러올 수 있지만 Q()를 사용하면 조건들을 다양하게 선언할 수 있다. 

# Person 중 name에 'peter'이 포함되고 27세 이상인 레코드 조회
Person.objects.filter(Q(name__contains="peter") | Q(age__gte=27))

 

3) F()

F()를 사용하면 기존의 다른 쿼리와는 달리 여러 필드들의 값을 비교할 수 있다. 기존에는 한 필드의 값에만 조건을 걸 수 있었다. 하지만 F()를 사용하면, 예를 들면 두 필드값을 비교하는 등의 필터링도 가능하다. 또한 필드값을 가져와서 연산 후에 조건을 거는 것도 가능하다. 

# House 레코드 중 number_of_people 필드와 number_of_cars 필드값이 같은 레코드만 조회
House.objects.filter(F("number_of_people") == F("number_of_cars"))

# House 레코드 중 price가 annual_people_income 필드값의 2배 이상인 레코드만 조회
House.objects.filter(price__gt=2*F("annual_people_income"))

 

4) Aggregation

또한 aggregate()를 사용하면 레코드의 총 개수, 특정 필드의 총합 등 특정 값을 추가해서 계산할 수 있다. Count, Avg, Max, Sum 등 다양한 기본 함수들을 사용할 수 있다. 

# 전체 Person 레코드의 개수 조회
Person.objects.aggregate(Count("id"))

 

5) Annotate

annotate()를 사용하면 쿼리셋에 포함되는 각 인스턴스 별로 aggregate()와 같은 쿼리를 생성할 수 있다. 

# 피자에 들어가는 토핑의 개수를 같이 조회
pizzas = Pizza.objects.annotate(number_of_toppings=Count("toppings")

# 첫 번째 피자에 들어간 토핑의 개수
pizzas[0].number_of_toppings

 

'server-side > Django' 카테고리의 다른 글

django routers  (1) 2023.12.21
django apps  (0) 2023.12.20
python - poetry 사용하기  (0) 2023.07.12
signals  (0) 2023.06.21
Model.select_related() vs Model.prefetch_related()  (0) 2022.07.11

+ Recent posts