Djangoで競合なくフィールドに1を足すには? F関数を使った アクセスカウンター(サンプルコードあり)

2021年12月4日Djangoの豆知識Django,F_function

課題:Djangoで競合なくDBのあるフィールドを+1したい

仮定としてあるブログアプリをDjangoで作っていたとします。

そのブログアプリで、ある記事のアクセス数を正確にカウントしたいとき次のような問題が生じます。

「同時に大量のアクセスがあったときに、Django側でアクセス数 += 1 をすると、上書きが起こって正確にカウントできない、、。」

この記事ではF関数を使ってこの 競合なく+1したい を解決する実装をします。

解決策

以下のように書けばできました。

# アクセス数を+1する
YourModelName.objects.filter(〇〇='your query').update(your_field=F('your_field') + 1)

前提

Djangoをある程度触ったことがある。

バージョン

Django == 3.2

psycopg2 == 2.9

とします。(SQLiteで大量アクセスを試すとエラーが起こることがあったのでDBにはPostgreSQLを使いました。)

サンプルコードで環境構築

ソースコードはGithubにあります。

コマンドラインで以下を実装してください

git clone -b counter https://github.com/yeconnect/django-baby-starter-template.git
cd django-baby-starter-template

docker compose build # 1分程度かかります
docker compose run --rm web python manage.py migrate
docker compose run --rm web python manage.py createsuperuser # 指示にしたがい管理ユーザを作る

# サーバーを起動 
docker compose up

管理画面 から記事を一つ追加しましょう。簡単のため、タイトルは sampletitle としました。

ソースコード解説

models.pyは以下のようにシンプルなArticleモデルです。

from django.db import models

class Article(models.Model):
  title = models.CharField(max_length=50, unique=True)
  content = models.TextField(null=True,blank=True)
  access_count = models.PositiveBigIntegerField(default=0)

  def __str__(self):
    return f'{self.title} アクセス数→{self.access_count}'

views.pyで2通りを試します。

from django.http.response import HttpResponse
from .models import Article
from django.db.models import F

def bad_increment(request):
  """ シンプルに+1をするとき """
  sample_article = Article.objects.get(title="sampletitle")
  sample_article.access_count += 1 # 一度アクセス数を取ってきて、それに1を加える
  sample_article.save() # それをDBに保存する
  return HttpResponse('+1 finished')

def good_increment(request):
  """ F関数を使った+1 """
  Article.objects.filter(title="sampletitle").update(access_count=F('access_count') + 1) # アクセス数はとって来ず、「アクセス数を1増やせ」とDBに命令する
  return HttpResponse('+1 finished')

bad_incrementは、タイトルがsampletitleのArticleを取ってくる→そのアクセスカウントに1を加える→それをDBに保存という3行です。

それに対してgood_incrementは、タイトルがsampletitleのArticleのアクセスカウントを+1して保存するという1行です。

つまりgood_incrementの方はArticleのアクセス数など知らず、「とりあえず+1」して、とDBにSQLを発行します。

性能を比較してみる。

ロートディレクトリにあるaccess_thousand_times.pyで性能をチェックします。(汚いコードですが、。)

access_thousand_times.pyは、並行処理で1000同時アクセスをするものです。(参考: Pythonの並行処理サンプル)

正常にアクセスをカウントできていれば、実行完了後にアクセス数が1000になっているはずです。

それでは別ターミナルを立ち上げ、cd django-baby-starter-templateした後、実行してみましょう。

大量アクセスをしてみる。

badを記入すると、bad_incrementを、goodを記入するとgood_incrementのURLを叩きます。

badの結果

badの結果のアクセス数

1000アクセスしたのに、547という結果になりました。(これはPCの機嫌次第ですが大体500強になりました)

同時に大量にアクセスがあると、結果が上書きされてしまい正確なアクセスカウンターではなくなってしまいました。

goodの結果

いったんsampletitleのアクセス数を0にして、今度は実行し、goodを記入します。

F関数を使ったgoodでの結果

1000アクセスして、ちょうど1000がカウントされました!アクセスカウンターの場合、F関数を使うと競合が起こることなく+1できることがわかりました。

まとめ

DjangoのF関数を使うと、+1する操作を上書きが起こることなく実行できる。

from django.db.models import F

# アクセス数を+1する
YourModelName.objects.filter(name='your query').update(your_field=F('your_field') + 1)

ちなみに以下のようなコードも動きます。(Djangoの公式の解説にありました。)

from django.db.models import F

your_instance = YourModelName.objects.get(name='your query')
your_instance.your_field = F('your_field') + 1
your_instance.save()

スポンサーリンク