들어가며

모든 백엔드의 아마 기초이자 핵심이 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

참고하면 좋은 글

답글 남기기

이메일 주소는 공개되지 않습니다. 필수 필드는 *로 표시됩니다


목차