장고 시작하기 10. ORM으로 장고 CRUD

들어가며

모든 백엔드의 아마 기초이자 핵심이 CRUD 이지 않을까 싶습니다. (아직 초보자라서 그래 보이는 걸지도…?) CRUD는 아시다시피 생성(Create), 읽기(Read), 업데이트(Update), 삭제(Delete)를 의미하며 데이터베이스의 데이터에 대해 수행할 수 있는 4가지 기본 작업을 나타내는데요. 이번 포스트에서는 장고 CRUD 는 어떻게 진행하는지 공부한 내용을 정리해보았습니다.

장고 CRUD 에 앞서

저희는 이전 포스트에서 클래스로 모델을 아래와 같이 만들고 migraion까지 완료했습니다.

models.py

from django.db import models

# 하나의 클래스가 하나의 테이블
class Articles(models.Model):
    # 필드명 = models.필드타입(옵션)
    title = models.CharField(max_length=50)
    content = models.TextField()
    created_at = models.DateTimeField(auto_now_add=True)
    updated_at = models.DateTimeField(auto_now=True)

그리고 파이썬 코드로 데이터베이스의 CRUD를 해줄 수 있게 하는 게 앞서 말한 ORM의 역할이라고 할 수 있습니다.

이번 포스트에서는 Django Shell 환경에서 CRUD를 연습해 보겠습니다.

Django Shell이란?

장고 DB 조회해보기

저희가 선언한 Articles라는 클래스를 이용해서 Articles DB를 조작할 수 있게 됩니다.

우선 조회부터 해보겠습니다.

objects.all()라는 Database API를 이용해서 전체 데이터를 조회할 수 있습니다

Articles.objects.all()

그러면 조회 결과를 QuerySet으로 반환해주는데요. 이는 리스트와 유사하게 인덱스로도 접근할 수 있는 자료형입니다.

현재는 아무런 데이터가 없기 때문에 빈 QuerySet이 반환 됨을 알 수 있습니다.

장고 DB 레코드 생성하기

레코드 생성 방법 1

아래와 같은 방법으로 Articles 모델에 레코드를 생성할 수 있습니다.

article = Articles(title='title test', content='This is first content test')
article.save()

레코드 생성 후, 다시 조회하면 이전과 달리 빈 QuerySet이 아닌 무언가 내용이 있는 것을 확인 할 수 있습니다.

레코드의 각 필드명은 닷(.)으로 조회할 수 있습니다.

article.title
article.content
article.created_at
article.id

레코드 생성 방법 2

Articles 모델에서 object.create() Database API를 사용해서 레코드를 생성할 수도 있습니다.

해당 방법은 save() 메소드가 추가로 필요하지 않습니다.

Articles.objects.create(title='제목 테스트', content='매니저를 사용하는 방법')
# 해당 방법은 save() 메소드가 추가로 필요하지 않습니다.

레코드 조회 출력 방식 변경하기

Model.objects.all() 과 같은 명령어로 레코드를 조회하면 QuerySet으로 결과를 반환한다는 것을 이전에도 살펴보았습니다. 그런데 터미널에서 조회 결과를 보기에 불편한 점이 있습니다.


Articles: Articles object (1) 처럼 실제 어떤 내용을 지닌 데이터가 조회 되었는지 한 눈에 파악하기 힘듭니다.

이를 개선하기 위해서 models.py에서 모델 클래스를 변경해 주면 됩니다.

__str__ 매직 메소드를 추가해주면 되는 겁니다.

(model을 수정하기는 했지만 DB 단에서의 수정사항이 아닌 파이썬으로 가져오는 부분만 변경했으므로 코드 수정 후 migration을 추가 진행할 필요는 없습니다.)

from django.db import models

# Create your models here.
# 하나의 클래스가 하나의 테이블

class Articles(models.Model):
    # 필드명 = models.필드타입(제약사항, 옵션)
    title = models.CharField(max_length=50)
    content = models.TextField()
    created_at = models.DateTimeField(auto_now_add=True)
    updated_at = models.DateTimeField(auto_now=True)

    def __str__(self) -> str:
        return f'{self.title} ({self.created_at})'

이제 다시 조회하면 QuerySet의 출력 형식이 바뀐 것을 확인 할 수 있습니다.

(바로 적용이 안되는 경우, Django shell을 재시작 해봅니다.)


장고 DB 조회하기

get() : 하나의 데이터만 조회하기

하나의 레코드만 조회할 때는 all()이 아닌 get()을 사용합니다.

인자로는 조건을 입력해줍니다. 여기서는 Article이라는 모델에서 id 값이 1인 데이터를 조회해 보겠습니다.

Articles.objects.get(id=1)

Note. 조건에 해당하는 QuerySet이 없는 경우

-DoesNotExist 예외가 발생됩니다.

Note. 2개 이상의 QuerySet을 get()으로 조회하는 경우

한 개 이상의 QuerySet이 존재하는 경우에는 MultipleObjectReturned 예외가 발생됩니다.

values() : 하나의 필드만 조회하기

SQL에서 SELECT 필드명 FROM 테이블 처럼 하나의 필드에 해당하는 데이터를 조회하려면 valus() 메소드를 사용하면 됩니다.

Mymodel.objects.values('필드 명')

filter() : 특정 조건으로 조회하기

특정 조건에 맞는 QuerySet을 개수에 상관없이 가져올 수 있게 해주는 코드는 MyModel.objects.filter(조건) 입니다.

Articles라는 모델에서 content의 내용이 ‘찾을 내용’ 인 경우, 아래와 같은 명령어를 사용합니다.

Article.objects.filter(content='찾을 내용')

여기서 조건으로 입력한 content='찾을 내용' 이 부분을 lookup이라고 부릅니다.

단순히 일치하는 조건 뿐만 아니라 여러 조건들을 lookup으로 제공하는데요, 자주 쓰이는 lookup들을 정리하면 아래와 같습니다.

LookupDescriptionExample Code
Exact (exact)지정된 필드가 제공된 값과 정확히 일치하는 개체를 검색MyModel.objects.filter(name__exact='John')
Case-insensitive Exact (iexact)대소문자를 무시하고 지정된 필드가 제공된 값과 정확히 일치하는 개체를 검색MyModel.objects.filter(name__iexact='john')
Contains (contains)지정된 필드에 제공된 값이 포함된 개체를 검색MyModel.objects.filter(name__contains='John')
Case-insensitive Contains
(icontains)
지정된 필드에 제공된 값이 대소문자 구분 없이 포함된 개체를 검색MyModel.objects.filter(name__icontains='john')
Starts With (startswith)지정된 필드가 제공된 값으로 시작하는 개체를 검색MyModel.objects.filter(name__startswith='John')
Ends With (endswith)지정된 필드가 제공된 값으로 끝나는 개체를 검색MyModel.objects.filter(name__endswith='Doe')
In (in)지정된 필드가 제공된 값들 중 일치하는 개체를 검색MyModel.objects.filter(age__in=[25, 30, 35])
Greater Than (gt)지정된 필드가 제공된 값보다 큰 개체를 검색MyModel.objects.filter(age__gt=30)
Less Than (lt)지정된 필드가 제공된 값보다 작은 개체를 검색MyModel.objects.filter(age__lt=30)
Greater Than or Equal To (gte)지정된 필드가 제공된 값보다 크거나 같은 개체를 검색MyModel.objects.filter(age__gte=30)
Less Than or Equal To (lte)지정된 필드가 제공된 값보다 작거나 같은 개체를 검색MyModel.objects.filter(age__lte=30)
Is Null (isnull)지정된 필드가 null이거나 null이 아닌 개체를 검색MyModel.objects.filter(email__isnull=True)
<lookup 정리> https://docs.djangoproject.com/en/4.2/topics/db/queries/#field-lookups

2개 이상의 조건으로 조회하기

filter chaining

여러 필터 호출을 연결하여 여러 조건을 적용할 수 있습니다.

queryset = MyModel.objects.filter(name__startswith='John').filter(age=25)

여러 인자 입력

두 개의 조건을 인자로 입력

queryset = MyModel.objects.filter(name__startswith='John', age=25)

OR 연산자 조회

Q와 연산자 |를 사용할 수 있습니다

queryset = MyModel.objects.filter(Q(name='John') | Q(age=25))

annotate() : 연산 결과 조회하기

조회한 querySet 결과에 각각에 추가적인 데이터를 제공해줄 수 있습니다.

가격 * 수량을 계산해서 total_price라는 필드에 넣어서 제공해주는 ORM은 아래와 같습니다.

queryset = Mymodel.objects.annotate(
  total_price = F('price') * F('quantity')
  )

object 매니저 뒤와 annoate 사이에는 all(), filter(), get() 등을 사용할 수 있습니다.

둘 사이에 값을 생략하면 Mymodel.objects.all().annotate(…) 와 같이 동작합니다.

F() 객체는 특정 필드를 참조하는 것을 의미합니다.

aggregate() : 집계하여 조회하기

SQL의 Avg, Sum, count와 같은 집계 함수를 사용하기 위해서는 aggregate() 메소드를 사용합니다.

querySet = Mymodel.objects.aggregate(
  Avg('price')
  )
  # {'price__avg': xxxxx}

querSet 결과의 키값은 사용한 필드의 집계함수 이름을 더블 언더스코어로 연결한 값을 자동으로 제공해줍니다.

만일 키 값을 변경하고 싶으면 아래와 같이 키 값을 인자로 입력해서 수정할 있습니다.

querySet = Mymodel.objects.aggregate(
  Avg_Price = Avg('price')
  )
  # {'Avg_Price': xxxxx}

Group by 적용하기

SQL에서 카테고리 별 연산을 도와주는 group by를 ORM에서는 2 단계를 통해서 수행합니다.

1.values() 로 내가 원하는 필드 데이터만 가져옴

2.annotate로 묶여서 group by

Mymodel.objects.values('필드명').annotate('집계 결과 필드명') = 집계함수('필드명')

예) Product 라는 모델에서 category 필드의 각 요소 별 개수는 다음과 같은 ORM으로 구할 수 있습니다

Product.objects.values('category').annotate(category_count = Count('category'))

order_by() : 정렬해서 조회하기

조회 코드 뒤에 order_by(“필드명”)을 입력해줍니다.

디폴트 정렬이 오름차순 입니다. 내림차순 정렬을 위해서는 필드명 앞에 ‘-‘을 붙여줍니다.

# id값을 기준으로 오름차순 정렬
queryset = MyModel.objects.all().order_by('id')

(id 장고 모델에서 기본적으로 생성하는 primary key 필드입니다. id를 pk로 입력해도 동일하게 작동합니다.)

# id값을 기준으로 내림차순 정렬
queryset = MyModel.objects.all().order_by('-id')

raw() : ORM에서 SQL로 쿼리하기

raw()을 이용해서 SQL문을 직접 입력하는 방식으로 querySet를 가져올 수도 있습니다.

Note : ORM에서 SQL로 쿼리하기 위해서는 “id”를 꼭 넣어줘야 함에 유의 합니다.

querySet = Mymodel.objects.raw(
'''
SELECT "id", ...
FROM ...
...
'''

조회 연습

다음과 같은 QuerySet이 있다고 가정해봅니다.

Articles.objects.all()

title에 ‘테스트’가 포함된 레코드를 조회합니다.

Articles.objects.filter(title__contains='테스트')

title에 ‘테스트’가 포함되면서 ‘2’로 끝나는 레코드를 조회합니다.

Articles.objects.filter(title__contains='테스트', content__endswith='2')

title에 ‘키썸’이 포함되거나 title에 ‘류준열’이 포함된 레코드를 조회합니다.

Articles.objects.filter(Q(title__contains='키썸') | Q(title__contains='류준열'))

장고 DB 레코드 수정하기

단일 데이터 수정

데이터를 수정하기 위해서는 조회-> 수정 -> 저장 단계를 거칩니다.

# Retrieve the object to modify
obj = MyModel.objects.get(id=1)  # id 1번 레코드를 변경하고 싶을 때, 우선 1번 QuerySet을 가져옵니다.

# Update the data
obj.title = 'New Name'  # title 내용을 새로 수정해줍니다.

# Save the changes
obj.save()  # 업데이트한 내용을 수정합니다.

여러 데이터 수정

여러 개의 데이터를 수정할 때는 objects.filter() 메소드와 for문을 사용하여 만들 수 있습니다.

# 수정할 레코드를 가져옵니다.
objects_to_edit = MyModel.objects.filter(content__contains='test')  # 'test' 가 내용으로 포함된 QuerySet을 조회

# QuerySet을 for문으로 변경하면서 저장
for obj in objects_to_edit:
    # 수정
    obj.title = f'{obj.title} (수정)'

    # 저장
    obj.save()

혹은 F 객체를 사용하여 여러 데이터를 수정할 수 있습니다.

F는 어떤 필드를 참조해야 할 때 사용합니다.

price 필드에서 모든 가격을 1000원 올린다고 가정하면 아래와 같이 ORM을 작성할 수 있습니다.

Mymodel.objects.update(price = F('price') + 1000)

장고 DB 레코드 삭제하기

MyModel이란 모델의 id가 2인 레코드를 삭제하려면 아래와 같이 진행 합니다.

article = MyModel.objects.get(id=2)
article.delete()

DB에 직접 연결하여 쿼리하기

connection를 이용해서 DB와 연결하고 쿼리 할 수 있습니다.

from django.db import connection

sql_query = '''
SELECT "category", COUNT("category") AS "category_count" 
FROM "products_product" 
GROUP BY "category"
'''
cursor = connection.cursor()
cursor.execute(sql_query)
result = cursor.fetchall()
print(result)

# [('F', 15), ('M', 15), ('O', 15), ('V', 5)]

마치며

기초적인 CRUD를 정리해보았는데요. 조회의 경우, 정말 다양한 SQL이 있듯이 다양한 objects 매소드들이 있습니다. 정리한 내용 외에 추가적인 기능은 필요할 때마다 공식 Docs를 참고해봐야 하겠습니다.

https://docs.djangoproject.com/en/4.2/topics/db/queries/#field-lookups

참고하면 좋은 글

Leave a Comment

목차