이 게시글은
김형종 강사님
의Django 웹개발
강의를 듣고 작성하였습니다.
DJango ORM
ORM과 QuerySets
ORM
ORM
이란 Object-Relational Mapping
의 약자로 객체(Object)
와 관계형 데이터베이스(Relational Database
)의 데이터를 매핑(Mapping
)해주는 것을 의미.
- 객체 간의 관계를 바탕으로
SQL
을 자동 생성하여sql쿼리문
없이도 데이터베이스의 데이터를 다룰 수 있게 해줌.- 데이터베이스의 테이블을 객체지향 프로그래밍에서 흔히 사용하는 객체(
Class
)처럼 사용할 수 있도록 해주는 기술.- 기존 쿼리문을 작성하여 데이터베이스를 조작하는 것을 넘어서서 더 효율적이고 가독성 및 유지 보수에 적합한 코드를 만들기 위해 나온 기술.
QuerySets
QuerySet
이란 데이터베이스에서 전달 받은 객체의 목록이다.
Django ORM
에서 발생한 자료형- 리스트와 구조는 같지만 파이썬 기본 자료구조가 아니기 때문에 파이썬에서 읽고 쓰기 위해 자료형 변환(
Casting
)
ORM 실습
실습 사전 준비
orm
앱 설치 후 기본 셋팅model
설정 및 db
테이블 추가models.py
코드
from django.db import models
class Member(models.Model):
name = models.CharField(verbose_name="이름", max_length=10)
birth = models.DateField(verbose_name="생년월일", null=True)
email = models.EmailField(verbose_name="이메일")
phone = models.CharField(verbose_name="전화번호", max_length=11)
created_at = models.DateTimeField(verbose_name="생성일", auto_now_add=True)
updated_at = models.DateTimeField(verbose_name="갱신일", auto_now=True)
def __str__(self):
return f"{self.name} ({self.email})"
class Meta:
verbose_name = "회원"
verbose_name_plural = "회원 목록"
class Product(models.Model):
NEW = "new"
OLD = "old"
STATUS_TYPE = (
(NEW, "신상품"),
(OLD, "재고상품"),
)
code = models.CharField(verbose_name="상품코드", max_length=8)
status = models.CharField(verbose_name="상품구분", max_length=3, choices=STATUS_TYPE)
name = models.CharField(verbose_name="상품명", max_length=30)
price = models.PositiveSmallIntegerField(verbose_name="가격(원)", default=0)
created_at = models.DateTimeField(verbose_name="생성일", auto_now_add=True)
updated_at = models.DateTimeField(verbose_name="갱신일", auto_now=True)
def __str__(self):
return f"{self.name} ({self.code})"
class Meta:
verbose_name = "상품"
verbose_name_plural = "상품 목록"
class PaymentOrderRecord(models.Model):
NOT_YET = 0
PAID = 1
REFUND = 2
STATUS_TYPE = (
(NOT_YET, "결제전"),
(PAID, "결제완료"),
(REFUND, "환불완료"),
)
code = models.CharField(verbose_name="결제코드", max_length=15)
status = models.IntegerField(verbose_name="결제구분", default=0, choices=STATUS_TYPE)
member = models.ForeignKey(
Member,
verbose_name="회원",
on_delete=models.CASCADE,
null=True,
related_name="member_paymentorderrecord",
)
product = models.ManyToManyField(
Product, verbose_name="상품", related_name="product_paymentorderrecord"
)
created_at = models.DateTimeField(verbose_name="생성일", auto_now_add=True)
updated_at = models.DateTimeField(verbose_name="갱신일", auto_now=True)
def __str__(self):
return f"{self.code} ({self.product.count()}개)"
class Meta:
verbose_name = "결제 주문서"
verbose_name_plural = "결제 주문서 목록"
orm
실습을 위한 db
생성
- 아래와 같이
management
폴더 구조 만들기
management
폴더와commands
폴더 내에 아무 내용도 들어 있지 않은__init__.py
파일 필수 생성commands
폴더 내에 실행 파일 생성
push_orm_data.py
:test db
생성 시 사용control_orm_data.py
:orm
실습 시 사용
push_orm_data.py
코드
terminal
창에서 명령어 실행poetry run python manage.py push_orm_data
import datetime
from django.core.management.base import BaseCommand
from orm.models import Member, PaymentOrderRecord, Product
# TODO: 커맨드 활용
# https://docs.djangoproject.com/en/3.2/howto/custom-management-commands/#testing
class Command(BaseCommand):
help = "PUSH TEST DB"
def handle(self, *args, **options):
member_list = [
("이병헌", "lbh@test.com", "01017472748", datetime.date(1970, 7, 12)),
("박해수", "phs@test.com", "01039584378", datetime.date(1981, 11, 21)),
("황정민", "hjm@test.com", "01012344624", datetime.date(1970, 9, 1)),
("강동원", "kdw@dummy.com", "01048572738", datetime.date(1981, 1, 18)),
("장동건", "jdk@dummy.com", "01020302949", datetime.date(1972, 3, 7)),
("송강호", "skh@movie.com", "01020584737", datetime.date(1967, 1, 17)),
]
product_list = [
("po3nuw01", "new", "휴지", 1500),
("92oif0rj", "new", "화장품", 30000),
("9oiuh67y", "old", "네스프레소", 300000),
("8ai2k8ww", "new", "수저", 1000),
("9oi8ujhy", "new", "젓가락", 500),
("8uhyjtd6", "old", "압력밥솥", 250000),
("9if56yhg", "new", "머그컵", 5000),
("23tyfuen", "old", "스마트폰", 1000000),
]
payment_list = [
("20220101-9oi8u7", 0, "이병헌", 2),
("20220101-8ujhbg", 0, "이병헌", 3),
("20220101-5tgvfr", 1, "박해수", 4),
("20220101-4rfcdE", 2, "박해수", 1),
("20220101-3edcxs", 1, "송강호", 2),
("20220101-2wsxdd", 0, "강동원", 1),
("20220101-9iuj78", 1, "강동원", 3),
("20220101-7ujhy6", 2, "장동건", 3),
]
for item in member_list:
member, is_created = Member.objects.get_or_create(
name=item[0], email=item[1], phone=item[2]
)
member.birth = item[3]
member.save()
for item in product_list:
Product.objects.get_or_create(code=item[0], status=item[1], name=item[2], price=item[3])
for item in product_list:
Product.objects.get_or_create(code=item[0], status=item[1], name=item[2], price=item[3])
for item in payment_list:
payment_order_record, is_created = PaymentOrderRecord.objects.get_or_create(
code=item[0]
)
payment_order_record.status = item[1]
payment_order_record.member = Member.objects.get(name=item[2])
for product in Product.objects.order_by("?")[: item[3]]:
payment_order_record.product.add(product)
payment_order_record.save()
control_orm_data.py
기초 코드 작성
import datetime
from django.core.management.base import BaseCommand
from orm.models import Member, PaymentOrderRecord, Product
# TODO: 커맨드 활용
# https://docs.djangoproject.com/en/3.2/howto/custom-management-commands/#testing
class Command(BaseCommand):
help = "CONTROL TEST DB"
def handle(self, *args, **options):
이제 이 다음부터 아래의 실습을 이어가게 될 것이며,
코드 작성 후, terminal
창에서 파일 실행 명령을 하면 됨.
poetry run python manage.py control_orm_data
control_orm_data.py
파일에서 handle
함수 부분의 코드에 해당.전체 데이터 조회 : all
control_orm_data.py
def handle(self, *args, **options): all_member = Member.objects.all() all_product = Product.objects.all() all_payment = PaymentOrderRecord.objects.all() # Member 전체 쿼리 조회 print(all_member) # Product 전체 쿼리 조회 print(all_product) # Payment 전체 쿼리 조회 print(all_payment)
terminal
실행 결과
# Member <QuerySet [<Member: 이병헌 (lbh@test.com)>, <Member: 박해수 (phs@test.com)>, <Member: 황정민 (hjm@test.com)>, <Member: 강동원 (kdw@dummy.com)>, <Member: 장동건 (jdk@dummy.com)>, <Member: 송강호 (skh@movie.com)>]> # Product <QuerySet [<Product: 휴지 (po3nuw01)>, <Product: 화장품 (92oif0rj)>, <Product: 네스프레소 (9oiuh67y)>, <Product: 수저 (8ai2k8ww)>, <Product: 젓가락 (9oi8ujhy)>, <Product: 압력밥솥 (8uhyjtd6)>, <Product: 머그컵 (9if56yhg)>, <Product: 스마트폰 (23tyfuen)>]> # Payment <QuerySet [<PaymentOrderRecord: 20220101-9oi8u7 (2개)>, <PaymentOrderRecord: 20220101-8ujhbg (3개)>, <PaymentOrderRecord: 20220101-5tgvfr (4개)>, <PaymentOrderRecord: 20220101-4rfcdE (1개)>, <PaymentOrderRecord: 20220101-3edcxs (2개)>, <PaymentOrderRecord: 20220101-2wsxdd (1개)>, <PaymentOrderRecord: 20220101-9iuj78 (3개)>, <PaymentOrderRecord: 20220101-7ujhy6 (3개)>]>
데이터 검색 (filter) : 데이터 포함 'contains' 외
Case 1. 전체 일치
control_orm_data.py
def handle(self, *args, **options): # Filter : filter_member = Member.objects.filter(name="강동원") print(filter_member)
terminal
실행 결과
<QuerySet [<Member: 강동원 (kdw@dummy.com)>]>
Case 2. 일부 포함 : '__contains='
control_orm_data.py
def handle(self, *args, **options): filter_member = Member.objects.filter(name__contains="강") print(filter_member) # 대소문자 구분 filter_payment = PaymentOrderRecord.objects.filter(code__contains="E") print(filter_payment) filter_payment = PaymentOrderRecord.objects.filter(code__contains="e") print(filter_payment)
terminal
실행 결과
<QuerySet [<Member: 강동원 (kdw@dummy.com)>, <Member: 송강호 (skh@movie.com)>]> # 대문자와 소문자를 다르게 결과가 같다?!! <QuerySet [<PaymentOrderRecord: 20220101-4rfcdE (1개)>, <PaymentOrderRecord: 20220101-3edcxs (2개)>]> <QuerySet [<PaymentOrderRecord: 20220101-4rfcdE (1개)>, <PaymentOrderRecord: 20220101-3edcxs (2개)>]>
🍗 contains
vs icontains
차이
contains
: 대문자
와 소문자
를 구분icontains
: 대문자
와 소문자
를 동일한 데이터로 취급
- 단,
sqlite
에서는contains
를 사용하더라도 대소문자 구분 없이 동일한 값을 조회.Django
를 이용한Search
기능 구현 시,icontains
를 많이 사용- 이것보다 한 단게 더 진화한게
ES (Elastic Search)
🍗 ES (Elastic Search)
: 추후 학습 시 내용 보충 예정
db
화 시켜서 search
를 진행추후 __startswith=
, __endswith=
내용 보충 예정
자매품 __istartswith=
, __iendswith=
도 있음.
데이터 검색 (filter) : 크기 비교 '__lt=' 외
__gt=
: greater than
__gte=
: greater than or equal to
__lt=
: less than
__lte=
: less than or equal to
control_orm_data.py
def handle(self, *args, **options): filter_product = Product.objects.filter(price__lte=1000) print(filter_product) for item in filter_product: print(item.price)
terminal
실행 결과
<QuerySet [<Product: 수저 (8ai2k8ww)>, <Product: 젓가락 (9oi8ujhy)>]> 1000 500
데이터 검색 (filter) : ChoiceField 관련 조회
ChoiceField
데이터 관련 filter
의 경우에는 status
값에 모델명.상태값
으로 지정해야 함.models.py
에서 Product
모델 클래스 내용
class Product(models.Model): NEW = "new" OLD = "old" STATUS_TYPE = ( (NEW, "신상품"), (OLD, "재고상품"), ) code = models.CharField(verbose_name="상품코드", max_length=8) status = models.CharField(verbose_name="상품구분", max_length=3, choices=STATUS_TYPE) name = models.CharField(verbose_name="상품명", max_length=30) price = models.PositiveSmallIntegerField(verbose_name="가격(원)", default=0) created_at = models.DateTimeField(verbose_name="생성일", auto_now_add=True) updated_at = models.DateTimeField(verbose_name="갱신일", auto_now=True) def __str__(self): return f"{self.name} ({self.code})" class Meta: verbose_name = "상품" verbose_name_plural = "상품 목록"
control_orm_data.py
def handle(self, *args, **options): filter_product = Product.objects.filter(price__lte=1000, status=Product.NEW) print(filter_product)
terminal
실행 결과
<QuerySet [<Product: 수저 (8ai2k8ww)>]>
🍓 filter
는 2
개 이상 적용 가능
데이터 개수 세기 (Count)
control_orm_data.py
def handle(self, *args, **options): all_member_count = len(all_member) print(all_member_count) all_member_count = all_member.count() print(all_member_count)
terminal
실행 결과
6 6
🍓 단, len
보다 count
를 사용할 것!!
len
도 결국 QuerySets
내부에 메서드
로 구현이 되어 있으나len
함수는 결국 내부에서 count
메서드를 다시 호출하는 구조로 되어있기 때문에 미세하지만 속도가 조금 더 느림.데이터 연산 (aggregate) - Sum, Avg / Max, Min
control_orm_data.py
# 아래 코드에서 'data'는 변수명이며, 임의로 지정 가능 from django.db.models import Sum, Avg, Max, Min def handle(self, *args, **options): # Sum all_product_total = all_product.aggregate(data=Sum("price")) print(all_product_total) # Avg all_product_average = all_product.aggregate(data=Avg("price")) print(all_product_average) # Max all_product_max = all_product.aggregate(data=Max("price")) print(all_product_max) # Min all_product_min = all_product.aggregate(data=Min("price")) print(all_product_min)
terminal
실행 결과
# Sum {'data': 1588000} # Avg {'data': 198500.0} # Max {'data': 1000000} # Min {'data': 500}
원하는 필드(컬럼)만 추출 (values)
control_orm_data.py
def handle(self, *args, **options): all_product_status = all_product.values("status") print(all_product_status) print(type(all_product_status)) all_product_status_list = all_product.values_list("status") print(all_product_status_list) print(type(all_product_status_list)) all_product_status_list = all_product.values_list("status", flat=True) print(all_product_status_list) print(type(all_product_status_list)) all_product_status_real_list = list(all_product_status_list) print(all_product_status_real_list) all_product_status_distinct_list = list(set(lall_product_status_list)) print(all_product_status_distinct_list)
terminal
실행 결과
<QuerySet [{'status': 'new'}, {'status': 'new'}, {'status': 'old'}, {'status': 'new'}, {'status': 'old'}, {'status': 'old'}, {'status': 'new'}, {'status': 'old'}]> <class 'django.db.models.query.QuerySet'> <QuerySet [('new',), ('new',), ('old',), ('new',), ('old',), ('old',), ('new',), ('old',)]> <class 'django.db.models.query.QuerySet'> <QuerySet ['new', 'new', 'old', 'new', 'old', 'old', 'new', 'old']> <class 'django.db.models.query.QuerySet'> ['new', 'new', 'old', 'new', 'old', 'old', 'new', 'old'] ['new', 'old']
데이터 정렬 (order by)
control_orm_data.py
def handle(self, *args, **options): # 오름차순 all_product_asc = all_product.order_by("price") print(all_product_asc) # 내림차순 all_product_dsc = all_product.order_by("-price") print(all_product_dsc) # 내림차순 정렬 후 가격만 확인 for item in all_product_dsc: print(item.price) # random으로 정렬 all_product_random = all_product.order_by("?") print(all_product_random) # 렌덤으로 하나의 결과만 추출 # 방법 1 choice_product_random = all_product.order_by("?")[0] print(choice_product_random) # 방법 2 index = randint(0, Product.objects.count() - 1) product = Product.objects.all()[index] print(product)
terminal
실행 결과
# 오름차순 <QuerySet [<Product: 젓가락 (9oi8ujhy)>, <Product: 수저 (8ai2k8ww)>, <Product: 휴지 (po3nuw01)>, <Product: 머그컵 (9if56yhg)>, <Product: 화장품 (92oif0rj)>, <Product: 압력밥솥 (8uhyjtd6)>, <Product: 네스프레소 (9oiuh67y)>, <Product: 스마트폰 (23tyfuen)>]> # 내림차순 <QuerySet [<Product: 스마트폰 (23tyfuen)>, <Product: 네스프레소 (9oiuh67y)>, <Product: 압력밥솥 (8uhyjtd6)>, <Product: 화장품 (92oif0rj)>, <Product: 머그컵 (9if56yhg)>, <Product: 휴지 (po3nuw01)>, <Product: 수저 (8ai2k8ww)>, <Product: 젓가락 (9oi8ujhy)>]> # 내림차순 정렬 후 가격만 확인 1000000 300000 250000 30000 5000 1500 1000 500 # random으로 정렬 <QuerySet [<Product: 휴지 (po3nuw01)>, <Product: 네스프레소 (9oiuh67y)>, <Product: 머그컵 (9if56yhg)>, <Product: 압력밥솥 (8uhyjtd6)>, <Product: 젓가락 (9oi8ujhy)>, <Product: 화장품 (92oif0rj)>, <Product: 수저 (8ai2k8ww)>, <Product: 스마트폰 (23tyfuen)>]> # random 정렬 후 하나만 뽑아오기 # 방법 1 화장품 (92oif0rj) # 방법 2 압력밥솥 (8uhyjtd6)
데이터 검색 (filter) : 외래키(Foreign Key) 참조
🥡 먼저, 각 모델에 대해 살펴보자.
PaymentOrderRecord
모델
member
라는 filed를 가지고 있는데, 이것은 Member
라는Field
를 외래키로 가지고 있음.member = models.ForeignKey( Member, verbose_name="회원", on_delete=models.CASCADE, null=True, related_name="member_paymentorderrecord", )
Member
모델
name
이라는 Field
를 가지고 있음.class Member(models.Model): name = models.CharField(verbose_name="이름", max_length=10) birth = models.DateField(verbose_name="생년월일", null=True) email = models.EmailField(verbose_name="이메일") phone = models.CharField(verbose_name="전화번호", max_length=11) created_at = models.DateTimeField(verbose_name="생성일", auto_now_add=True) updated_at = models.DateTimeField(verbose_name="갱신일", auto_now=True)
🥡 이것을 이용한 데이터 조회
control_orm_data.py
def handle(self, *args, **options): # 송강호라는 사람이 구매한 주문서 출력 payment_by_name = all_payment.filter(member__name="송강호") print(payment_by_name) # 머그컵을 구매한 주문서 출력 payment_by_product = all_payment.filter(product__name="머그컵") print(payment_by_product)
terminal
실행 결과
<QuerySet [<PaymentOrderRecord: 20220101-3edcxs (2개)>]> <QuerySet [<PaymentOrderRecord: 20220101-9oi8u7 (2개)>]>
데이터 검색 (filter) : 가상의 필드(
annotate) 이용
🥡 PaymentOrderRecord
라는 모델에 member
라는 필드는 있지만, member_name
이라는 필드는 없음.
annotate
의 역할
- 주의할 것!!
- 어떤 필드에 접근하려면
하나의 객체
여야만 함.- 쿼리셋에 대한 리스트이면 특정 필드에 접근할 수 없음.
- 그래서
last()
메서드를 붙여서 하나의 객체를 추출한 것임.
control_orm_data.py
def handle(self, *args, **options): all_payment_annotate = all_payment.annotate(member_name=F("member__name")) print(type(all_payment_annotate)) print(type(all_payment_annotate.last())) # print(all_payment_annotate.member_name) print(all_payment_annotate.last().member_name) for item in all_payment_annotate: print(item.member_name)
terminal
실행 결과
<class 'django.db.models.query.QuerySet'> # 쿼리셋 <class 'orm.models.PaymentOrderRecord'> # 하나의 row 장동건 이병헌 이병헌 박해수 박해수 송강호 강동원 강동원 장동건
데이터 검색 (filter) : 특정 범위 내에서 찾기(in)
🥡 내가 찾고싶은 리스트 목록을 미리 지정하고, 내가 가진 데이터 값들 중 해당 리스트 내에 있는 것들만 조회할 때 사용
control_orm_data.py
def handle(self, *args, **options): price_list = [500, 1000] product_in_the_list = all_product.filter(price__in=price_list) print(product_in_the_list)
terminal
실행 결과
<QuerySet [<Product: 수저 (8ai2k8ww)>, <Product: 젓가락 (9oi8ujhy)>]>
개별 실습
Q1. 결제 전 상태의 고객의 이름을 출력
control_orm_data.py
def handle(self, *args, **options): find_customer_name = all_payment.filter(status=PaymentOrderRecord.NOT_YET).values_list( "member__name", flat=True ) print(find_customer_name) print(list(set(find_customer_name)))
terminal
실행 결과
<QuerySet ['이병헌', '이병헌', '강동원']> ['이병헌', '강동원']
Q2. 화장품을 주문하고, 환불한 고객의 이름 출력
control_orm_data.py
def handle(self, *args, **options): find_customer_name = all_payment.filter(product__name="화장품", status=PaymentOrderRecord.REFUND).values_list("member__name", flat=True) print(find_customer_name)
terminal
실행 결과
<QuerySet []>
Django 공식 문서 > QuerySet API reference
ORM 쿼리 최적화
Django > 반드시 알아야 할 5가지 ORM 쿼리
(엑셀만큼 쉬운) Django Annotation/Aggregation