ORM은 파이썬 "객체" 와 "관계형 데이터베이스"를 매핑 시켜주는 것입니다.
CRUD
1. Create : 새로운 데이터를 데이터베이스에 추가하는 작업이다.
2. Read : 데이터베이스에 저장된 데이터를 조회하는 작업이다.
3. Update: 기존 데이터를 수정하는 작업이다.
4. Delete : 데이터베이스에서 데이터를 삭제하는 작업이다.
쿼리셋은 데이터베이스에서 데이터를 조회하기 위한 API이다. 쿼리셋을 사용하면 데이터베이스에서 데이터를 필터링, 정렬, 집계하는 등의 작업을 수행할 수 있다.
all_books = Book.objects.all()
books_by_author = Book.objects.filter(author_name='Yoon')
Django ORM은 기본적으로 Lazy-loading이다.
우리가 ORM을 사용해서 입력한다고 해서 바로 SQL로 치환하여 동작하지 않고 실제로 데이터를 가져와야하는 부분에서 필요한 데이터를 가져오도록 동작한다는 이야기이다.
users = User.objects.filter(name='Yoon')
for user in users:
print(user.name)
: 위 코드에서 users에서 ORM을 선언했지만 저 구간에서 데이터를 아직 가져오지는 않는다. 실질적으로 작동을 하는 반복문에서 호출이 되는것이다.
이는 효율적으로 작동하는 경우가 있겠지만 아닌경우도 있다.
users = User.objects.filter(name='Yoon')
userName = users[0].name
user_list = list(users)
다음과 같은 상황을 생각해보자, 이런 상황에서는 users를 2곳에서 사용하기 때문에 2번 데이터를 불러오게 되는것이다. 이는 불필요한 동작을 수행한 것이다.
이를 해결하기 위해서는 자주 불러와야 하는 데이터는 따로 리스트로 할당해주는 방법이 있다.
users = User.objects.filter(name='Yoon')
user_list = list(users)
userName = user_list[0].name
Django ORM은 특정 데이터를 한번 불러온 경우 캐싱을 사용한다. 데이터에 추가적인 변경을 일으키지 않는 쿼리 데이터를 재활용 하는 경우에는 이전 데이터를 캐싱해 두었다가 다시 사용하는 것이다.
위에 있던 마지막 코드에서 ORM이 한번만 호출될 수 있는 이유는 ORM의 두번째 특징인 Caching 때문이다.
# ORM이 두번 실행되는 경우
# ORM 결과에서 첫번째 내용만 필요함(실질적인 SQL 실행이 달라짐)
first_user = users[0] # like SQL -> SELECT * FROM User u WHERE name='Yoon' LIMIT 1
# 위에서 실행한 ORM의 캐시내용은 첫번째 내용밖에 담고 있지 않음 -> 전체를 리스트화하려면 다시 ORM을 요청해야야함
user_list = list(users) # like SQL -> SELECT * FROM User u WHERE name='Yoon'
이런 식으로 다른 결과를 도출하는 ORM호출에 대해서는 사실상 SQL문이 달라지기 때문에 캐싱이 되지 않고 여러번 호출이 되는것이다.
Lazy-loading이 지금 필요한 데이터를 한정하여 가져왔다면,Eager-loading은 반대의 개념으로 지금 당장 사용하지 않을 데이터도 포함하여 Query문을 실행하기 때문에 Lazy-loading의 N+1문제의 해결책으로 많이 사용하게 된다.
아래와 같은 모델이 있다고 생각해 보겠다.
class User(AbstractBaseUser, PermissionsMixin, models.Model):
# Data fields
username = models.CharField(max_length=200, null=False, blank=True, unique=True)
first_name = models.CharField(max_length=200, null=False, blank=True)
last_name = models.CharField(max_length=200, null=False, blank=True)
class Meta:
db_table = 'user'
class UserProfile(models.Model):
user = models.OneToOneField(User, on_delete=models.CASCADE, related_name='user_profile')
phone_number = models.CharField(max_length=50, default='')
zip_code = models.CharField(max_length=20, default='')
home_address = models.CharField(max_length=200,default='')
class Meta:
db_table = 'user'
여기에서 만약 김씨인 사용자들의 UserProfile의 주소를 가져오기 위해서는
kims_users = User.objects.filter(first_name='kim')
for user in kims_users:
user_address = user.user_profile.home_address
print(f'user address :: {user_address}')
이런식으로 코드를 작성할 수 있다.
위 코드를 실행 시켜 ORM을 추적해보면 kims_users를 가져와서 호출하는 과정에서 ORM이 한번돌고, user_address를 부르는 부분마다 ORM을 계속해서 실행시킨다.
user.user_profile.home_address로 표기되어 마치 연결된것처럼 보이고, 한번에 가져와서 보여주는 듯 하지만 home_address 가져오기 위해 호출하는 user.user_profile.home_address 부분에는 우리가 지정했던 kims_users라는 ORM이 가져온 쿼리데이터 안에는 없다.
그렇기때문에 외래키로 연결되어있는 user_profile의 home_address의 정보를 가져오려면 for문이 실행되어 한명한명의 주소를 가져올때마다 추가로 user_profile을 가져오는 ORM을 실행하는 것이다.
# 반복문 실행시 ORM이 진행하는 쿼리의 예시
# kims_users를 불러오기위한 ORM 실행
SELECT * FROM user u WHERE first_name='kim'
# for문 진입하여 user_profile의 address를 가져와야함
# -> 위 ORM에 데이터가 없기때문에 해당하는 user마다 user_profile을 호출하는 추가 쿼리가 반복적으로 실행
SELECT up.home_address FROM user_profile up WHERE user_id=u.id=1
SELECT up.home_address FROM user_profile up WHERE user_id=u.id=2
SELECT up.home_address FROM user_profile up WHERE user_id=u.id=3
...
이처럼 불필요한 데이터를 계속해서 불러오게 되는것이므로, 사용자가 많아진다면 성능적으로 매우 안좋다고 할 수 있다.
이전 코드를 select_related를 이용해서 수정한다.
kims_users = User.objects.select_related('user_profile').filter(first_name='kim')
for user in kims_users:
user_address = user.user_profile.home_address
print(f'user address :: {user_address}'
for문에는 변화가 없지만, select_related를 추가하여 user_profile 부분도 같이 가져오도록 지정했다. 그렇기 때문에 for문이 실행되는 구간에서 SQL을 다시 해오지 않고, 위에서 캐싱된 데이터를 통해서 가져오기때문에 SQL이 한번만 수행된다.
OneToOneField를 사용하면 Django는 자동으로 역참조를 설정한다. 즉, UserProfile 모델의 user 필드가 User 모델을 참조하고 있기 때문에, User 모델 인스턴스에서 UserProfile 인스턴스에 접근할 수 있는 방법이 생긴다.
<주의 사항>
one-to-many, many-to-many 모델의 경우는 또 다른 방법인 prefetch_related를 사용한다.
kims_users = User.objects.prefetch_related('user_profile').filter(first_name='kim')
for user in kims_users:
user_address = user.user_profile.home_address
print(f'user address :: {user_address}')
이렇게 작성하면 쿼리는 총 2번이 된다.
SELECT * FROM auth_user WHERE first_name = 'kim';
# 여기서 1, 2, 3, ...는 첫 번째 쿼리에서 가져온 사용자들의 ID입니다.
SELECT * FROM user_profile WHERE user_id IN (1, 2, 3, ...);
select_related보다는 비효율적으로 보일 수 있지만, 특정한 상황들에서는 prefetch_related가 조금 더 효율적인 경우도 있으니 적절하게 사용하는 방법을 길러야 할 필요성이 있다.