[Re:Django] 7. ORM과 QuerySet, QuerySet Method (1)

Magit·2020년 5월 5일
10

Django

목록 보기
7/13

[Re:Django] 4. 장고 ORM을 사용해서 DB CRUD 구현하기에서 간단하게 ORMrhk Queryset에 대해서 알아봤는데, 이번에는 조금더 자세하게 ORM과 Queryset에 대해서 알아보고자 한다.

Django ORM 이란?

ORM 알기위해서 먼저 SQL에 대해서 알아야한다. SQL(Structured query language)이란 관계형 데이터베이스에서 수정, 삭제 등의 동작을 할 때 사용되는 언어이다. MySQL같은 데이터베이스를 사용하기 위해서는 해당 SQL 언어를 잘 알아야한다.

ORM은 객체(Object)관계(Relational)연결(Mapper)해주는 것을 의미한다. 객체 지향적인 방법을 사용하여 데이터베이스의 데이터를 쉽게 조작할 수 있게 해준다. 즉, Django의 ORM은 파이썬과 데이터베이스의 SQL 사이의 통역사 역할을 해준다. SQL 쿼리문 없이도 데이터베이스의 데이터들을 다룰 수 있게되는데, Model Class를 통해서 객체를 만들고 이 객체를 통해서 DB에 접근하는 형식이다.

User.objects.filter(age=30) # Python Django

SELECT * FROM users WHERE age=30 # SQL

ORM의 장점

  • 불필요하게 많이 적지 않아도 되서 빠르게 개발이 가능하고 생산성이 좋아진다. 사소한것들을 자동으로 처리해줘서 개발자가 신경쓰지 않게 도와준다.
  • 선언문, 할당, 종료같은 부수적인 코드가 줄어들고, 객체에 대한 코드를 별도로 작성하다보니 가독성이 좋아진다.
  • 유지보수도 편리하고 코드의 재사용성이 높아진다.

ORM의 단점

  • 해당 프로그래밍 언어를 사용하지만 ORM 라이브러리는 따로 배워야한다.
  • 규모가 큰 프로젝트나 복잡한 프로젝트는 sql로 작성하는게 훨씬 좋을 수 있다.
  • 정확히 원리를 이해하지 않고도 쓸 수 있다보니 문제 대처능력이 떨어질 수 있다.
  • SQL 구문 생성을 추상화해서 구현했으니, 복잡한 쿼리는 오히려 비효율적으로 SQL 구문이 생성될 수 있다.

쿼리셋(QuerySet) 이란?

쿼리셋(QuerySet)은 전달받은 모델의 객체 목록을 말한다. 데이터베이스로부터 데이터를 읽고 필터를 걸거나 정렬 등을 할 수 있다. 리스트와 구조는 같지만 파이썬 기본 자료구조가 아니기에 읽고 쓰기 위해서는 자료형 변환을 해줘야한다. 쿼리셋은 데이터베이스의 여러 레코드(row)를 나타낸다.

from .models import Book
Book.objects.all()	# Book 모델(테이블)의 모든 데이터를 갖고와라!
<QuerySet [<Book: 책 제목1>, <Book: 책 제목2>]>

여기서 objectsModel Manager이다. DB와 Django Model 사이의 Query Operation(질의연산)의 인터페이스 역할을 한다. 이 때, objects 를 사용해서 다수의 데이터를 갖고오는 함수를 사용할 때, 반환되는 객체가 QuerySet이다.

이 Manager 각 모델(클래스)가 최소 하나씩 갖고 있다.
Person.objects 의 의미는, objects라는 이름의 manager가 Person 데이터베이스를 QuerySet 형태로 만들겠다는 의미이다. 그 QuerySet에서 데이터를 검색하게 만들 수 있다.



QuerySet Method 정리하기 (1)

Django Model API에는 기본적으로 제공하는 여러 쿼리 메서드들이 있는데, 자주 사용되는 메서드를 살펴보자.

.all()

  • 테이블 데이터를 전부 갖고온다. 즉, 해당 쿼리셋의 모든 요소를 반환한다. 현재 쿼리셋의 복사본을 반환한다고 볼 수 있다.

    >>> Person.objects.all()
    <QuerySet [<Person: 홍길동>, <Person: 임꺽정>]>

.values()

쿼리셋의 내용을 딕셔너리로 각 객체정보를 갖는 리스트 형태로 반환한다.

#1
>>> Blog.objects.filter(name__startswith='Beatles')
<QuerySet [<Blog: Beatles Blog>]>

#2
>>> Blog.objects.filter(name__startswith='Beatles').values()
<QuerySet [{'id': 1, 'name': 'Beatles Blog', 'tagline': 'All the latest Beatles news.'}]>

1번 코드를 통해 필터링을 해서 하나의 데이터가 추출되었다. 2번 코드는 1번 코드에서 .values() 를 추가했다. 추출된 데이터가 Blog 객체이고 이 객체의 내부 데이터를 딕셔너리 형태로 내보내주는 게 values() 가 한 일이다.

values()는 인자값으로 필드이름을 넘겨서 원하는 필드 정보만 딕셔너리로 구성할 수도 있다.

>>> Blog.objects.values()
<QuerySet [{'id': 1, 'name': 'Beatles Blog', 'tagline': 'All the latest Beatles news.'}]>

>>> Blog.objects.values('id', 'name')
<QuerySet [{'id': 1, 'name': 'Beatles Blog'}]>

ForeignKey로 설정되어 있는 필드에 대해서는 filed, filed_id 둘 다 같은 결과를 반환한다.

>>> Entry.objects.values('blog')
<QuerySet [{'blog': 1}, ...]>

>>> Entry.objects.values('blog_id')
<QuerySet [{'blog_id': 1}, ...]>

filter(), order_by()values()와 같이 쓰면 어느 순서든 같은 결과를 반환한다.

Blog.objects.values().order_by('id')
Blog.objects.order_by('id').values()

values()를 먼저 꺼내고 정렬하나, 정렬하고 values()를 꺼내나 같은 결과를 보여준다.

ManyToManyField나 역참조관계에서는 하나의 필드에 여러 데이터가 포함될 수 있기 때문에 values()를 쓰면 너무 많은 결과를 출력할 수 있다.

.filter()

특정 조건에 맞는 Row들을 갖고오기 위해서 사용되는 메소드이다.
괄호안에 lookup parameter(지정된 매개변수)와 일치하는 객체를 포함한 새로운 쿼리셋을 반환한다. 주로 특정 조건에 맞는 데이터를 추출하는데 사용하고 복잡한 쿼리셋을 만들이 위해 Q objects를 사용할 수 있다.

Post.objects.filter(title__startswith='First')
# 제목이 First 로 시작하는 post만 추출한다.

.exclude()

특정 조건을 제외한 나머지 Row들을 갖고오기 위해서 사용되는 메소드이다.
지정된 parameter(매개변수)와 일치하지 않는 객체를 포함한 QuerySet을 반환한다. filter()와 반대로 동작한다.

Post.objects.exclude(title__startswith='First')  
# 제목이 First로 시작하는 post만 제외하고 나머지를 추출한다.
Entry.objects.exclude(pub_date__gt=datetime.date(2005, 1, 3), headline='Hello')

# sql
SELECT ...
WHERE NOT (pub_date > '2005-1-3' AND headline = 'Hello')

괄호안에 동시에 조건을 적용하면 AND연산처럼 동작한다. 발행일자가 2005년 1월 3일 이후이면서 headline이 'Hello'인 entry만 제외하고 나머지를 추출한다.

Entry.objects.exclude(pub_date__gt=datetime.date(2005, 1, 3)).exclude(headline='Hello')

# sql
SELECT ...
WHERE NOT pub_date > '2005-1-3'
AND NOT headline = 'Hello'

괄호 밖에 새로 .exclude()를 지정하면 OR연산처럼 작동한다.
발행일자가 2005년 1월 3일 이후이거나 headline이 'Hello'인 모든 항목을 제외하고 추출한다.

.get()

하나의 Row만 갖고오기 위해서 사용되는 메소드이다.
.get()쿼리셋이 아니라 모델 객체를 반환하는 메소드이다. 특정 column 조건에 해당하는 결과를 객체로 반환하는 함수이다. 즉, .get()은 쿼리셋을 호출하는 것이 아니라서 뒤에 다른 메소드를 추가할 수 없다.

값은 1개만 불러온다. .filter()와 다른 점은 값이 없을 때 .filter()는 빈 쿼리셋을 불러 오고 .get()DoesNotExist라는 메시지를 띄운다. 그리고 해당하는 값이 한 개가 아닐 경우에는 MultipleObjectsReturned라는 메시지를 띄운다. 그러므로 try-except 구문 등으로 예외처리를 해주는 것이 좋다.

# 실제 해당하는 조건의 결과가 없을 경우 발생하는 예외
>>> Account.objects.get(user_account='james')
Traceback (most recent call last):
  File "<console>", line 1, in <module>
  File "/Users/YB/miniconda3/envs/django_tutorial/lib/python3.8/site-packages/django/db/models/manager.py", line 82, in manager_method
    return getattr(self.get_queryset(), name)(*args, **kwargs)
  File "/Users/YB/miniconda3/envs/django_tutorial/lib/python3.8/site-packages/django/db/models/query.py", line 415, in get
    raise self.model.DoesNotExist(
account.models.Account.DoesNotExist: Account matching query does not exist.
# try-except 사용예제
try:
    if Account.objects.filter(user_account=account_data['user_account']).exists():
        account = Account.objects.get(user_account=account_data['user_account'])

        if account.password == account_data['password']:
            return JsonResponse({'message':'Welcome back!'}, status=200)
        return HttpResponse(status=401)

      return HttpResponse(status=400)

    except KeyError:
        return HttpResponse(status=400)

.count()

데이터의 갯수 (row의 수)를 세기 위해 사용되는 메소드이다.
QuerySet과 일치하는 DB 객체의 수를 나타내는 정수를 반환한다.

# 데이터베이스에 있는 entry의 갯수를 반환한다.
Entry.objects.count()

# Returns the number of entries whose headline contains 'Lennon'
Entry.objects.filter(headline__contains='Lennon').count()

.count()SELECT COUNT(*) 를 수행하므로, 모든 레코드를 파이썬 객체에 로드하고 len() 를 호출하는 것보다는 count() 를 사용하는게 더 빠르다.(객체를 메모리에 로드하지 않을 때는 len()이 더 빠르다.)

.first()

데이터들 중 처음에 있는 row 만을 반환한다.
QuerySet과 일치하는 첫 객체를 반환하는데, 정렬을 정의하지 않으면 pk로 자동 정렬한다. order_by() 와 상호작용에 영향을 미칠 수 있다.

p = Article.objects.order_by('title', 'pub_date').first()

# 위 코드는 아래 코드와 동일하게 작동한다.(first()의 간편함을 이용하자.)
try:
    p = Article.objects.order_by('title', 'pub_date')[0]
except IndexError:
    p = None

.last()

데이터들 중 마지막에 있는 row 만을 반환한다.
QuerySet과 일치하는 마지막 객체를 반환하는데, 그외에는 first()와 동일하다.

참고사이트
_CHAEN_ 님 블로그
Jo Seung Hyun님의 글
초보몽키님의 개발블로그
Doky's Blog - 강추
QuerySet API reference
ybear90 님 블로그
QuerySet (쿼리셋)
django - queryset과 기본 검색 방법

profile
이제 막 배우기 시작한 개발자입니다.

3개의 댓글

comment-user-thumbnail
2021년 7월 22일

.all()에 대해 질문이 있습니다. "해당 쿼리셋의 모든 요소를 반환한다. 현재 쿼리셋의 복사본을 반환한다고 볼 수 있다. " 라고 쓰여있는데, '쿼리셋의 모든요소', '현재 쿼리셋의 복사본'이라는건 무슨 뜻인가요? 복사본이라면 쿼리셋의 오리지널(?)도 있다는 뜻 같은데, 무슨 차이인지 잘 모르겠습니다;
가르쳐 주실 수 있나요?

답글 달기

대단히 감사합니다. 이 페이지 지워지면 울면서 장고 포기할지도 모릅니다. 오래오래 보존되면 좋겠습니다. 감사한 마음으로 잘 읽고 배웠습니다!

답글 달기
comment-user-thumbnail
2024년 6월 28일

잘 읽었습니다~!

SELECT ...
WHERE NOT (pub_date > '2005-1-3' AND headline = 'Hello')

위 구문이 OR 조건이라 다음과 같이 표현할 수 있고:

SELECT ...
WHERE pub_date <= '2005-1-3' OR headline != 'Hello'

SELECT ...
WHERE NOT pub_date > '2005-1-3'
AND NOT headline = 'Hello'

위 구문이 AND 조건이라 다음과 같이 표현할 수 있지 않은가요~?

SELECT ...
WHERE pub_date <= '2005-1-3'
AND headline != 'Hello'

답글 달기