들어가며
모든 백엔드의 아마 기초이자 핵심이 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들을 정리하면 아래와 같습니다.
Lookup | Description | Example 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) |
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