장고 공식문서를 보던 중 쿼리 표현식에서 F()
라는 표현식을 보게 되었는데, 장고 초보인 나에게 어려워서 정리하는 내용..
F()
장고의 공식문서를 보면 F()
를 사용하면 실제 데이터 베이스에서 Python 메모리로 가져오지 않고 모델의 필드값을 사용해 데이터베이스 작업을 수행한다. 때문에 데이터베이스 레벨에서 해당하는 쿼리를 생성한다고 한다...
처음에 이해할 때에는 어차피 get()
메소드를 사용하면 Python 객체로 불러오는데 무슨 차이인지 알지 못했고, 생성되는 쿼리를 확인하면서 이해할 수 있었다.
# 여기서 사용할 간단한 모델
class Post(models.Model):
title = models.CharField(max_length=200)
text = models.TextField()
read_count = models.IntegerField(default=0)
post = Post.objects.get(pk=pk)
post.read_count += 1
post.save()
만약 Post가 한 번씩 조회될 때마다 read_count
가 하나씩 증가하도록 만들어보려고 했을 때, 간단하게 코드를 작성해 보았다.
UPDATE post
SET title = 'title1' , text = 'text1', read_count = 1
WHERE Post.id = 1
이렇게 되면 Python에서 불러온 값을 참조해 기존에 저장된 값 + 1한 값으로 Update하는 쿼리가 발생하게 된다
from django.db.models import F
post = Post.objects.get(pk=pk)
post.read_count = F('read_count') + 1
post.save()
같은 기능을 작동하는 코드를 F()
을 사용한다면 위와 같이 작성할 수 있다.
UPDATE post
SET title = 'title1' , text = 'text1', read_count = (post.read_count + 1)
WHERE Post.id = 1
그럴때에는 실제 값으로 Update가 되지 않고, 데이터베이스 내에 존재하는 값을 참조해 값을 하나 증가시키게 된다!
이 때 주의할 점은 python 코드 내에서 값을 변경하고 save()
까지 호출했지만, post
의 값은 변경되지 않아 실제 변경된 값을 알지 못하는 상태로 저장이 되어 있다.
>>> post.read_count
<CombinedExpression: F(read_count) + Value(1)>
따라서 값을 가져오고 싶다면 get()
메소드나 refresh_from_db()
를 사용해 다시 불러와야 한다.
post = Post.objects.get(pk=pk)
post.refresh_from_db()
장점
공식 문서에서 소개된 F()
를 사용했을 때의 장점은
- Python이 아닌 데이터베이스에서 연산을 처리
- 작업에 필요한 쿼리 수를 줄일 수 있다
- 경쟁 조건(race condition)을 피할 수 있다
경쟁 조건 피하기
만약 Python에서 두 개의 스레드가 실행되고 있던 중 위의 코드를 실행했다고 가정하자
그렇다면 두 개의 스레드 모두 같은 read_count
값을 가지게 된다. 하지만 F()
를 사용하지 않았다면 같은 값에서 모두 +1씩 증가한 값으로 Update를 진행하기 때문에 한 개가 손실되게 된다.
- A 스레드 조회(A.read_count = 1)
- B 스레드 조회(B.read_count = 1)
- A 스레드 업데이트(post.read_count = A.read_count + 1 = 2)
- B 스레드 업데이트(post.read_count = B.read_count + 1 = 2)
=> 실제 값은 3이 되어야 하지만 2로 갱신되어 1 손해!
하지만 F()
를 사용한다면 데이터베이스의 값을 기준으로 값을 변경하기 때문에 경쟁 조건을 피할 수 있게 되는 것이다.
주의 해야할 점
post = Post.objects.get(pk=pk) # read_count = 1
post.read_count = F('read_count') + 1
post.save() # read_count = 2
post.title = "New Title"
post.save() # read_count = 3
위에서 봤던 것처럼 F()
를 사용해 값을 업데이트 하게 되면 post에는 값이 들어있지 않고 표현식이 들어가게 된다. 때문에 처음 save()
를 실행하게 되면 데이터베이스 값에서 1을 더한 값으로 업데이트가 된다.
이후에도 아직 post.read_count
에는 기존 값에서 하나 증가시킨 값으로 변경하라는 표현식이 들어가있기 때문에 다시 save()
를 호출하면 한 번 더 값이 증가하게되어 예상하지 못했던 값으로 변경될 수 있다
📚 Reference
https://docs.djangoproject.com/en/3.2/ref/models/expressions/#f-expressions