본문 바로가기

Pyhon/Django

[Pyhon/Django] 장고 기초#009. 장고 ORM 쿼리셋(Queryset)

ORM(Object Relational Mapping)

ORM은 객체와 RDS(관계형 데이터베이스)를 자동으로 연결(Mapping)해주는 것을 의미합니다. 객체는 클래스(models.py)를 사용하며, RDS(sqlite)는 테이블을 사용합니다. ORM을 사용하면 기본적으로 OOP 코드를 제공하므로 비즈니스 로직에 더 집중 할 수 있도록 도와줍니다. 이는 장고의 가장 기본적은 특징 중 하나입니다.

쿼리셋(Queryset)

쿼리셋을 사용하면 ORM에서의 객체를 직접적으로 핸들링 할 수 있습니다. 쿼리셋으로 모델의 정보를 데이터베이스(sqlite)에서 읽고, 쓰고, 수정 할 수 있습니다. 쿼리셋을 사용하기 위해서, 다음과 같이 터미널에서 장고 인터렉티브 콘솔(Interactive console)로 진입합니다.

$ python3 manage.py shell
Python 3.9.9 (main, Nov 30 2021, 17:19:48) 
[Clang 10.0.0 (clang-1000.10.44.4)] on darwin
Type "help", "copyright", "credits" or "license" for more information.
(InteractiveConsole)

인터렉티브 콘솔에서는 파이썬 프롬프트(Python prompt) 기능과 장고의 쿼리셋과 같은 기능까지 모두 사용 할 수 있습니다. 인터렉티브 콘솔을 사용하여 간단한 쿼리셋 몇 가지를 사용해보도록 하겠습니다.

객체 조회(all)

우리가 작성한 Post 모델 데이터를 전체 출력합니다. objects.all() 쿼리셋을 사용하면 되며, 첫 실행 시 아래와 같은 에러가 발생합니다.

>>> Post.objects.all()
Traceback (most recent call last):
  File "<console>", line 1, in <module>
NameError: name 'Post' is not defined

인터렉티브 콘솔 역시 인터프리터(Interpreter) 기반이기 때문에 쉘을 실행한 시점부터 접근하기 위한 모듈을 import해야 합니다. Post 모듈을 import하고, 다시 object.all() 쿼리셋을 실행하면 Post 모델에 대한 데이터베이스 테이블에 생성되어 있는 데이터 리스트를 확인 할 수 있습니다. 저는 아무것도 생성한 것이 없기 때문에 빈 리스트([])를 출력하고 있습니다. 

>>> from blog.models import Post
>>> Post.objects.all()
<QuerySet []>

 

객체 생성(create), 세부 조회(values)

모델의 객체(데이터)를 생성하기 위해서는 objects.create() 쿼리셋을 사용합니다. 다음은 Post 모델에 새로운 데이터를 작성하는 명령어입니다.

>>> Post.objects.create(author=me, title='Sample Title', text='Test')
Traceback (most recent call last):
  File "<console>", line 1, in <module>
NameError: name 'me' is not defined

이번에는 me라는 author이 존재하지 않는다는 에러가 발생합니다. Post 모델에 대한 클래스 정의를 확인하면, author 변수(데이터베이스 필드, 열)에 해당하는 속성이 사용자 계정(User)에 대해서 외래키로 지정되어 있는 것을 확인 할 수 있습니다.

class Post(models.Model):
    author = models.ForeignKey(settings.AUTH_USER_MODEL, on_delete=models.CASCADE)

우리는 장고 관리자(Administraion, http://127.0.0.1:8000/admin/)에 처음 접근 할 때 생성한 계정(https://namepgb.tistory.com/8)이 있습니다. 이 계정 역시 User 모델에 포함되며, 이를 확인하기 위해서 아래 절차를 따라 진행합니다. User 모듈에 접근하기 위해서 import하고, User 모델에 대한 데이터를 전부 출력(objects.all())합니다. User 모델에 대한 데이터를 상세하게 확인하기 이해서 objects.*.values() 쿼리셋을 사용 할 수도 있습니다. 저의 경우 username이 admin인 User 데이터가 존재하는 것을 확인 할 수 있습니다. 이 객체를 me 변수에 할당하고, me를 다시 출력해보면 데이터가 변수에 정상적으로 할당된 것을 확인 할 수 있습니다.

>>> from django.contrib.auth.models import User
>>> User.objects.all()
<QuerySet [<User: admin>]>
>>> User.objects.all().values()
<QuerySet [{'id': 1, 'password': 'pbkdf2_sha256$260000$2kAhQkfSEi1KPTzuFgDowi$ijaBi1bcx7u1gXrQ2ecDv0WqVB4jXwEeBdHNGH9Pitc=', 'last_login': datetime.datetime(2021, 12, 28, 18, 13, 40, 572840, tzinfo=<UTC>), 'is_superuser': True, 'username': 'admin', 'first_name': '', 'last_name': '', 'email': '', 'is_staff': True, 'is_active': True, 'date_joined': datetime.datetime(2021, 12, 12, 21, 2, 19, 178873, tzinfo=<UTC>)}]>
>>> me = User.objects.get(username='admin')
>>> me
<User: admin>

이제 변수 me에 할당 된 User 객체를 이용해서 다시 Post 객체를 생성하도록 하겠습니다. 이전과 차이점은 author(외래키를 사용하는 필드)에 실제하는 User 객체가 할당되었다는 점이며, 이번에는 Post 객체가 잘 생성되는 것을 확인 할 수 있습니다.

>>> Post.objects.create(author=me, title='Sample Title', text='Test')
<Post: Sample Title>
>>> Post.objects.all()
<QuerySet [<Post: Sample Title>]>

 

객체 검색(filter, get)

특정 필드를 조건문으로 검사하여 조건에 일치하는 객체만 반환 받을 수 있습니다. objects.filter() 쿼리셋을 사용하면 되며, 다음과 같이 Post 모델에 대해서 필터를 걸 수 있습니다.

>>> Post.objects.filter(title='Sample Title').values()
<QuerySet [<Post: Sample Title>]>

객체를 검색하는 다른 방법은 objects.get() 쿼리셋을 사용하는 것입니다.

>>> Post.objects.get(title='Sample Title')
<Post: Sample Title>

두 쿼리셋은 동일한 결과를 반환하는 것같지만, 반환 된 데이터 타입이 하나는 쿼리셋(filter)이고 다른 하나는 Post 객체(get)에서 차이가 있습니다. 쿼리셋을 다시 반환하는 filter 쿼리셋을 사용하면, filter 결과를 이용하여 다른 쿼리셋을 이어서 호출 할 수 있습니다. 이를 Chaining이라고 부르며, 일반적으로 프로그래밍에서 사용하는 Method chaining 패턴과 동일합니다. 아래는 filter 직후 검색 데이터에 대해서 다시 한 번 values 쿼리셋을 실행하고 있습니다.

>>> Post.objects.filter(title='Sample Title').values()
<QuerySet [{'id': 3, 'author_id': 1, 'title': 'Sample Title', 'text': 'Test', 'created_date': datetime.datetime(2022, 1, 1, 10, 8, 47, 508928, tzinfo=<UTC>), 'published_date': None}]>

반면 get 쿼리셋에서 연이어 다른 쿼리셋을 사용하려고 하면 오류가 발생합니다. get 쿼리셋에 대한 리턴 값은 Post 객체이기 때문입니다.

>>> Post.objects.get(title='Sample Title').values()
Traceback (most recent call last):
  File "<console>", line 1, in <module>
AttributeError: 'Post' object has no attribute 'values'

반면 Post 객체를 직접 리턴받는 get 쿼리셋을 이용하면, 해당 객체가 포함하고 있는 멤버 함수를 호출 할 수 있다는 것에서 장점이 있습니다.

>>> Post.objects.filter(title='Sample Title').values('published_date')
<QuerySet [{'published_date': None)}]>

Post 모델에 대해서 published_date 값을 출력하면, 우리가 인터렉티브 콘솔에서 직접 생성(create)한 객체는 published_date에 값이 할당되어 있지 않은 것을 확인 할 수 있습니다. 우리는 Post 클래스를 정의할 때 publish 함수를 작성하였는데, 다음과 같이 get 쿼리셋으로 객체를 검색하여 publish 함수를 호출함으로써 published_date에 값을 할당 할 수 있습니다.

>>> Post.objects.get(title='Sample Title').publish()
>>> Post.objects.filter(title='Sample Title').values('published_date')
<QuerySet [{'published_date': datetime.datetime(2022, 1, 1, 11, 0, 32, 467278, tzinfo=<UTC>)}]>

다음 문서에서는 장고의 쿼리셋과 템플릿 기능 두 가지를 결합하여 뷰에서 동적인 html 문서를 생성(렌더링)해보도록 하겠습니다.