모델 계층 구축
데이터베이스 연결 설정
Django는 기본적으로 SQLite3를 사용하지만, 시간 필드 처리에 민감하지 않아 일반적으로 다른 데이터베이스를 선택합니다.
가장 흔히 사용되는 MySQL 데이터베이스 연결을 위해 설정 파일을 수정해야 합니다:
# settings.py 설정 변경
DATABASES = {
'default': {
'ENGINE': 'django.db.backends.mysql',
'NAME': 'my_database',
'HOST': '127.0.0.1',
'PORT': 3306,
'USER': 'db_user',
'PASSWORD': 'secure_password',
'CHARSET': 'utf8'
}
}
참고: MySQL 첫 연결시 오류가 발생할 수 있으며, mysqlclient 모듈 설치가 필요합니다.
모델 클래스 정의
ORM을 사용하면 클래스와 객체를 이용해 테이블과 레코드를 매핑할 수 있습니다. 이러한 클래스는 models.py 파일에 정의합니다:
from django.db import models
class Member(models.Model):
# 필드명 = 필드타입 + 제약조건
member_id = models.AutoField(primary_key=True)
username = models.CharField(max_length=32)
age = models.IntegerField()
이 클래스는 Member 테이블을 생성하며, 세 개의 필드(자동 증가 기본 키 포함)를 가집니다.
터미널에서 manage.py 명령어를 실행하여 마이그레이션을 수행합니다:
python manage.py makemigrations # 데이터베이스 변경사항 기록
python manage.py migrate # 변경사항을 데이터베이스에 적용
독립 테스트 환경 구성
Django는 기본적으로 단일 Python 파일의 독립 실행을 허용하지 않습니다. 특히 모델 계층 테스트 시 전체 프로젝트를 실행하는 것은 번거롭습니다.
PyCharm 콘솔을 이용한 즉시 테스트(코드 저장 불가) 또는 자체 환경을 구성할 수 있습니다:
import os
def setup_django():
os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'myproject.settings')
import django
django.setup()
if __name__ == '__main__':
setup_django()
# 테스트 코드 작성
ORM 기본 사용법
ORM SQL 확인 방법
- ORM 쿼리는 보통 QuerySet 객체를 반환하며, 이 객체의
query속성에 실제 SQL 문이 저장됩니다:
result = models.Member.objects.filter()
print(result.query)
# 출력: SELECT `app01_member`.`member_id`, `app01_member`.`username`, `app01_member`.`age` FROM `app01_member`
참고: 모든 ORM 메서드가 QuerySet을 반환하는 것은 아닙니다.
- 설정 파일에 다음 코드를 추가하면 모든 데이터베이스 작업 시 SQL 문이 출력됩니다:
LOGGING = {
'version': 1,
'disable_existing_loggers': False,
'handlers': {
'console':{
'level':'DEBUG',
'class':'logging.StreamHandler',
},
},
'loggers': {
'django.db.backends': {
'handlers': ['console'],
'propagate': True,
'level':'DEBUG',
},
}
}
기본 CRUD 메서드
| 메서드 | 반환값 |
|---|---|
| create(필드명=값) | 생성된 레코드 객체 |
| filter(조건) | QuerySet 리스트 객체 |
| filter().update(수정내용) | 영향 받은 행 수 |
| filter().delete() | 영향 받은 행 수 (딕셔너리 형태) |
QuerySet 반환 메서드
| 메서드 | 설명 | 비고 |
|---|---|---|
| all() | 모든 레코드 조회 | |
| filter() | 조건에 맞는 레코드 조회 | 괄호에 조건 명시 |
| exclude() | 조건에 맞지 않는 레코드 조회 | 괄호에 조건 명시 |
| order_by() | 특정 필드 기준 정렬 | 괄호에 필드명 명시 |
| reverse() | 정렬된 결과 역순 정렬 | 정렬된 QuerySet에만 적용 가능 |
| distinct() | 중복 제거 | 주로 values()와 함께 사용 |
| raw(SQL) | 원시 SQL 실행 | 실제 테이블명과 모델 클래스명 구분 주의 |
특수 QuerySet 반환 메서드
| 메서드 | 설명 | 반환 형태 |
|---|---|---|
| values() | 특정 필드만 조회 | <QuerySet [{}, {}, {}]> (딕셔너리) |
| values_list() | 특정 필드만 조회 | <QuerySet [(), (), ()]> (튜플) |
객체 반환 메서드
| 메서드 | 설명 | 반환값 |
|---|---|---|
| create(**kwargs) | 레코드 삽입 | 생성된 객체 |
| QuerySet.first() | 첫 번째 레코드 | 객체 또는 None |
| QuerySet.last() | 마지막 레코드 | 객체 또는 None |
| QuerySet.get(조건) | 조건에 맞는 레코드 | 객체 (없으면 오류) |
조건 필터링
기본 필터
filter()에서 기본 조건은 = (동등)이며, 여러 조건은 AND로 처리됩니다.
Django에서는 필드명 뒤에 __조건을 붙여 다양한 조건을 표현합니다:
# 크기 비교
Member.objects.filter(age__gt=18) # age > 18
Member.objects.filter(age__lt=18) # age < 18
Member.objects.filter(age__gte=18) # age >= 18
Member.objects.filter(age__lte=18) # age <= 18
# IN 연산
Member.objects.filter(age__in=(18, 32, 34))
# 범위 (BETWEEN)
Member.objects.filter(age__range=(18, 38))
# LIKE 검색
Member.objects.filter(username__contains="lee") # 대소문자 구분
Member.objects.filter(username__icontains="lee") # 대소문자 미구분
# 날짜 필터
Member.objects.filter(join_date__year=2022)
Member.objects.filter(join_date__month=12)
F와 Q 객체
F 객체
두 필드를 비교하는 조건에 사용합니다.
from django.db.models import F
# 판매량이 재고보다 많은 책 조회
Book.objects.filter(sales__gt=F("stock")).values("title")
Q 객체
OR, NOT 조건을 구현할 때 사용합니다.
from django.db.models import Q
# 기본 키가 1이거나 가격이 2000 이상인 책
Book.objects.filter(Q(pk=1) | Q(price__gt=2000)).values("title")
# 기본 키가 1이 아닌 책
Book.objects.filter(~Q(pk=1)).values("title")
Q 객체 고급 활용
q_obj = Q()
q_obj.connector = 'or' # 기본 AND -> OR로 변경
q_obj.children.append(("pk", 1))
q_obj.children.append(("price__gt", 50))
result = Book.objects.filter(q_obj)
print(result)
외래 키
모델에서 외래 키 정의
class Book(models.Model):
title = models.CharField(max_length=32)
price = models.FloatField()
# 1:N 관계
publisher = models.ForeignKey(to="Publisher", on_delete=models.CASCADE)
# M:N 관계
authors = models.ManyToManyField(to="Author")
class Publisher(models.Model):
name = models.CharField(max_length=32)
address = models.CharField(max_length=64)
class Author(models.Model):
name = models.CharField(max_length=32)
# 1:1 관계
profile = models.OneToOneField(to="AuthorProfile", on_delete=models.CASCADE)
class AuthorProfile(models.Model):
phone = models.CharField(max_length=16)
address = models.CharField(max_length=64)
외래 키를 활용한 관계 설정
# 1:N 및 1:1 관계
AuthorProfile.objects.create(phone='1111', address="Seoul")
Author.objects.create(name="Kim", profile_id=1)
profile = AuthorProfile.objects.filter(pk=2).first()
Author.objects.filter(name="Kim").update(profile=profile)
# M:N 관계
book = Book.objects.filter(pk=1).first()
book.authors.add(1, 5) # 기본 키로 관계 추가
author = Author.objects.filter(pk=7).first()
book.authors.add(author) # 객체로 관계 추가
book.authors.set([6, 7, 8]) # 기존 관계 삭제 후 새 관계 설정
book.authors.remove(1) # 관계 제거
크로스 테이블 쿼리
| 방향 | 객체 기반 | 필드 기반 |
|---|---|---|
| 정방향 | book_obj.publisher.name |
Book.objects.filter(title="...").values("publisher__name") |
| 역방향 | publisher_obj.book_set.all() |
Publisher.objects.filter(name="...").values("book__title") |
M:N 관계 심화
완전 자동, 수동, 그리고 최적화된 수동 방식이 있습니다:
# 수동 방식 (확장 가능)
class Book(models.Model):
title = models.CharField(max_length=32)
class Author(models.Model):
name = models.CharField(max_length=32)
class BookAuthor(models.Model):
book = models.ForeignKey(to='Book', on_delete=models.CASCADE)
author = models.ForeignKey(to='Author', on_delete=models.CASCADE)
join_date = models.DateField(auto_now_add=True)
# 최적화된 수동 방식
class Book(models.Model):
title = models.CharField(max_length=32)
authors = models.ManyToManyField(to='Author',
through='BookAuthor',
through_fields=('book', 'author'))
class Author(models.Model):
name = models.CharField(max_length=32)
class BookAuthor(models.Model):
book = models.ForeignKey(to='Book', on_delete=models.CASCADE)
author = models.ForeignKey(to='Author', on_delete=models.CASCADE)
join_date = models.DateField(auto_now_add=True)
집계 함수와 그룹 쿼리
집계 함수
from django.db.models import Max, Min, Sum, Count, Avg
result = Book.objects.aggregate(
max_price=Max('price'),
min_price=Min('price'),
total=Sum('price'),
avg_price=Avg('price'),
count=Count('pk')
)
print(result)
# {'max_price': 99.0, 'min_price': 99.0, 'total': 396.0, 'avg_price': 99.0, 'count': 4}
그룹 쿼리
# 필드 기준 그룹화
Book.objects.values("publisher_id").annotate(Min("price"))
# 테이블 기준 그룹화
Publisher.objects.annotate(min_price=Min("book__price")).values("name", "min_price")
# HAVING 조건 (filter 사용)
Book.objects.annotate(author_count=Count('authors__pk'))\
.filter(author_count__gt=1)\
.values("title", "author_count")
쿼리 최적화
지연 로딩 (Lazy Loading)
result = Book.objects.filter() # 아직 쿼리 실행 안 됨
# print(result) # 이 시점에서 쿼리 실행
only()와 defer()
# only: 특정 필드만 로드
result = Book.objects.only("title", "price")
for book in result:
print(book.title) # 추가 쿼리 없음
# print(book.author) # 추가 쿼리 발생
# defer: 특정 필드 제외하고 로드
result = Book.objects.defer("price")
for book in result:
print(book.title) # 추가 쿼리 없음
# print(book.price) # 추가 쿼리 발생
select_related와 prefetch_related
# select_related: JOIN 사용
result = Book.objects.select_related("publisher")
for book in result:
print(book.publisher.name) # 추가 쿼리 없음
# prefetch_related: 서브쿼리 사용
result = Book.objects.prefetch_related("publisher")
for book in result:
print(book.publisher.name) # 추가 쿼리 없음
벌크 연산
from apps import models
def bulk_create_view(request):
data_list = [{"title": f"Book {i}", "price": i * 10} for i in range(100000)]
# 비효율적 방법
# for data in data_list:
# models.Book.objects.create(**data)
# 효율적 방법
book_objects = [models.Book(**data) for data in data_list]
models.Book.objects.bulk_create(book_objects)
트랜잭션
전역 트랜잭션
DATABASES = {
'default': {
'ATOMIC_REQUESTS': True, # 모든 요청이 하나의 트랜잭션
}
}
데코레이터
from django.db import transaction
from django.db.models import F
@transaction.atomic
def transfer_view(request):
models.User.objects.filter(pk=1).update(account=F("account") + 30)
# 오류 발생 시 자동 롤백
models.User.objects.filter(pk=2).update(account=F("account") - 30)
return HttpResponse("이체 완료")
컨텍스트 매니저
with transaction.atomic():
models.Book.objects.filter(pk=1).update(price=F("price") + 30)
models.Book.objects.filter(pk=2).update(price=F("price") - 30)
주요 필드 타입
| 필드 타입 | 파라미터 | 설명 |
|---|---|---|
| AutoField | primary_key | 자동 증가 기본 키 |
| CharField | max_length | 고정 길이 문자열 |
| IntegerField/BigIntegerField | 정수형 | |
| DecimalField | max_digits, decimal_places | 소수점 포함 숫자 |
| DateField/DateTimeField | auto_now, auto_now_add | 날짜/시간 |
| BooleanField | True/False 저장 (DB: 1/0) | |
| TextField | 대용량 텍스트 | |
| EmailField | 이메일 형식 검증 | |
| FileField | upload_to | 파일 업로드 (DB: 경로 저장) |
| ForeignKey/OneToOneField | to, to_field, on_delete | 외래 키 관계 |
| ManyToManyField | to, to_field | 다대다 관계 |
사용자 정의 필드
class CharFieldCustom(models.Field):
def __init__(self, max_length, *args, **kwargs):
self.max_length = max_length
super().__init__(max_length=max_length, *args, **kwargs)
def db_type(self, connection):
return 'char(%s)' % self.max_length
class User(models.Model):
name = models.CharField(max_length=32)
info = CharFieldCustom(max_length=64)
주요 필드 파라미터
공통 파라미터
| 파라미터 | 설명 |
|---|---|
| primary_key | 기본 키 설정 |
| verbose_name | 필드 설명 (관리자 페이지 등) |
| null | NULL 허용 여부 |
| default | 기본값 |
| unique | 고유성 제약 |
| db_index | 데이터베이스 인덱스 추가 |
특수 파라미터
- choices: 열거 가능한 필드에 사용 (성별, 학력 등)
class User(models.Model):
GENDER = (
(1, 'Male'),
(2, 'Female'),
)
gender = models.IntegerField(choices=GENDER, null=True)
user = User.objects.filter(pk=1).first()
user.gender # 실제 저장값 (1)
user.get_gender_display() # 'Male'
외래 키 관련 파라미터
| 파라미터 | 설명 |
|---|---|
| to | 연결할 모델 |
| to_field | 연결할 필드 (기본: 기본 키) |
| on_delete | 연결된 데이터 삭제 시 동작 |
| related_name | 역방향 쿼리명 (기본: 모델명_set) |
| related_query_name | 역방향 필드명 |
| db_constraint | 데이터베이스 외래 키 제약 생성 여부 (기본: True) |
on_delete 옵션
- CASCADE: 참조하는 데이터도 함께 삭제
- SET_NULL: NULL로 설정 (필드는 null=True여야 함)
- PROTECT: 삭제 불가 (ProtectedError 발생)
- SET_DEFAULT: 기본값으로 설정 (필드에 default 필요)
- SET(value): 지정된 값으로 설정
- DO_NOTHING: 아무 작업 안 함 (데이터베이스 수준 제약에 따름)