Quantcast
Channel: django - akiyoko blog
Viewing all 52 articles
Browse latest View live

新刊『現場で使える Django 管理サイトのつくり方』頒布のお知らせ

$
0
0

2020/9/12(土)から開催される「技術書典9@技術書典オンラインマーケット」まであと1ヶ月となりましたが、そこで「あきよこブログ」として5回目のサークル参加をします。


4冊目の新刊は『現場で使える Django 管理サイトの作り方』です。


f:id:akiyoko:20200807103518p:plain:w350

安心してください。今回も Django 本ですよ~ 😉


タイトルからお察しの通り、Django の管理サイト(Django Admin)だけにフォーカスした、ニッチでオンリーワンな一冊です。注目すべきはイカレたその分厚さ。「Django」という Python 製の Webフレームワークの中の「管理サイト」という一機能だけに特化したオンリー本でありながら、本文 140ページの大ボリュームに仕上がっています。


f:id:akiyoko:20200808134231p:plain:w500

技術書典9の開催まであと1ヶ月あるのですが、実は すでに執筆は終わっていて、あとは入稿するだけという状況です。というのも、この本は2月・3月に開催予定だった「技術書典8」で頒布するはずだったのですが、新型コロナウィルスの感染拡大防止に伴ってイベント自体がなくなってしまった(その後オンラインで開催)のと子供の出産が4月に控えていたため、8割ほど完成させていたにもかかわらず途中で放置してしまっていたのでした。そしてこの度、育休を取れたのをきっかけに無事執筆を終えることができたという次第なのです。

そしてこの度、ヒマだったので事前にアンケート調査をしてみました(アンケートはすでに締め切っています)。

docs.google.com

皆さま、ご協力ありがとうございました。アンケートの結果発表を兼ねて、新刊『現場で使える Django 管理サイトのつくり方』がどんな本なのか? どんな課題を解決してくれるのか? について解説します。


どんな本なの?

皆さんは、管理サイト(Django Admin)を使っていますか?

おそらく Django を利用している開発者の ほとんどが「イエス」と答えるでしょう。実際、事前アンケートでは Django 利用者の9割近くが「いつも使っている」あるいは「たまに使っている」と回答しています。


f:id:akiyoko:20200812091900p:plain:w450

しかしながら、その仕組みをきちんと理解せずに使っている人が意外と多いのではないでしょうか。管理サイトは Django の仕組みや設計思想をうまく活かしたアプリになっており、管理サイトを理解することはそれらを知る手助けにもなります。これからも Django を長く使っていくのであれば、管理サイトを深く知っておくことは必ず強力な武器になるでしょう。


また、管理サイトはあらゆるモデルに対応できるように汎用的に作られており、ある程度のカスタマイズも考慮されていて幅広いユースケースで利用することができます。実際の現場では「開発中のテストデータ投入」や「システム利用ユーザーの情報管理」で利用しているケースが多いようです。


f:id:akiyoko:20200812091920p:plain:w500

管理サイトには利用するメリットがたくさんありますが、これについても事前にアンケートを採ってみました。


f:id:akiyoko:20200812091936p:plain:w600

結果を見ると、デフォルトで使える点やほんの数行書くだけでモデルの CRUD 機能が追加できる点が特に高く評価されていて、お手軽で簡単に使えるというのが管理サイトの大きなメリットになっています。

しかし、当然ながら管理サイトは「万能」ではありません。たとえば「少しカスタマイズすれば一般ユーザー向けの画面として使えそう」と目論んでいたら、後になって管理サイトの特性や限界を無視した要望がいっぱい出てきて余計に工数が膨らんでしまったというのはありがちな失敗パターンです。

メリットばかりがクローズアップされがちな管理サイトですが、ここで敢えて負の面に目を向けてみます。次のアンケート結果を見てください。


f:id:akiyoko:20200812092003p:plain:w600

「ある程度以上のカスタマイズになると難易度が上がる」や「簡単にカスタマイズできるかどうかのジャッジにノウハウや調査が必要」など、カスタマイズ系のデメリットが圧倒的に多いことが分かります。「日本語の情報が少ない」や「仕様を把握するのにひと苦労」が多いのは、困ったときのヒントが得られにくいという状況を表しているものと考えられます。これらを踏まえると、管理サイトをカスタマイズする場合は 事前にその限界(基本仕様でどこまでできるのか)とカスタマイズの特性(どんなカスタマイズが簡単でどんなカスタマイズが難しいのか)を十分に把握しておく必要があるでしょう。


そこで本書では、いつも使う管理サイトだからこそ知っておきたい現場レベルの知識やノウハウについて、次の3つのポイントを中心に解説していきます。

  1. 管理サイトの基本仕様
  2. 管理サイトの仕組みを活かしたカスタマイズ戦略
  3. カスタマイズ後のテスト


本書を読めば、管理サイトの基本から応用に至るまでの幅広い知識が得られ、Django への理解がさらに深まるでしょう。


 

対象読者

本書の読者としては、

  • 管理サイト(Django Admin)のことをもっと知りたい方
  • これから管理サイトのカスタマイズをしようとしている方

を想定しています。

特に、これから管理サイトのカスタマイズをしようとしている方には是非とも読んでほしい内容になっています。

もし管理サイトに興味がなくても、

  • ユーザーモデルやパーミッションの仕組み
  • テンプレートや静的ファイルのルックアップの仕組み
  • Selenium を使ったブラウザテストの書き方

などに興味があれば刺さるかもしれません。

最低限必要な知識としては「Django の仕組みが何となく理解できていること」です。Django 公式チュートリアルDjango Girls チュートリアルをひと通り終えたくらいであれば問題はないでしょう。また、拙著『現場で使える Django の教科書《基礎編》』を読み終えたくらいの知識があれば万全です。



以降で、章ごとの読みどころを紹介していきます。



第1章: 管理サイトの基本仕様

管理サイトは意外と機能が豊富で、その仕様を把握するのにもひと苦労です。そこで本書では手始めに、管理サイトの基本仕様を詳しく紹介しています。

まず、管理サイトの全体像が捉えやすいように画面遷移図(紙面サンプル ① を参照)を示しています。こういった画面遷移図ってググっても何故かなかなか見つからないんですよね。。

その後、Django 初心者向けに利用手順を紹介したあとで、管理サイトが利用している Django の仕組みである「ユーザーモデル」「パーミッションによるアクセス制御」「変更履歴」について解説しています。パーミッションあたりはあまり理解していない人も多いのではないでしょうか。

最後に、画面ごとに詳細な説明をしています。管理サイトは普段は気付かないような隠れ機能があったりするので、手の込んだ仕掛けに驚かされるでしょう。


  • 第1章: 管理サイトの基本仕様
    • 1.1: 管理サイトとは
    • 1.2: 基本機能
    • 1.3: 利用手順
    • 1.4: ユーザーモデルについて
    • 1.5: パーミッションによるアクセス制御
    • 1.6: 変更履歴について
    • 1.7: 各画面の詳細説明
    • 1.8: まとめ


《 紙面サンプル ① 》

f:id:akiyoko:20200806155855p:plain

《 紙面サンプル ② 》

f:id:akiyoko:20200807014635p:plain

第2章: 管理サイトのカスタマイズ

この章で気を付けたのは、なるべく図を多くするということです。公式ドキュメントや特に Stack Overflow などで検索した場合は図が無かったりするので、「一体どんなカスタマイズができるのかイメージが掴めない」ということが多いのです。そういう不満を払しょくするために、このカスタマイズをするとどんなことが実現できるのかがビジュアルで掴めるようにしてみました。

そしてこの章の目玉は、カスタマイズに利用できる AdminSite と ModelAdmin のクラス変数とメソッドの一覧表(紙面サンプル ④ を参照)と、管理サイトで使われるテンプレートファイルの一覧表です。こういうのがあると便利だなという気持ちと書くのがめちゃくちゃ大変だなという気持ちで随分葛藤しましたが、今となっては書いて正解だったと感じています。


  • 第2章: 管理サイトのカスタマイズ
    • 2.1: 内部構造とカスタマイズ方針
    • 2.2: AdminSite を利用したカスタマイズ
    • 2.3: ModelAdmin を利用したカスタマイズ
    • 2.4: テンプレートのカスタマイズ
    • 2.5: CSS のカスタマイズ
    • 2.6: Django パッケージを使ったカスタマイズ
    • 2.7: まとめ


《 紙面サンプル ③ 》

f:id:akiyoko:20200807014228p:plain

《 紙面サンプル ④ 》

f:id:akiyoko:20200807113843p:plain


第3章: 管理サイトのテスト

管理サイトのカスタマイズをする場合は AdminSite や ModelAdmin のクラス変数、メソッドをオーバーライドすることになりますが、それらの断片化したコードだけをユニットテストして例えカバレッジを100%にしたところで、機能が想定通りに動作することを保証したことにはなりません。そんなわけで、管理サイトのテストでは画面ごとにビューのテストをした上で、「lxml」などのパッケージを利用してレンダリングされた HTML から要素をパースして検証するなどの工夫が必要になります。第3章の前半では、そういったテストケースのコード例を挙げて解説しています。

章の後半では、Selenium を使ったブラウザテストについて解説しています。Selenium を使ったテストでは通常、テンプレートの HTML 要素自体や class 属性に変更が加わるとテスト側にも修正を加えねばならず保守が大変になりますが、管理サイトはテンプレートが固まっているためブラウザテストとの相性がよいです。特に、管理サイトでは、Selenium によるブラウザテストをするための AdminSeleniumTestCase が提供されています。章の最後に、このクラスを継承したテストコード例を紹介しています。


  • 第3章: 管理サイトのテスト
    • 3.1: テスト方針
    • 3.2: 通常のユニットテスト
    • 3.3: Selenium によるブラウザテスト
    • 3.4: まとめ


《 紙面サンプル ⑤ 》

f:id:akiyoko:20200806160103p:plain

《 紙面サンプル ⑥ 》

f:id:akiyoko:20200807013459p:plain



頒布本情報

これから入稿するので価格はまだ決めていませんが、技術書典9向けの特別価格として「送料込み 1,200円」くらいを予定しています。

価格については決まり次第、更新します。



最後に

今回、いわゆる「管理サイト本」を書いた理由としては、自分の知識の整理のためということもありますが、どちらかというと Django をもっと現場に普及させたいという気持ちの方が強いです。つまり、現場で頻繁に使う管理サイトのまとまった日本語情報があれば、Django がもっと現場で使われやすくなるんじゃないかと思ったのです。


きっかけは、現場で管理サイトのカスタマイズ案件が二つ続いたことでした。そこには主にレビューで参加したのですが、「担当者が違うとこんなにも書き方が違うのか。保守が大変そうだなぁ」「わざわざこんなことしなくてもフックポイントがあるのに」「あらら、全部 Selenium テストで書いちゃったのね」などとガク然としたのです。そのときに「これを見といてね」と言えるものがなかった苦い経験から、管理サイトを少し真面目に使おうとするときに現場に一冊あれば安心な本を書こうと思い至ったのでした。


ということで、Django 開発のお供に『現場で使える Django 管理サイトのつくり方』を是非どうぞ!!😊


f:id:akiyoko:20200812092310p:plain:w200


 

宣伝

これまで Django の本を3冊出しました。Django 開発のお供にどうぞ。

現場で使える Django の教科書《基礎編》

「現場で使える Django の教科書」シリーズ第1弾となる Django の技術同人誌。Django を現場で使うための基礎知識やベストプラクティスについて、初心者・初級者向けに解説した本です。B5・本文180ページ。


★ Amazon(電子版/ペーパー版)


★ BOOTH(ペーパー版)


現場で使える Django の教科書《実践編》

《基礎編》の続編にあたる「現場で使える Django の教科書」シリーズの第2弾。認証まわり、ファイルアップロード、ユニットテスト、デプロイ、セキュリティ、高速化など、さらに実践的な内容に踏み込んでいます。現場で Django を本格的に活用したい、あるいはすでに活用している方にピッタリの一冊。B5・本文180ページ。


★ Amazon(電子版)


★ BOOTH(ペーパー版)


現場で使える Django REST Framework の教科書

Django で REST API を構築する際の鉄板ライブラリである「Django REST Framework」(通称「DRF」)にフォーカスした、「現場で使える Django の教科書」シリーズの第3弾。B5・本文204ページ。


★ Amazon(電子版)


★ BOOTH(ペーパー版)


Django のトランザクションについて(初級者向け)

$
0
0

この投稿は 「Django Advent Calendar 2020 - Qiita」 3日目の記事です。

akiyoko です。
Django のトランザクションについては拙著『現場で使える Django の教科書《基礎編》』の第6章「モデル(Model)」でも触れていますが、この記事ではもう少し詳しく解説してみたいと思います。




検証環境
  • Windows 10 Home
  • Django 3.1
  • PostgreSQL 13.1



 

はじめに

トランザクションは連続した複数のデータベース操作をひとつにまとめたものです。

トランザクションは、データベースをある一貫した状態から別の一貫した状態へ変更するアクションを1つに束ねたものである。トランザクション処理は、既知の一貫した状態のデータベースを維持するよう設計されており、相互依存のある複数の操作が全て完了するか、全てキャンセルされることを保証する。


トランザクション処理 - Wikipedia


一連の処理がすべて完了したらレコードの登録・変更・削除をまとめてデータベースに反映(コミット)し、エラーなどで一連の処理が完了しなかった場合はその過程でおこなわれたレコードの登録・変更・削除を無かったことにする(ロールバック)必要がある場合にトランザクションが利用されます。


 

デフォルトはオートコミット

Django のデフォルトでは、データベースのレコードを登録・更新・削除するための各クエリは、モデルオブジェクトの save() や delete() が実行された時点で即座にデータベースに反映されます。これは「オートコミットモード」と呼ばれます。*1

つまり、デフォルトではトランザクションは利用されません。


例えば、本の購入決済をするためのビューの中で、次のように連続したレコード操作があるとします。

f:id:akiyoko:20201127151205p:plain

デフォルトでは、「① 注文情報を登録」「③ 在庫数を 1 減らす」「④ 注文情報のステータスを更新」といったクエリはそれぞれの時点でコミット(データベースに反映)されます。

PostgreSQL の SQL ログは次のように出力されます。


《 PostgreSQL の SQL ログ 》

[pid-1] SET TIME ZONE 'UTC'
[pid-1] SELECT "book"."id", "book"."title", "book"."price" FROM "book" WHERE "book"."id" = 1 LIMIT 21
[pid-1] INSERT INTO "order" ("status", "total_amount", "ordered_by_id", "ordered_at") VALUES ('01', 1000, 1, '2020-12-01T03:00:18.900883+00:00'::timestamptz) RETURNING "order"."id"
[pid-1] SELECT "stock"."id", "stock"."book_id", "stock"."quantity", "stock"."updated_at" FROM "stock" WHERE "stock"."book_id" = 1 LIMIT 21
[pid-1] UPDATE "stock" SET "book_id" = 1, "quantity" = 0, "updated_at" = '2020-12-01T03:00:18.927935+00:00'::timestamptz WHERE "stock"."id" = 1
[pid-1] UPDATE "order" SET "status" = '02', "total_amount" = 1000, "ordered_by_id" = 1, "ordered_at" = '2020-12-01T03:00:18.900883+00:00'::timestamptz WHERE "order"."id" = 1

この中の INSERT や UPDATE は逐一コミットされます。




モデル、およびビューのソースコードのイメージは次の通りです。

shop/models.py(モデル)

from django.contrib.auth import get_user_model
from django.db import models

User = get_user_model()


classBook(models.Model):
    """本モデル"""classMeta:
        db_table = 'book'
        verbose_name = verbose_name_plural = '本'

    title = models.CharField('タイトル', max_length=255, unique=True)
    price = models.PositiveIntegerField('価格', null=True, blank=True)

    def__str__(self):
        return self.title


classBookStock(models.Model):
    """在庫モデル"""classMeta:
        db_table = 'stock'
        verbose_name = verbose_name_plural = '在庫'

    book = models.OneToOneField(Book, verbose_name='本', on_delete=models.CASCADE)
    quantity = models.PositiveIntegerField('在庫数', default=0)
    updated_at = models.DateTimeField('更新日時', auto_now=True)

    def__str__(self):
        return f'{self.book.title} ({self.quantity})'classOrder(models.Model):
    """注文モデル"""classMeta:
        db_table = 'order'
        verbose_name = verbose_name_plural = '注文'

    STATUS_PAYMENT_PROCESSING = '01'
    STATUS_PAYMENT_OK = '02'
    STATUS_PAYMENT_NG = '03'
    STATUS_PAYMENT_ERROR = '09'
    STATUS_CHOICES = (
        (STATUS_PAYMENT_PROCESSING, '決済中'),
        (STATUS_PAYMENT_OK, '決済OK'),
        (STATUS_PAYMENT_NG, '決済NG'),
        (STATUS_PAYMENT_ERROR, '決済エラー'),
    )

    status = models.CharField('ステータス', max_length=2, choices=STATUS_CHOICES)
    total_amount = models.PositiveIntegerField('金額合計')
    ordered_by = models.ForeignKey(User, verbose_name='注文者', on_delete=models.PROTECT, editable=False)
    ordered_at = models.DateTimeField('注文日時', auto_now_add=True)

    def__str__(self):
        return f'{self.get_status_display()} ({self.ordered_at:%Y-%m-%d %H:%M})'


shop/views.py(ビュー)

from django.http.response import HttpResponseRedirect
from django.shortcuts import get_object_or_404, reverse
from django.views import View

from .models import Book, BookStock, Order


classCheckoutView(View):
    ...(略)...

    defpost(self, request, *args, **kwargs):
        book = get_object_or_404(Book, pk=kwargs['pk'])

        # ① 注文情報を登録
        order = Order(
            status=Order.STATUS_PAYMENT_PROCESSING,
            total_amount=book.price,
            ordered_by=request.user,
        )
        order.save()

        # ② 在庫数を確認
        book_stock = get_object_or_404(BookStock, book=book)
        # ③ 在庫数を1減らして更新
        book_stock.quantity -= 1
        book_stock.save()

        ...(決済処理)...

        # ④ 注文情報のステータスを更新
        order.status = Order.STATUS_PAYMENT_OK
        order.save()

        ...(略)...

        return HttpResponseRedirect(reverse('shop:checkout_complete'))


PostgreSQL へのデータベース接続設定は次のようになります。

config/settings.py(設定ファイル)

DATABASES = {
    'default': {
        'ENGINE': 'django.db.backends.postgresql',
        'NAME': 'mysite',
        'USER': 'mysiteuser',
        'PASSWORD': 'mysiteuserpass',
        'HOST': 'localhost',
        'PORT': '5432',
    }
}


ここまでは OK ですよね。



次に、ビューの中で予期せぬエラーが発生した場合について考えてみましょう。

f:id:akiyoko:20201127151541p:plain


デフォルトのオートコミットモードでは、エラーが発生する前の処理はすでにコミットされてしまっているので、プログラムで明示的に後始末処理(いわゆる手動ロールバック)をしないといけません。いろんなところで予期せぬエラーが発生することを考えると、いちいち手動でロールバック処理を書くのは大変ですよね。そこで Django ORM が提供している「トランザクション」 *2の機能を利用します。


f:id:akiyoko:20201127152322p:plain

このように一連の処理をトランザクションで囲んでおけば、エラーが発生した場合のロールバック処理を自動でおこなってくれるのです。


Django でトランザクションを利用するには、大きく次の二つの方法があります。

  • 【方法1】ATOMIC_REQUESTS を有効化する
  • 【方法2】transaction.atomic() で囲む


それぞれについて詳しく見ていきましょう。



 

【方法1】ATOMIC_REQUESTS を有効化する

トランザクションを適用するには、データベースの「ATOMIC_REQUESTS」設定を有効化するのが一番手っ取り早いです。有効化するには、次のように設定ファイルの「DATABASES」の「ATOMIC_REQUESTS」を True(デフォルトは False)にします。*3


config/settings.py(設定ファイル)

DATABASES = {
    'default': {
        'ENGINE': 'django.db.backends.postgresql',
        'NAME': 'mysite',
        'USER': 'mysiteuser',
        'PASSWORD': 'mysiteuserpass',
        'HOST': 'localhost',
        'PORT': '5432',
        'ATOMIC_REQUESTS': True,  # 追加
    }
}


モデルやビューには何も手を加える必要はないので簡単ですよね。これだけで、ビュー全体のデータベース操作がひとつのトランザクションで囲まれます。


f:id:akiyoko:20201127170848p:plain


なお、ミドルウェア内のデータベース操作はこのトランザクションの範囲外となるので要注意です(あくまでビューの開始から終了までが一つのトランザクションになります)。



SQLクエリログは次のように出力されます。

《 PostgreSQL の SQL ログ 》

[pid-1] SET TIME ZONE 'UTC'
[pid-1] BEGIN
[pid-1] SELECT "book"."id", "book"."title", "book"."price" FROM "book" WHERE "book"."id" = 1 LIMIT 21
[pid-1] INSERT INTO "order" ("status", "total_amount", "ordered_by_id", "ordered_at") VALUES ('01', 1000, 1, '2020-12-01T03:01:53.319483+00:00'::timestamptz) RETURNING "order"."id"
[pid-1] SELECT "stock"."id", "stock"."book_id", "stock"."quantity", "stock"."updated_at" FROM "stock" WHERE "stock"."book_id" = 1 LIMIT 21
[pid-1] UPDATE "stock" SET "book_id" = 1, "quantity" = 0, "updated_at" = '2020-12-01T03:01:53.334689+00:00'::timestamptz WHERE "stock"."id" = 1
[pid-1] UPDATE "order" SET "status" = '02', "total_amount" = 1000, "ordered_by_id" = 1, "ordered_at" = '2020-12-01T03:01:53.319483+00:00'::timestamptz WHERE "order"."id" = 2
[pid-1] COMMIT

すべての SQL が「BEGIN」と「COMMIT」で囲まれていて、これがひとつのトランザクションになっています。



予期せぬエラーなどで ビューから例外が漏れた場合、Django ORM によるロールバックが自動でおこなわれます。その際の SQL ログは次のようになります。ちなみに、検査制約「CONSTRAINT stock_quantity_check CHECK (quantity >= 0)」が付いた在庫テーブルの在庫数(quantity)が「0」の場合に在庫を減らそうとしてエラーが出ちゃった例です。

《 PostgreSQL の SQL ログ 》

[pid-1] SET TIME ZONE 'UTC'
[pid-1] BEGIN
[pid-1] SELECT "book"."id", "book"."title", "book"."price" FROM "book" WHERE "book"."id" = 1 LIMIT 21
[pid-1] INSERT INTO "order" ("status", "total_amount", "ordered_by_id", "ordered_at") VALUES ('01', 1000, 1, '2020-12-01T03:02:29.849050+00:00'::timestamptz) RETURNING "order"."id"
[pid-1] SELECT "stock"."id", "stock"."book_id", "stock"."quantity", "stock"."updated_at" FROM "stock" WHERE "stock"."book_id" = 1 LIMIT 21
[pid-1] UPDATE "stock" SET "book_id" = 1, "quantity" =  -1, "updated_at" = '2020-12-01T03:02:29.856001+00:00'::timestamptz WHERE "stock"."id" = 1
[pid-1] ERROR:  new row for relation "stock" violates check constraint "stock_quantity_check"
[pid-1] ROLLBACK

ERROR の後に、ROLLBACK がおこなわれています。



「ATOMIC_REQUESTS」の利点は導入の手軽さですが、お手軽な反面、公式ドキュメントにもあるように、同時アクセス数が多いようなシステムではスループットを下げてしまう可能性もあり、性能面での注意が必要です。

このトランザクションモデルは簡潔ではありますが、トラフィックが増加するときには非効率となります。全てのビューでトランザクションを扱うとオーバーヘッドが増加します。パフォーマンスへの影響は、アプリケーションのクエリパターンと、どれだけうまくデータベースがロッキングを扱うかに依存します。


データベースのトランザクション | Django ドキュメント | Django


 

【方法2】transaction.atomic() で囲む

「ATOMIC_REQUESTS」の代わりに、transaction.atomic()を使ってトランザクションを実現する方法もあります。「ATOMIC_REQUESTS」がビュー全体をトランザクションで囲むのに対し、transaction.atomic() は with 句を使ってトランザクションの適用範囲を自由に設定することができます。


先の購入決済のビューの例では、在庫数の更新は可能な限り迅速にデータベースに反映しておく必要があるでしょう。例えば、在庫があと「1」しか残っていない状態で、あるユーザが決済処理を実行中だと、在庫は実質「0」ですよね。このタイミングで他のユーザが在庫テーブルを参照した場合に在庫数が「1」のままにならないように、在庫数を減らす更新を決済処理の前に一旦コミットして在庫を引き当て(取り置き)しておくようにしてみます。


transaction.atomic() を適用したコード例は次のようになります。設定ファイルの「ATOMIC_REQUESTS」の設定は無効化しておきます。

shop/views.py(ビュー)

from django.db import transaction  # 追加from django.http.response import HttpResponseRedirect
from django.shortcuts import get_object_or_404, reverse
from django.views import View

from .models import Book, BookStock, Order


classCheckoutView(View):
    ...(略)...

    defpost(self, request, *args, **kwargs):
        book = get_object_or_404(Book, pk=kwargs['pk'])

        with transaction.atomic():  # 追加# ① 注文情報を登録
            order = Order(
                status=Order.STATUS_PAYMENT_PROCESSING,
                total_amount=book.price,
                ordered_by=request.user,
            )
            order.save()

            # ② 在庫数を確認
            book_stock = get_object_or_404(BookStock, book=book)
            # ③ 在庫数を1減らして更新
            book_stock.quantity -= 1
            book_stock.save()

        ...(決済処理)...

        with transaction.atomic():  # 追加# ④ 注文情報のステータスを更新
            order.status = Order.STATUS_PAYMENT_OK
            order.save()

        ...(略)...

        return HttpResponseRedirect(reverse('shop:checkout_complete'))


これで、次のようにトランザクションを細かく分けることができるようになりました。*4

f:id:akiyoko:20201127220410p:plain


《 PostgreSQL の SQL ログ 》

[pid-1] SET TIME ZONE 'UTC'
[pid-1] SELECT "book"."id", "book"."title", "book"."price" FROM "book" WHERE "book"."id" = 1 LIMIT 21
[pid-1] BEGIN
[pid-1] INSERT INTO "order" ("status", "total_amount", "ordered_by_id", "ordered_at") VALUES ('01', 1000, 1, '2020-12-01T03:03:11.428997+00:00'::timestamptz) RETURNING "order"."id"
[pid-1] SELECT "stock"."id", "stock"."book_id", "stock"."quantity", "stock"."updated_at" FROM "stock" WHERE "stock"."book_id" = 1 LIMIT 21
[pid-1] UPDATE "stock" SET "book_id" = 1, "quantity" = 0, "updated_at" = '2020-12-01T03:03:11.442959+00:00'::timestamptz WHERE "stock"."id" = 1
[pid-1] COMMIT
[pid-1] BEGIN
[pid-1] UPDATE "order" SET "status" = '02', "total_amount" = 1000, "ordered_by_id" = 1, "ordered_at" = '2020-12-01T03:03:11.428997+00:00'::timestamptz WHERE "order"."id" = 3
[pid-1] COMMIT

「BEGIN」と「COMMIT」で囲まれたトランザクションを2つ確認することができました。




 

(おまけ)ATOMIC_REQUESTS を有効化しているときに、特定のビュー内で自前でトランザクションを切る方法

ほとんどのビューではビュー全体をひとつのトランザクションで囲めばよいが、一部のビューでのみトランザクションを細かく切らないといけないといったケースがあります。そのような場合には、ATOMIC_REQUESTS を有効にした上で、対象のビューに「django.db.transaction.non_atomic_requests」を適用して ATOMIC_REQUESTS の効果を無効化してしまえばよいです。注意点としては、Django のクラスベースビューにこれを適用するには、method_decoratorを使って dispatch() メソッドに non_atomic_requestsを適用してあげる必要があります。


参考
- データベースのトランザクション | Django ドキュメント | Django
- クラスベースビュー入門 | Django ドキュメント | Django


デコレータ以外のコードは先ほどのものと同じ内容です。

shop/views.py(ビュー)

from django.db import transaction
from django.http.response import HttpResponseRedirect
from django.shortcuts import get_object_or_404, reverse
from django.utils.decorators import method_decorator  # 追加from django.views import View

from .models import Book, BookStock, Order


@method_decorator(transaction.non_atomic_requests, name='dispatch')  # 追加classCheckoutView(View):
    ...(略)...

    defpost(self, request, *args, **kwargs):
        book = get_object_or_404(Book, pk=kwargs['pk'])

        with transaction.atomic():
            # ① 注文情報を登録
            order = Order(
                status=Order.STATUS_PAYMENT_PROCESSING,
                total_amount=book.price,
                ordered_by=request.user,
            )
            order.save()

            # ② 在庫数を確認
            book_stock = get_object_or_404(BookStock, book=book)
            # ③ 在庫数を1減らして更新
            book_stock.quantity -= 1
            book_stock.save()

        ...(決済処理)...

        with transaction.atomic():
            # ④ 注文情報のステータスを更新
            order.status = Order.STATUS_PAYMENT_OK
            order.save()

        ...(略)...

        return HttpResponseRedirect(reverse('shop:checkout_complete'))


こうすることで、特定のビュー内で ATOMIC_REQUESTS の効果を無効にして、自前でトランザクションを切ることができます。

[pid-1] SET TIME ZONE 'UTC'
[pid-1] SELECT "book"."id", "book"."title", "book"."price", "book"."updated_at" FROM "book" WHERE "book"."id" = 1 LIMIT 21
[pid-1] SELECT "stock"."id", "stock"."book_id", "stock"."quantity", "stock"."updated_at" FROM "stock" WHERE "stock"."book_id" = 1 LIMIT 21
[pid-1] BEGIN
[pid-1] UPDATE "stock" SET "book_id" = 1, "quantity" = 0, "updated_at" = '2020-12-01T03:06:00.940420+00:00'::timestamptz WHERE "stock"."id" = 1
[pid-1] INSERT INTO "order" ("status", "total_amount", "ordered_by_id", "ordered_at") VALUES ('01', 1000, 1, '2020-12-01T03:06:00.945405+00:00'::timestamptz) RETURNING "order"."id"
[pid-1] COMMIT
[pid-1] BEGIN
[pid-1] UPDATE "order" SET "status" = '02', "total_amount" = 1000, "ordered_by_id" = 1, "ordered_at" = '2020-12-01T03:06:00.945405+00:00'::timestamptz WHERE "order"."id" = 4
[pid-1] COMMIT

 

まとめ

Django はデフォルトではトランザクションを利用しておらず、すべてのクエリ操作が逐一コミットされます。Django でトランザクションを利用するには、大きく「ATOMIC_REQUESTS を有効化する」方法と「transaction.atomic() で囲む」方法の二つがあります。前者は導入が超簡単ですが、性能面での影響を考慮する必要があります。


ちなみに Django が提供しているトランザクションの API は今回紹介したものだけではありません。詳しい説明については 公式ドキュメントの後半部分をお読みください。なお、DjangoCongress JP 2019 の Denzow さんの「どうなってるの?Djangoのトランザクション」の資料が分かりやすいのでぜひ合わせて参照ください。


speakerdeck.com



 

宣伝

Django の技術同人誌をこれまでに4冊出しました。開発のお供にどうぞ。



現場で使える Django の教科書《基礎編》

「現場で使える Django の教科書」シリーズ第1弾となる Django の技術同人誌。Django を現場で使うための基礎知識やベストプラクティスについて、初心者・初級者向けに解説した本です。B5・本文180ページ。


★ Amazon(電子版/ペーパー版)


★ BOOTH(ペーパー版)



現場で使える Django の教科書《実践編》

《基礎編》の続編にあたる「現場で使える Django の教科書」シリーズの第2弾。認証まわり、ファイルアップロード、ユニットテスト、デプロイ、セキュリティ、高速化など、さらに実践的な内容に踏み込んでいます。現場で Django を本格的に活用したい、あるいはすでに活用している方にピッタリの一冊。B5・本文180ページ。


★ Amazon(電子版)


★ BOOTH(ペーパー版)



現場で使える Django REST Framework の教科書

Django で REST API を構築する際の鉄板ライブラリである「Django REST Framework」(通称「DRF」)にフォーカスした、「現場で使える Django の教科書」シリーズの第3弾。B5・本文204ページ。


★ Amazon(電子版)


★ BOOTH(ペーパー版)



現場で使える Django 管理サイトのつくり方

Django の管理サイト(Django Admin)だけに特化した、ニッチでオンリーワンな一冊。管理サイトをカスタマイズする前に絶対に読んでほしい本です。B5・本文152ページ。


★ Amazon(電子版)


★ BOOTH(ペーパー版)

*1:https://docs.djangoproject.com/ja/3.1/topics/db/transactions/#django-s-default-transaction-behavior

*2:https://docs.djangoproject.com/ja/3.1/topics/db/transactions/#managing-database-transactions

*3:https://docs.djangoproject.com/ja/3.1/topics/db/transactions/#tying-transactions-to-http-requests

*4:なお、決済処理以降でエラーが出た場合に1つ目のトランザクションの処理を手動でロールバックする必要がありますが、それらについては割愛します。

Django で楽観的排他制御を簡単に実装する方法(初級者向け)

$
0
0

この投稿は 「Django Advent Calendar 2020 - Qiita」 4日目の記事です。


akiyoko です。
この記事では、Django で簡単に楽観的排他制御を実装する方法について説明します。Django の楽観的排他制御については拙著『現場で使える Django REST Framework の教科書』の第11章「現場で使える Tips 集」の中でも触れていますが、もう少し詳しく解説してみたいと思います。


f:id:akiyoko:20201204095634p:plain



検証環境
  • Windows 10 Home
  • Django 3.1
  • PostgreSQL 13.1



 

はじめに

複数ユーザからほぼ同じタイミングで同じレコードに対して更新をおこなうと、タイミングによってはレースコンディション(競合状態)が発生してしまい、更新内容が上書きされてしまうことがあります。

次の図を見てください。

f:id:akiyoko:20201127234524p:plain


一例として、決済処理の中で在庫テーブルを 1 ずつ減らして更新する処理を抜き出したものです。Aさんが在庫テーブルを参照(①)して更新(③)するまでのわずかな時間の中に、Bさんからの在庫テーブルの参照(②)が挟まっている状況を示しています。AさんとBさんが在庫テーブルを参照した時点(① および ②)では双方とも在庫数が「1」となっているのですが、Aさんが在庫テーブルを更新した③のタイミングで在庫数が「0」になるのにも関わらず、Bさんがレコードを「0」に更新(すなわち上書き)できてしまっています。なんと、在庫が「1」だったにも関わらず、二回も購入ができてしまっているのです。


これがレースコンディション(競合状態)です。解決方法としては「排他制御」と呼ばれる対策が採られることが多いです。中でも、更新時にデータが最新のものかどうかをチェックして、データが最新でない場合にエラーを出してレコードを更新しないようにする方式のものを特に「楽観的排他制御」と呼びます。



楽観的排他制御と悲観的排他制御

排他制御には、「楽観的排他制御」(楽観ロック)と「悲観的排他制御」(悲観ロック)があります。特徴や違いについては参考記事に譲ります。


参考


引用してまとめるとこうなります。



楽観的排他制御

  • 更新対象のデータがデータ取得時と同じ状態であることを確認してから更新することで、データの整合性を保証する方式
  • データ取得時の最終更新日時またはバージョン番号を条件に含めて更新する
  • 同時更新されることがあまりない場合に使う

悲観的排他制御

  • 更新対象のデータを取得する際にロックをかけることで、他のトランザクションから更新されないようにする方式
  • 「SELECT FOR UPDATE」を使う
  • 同時更新されることが多く、トランザクションが短い場合に使う


 
Django でも「SELECT ... FOR UPDATE」を使うことができますが、本記事では比較的実装が簡単な「楽観的排他制御」の具体的な実装方法について説明します。


次に、データベースのトランザクション分離レベル(isolation level)の話をします。



 

READ COMMITTED の挙動

Django においては、データベースのトランザクション分離レベルは「READ COMMITTED」がベストとされていて、データベース接続設定のデフォルトのトランザクション分離レベルも「READ COMMITTED」となっています。*1


ちなみに、Django 2.0 以降、MySQL をバックエンドにした場合の分離レベルのデフォルト設定も「READ COMMITTED」に変更になりました。MySQL(InnoDB)自体のデフォルトのトランザクション分離レベルは「REPEATABLE READ」ですが、もしそれに合わせたい場合は、Django 側のデータベースオプションで明示的に

DATABASES = {
    'default': {
        'ENGINE': 'django.db.backends.mysql',
        ...
        'OPTIONS': {
            ...
            'isolation_level': 'REPEATABLE READ',
        }
    }
}

として、「isolation_level」の指定をしないといけません。



「READ COMMITTED」のトランザクション内では、「REPEATABLE READ」とは異なり、他のトランザクションのコミットによる変更が参照できます。例えば、次の図のように、他のユーザのトランザクションが終了した瞬間から最新の値が参照できるようになっています。


f:id:akiyoko:20201129144914p:plain


つまり、「READ COMMITTED」では、更新するギリギリのタイミングで最新のデータを参照することでレースコンディション(競合状態)を起こしにくくすることができると言えるでしょう。


これを踏まえた上で、楽観的排他制御の具体的な実装方法としては、例えばモデルにバージョンを保持するフィールドを追加し、データが更新されるたびに値をインクリメントして更新するようにします。そして更新の際にバージョンの値を検索条件に含め、更新件数が0件となった場合に排他エラーを発生させます。このような仕組みを自前で実装してもよいのですが、django-concurrencyパッケージを利用すると簡単に実装することができます。



 

django-concurrency パッケージを利用する

django-concurrencyパッケージを利用すると、楽観的排他制御が簡単に実現できます。

まず、pip で django-concurrency をインストールします。

(venv) > pip install django-concurrency==2.2.*


あとは、排他制御したいモデルに次のように concurrency.fields.AutoIncVersionField を使って「version」フィールドを追加するだけです。


shop/models.py(モデル)

from concurrency.fields import AutoIncVersionField  # 追加from django.db import models


classBook(models.Model):
    """本モデル"""classMeta:
        db_table = 'book'
        verbose_name = verbose_name_plural = '本'

    title = models.CharField('タイトル', max_length=255, unique=True)
    price = models.PositiveIntegerField('価格', null=True, blank=True)

    def__str__(self):
        return self.title


classBookStock(models.Model):
    """在庫モデル"""classMeta:
        db_table = 'stock'
        verbose_name = verbose_name_plural = '在庫'

    book = models.OneToOneField(Book, verbose_name='本', on_delete=models.CASCADE)
    quantity = models.PositiveIntegerField('在庫数', default=0)
    updated_at = models.DateTimeField('更新日時', auto_now=True)
    version = AutoIncVersionField(verbose_name='バージョン')   # 追加def__str__(self):
        return f'{self.book.title} ({self.quantity})'


AutoIncVersionField は初回登録時に初期値「1」で自動保存され、更新するたびに自動でインクリメントされていきます。以下は Django シェル環境での実行例です。

>>> s1 = BookStock.objects.create(book_id=1, quantity=10)
>>> s1.version
1
>>> s1.save()
>>> s1.version
2

レコードの更新時にはバージョンの値が更新条件に自動的に加えられ、条件に合致しない場合は更新件数が0件になり、RecordModifiedError が発生します。

>>> s2 = BookStock.objects.get(book_id=1)
>>> s2.version
2
>>> s1.save()
>>> s1.version
3
>>> s2.save()
Traceback (most recent call last):
  ...(略)...
concurrency.exceptions.RecordModifiedError: Record has been modified


先ほどの決済処理の例では次のようにBさんの処理がエラーになり、更新ができなくなります。

f:id:akiyoko:20201127234543p:plain


「ATOMIC_REQUESTS」や「transaction.atomic() 」でトランザクションを利用していて、RecordModifiedError がトランザクション内でキャッチされなかった場合はトランザクションがロールバックされます。


PostgreSQL の SQL ログの例は次のようになります(「pid-1」および「pid2」はプロセス番号を分かりやすいように書き換えたものです)。

《 PostgreSQL の SQL ログ 》

12:00:11.976 JST [pid-1] SET TIME ZONE 'UTC'
12:00:11.980 JST [pid-1] SELECT "book"."id", "book"."title", "book"."price" FROM "book" WHERE "book"."id" = 1 LIMIT 21
12:00:11.984 JST [pid-1] BEGIN
12:00:11.986 JST [pid-1] INSERT INTO "order" ("status", "total_amount", "ordered_by_id", "ordered_at") VALUES ('01', 1000, 1, '2020-12-01T03:00:11.983496+00:00'::timestamptz) RETURNING "order"."id"
12:00:11.992 JST [pid-1] SELECT "stock"."id", "stock"."book_id", "stock"."quantity", "stock"."updated_at", "stock"."version" FROM "stock" WHERE "stock"."book_id" = 1 LIMIT 21

12:00:12.011 JST [pid-2] SET TIME ZONE 'UTC'
12:00:12.013 JST [pid-2] SELECT "book"."id", "book"."title", "book"."price" FROM "book" WHERE "book"."id" = 1 LIMIT 21
12:00:12.019 JST [pid-2] BEGIN
12:00:12.020 JST [pid-2] INSERT INTO "order" ("status", "total_amount", "ordered_by_id", "ordered_at") VALUES ('01', 1000, 1, '2020-12-01T03:00:12.016340+00:00'::timestamptz) RETURNING "order"."id"
12:00:12.028 JST [pid-2] SELECT "stock"."id", "stock"."book_id", "stock"."quantity", "stock"."updated_at", "stock"."version" FROM "stock" WHERE "stock"."book_id" = 1 LIMIT 21

12:00:13.109 JST [pid-1] SELECT (1) AS "a" FROM "stock" WHERE "stock"."id" = 1 LIMIT 1
12:00:13.112 JST [pid-1] UPDATE "stock" SET "book_id" = 1, "quantity" = 0, "updated_at" = '2020-12-01T03:00:13.106776+00:00'::timestamptz, "version" = 2 WHERE ("stock"."id" = 1 AND "stock"."version" = 1)
12:00:13.115 JST [pid-1] COMMIT
12:00:13.124 JST [pid-1] BEGIN
12:00:13.126 JST [pid-1] UPDATE "order" SET "status" = '02', "total_amount" = 1000, "ordered_by_id" = 1, "ordered_at" = '2020-12-01T03:00:11.983496+00:00'::timestamptz WHERE "order"."id" = 55
12:00:13.128 JST [pid-1] COMMIT

12:00:14.412 JST [pid-2] SELECT (1) AS "a" FROM "stock" WHERE "stock"."id" = 1 LIMIT 1
12:00:14.417 JST [pid-2] UPDATE "stock" SET "book_id" = 1, "quantity" = 0, "updated_at" = '2020-12-01T03:00:14.409133+00:00'::timestamptz, "version" = 2 WHERE ("stock"."id" = 1 AND "stock"."version" = 1)
12:00:14.419 JST [pid-2] ROLLBACK

UPDATE 時に version の値が検索条件として加えられているのが確認できます。「pid-2」の UPDATE 時にRecordModifiedError が発生し、トランザクションがロールバックされています。




django-concurrency の「concurrency.middleware.ConcurrencyMiddleware」というミドルウェアを使うことで、独自の排他エラー画面に遷移させることも可能です。

config/settings.py(設定ファイル)

MIDDLEWARE = [
    'concurrency.middleware.ConcurrencyMiddleware',  # 追加'django.middleware.security.SecurityMiddleware',
    'django.contrib.sessions.middleware.SessionMiddleware',
    'django.middleware.common.CommonMiddleware',
    'django.middleware.csrf.CsrfViewMiddleware',
    'django.contrib.auth.middleware.AuthenticationMiddleware',
    'django.contrib.messages.middleware.MessageMiddleware',
    'django.middleware.clickjacking.XFrameOptionsMiddleware',
]

ConcurrencyMiddleware には ビューから例外が漏れたとき実行される process_exception が実装されていて、例外クラスが RecordModifiedError の場合に「concurrency.views.conflict」というコールバックが呼ばれ、最終的に「409.html」という名前のテンプレートがレンダリングされるような作りになっています。


そこで、設定ファイルの「TEMPLATES」の設定を次のように修正した上で、

config/settings.py(設定ファイル)

TEMPLATES = [
    {
        'BACKEND': 'django.template.backends.django.DjangoTemplates',
        'DIRS': [BASE_DIR / 'templates'],  # 修正'APP_DIRS': True,
        ...(略)...
    },
]

BASE_DIR の直下の「templates」ディレクトリの下に 409.html を配置しておくと、排他エラー発生時にこの画面に遷移させることが可能です(templates/base.html のコードは省略)。

templates/409.html

{% extends "base.html" %}

{% block title %}排他エラー{% endblock %}

{% block content %}
<h3>排他エラー</h3><hr><p>排他エラーが発生しました。</p>
{% endblock %}


f:id:akiyoko:20201128155216p:plain:w400


排他制御エラー発生時のコールバック関数をカスタマイズすることも可能です。公式ドキュメントに書かれている通り、設定ファイルに「CONCURRENCY_HANDLER409」を定義します。


config/settings.py(設定ファイル)

MIDDLEWARE = [
    'concurrency.middleware.ConcurrencyMiddleware',
    'django.middleware.security.SecurityMiddleware',
    'django.contrib.sessions.middleware.SessionMiddleware',
    'django.middleware.common.CommonMiddleware',
    'django.middleware.csrf.CsrfViewMiddleware',
    'django.contrib.auth.middleware.AuthenticationMiddleware',
    'django.contrib.messages.middleware.MessageMiddleware',
    'django.middleware.clickjacking.XFrameOptionsMiddleware',
]

CONCURRENCY_HANDLER409 = 'common.handlers.conflict'# 追加


common/handlers.py

from django.shortcuts import render

from shop.models import BookStock


defconflict(request, target=None, template_name='409.html'):
    ifisinstance(target, BookStock):
        template_name = 'shop/book_stock_conflict.html'try:
        saved = target.__class__._default_manager.get(pk=target.pk)
    except target.__class__.DoesNotExists:
        saved = None

    context = {
        'target': target,  # 更新しようとしたモデルオブジェクト'saved': saved,  # 最新状態のモデルオブジェクト
    }
    return render(request, template_name, context)


templates/shop/book_stock_conflict.html

{% extends "base.html" %}

{% block title %}排他エラー{% endblock %}

{% block content %}
<h3>排他エラー</h3><hr><p>在庫レコードの更新時に排他エラーが発生しました。</p>
{% endblock %}


f:id:akiyoko:20201128181553p:plain:w400



なお、disable_concurrencyをデコレータや with 句で利用することで、特定のビューやモデル操作に対して排他制御をしないようにすることもできます。またこの django-concurrency は、管理サイトや Django REST Framework(DRF)でも利用できるので大変便利です。



 

まとめ

複数ユーザからほぼ同じタイミングで同じレコードに対して更新をおこなうと、タイミングによってはレースコンディション(競合状態)が発生し、更新内容が上書きされてしまうことがあります。レースコンディションが業務にクリティカルな影響を与えてしまう場合は排他制御が必要になります。

Django で楽観的排他制御を実現するには、django-concurrency パッケージを利用すれば非常に簡単に実装することができます。



 

宣伝

Django の技術同人誌をこれまでに4冊出しました。開発のお供にどうぞ。



現場で使える Django の教科書《基礎編》

「現場で使える Django の教科書」シリーズ第1弾となる Django の技術同人誌。Django を現場で使うための基礎知識やベストプラクティスについて、初心者・初級者向けに解説した本です。B5・本文180ページ。


★ Amazon(電子版/ペーパー版)


★ BOOTH(ペーパー版)



現場で使える Django の教科書《実践編》

《基礎編》の続編にあたる「現場で使える Django の教科書」シリーズの第2弾。認証まわり、ファイルアップロード、ユニットテスト、デプロイ、セキュリティ、高速化など、さらに実践的な内容に踏み込んでいます。現場で Django を本格的に活用したい、あるいはすでに活用している方にピッタリの一冊。B5・本文180ページ。


★ Amazon(電子版)


★ BOOTH(ペーパー版)



現場で使える Django REST Framework の教科書

Django で REST API を構築する際の鉄板ライブラリである「Django REST Framework」(通称「DRF」)にフォーカスした、「現場で使える Django の教科書」シリーズの第3弾。B5・本文204ページ。


★ Amazon(電子版)


★ BOOTH(ペーパー版)



現場で使える Django 管理サイトのつくり方

Django の管理サイト(Django Admin)だけに特化した、ニッチでオンリーワンな一冊。管理サイトをカスタマイズする前に絶対に読んでほしい本です。B5・本文152ページ。


★ Amazon(電子版)


★ BOOTH(ペーパー版)

Django でデータベースビューを扱う方法(初級者向け)

$
0
0

この投稿は 「Django Advent Calendar 2020 - Qiita」 5日目の記事です。


akiyoko です。
この記事では、Django でデータベースビューを扱う方法について説明します。「Django のコンポーネントとしてのビュー(View)」ではなく、「データベースのビュー」の話です。本記事では区別のために「データベースビュー」と表記することにします。



f:id:akiyoko:20201205082412p:plain:w350



検証環境
  • Windows 10 Home
  • Django 3.1
  • PostgreSQL 13.1



 

はじめに

あまり知られていないかもしれませんが、Django でもデータベースビューを扱うことができます。具体的には、データベースビューに対応するモデルを用意することで、データベースビューのレコードをモデルオブジェクトとして取り扱うことができます(しかし当然ながら、データベースビューへのレコードの登録や更新、削除はできませんのでご注意を)。



ところで、データベースビューを使うと何が嬉しいのでしょうか?

例えば、テーブルの結果を 集計したり、サブクエリを使ったり、複数のテーブルを UNIONしたりするには、Django ORM の API を使って実現できないこともありませんが、コードが複雑になり、思わぬバグが混入する可能性が高くなってしまいます。それならいっそのこと SQL で書いてしまう方が早くて確実だというケースもあるでしょう。そんなときにデータベースビューを使えば、モデル側のコードをすっきりさせることができるのです。 *1




 

データベースビューに対応するモデル

通常のモデルとの違いは、Meta に「managed = False」を指定するだけです。これで、モデルをマイグレーションの対象外にすることができます。*2


次の例を見てください。

sales/models.py(モデル)

from django.db import models


classSalesResultPerMonth(models.Model):
    classMeta:
        managed = False
        db_table = 'sales_result_per_month'
        verbose_name = verbose_name_plural = '月別合計売上金額'

    sales_month = models.DateField('売上月')
    total_amount = models.PositiveIntegerField('合計売上金額')
    target_amount = models.PositiveIntegerField('目標売上金額')


モデルの Meta クラスに「managed = False」を指定しています。ちなみにデータベースビューに対応したモデルには「null=True」や「blank=True」などのフィールドオプションは付ける必要はありません。データベースビューには登録や更新ができないため、付けてもあまり意味がないからです。



モデルをマイグレーションの対象外にすると、マイグレーション(migrate)コマンドを実行したときにマイグレーションファイルの内容がデータベースのテーブルに反映されず、テーブルが新たに作成されたり、テーブル構造が変更されたりすることがなくなります。


f:id:akiyoko:20201205100932p:plain:w550


 

具体例

簡単な例として、売上実績テーブルから月次の売上金額の合計を集計して、売上目標テーブルの売上目標金額と比較することを取り上げてみます。これをデータベースビューとして利用するイメージは次のようになります。


f:id:akiyoko:20201201094402p:plain

通常のモデルを作成する

まず、sales アプリケーションに「売上目標モデル」と「売上実績モデル」を次のように定義します。これらはマイグレーション対象となる通常のモデルです。

sales/models.py(モデル)

from django.db import models


classSalesTarget(models.Model):
    """売上目標モデル"""classMeta:
        db_table = 'sales_target'
        verbose_name = verbose_name_plural = '売上目標'

    sales_month = models.DateField('売上月')
    amount = models.PositiveIntegerField('目標金額')
    created_at = models.DateTimeField('登録日時', auto_now_add=True)

    def__str__(self):
        return f'{self.sales_month:%Y年%m月}'classSalesResult(models.Model):
    """売上実績モデル"""classMeta:
        db_table = 'sales_result'
        verbose_name = verbose_name_plural = '売上実績'

    sales_date = models.DateField('売上日')
    amount = models.PositiveIntegerField('売上金額')
    subject = models.CharField('件名', max_length=255)
    created_at = models.DateTimeField('登録日時', auto_now_add=True)

    def__str__(self):
        return f'{self.sales_date:%Y年%m月%d日} - {self.subject}'


makemigrations コマンドでマイグレーションファイルを作成して、migrate コマンドでマイグレーションを実行すると、次のようなテーブルが作成されます。

(venv) > python manage.py makemigrations sales
(venv) > python manage.py migrate sales


f:id:akiyoko:20201201094218p:plain

データベースビュー作成用の DDL をマイグレーションファイルに書く

データベースビューを作成するための DDL は、マイグレーションファイルに書くのがよいでしょう。次のように「--empty」オプションを付けて makemigrations コマンドを実行すると、空のマイグレーションファイルを作成することができます。

(venv) $ python manage.py makemigrations sales --empty


上記のコマンドを実行すると、次のようなマイグレーションファイルが「sales/migrations」ディレクトリの下に生成されます。


sales/migrations/0002_auto_20201204_2134.py(マイグレーションファイル)

# Generated by Django 3.1.3 on 2020-12-04 12:34from django.db import migrations


classMigration(migrations.Migration):

    dependencies = [
        ('sales', '0001_initial'),
    ]

    operations = [
    ]


このひな型ファイルを次のように編集して、sales_result_per_month ビューを作成する DDL を書き加えます。

# Generated by Django 3.1.3 on 2020-12-04 12:34from django.db import migrations


classMigration(migrations.Migration):

    dependencies = [
        ('sales', '0001_initial'),
    ]

    sql = """        CREATE VIEW sales_result_per_month AS            SELECT                ROW_NUMBER() OVER() AS id,                t.sales_month,                t.amount AS target_amount,                SUM(r.amount) AS total_amount            FROM                sales_target t            LEFT OUTER JOIN                sales_result r            ON                TO_CHAR(t.sales_month, 'YYYY-MM') = TO_CHAR(r.sales_date, 'YYYY-MM')            GROUP BY                t.sales_month, t.amount            ORDER BY                t.sales_month;"""

    reverse_sql = """        DROP VIEW IF EXISTS sales_result_per_month;"""

    operations = [
        migrations.RunSQL(sql, reverse_sql),
    ]

RunSQL の第一引数には migrate コマンドが実行されたときに発行する SQL を、第二引数には特定のバージョンのマイグレーションの状態に戻す *3ときに発行する SQL を指定します(第二引数は省略可)。ちなみにこの SQL は PostgreSQL 向けのものです。


マイグレーションファイルを手動で書くやり方については、次の記事を参考にしてみてください。

参考



マイグレーションを実行すると、データベースビューが作成されます。

(venv) $ python manage.py migrate sales


pgAdmin 4 上で確認すると、このようになっています。


f:id:akiyoko:20201201114913p:plain


ビューに対応するモデルを作成する

sales_result_per_month ビューに対応するモデルを作成します。ここで Meta クラスに「managed = False」を指定します。繰り返しになりますが、これでマイグレーションの対象から外れます。


sales/models.py(モデル)

from django.db import models


classSalesTarget(models.Model):
    """売上目標モデル"""
    ...(略)...


classSalesResult(models.Model):
    """売上実績モデル"""
    ...(略)...


classSalesResultPerMonth(models.Model):  # 追加"""月別合計売上金額モデル"""classMeta:
        managed = False
        db_table = 'sales_result_per_month'
        verbose_name = verbose_name_plural = '月別合計売上金額'

    sales_month = models.DateField('売上月')
    total_amount = models.PositiveIntegerField('合計売上金額')
    target_amount = models.PositiveIntegerField('目標売上金額')



モデルを作成する際には、次のように

(venv) > python manage.py inspectdb sales_result_per_month

inspectdbという既存のテーブルからモデルを自動生成する Django コマンドを実行した結果を参考にするとよいでしょう。


(出力結果例)

# This is an auto-generated Django model module.# You'll have to do the following manually to clean this up:#   * Rearrange models' order#   * Make sure each model has one field with primary_key=True#   * Make sure each ForeignKey and OneToOneField has `on_delete` set to the desired behavior#   * Remove `managed = False` lines if you wish to allow Django to create, modify, and delete the table# Feel free to rename the models, but don't rename db_table values or field names.from django.db import models


classSalesResultPerMonth(models.Model):
    id = models.BigIntegerField(blank=True, null=True)
    sales_month = models.DateField(blank=True, null=True)
    target_amount = models.IntegerField(blank=True, null=True)
    total_amount = models.BigIntegerField(blank=True, null=True)

    classMeta:
        managed = False# Created from a view. Don't remove.
        db_table = 'sales_result_per_month'



 

管理サイトでデータベースビューの内容を確認する

最後に、このデータベースビューの内容を管理サイト上で確認できるようにしてみましょう。sales/admin.py を次のように編集します。


sales/admin.py(管理サイト用モジュール)

from django.contrib import admin

from common.helpers.admin_helper import format_yen, format_yyyy_nen_mm_gatsu
from .models import SalesResult, SalesResultPerMonth, SalesTarget


classSalesResultAdmin(admin.ModelAdmin):
    """売上実績モデル用 ModelAdmin"""################################ モデル一覧画面のカスタマイズ###############################
    list_display = ('sales_date', 'format_amount', 'subject')
    ordering = ('sales_date', 'created_at')

    defformat_amount(self, obj):
        return format_yen(obj.amount)

    format_amount.short_description = '売上金額'
    format_amount.admin_order_field = 'amount'################################ モデル追加・変更画面のカスタマイズ###############################
    readonly_fields = ('id', 'created_at')


classSalesTargetAdmin(admin.ModelAdmin):
    """売上目標モデル用 ModelAdmin"""################################ モデル一覧画面のカスタマイズ###############################
    list_display = ('format_sales_month', 'format_amount')
    ordering = ('sales_month',)

    defformat_sales_month(self, obj):
        return format_yyyy_nen_mm_gatsu(obj.sales_month)

    format_sales_month.short_description = '売上月'
    format_sales_month.admin_order_field = 'sales_month'defformat_amount(self, obj):
        return format_yen(obj.amount)

    format_amount.short_description = '目標金額'
    format_amount.admin_order_field = 'amount'################################ モデル追加・変更画面のカスタマイズ###############################
    readonly_fields = ('id', 'created_at')


classSalesResultPerMonthAdmin(admin.ModelAdmin):
    """月別合計売上金額モデル用 ModelAdmin"""################################ モデル一覧画面のカスタマイズ###############################
    list_display = ('format_sales_month', 'format_target_amount', 'format_total_amount')
    ordering = ('sales_month',)

    defformat_sales_month(self, obj):
        return format_yyyy_nen_mm_gatsu(obj.sales_month)

    format_sales_month.short_description = '売上月'
    format_sales_month.admin_order_field = 'sales_month'defformat_target_amount(self, obj):
        return format_yen(obj.target_amount)

    format_target_amount.short_description = '目標売上金額'
    format_target_amount.admin_order_field = 'target_amount'defformat_total_amount(self, obj):
        return format_yen(obj.total_amount)

    format_total_amount.short_description = '合計売上金額'
    format_total_amount.admin_order_field = 'total_amount'################################ その他のカスタマイズ###############################defhas_add_permission(self, request):
        returnFalsedefhas_change_permission(self, request, obj=None):
        returnFalsedefhas_delete_permission(self, request, obj=None):
        returnFalse


admin.site.register(SalesTarget, SalesTargetAdmin)
admin.site.register(SalesResult, SalesResultAdmin)
admin.site.register(SalesResultPerMonth, SalesResultPerMonthAdmin)



管理サイトの月別合計売上金額モデルの一覧画面はこのように表示されます。


f:id:akiyoko:20201202105916p:plain:w500

何がどうなっているのか?が気になる方は、拙著『現場で使える Django 管理サイトのつくり方』にいろいろと書いてあるので、ぜひお手に取ってご確認くださいませ 🙇

akiyoko.hatenablog.jp



 

(おまけ)マイグレーションファイルの SQL を複数種類のデータベースに対応させる方法

例示したマイグレーションファイルの SQL は PostgreSQL だけにしか使えません。PostgreSQL を使うだけならこれでもよいのですが、ローカルでは SQLite を使って、本番環境では PostgreSQL を使って・・といったケースには対応できません。そのような場合は、RunPython を使って次のように書き分けることができます。

# Generated by Django 3.1.3 on 2020-12-04 12:34from django.db import migrations
from django.db.migrations import exceptions


defcode(apps, schema_editor):
    if schema_editor.connection.vendor == 'sqlite':
        schema_editor.execute("""            CREATE VIEW sales_result_per_month AS                SELECT                    ROW_NUMBER() OVER() AS id,                    t.sales_month,                    t.amount AS target_amount,                    SUM(r.amount) AS total_amount                FROM                    sales_target t                LEFT OUTER JOIN                    sales_result r                ON                    STRFTIME('%Y-%m', t.sales_month) = STRFTIME('%Y-%m', r.sales_date)                GROUP BY                    t.sales_month                ORDER BY                    sales_month;""")
    elif schema_editor.connection.vendor == 'postgresql':
        schema_editor.execute("""            CREATE VIEW sales_result_per_month AS                SELECT                    ROW_NUMBER() OVER() AS id,                    t.sales_month,                    t.amount AS target_amount,                    SUM(r.amount) AS total_amount                FROM                    sales_target t                LEFT OUTER JOIN                    sales_result r                ON                    TO_CHAR(t.sales_month, 'YYYY-MM') = TO_CHAR(r.sales_date, 'YYYY-MM')                GROUP BY                    t.sales_month, t.amount                ORDER BY                    t.sales_month;""")
    elif schema_editor.connection.vendor == 'mysql':
        schema_editor.execute("""            CREATE VIEW sales_result_per_month AS                SELECT                    ROW_NUMBER() OVER() AS id,                    t.sales_month,                    t.amount AS target_amount,                    SUM(r.amount) AS total_amount                FROM                    sales_target t                LEFT OUTER JOIN                    sales_result r                ON                    DATE_FORMAT(t.sales_month, '%Y-%m') = DATE_FORMAT(r.sales_date, '%Y-%m')                GROUP BY                    t.sales_month;                ORDER BY                    t.sales_month;""", params=None)
    else:
        raise exceptions.BadMigrationError(
            'Database vendor should be SQLite, PostgreSQL, or MySQL.')


defreverse_code(apps, schema_editor):
    schema_editor.execute('DROP VIEW IF EXISTS sales_result_per_month;')


classMigration(migrations.Migration):
    dependencies = [
        ('sales', '0001_initial'),
    ]

    operations = [
        migrations.RunPython(code, reverse_code, atomic=False)
    ]


 

まとめ

モデルの Meta クラスに「managed = False」を指定すると、マイグレーションの対象から外すことができるため、データベースビューに対応するモデルを作成することができます。


またこの方法を利用すれば、すでにあるテーブルに対応するモデルを作成することも可能です。いろいろ便利に使えそうですね!



 

宣伝

Django の技術同人誌をこれまでに4冊出しました。開発のお供にどうぞ。



現場で使える Django の教科書《基礎編》

「現場で使える Django の教科書」シリーズ第1弾となる Django の技術同人誌。Django を現場で使うための基礎知識やベストプラクティスについて、初心者・初級者向けに解説した本です。B5・本文180ページ。


★ Amazon(電子版/ペーパー版)


★ BOOTH(ペーパー版)



現場で使える Django の教科書《実践編》

《基礎編》の続編にあたる「現場で使える Django の教科書」シリーズの第2弾。認証まわり、ファイルアップロード、ユニットテスト、デプロイ、セキュリティ、高速化など、さらに実践的な内容に踏み込んでいます。現場で Django を本格的に活用したい、あるいはすでに活用している方にピッタリの一冊。B5・本文180ページ。


★ Amazon(電子版)


★ BOOTH(ペーパー版)



現場で使える Django REST Framework の教科書

Django で REST API を構築する際の鉄板ライブラリである「Django REST Framework」(通称「DRF」)にフォーカスした、「現場で使える Django の教科書」シリーズの第3弾。B5・本文204ページ。


★ Amazon(電子版)


★ BOOTH(ペーパー版)



現場で使える Django 管理サイトのつくり方

Django の管理サイト(Django Admin)だけに特化した、ニッチでオンリーワンな一冊。管理サイトをカスタマイズする前に絶対に読んでほしい本です。B5・本文152ページ。


★ Amazon(電子版)


★ BOOTH(ペーパー版)

*1:モデルマネージャの raw() や django.db.connection.cursor オブジェクトの execute() を使えば、生の SQL を書くことも可能です。

*2:https://docs.djangoproject.com/ja/3.1/ref/models/options/#managed

*3:マイグレーション | Django ドキュメント | Django

Django REST Framework で API ドキュメンテーション機能を利用する方法(DRF 3.12 最新版)

$
0
0

この投稿は 「Django Advent Calendar 2020 - Qiita」 8日目の記事です。


akiyoko です。
この記事では、Django REST Framework(通称「DRF」)で API スキーマを自動生成する「API ドキュメンテーション」機能を簡単に利用する方法について説明します。


f:id:akiyoko:20201208093319p:plain:w600



検証環境
  • Windows 10 Home
  • Django 3.1
  • Django REST Framework 3.12



 

はじめに

Django REST Framework には、API スキーマ(どのような URL にどのようなリクエストを送ればどのようなレスポンスが返ってくるかという API の定義を記述したもの)を出力してくれる「API ドキュメンテーション」機能が備わっています。



DRF 公式ドキュメント


API スキーマの仕様は「OpenAPI 3.0」に準拠しています。「OpenAPI 3.0」は、REST API インタフェース記述の標準化を目指して Swagger 2.0 を統合して策定された仕様です。

swagger.io


OpenAPI スキーマファイル(YAML形式)の例を次に示します。

openapi:"3.0.0"info:title: Simple API overview
  version: 2.0.0
paths:/:get:operationId: listVersionsv2
      summary: List API versions
      responses:'200':description: |-
            200 response
          content:application/json:examples:foo:value:{"versions":[{"status":"CURRENT",
                            "updated":"2011-01-21T11:33:21Z",
                            "id":"v2.0",
                            "links":[{"href":"http://127.0.0.1:8774/v2/",
                                    "rel":"self"}]},
                        {"status":"EXPERIMENTAL",
                            "updated":"2013-07-23T11:33:21Z",
                            "id":"v3.0",
                            "links":[{"href":"http://127.0.0.1:8774/v3/",
                                    "rel":"self"}]}]}

...(略)...

https://github.com/OAI/OpenAPI-Specification/blob/master/examples/v3.0/api-with-examples.yamlより引用



 

DRF 3.12 の状況

現在の最新バージョンの DRF 3.12で OpenAPI 3.0 に準拠した API スキーマを出力する方法として、筆者は次の二つの方法を推奨します。ひとつは DRF 標準の generateschema コマンドを使って OpenAPIスキーマファイルを出力する方法、もうひとつは 「drf-spectacular」というサードパーティ製パッケージを使う方法です。*1



ちなみに、DRF で API スキーマを出力するにはこれまで「CoreAPI」ベースの「rest_framework.schemas.coreapi.AutoSchema」を使うのがデフォルトだったのですが、これは DRF 3.10 で非推奨になり、DRF 3.12 以降で削除予定となっています。*2
なおこの記事を書いている時点ではまだ削除はされていないようですが、今後いつ削除されるとも分かりません。

Django REST Framework v3.10 でネイティブな OpenAPI ベースのスキーマ生成が導入されたことにより、CoreAPI ベースのスキーマの使用は非推奨となりました。

Django REST Framework v3.12 で CoreAPI 関連のコードはすべて削除されます。それまでに OpenAPI スキーマに切り替えてください。

https://www.django-rest-framework.org/coreapi/の内容を抜粋して翻訳)



本記事では、DRF 3.12 で OpenAPI 3.0 に準拠した API スキーマを出力する方法として

  • 【方法1】generateschema コマンドを使って OpenAPIスキーマファイルを出力する方法
  • 【方法2】drf-spectacular を利用する方法

について紹介します。


 

【方法1】DRF 標準の generateschema コマンドを使ってスキーマファイルを出力する

DRF 標準の generateschema コマンドを使って OpenAPI スキーマファイルを出力するには、PyYAML パッケージと uritemplate パッケージが必要です。


そこで、次のようにして PyYAML と uritemplate をそれぞれインストールします。

(venv) > pip install PyYAML==5.3.* uritemplate==3.0.*


次のように generateschema コマンドを実行すれば、OpenAPI 3.0 に対応した YAML形式のスキーマファイルを出力することができます。ちなみに format オプションで「openapi-json」を指定すれば、JSON 形式のファイルを出力することも可能です。

(venv) > python manage.py generateschema --file schema.yml


コマンドを実行すると、次のようなスキーマファイルが出力されます。

openapi: 3.0.2
info:title:''version:''paths:/api/v0/books/:get:operationId: listBookListCreates
      description:"\u672C\u30E2\u30C7\u30EB\u306E\u53D6\u5F97\uFF08\u4E00\u89A7\uFF09\        API\u306B\u5BFE\u5FDC\u3059\u308B\u30CF\u30F3\u30C9\u30E9\u30E1\u30BD\u30C3\
        \u30C9"
      parameters:[]responses:'200':content:application/json:schema:type: array
                items:{}description:''tags:- api

      ...(略)...



出力したスキーマファイルを、OpenAPI に対応した Swagger EditorSwagger Inspectorなどのツールに読み込ませて、ツール上で API の定義を確認したり、API クライアント(API へのリクエストを実行できるツール)として利用したりすることができます。



オンライン API ドキュメントエディタ Swagger Editor の画面例


f:id:akiyoko:20201202234527p:plain:w500

Swagger Inspector の画面例


f:id:akiyoko:20201202234559p:plain:w500

このほかにも OpenAPI スキーマファイルが利用できるツールは多数存在します。

openapi.tools


公式ドキュメントでは、URLconf やテンプレートの準備をして Swagger UI と ReDoc の二種類の API ドキュメント画面を表示させる方法を紹介していますが、次に示す drf-spectacularを利用する方が簡単なので筆者はそちらを推します(公式ドキュメントでも代替案として drf-spectacular を紹介しています)。



 

【方法2】drf-spectacular を利用する

これまで DRF で標準になっていた CoreAPI ベースの「AutoSchema」を使えば、次のような API ドキュメント画面を簡単に構築することができました。*3


f:id:akiyoko:20201204000317p:plain:w450

しかし先述のように、CoreAPI ベースの「AutoSchema」の利用は非推奨になってしまいました。そこで、代わりに「drf-spectacular」を利用すれば、OpenAPI 3.0 準拠の API スキーマファイルを出力できるのに加えて、Swagger UI 形式と ReDoc 形式の二種類の API ドキュメント画面を自動生成することができます。

 

導入方法

導入方法はほぼ 公式ドキュメントに書いてある通りです。


まず、drf-spectacular をインストールします。*4

(venv) > pip install drf-spectacular==0.12.*


次に、設定ファイルの「INSTALLED_APPS」に「drf_spectacular」を追加し、「REST_FRAMEWORK」の「DEFAULT_SCHEMA_CLASS」に「drf_spectacular.openapi.AutoSchema」を設定します。


config/settings.py(設定ファイル)

INSTALLED_APPS = [
    'django.contrib.admin',
    'django.contrib.auth',
    'django.contrib.contenttypes',
    'django.contrib.sessions',
    'django.contrib.messages',
    'django.contrib.staticfiles',

    # 3rd party apps'drf_spectacular',  # 追加

    ...
]

...

REST_FRAMEWORK = {
    'DEFAULT_SCHEMA_CLASS': 'drf_spectacular.openapi.AutoSchema',  # 追加
}


OpenAPI スキーマファイルを出力するだけなら設定はここまでで OK です。コマンドプロンプトから spectacular コマンドを使えば、API スキーマファイルを出力することができます。

(venv) > python manage.py spectacular --file schema.yml



APIドキュメント画面を自動生成したいのであれば、続けて URLconf に次の設定を書き加えます。ちなみに、DEBUG が False の場合にのみ APIドキュメント画面を表示できるようにしています。


config/urls.py(URLconf)

from django.urls import include, path
from drf_spectacular.views import SpectacularAPIView, SpectacularRedocView, SpectacularSwaggerView  # 追加

urlpatterns = [
    ...(略)...
]

if settings.DEBUG:
    urlpatterns += [
        path('api/schema/', SpectacularAPIView.as_view(), name='schema'),                                      # 追加
        path('api/schema/swagger-ui/', SpectacularSwaggerView.as_view(url_name='schema'), name='swagger-ui'),  # 追加
        path('api/schema/redoc/', SpectacularRedocView.as_view(url_name='schema'), name='redoc'),              # 追加
    ]


 

Swagger UI 形式の APIドキュメント画面

設定完了後に runserver を起動し、(起動 URL が「127.0.0.1:8000」の場合)ブラウザで「http://127.0.0.1:8000/api/schema/swagger-ui/」にアクセスすれば、Swagger UI 形式の APIドキュメント画面が表示できます。


f:id:akiyoko:20201202231159p:plain:w550

API ごとの「Try it out」ボタンをクリックして、「Parameters」を適宜入力して「Execute」ボタンをクリックすることでリクエストを送信することができるので、API クライアントとしても利用することができます。



 

ReDoc 形式の APIドキュメント画面

ReDoc 形式の APIドキュメント画面(http://127.0.0.1:8000/api/schema/redoc/)は次のようになります。


f:id:akiyoko:20201202232040p:plain:w550



 

カスタマイズ

これでよいかと思いきや、ちょっとしたカスタマイズが必要な箇所もあります。

例えば、ListAPIView や RetrieveAPIView などの汎用 APIView 系ビューや ModelViewSet 系ビューなど(クラス変数 serializer_class を指定するもの)は入出力のパラメータが自動判定されるのですが、APIView を継承しているビューではドキュメンテーションが不完全になります。その場合は、extend_schema でドキュメンテーション用の追加設定をしてあげる必要があります。


apiv0/views.py(ビュー)

from drf_spectacular.utils import extend_schema
from rest_framework import status, views
from rest_framework.response import Response

from shop.models import Book
from .serializers import BookSerializer


classBookListCreateAPIView(views.APIView):
    """本モデルの取得(一覧)・登録APIクラス"""@extend_schema(
        responses={200: BookSerializer},
    )
    defget(self, request, *args, **kwargs):
        """本モデルの取得(一覧)APIに対応するハンドラメソッド"""

        ...(略)...

        return Response(serializer.data, status.HTTP_200_OK)

    @extend_schema(
        request=BookSerializer,
        responses={201: BookSerializer},
    )
    defpost(self, request, *args, **kwargs):
        """本モデルの登録APIに対応するハンドラメソッド"""

        ...(略)...

        return Response(serializer.data, status.HTTP_201_CREATED)


他にもいろいろ追加設定ができますが、詳細については公式ドキュメントを参照してください。

Settings — drf-spectacular documentation





 

まとめ

Django REST Framework(DRF)にはデフォルトで OpenAPI 3.0 に対応した API スキーマを出力してくれる「API ドキュメンテーション」機能が備わっています。利用方法は簡単で、generateschema コマンドでスキーマファイルを出力するだけです(ただし、PyYAML と uritemplate のインストールが必要)。


API スキーマをリアルタイムに反映した API ドキュメント画面を表示したいのであれば、drf-spectacular パッケージを利用するのが簡単です。なお、DRF 3.12 以降は、DRF 3.10 までで標準だった CoreAPI ベースの APIドキュメント画面が利用できなくなるので注意が必要です。



 

宣伝

Django の技術同人誌をこれまでに4冊出しました。開発のお供にどうぞ。



現場で使える Django の教科書《基礎編》

「現場で使える Django の教科書」シリーズ第1弾となる Django の技術同人誌。Django を現場で使うための基礎知識やベストプラクティスについて、初心者・初級者向けに解説した本です。B5・本文192ページ。最新の Django 3.2 LTS に対応。


★ Amazon(電子版/ペーパー版)


★ BOOTH(ペーパー版)



現場で使える Django の教科書《実践編》

《基礎編》の続編にあたる「現場で使える Django の教科書」シリーズの第2弾。認証まわり、ファイルアップロード、ユニットテスト、デプロイ、セキュリティ、高速化など、さらに実践的な内容に踏み込んでいます。現場で Django を本格的に活用したい、あるいはすでに活用している方にピッタリの一冊。B5・本文180ページ。


★ Amazon(電子版)


★ BOOTH(ペーパー版)



現場で使える Django REST Framework の教科書

Django で REST API を構築する際の鉄板ライブラリである「Django REST Framework」(通称「DRF」)にフォーカスした、「現場で使える Django の教科書」シリーズの第3弾。B5・本文204ページ。


★ Amazon(電子版)


★ BOOTH(ペーパー版)



現場で使える Django 管理サイトのつくり方

Django の管理サイト(Django Admin)だけに特化した、ニッチでオンリーワンな一冊。管理サイトをカスタマイズする前に絶対に読んでほしい本です。B5・本文152ページ。


★ Amazon(電子版)


★ BOOTH(ペーパー版)

*1:サードパーティ製パッケージとしては「drf-yasg」も有名ですが、READMEにも書かれている通り、OpenAPI 3.0 には当分対応しないとのことなので検討を除外しています。

*2:DRF 3.10 以降はデフォルトで OpenAPI ベースの「rest_framework.schemas.openapi.AutoSchema」を使う設定になっているので、追加設定は特に必要ありません。

*3:https://www.django-rest-framework.org/coreapi/

*4:依存パッケージの PyYAML と uritemplate もインストールされます。

Django 組み込みのパスワード再設定(パスワードリセット)の仕組み

$
0
0

この投稿は 「Calendar for Django | Advent Calendar 2021 - Qiita」 1日目の記事です。


akiyoko です。
この記事では、Django 組み込みで提供されているパスワード再設定(パスワードリセット)の仕組みを深掘りします。 Django のパスワード再設定機能に関しては、利用方法についてはさまざまな記事で紹介されていたりするのですが、その内部仕様について詳しく紹介した記事をあまり見かけたことがなかったので、自分のメモがてら残しておこうと思い、ほぼ一年ぶりに筆を執りました。何かのお役に立てば幸いです。




検証環境
  • Django 3.2


 

はじめに

Django は Webアプリケーションを作成するために必要な機能が何でも揃っているフルスタックフレームワークで、ユーザー認証まわりの機能(ユーザーモデル、パーミッション、ユーザーセッションなど)などの便利な機能がデフォルトで用意されています。そしてあまりよく知られていませんが、パスワード再設定(パスワードリセット)機能(で利用できるビューやフォーム)もユーザー認証まわりの機能の一部として「django.contrib.auth(認証システム)」パッケージに含まれています。

なお、「django.contrib.admin(管理サイト)」パッケージにはパスワード再設定系のテンプレートが用意されているので、管理サイト内でパスワード再設定機能を利用するのは非常に簡単です。


 

管理サイトでのパスワード再設定機能の利用方法

管理サイトではパスワード再設定系の機能はデフォルトでオフになっているため、利用するには次のように URLconf にパスワード再設定用の4つの URLパターンを追加しなければいけません(詳しくは公式ドキュメント *1を参照)。

config/urls.py(URLconf)

from django.contrib import admin
from django.contrib.auth import views as auth_views
from django.urls import path

urlpatterns = [
    path('admin/password_reset/', auth_views.PasswordResetView.as_view(), name='admin_password_reset'),
    path('admin/password_reset/done/', auth_views.PasswordResetDoneView.as_view(), name='password_reset_done'),
    path('reset/<uidb64>/<token>/', auth_views.PasswordResetConfirmView.as_view(), name='password_reset_confirm'),
    path('reset/done/', auth_views.PasswordResetCompleteView.as_view(), name='password_reset_complete'),
    path('admin/', admin.site.urls),
]


上で示した「admin_password_reset」というURLパターンが登録されていれば、管理サイトのログイン画面に、次のように「パスワードまたはユーザー名を忘れましたか?」というリンクが表示されるようになります。


f:id:akiyoko:20211127070253p:plain:w350

この追加設定により、次のような画面遷移ができます。

f:id:akiyoko:20211128090512p:plain


それぞれの挙動は参考情報に示したビューやフォームを読めば分かるのですが、ちょっと理解しずらいのが「パスワード再設定メール」の仕組みです。


パスワード再設定メールの仕組み


f:id:akiyoko:20211128100012p:plain:w450

パスワード再設定メール送信画面で「パスワードをリセット」ボタンを押下すると、django.contrib.auth.views.PasswordResetView がリクエストを受け取り、入力したメールアドレスに紐付くアクティブ(is_active が True)なユーザーが存在する場合にのみパスワード再設定用のメールが送信されます。

例えばホストが「127.0.0.1:8000」の場合のパスワード再設定メールは次のようになります。

Subject: 127.0.0.1:8000 のパスワードリセット
From: webmaster@localhost
To: admin@example.com
Date: Fri, 26 Nov 2021 21:47:26 -0000
Message-ID: 
 <163796324660.443.3589748126282107509@1.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.ip6.arpa>
このメールは 127.0.0.1:8000 で、あなたのアカウントのパスワードリセットが要求されたため、送信されました。
次のページで新しいパスワードを選んでください:
http://127.0.0.1:8000/reset/MQ/awrev2-3a07a0471392cfbc3b885b713b2846d5/
あなたのユーザー名 (もし忘れていたら): admin
ご利用ありがとうございました!
 127.0.0.1:8000 チーム

メール本文にはパスワード再設定画面に遷移するためのリンクが含まれており、リンクの有効期限はデフォルトでは3日間となっています。*2


このリンクの URL は、「password_reset_confirm」という URL パターンで URLconf に登録されている「reset/<uidb64>/<token>/」に相当します。<uidb64>の部分、すなわち上のメールの例の「MQ」は、Base64 でエンコードされたユーザーの PK で、Base64 でデコードすると「1」になります。

<token>の「-」の左側の部分(上の例では「awrev2」)は、メール送信時のタイムスタンプ(2001/1/1 00:00:00 からの経過秒数)を Base34 でエンコードしたもので、リンクの有効期限をチェックするために使われます。残りの右側の部分(上の例では「3a07a0471392cfbc3b885b713b2846d5」)は改ざん防止のためのハッシュです。*3


django.contrib.auth.views.PasswordResetConfirmView では、パスワード再設定URL のリクエストを受け取ると、URL の妥当性(ユーザーが存在するかどうか、URL が改ざんされていないかどうか、有効期限を過ぎていないか)が検証され、「/reset/<uidb64>/set-password/」にリダイレクトされてパスワード再設定画面が表示されます。


f:id:akiyoko:20211128111830p:plain:w450

ちなみに、URL の<token>は(ハッシュ化された)パスワード文字列などから生成されているため(下記参照)、パスワード再設定によってパスワードが変更された後は同じ URL は利用できなくなっています。

django.contrib.auth.tokens.PasswordResetTokenGenerator._make_hash_value

def_make_hash_value(self, user, timestamp):
        """        Hash the user's primary key, email (if available), and some user state        that's sure to change after a password reset to produce a token that is        invalidated when it's used:        1. The password field will change upon a password reset (even if the           same password is chosen, due to password salting).        2. The last_login field will usually be updated very shortly after           a password reset.        Failing those things, settings.PASSWORD_RESET_TIMEOUT eventually        invalidates the token.        Running this data through salted_hmac() prevents password cracking        attempts using the reset token, provided the secret isn't compromised."""# Truncate microseconds so that tokens are consistent even if the# database doesn't support microseconds.
        login_timestamp = ''if user.last_login isNoneelse user.last_login.replace(microsecond=0, tzinfo=None)
        email_field = user.get_email_field_name()
        email = getattr(user, email_field, '') or''return f'{user.pk}{user.password}{login_timestamp}{timestamp}{email}'


f:id:akiyoko:20211128111923p:plain:w450

 

まとめ

Django はパスワード再設定(パスワードリセット)機能を組み込みで提供しており、管理サイト内でパスワード再設定機能を利用するのは非常に簡単です。管理サイト外でパスワード再設定を利用する場合は、「django.contrib.auth(認証システム)」パッケージのビューやフォームを使用し、「django.contrib.admin(管理サイト)」パッケージのテンプレートを参考にすればよいでしょう。

パスワード再設定系のビューやフォーム、テンプレートについては次の参考情報をご確認ください。


 

参考情報

パスワード再設定系のビュー・フォーム

  • django.contrib.auth.views.PasswordResetView
  • django.contrib.auth.views.PasswordResetDoneView
  • django.contrib.auth.views.PasswordResetConfirmView
  • django.contrib.auth.views.PasswordResetCompleteView
  • django.contrib.auth.forms.PasswordResetForm
  • django.contrib.auth.forms.SetPasswordForm

パスワード再設定系のテンプレート

  • django/contrib/admin/templates/registration/password_reset_form.html
  • django/contrib/admin/templates/registration/password_reset_done.html
  • django/contrib/admin/templates/registration/password_reset_confirm.html
  • django/contrib/admin/templates/registration/password_reset_complete.html

(メールタイトル・本文のテンプレート)

  • django/contrib/auth/templates/registration/password_reset_subject.txt
  • django/contrib/admin/templates/registration/password_reset_email.html


 

宣伝

Django の技術同人誌をこれまでに4冊出しました。開発のお供にどうぞ。



現場で使える Django の教科書《基礎編》

「現場で使える Django の教科書」シリーズ第1弾となる Django の技術同人誌。Django を現場で使うための基礎知識やベストプラクティスについて、初心者・初級者向けに解説した本です。B5・本文192ページ。最新の Django 3.2 LTS に対応。


★ Amazon(電子版/ペーパーバック)


★ BOOTH(紙の本)



現場で使える Django の教科書《実践編》

《基礎編》の続編にあたる「現場で使える Django の教科書」シリーズの第2弾。認証まわり、ファイルアップロード、ユニットテスト、デプロイ、セキュリティ、高速化など、さらに実践的な内容に踏み込んでいます。現場で Django を本格的に活用したい、あるいはすでに活用している方にピッタリの一冊。B5・本文180ページ。


★ Amazon(電子版/ペーパーバック)


★ BOOTH(紙の本)



現場で使える Django REST Framework の教科書

Django で REST API を構築する際の鉄板ライブラリである「Django REST Framework」(通称「DRF」)にフォーカスした、「現場で使える Django の教科書」シリーズの第3弾。B5・本文220ページ。


★ Amazon(電子版/ペーパーバック)


★ BOOTH(紙の本)



現場で使える Django 管理サイトのつくり方

Django の管理サイト(Django Admin)だけに特化した、ニッチでオンリーワンな一冊。管理サイトをカスタマイズする前に絶対に読んでほしい本です。B5・本文152ページ。


★ Amazon(電子版)


★ BOOTH(紙の本)

*1:https://docs.djangoproject.com/ja/3.2/ref/contrib/admin/#adding-a-password-reset-feature

*2:有効期限をデフォルトの3日間から変更したい場合は、「PASSWORD_RESET_TIMEOUT」を設定ファイルに定義することで変更可能です。なお、Django 2.2 以前で利用されていた「PASSWORD_RESET_TIMEOUT_DAYS」は非推奨になっており、Django 4.0 で削除される予定です。https://docs.djangoproject.com/en/3.2/internals/deprecation/#deprecation-removed-in-4-0

*3:ハッシュ化アルゴリズムには Django 3.1 以降でデフォルトになった「sha256」が使われています。

Django REST Framework はじめの一歩 〜押さえておきたい3つのポイント〜

$
0
0

この投稿は 「Calendar for Django | Advent Calendar 2022 - Qiita」 1日目の記事です。


この記事では、11月に「みんなのPython勉強会#87」で発表したトーク 「Django REST Framework はじめの一歩 〜押さえておきたい3つのポイント〜」の内容を詳しく説明したいと思います。トークの目的は「Django REST Framework(通称 DRF)がどんなものかをざっくり理解する」です。 DRF はよく分からないけど、Django なら何となく分かるという DRF 初心者の方を理想のトーク対象者として想定しています(もちろん Django を知らなくてもウェルカム!)。

ちなみに、各種バージョンは

  • Django : 3.2 LTS(現時点での最新 LTS)
  • DRF : 3.14(現時点での最新バージョン)

で動作確認をしています。



 

はじめに



今回のトークに先立って事前に Twitter でアンケートを取ったところ、約73%の方が「DRF を使っている」と回答していたのですが、私の予想を遥かに超えて DRF が使われていることに驚きました(正直半々くらいかなと思っていました…)。

ここで、アンケートにご協力いただいた皆さまにこの場を借りて感謝申し上げたいと思います。ご協力ありがとうございました。





また、「Django Developers Community Survey 2021」という7000名を超える開発者アンケートでは、「好きな Django パッケージは何ですか?(複数回答ありで5つまで回答可)」という質問に対して、DRF が第1位に選出されていました。2位の「django-celery」と比較してもダブルスコアというぶっちぎりの結果になっていますね。



 

DRF とは?



本題に入る前にまず、DRF とは何か?を説明したいと思います。一言でいえば、DRF は「REST API を作成する際の多彩なニーズに応えるための超ド定番 Django パッケージ」です。 Django 単体では不足しているさまざまな機能を、DRF で補完してくれているというイメージになります。特に、Django 開発者なら理解しやすい作りになっているというのが、Django 開発者にとってうれしい特徴です。






続いて、REST API の概要についても説明します。
REST API はスマホや SPA のバックエンドとして よく利用されています(左図)。そして、REST API は、URI でリソースを特定し、HTTP メソッドでそのリソースへの操作を表すという直感的に理解しやすいシンプルな設計になっています(右図)。そんな共通の枠組みに則ったデータを介すことで、フロントエンドとバックエンドが疎結合化、すなわち、お互いのプログラミング言語やアーキテクチャを意識することなくデータをやり取りすることができるのです。なお、REST API はリソース中心の設計となっているので、モデル中心の Django と親和性が高いというのも DRF を使うメリットになるかと思います。






DRF が一番得意なのは、単一リソースの CRUD を処理する REST APIです。上の表に示したように、単一リソースの CRUD を処理する REST API は、「GET」「POST」「PUT」「PATCH」「DELETE」という HTTPメソッドそれぞれに意味を持たせ、リソースを表す URI と組み合わせて、「リソース一覧の取得」「リソース詳細の取得」「リソースの登録」「リソースの更新」「リソースの一部更新」「リソースの削除」といった API を構成します。これを DRF で実装するのはすごく簡単で、モデル除けば最短20行くらいで書くことができます(コード例については後ほど紹介します)。






ここで、DRF についてよくある「誤解」を紹介します。
よくある誤解その①「DRF では単一リソースを扱う API 以外は作れない」は「✕」です。先に説明した 単一リソースを扱う REST API 以外にも、上の表に示したような、リソースがネストした API、独自アクションを追加した API、リソースに紐付いていない API なども DRF で作成することができます。





よくある誤解その②「DRF なしでは JSON のやり取りができない」も「✕」です。Django 単体でも(やろうと思えば)JSON データを受け取って JSON データを返すことができます。下の例を見てください。

api/views.py

import json
from django.forms.models import model_to_dict
from django.http.response import JsonResponse
from django.views import View
from shop.models import Book


classBookCreateView(View):
    """本モデルの登録APIクラス"""defpost(self, request, *args, **kwargs):
        """本モデルの登録APIに対応するハンドラメソッド"""
        data = json.loads(request.body)
        # バリデーションを実行ifnot data.get('title'):
            return JsonResponse({'message': 'タイトルは必須です。'}, status=400)
        # モデルオブジェクトを登録
        book = Book(**data)
        book.save()
        # レスポンスオブジェクトを作成して返すreturn JsonResponse(model_to_dict(book), status=201)

入力パラメータとなる JSON データは request.body に入っているのでそれをパースして、(この例ではめちゃくちゃ適当ですが)バリデーションを適宜実行して、モデルオブジェクトにデータを詰め替えて永続化して、最後に JsonResponse オブジェクトを作成して返しています。でもこんなことを毎回するのは大変ですよね。できれば Django っぽい書き方で、こういった処理を共通化したいというニーズから DRF が誕生したんじゃないかなと思います。






次は、DRF を使うメリットについて。
まず、Django のノウハウやエコシステムが活用できるというのが、DRF を使う大きなメリットになります。むしろこれがほとんどすべてと言っても過言ではありません。DRF プロジェクトのベースはあくまでも Django なので、ORM やマイグレーションを始めとする多くの機能をフル活用できたり、いつも使っている Django パッケージが使えたりといったように、Django のノウハウやエコシステムをほぼそのまま使うことができます。

そして次に、人気があって枯れていることもメリットです。DRF は Django で REST API を作るとなった場合にほぼ一択になるほど人気で、特に英語圏での情報がたくさんあります。公式ドキュメントやチュートリアルも(英語になりますが)かなり充実しています。

機能が充実していることも DRF ならではのメリットです。例えば、Cookie認証、トークン認証、JWT認証などの API でよく利用される認証を簡単に利用することができます。また、アクセス権制御をするための「パーミッション」機能、クエリ文字列による検索をするための「フィルタリング」機能、「ページネーション」機能、APIの利用回数制限をするための「スロットリング」機能などが用意されており、それぞれが差し替えできるようになっています。さらに、画面で API を実行確認できる「Browsable API」と呼ばれるテストクライアントがすぐに使えたり、OpenAPI に準拠した API ドキュメントを出力したりすることもできます。こういった REST API を作成するのに必要な機能がたいてい揃っていて、簡単な設定をするだけで幅広いニーズを満たすことができるというのが大きな特徴になります。






逆に、DRF にはどんなデメリットがあるのでしょうか。

ズバリ… 複雑です。

Django だけでも十分複雑なのに、そこに DRF が加わったらさらに複雑になってしまいますよね。






そこでここからは、DRF 初心者が押さえておきたい次の3つのポイントを解説していきたいと思います。

  • ポイント①:全体像を把握しよう
  • ポイント②:シリアライザはフォームっぽく使える
  • ポイント③:起点は DRF 用ビュー

この3つについてそれぞれ詳しく説明していきます。



 

ポイント①:全体像を把握しよう

ポイント①「全体像を把握しよう」について。



まず比較のために、Django プロジェクトの全体像と登場人物について説明します。図の左側から来たリクエストを、URLディスパッチャと呼ばれる Django のコアコンポーネントがハンドリングし、URLconf で登録したビュー関数を呼び出して、ビューの中でフォームやモデル、テンプレートを使って、レスポンスを作成して返し、URLディスパッチャがレスポンスを処理するという流れになっています。ミドルウェアは、ビューが呼び出される前や後で何らかの処理をするために利用されます。






REST API を構築するための DRF プロジェクトでは、(HTML のフォーム要素を扱わないし、レスポンスとして画面をレンダリングした HTML を返さないので)基本的にフォームとテンプレートは使いません(後述しますが、Django を共存させる場合にはその限りではありません)。その代わり、DRF では「シリアライザ」というコンポーネントを使います。シリアライザは入出力の JSON(およびモデルとの連携)定義とバリデーションを担当します。ポイント②で詳しく説明しますが、シリアライザはフォームに似せて作られていて、フォームと同じように使うことができます。

そしてビューは、DRF用のビューを使います。これもポイント③で説明しますが、図を見ると分かるように、DRF の起点となるのは「DRF 用ビュー」です。図では「✕」で示していますが、通常の Django のビューを URLconf に登録しておくことも可能です。

それ以外のモデル、URLconf、ミドルウェアは通常の Django と同じように使います。






ポイント①のまとめです。DRF では、DRF用のビュー を作成して URLconf に登録します。そしてシリアライザというコンポーネントを使い、フォームとテンプレートは使いません。ただし、Django と DRF のビューをそれぞれ登録することができるので、そういった共存をさせる場合にはフォームもテンプレートも使うことになるので誤解のないようにしてください。それ以外は通常の Django とだいたい同じように使います。こういった全体像を押さえておくのが第一のポイントです。




 

ポイント②:シリアライザはフォームっぽく使える

ポイントその②「シリアライザはフォームっぽく使える」について。



その前にまず、DRF の独自コンポーネントである「シリアライザ」の使いどころを説明します。シリアライザが担当する役目は、

  • 入力データのバリデーション
  • モデルオブジェクトの永続化
  • 出力データの作成

です。上の図はどこでシリアライザを使うかを示しているのですが、登録API、更新API、取得API のそれぞれでシリアライザの使いどころが異なります(削除API のようにシリアライザを使わない場合もあります)。例えば登録 API だと、リクエストされた JSON データを元にしてシリアライザオブジェクトを作成し、シリアライザオブジェクトのバリデーションメソッドを実行し、シリアライザの永続化メソッドを実行して、最後にレスポンスオブジェクトの引数にシリアライザのデータを渡すという流れになります。このように、ほとんどの場合でシリアライザを使うことになるため、DRF を使うときにはシリアライザは避けて通ることができません。






さて、本題です。シリアライザはフォームっぽく使えるということについて説明します。リソース(すなわちモデル)と紐付いた REST API の場合は「ModelSerializer」を使うのが基本なのですが、ModelSerializer の使い方は(フォームで特定のモデルを扱うための)ModelForm とほぼ同じです。 ModelSerializer と ModelForm の引数や属性、バリデーション、永続化の流れを上の図に並べて示したのですが、仕組みがほぼ同じになっています。バリデーションメソッドは「is_valid」、永続化メソッドは「save」でこちらも同じです。ただし、バリデーション済みのデータは「validated_data」となっていて、ModelForm(親クラスの Form も同様)と変数名が異なるので注意してください。






シリアライザの実装イメージは次のようになります。

api/serializers.py

from rest_framework import serializers

from shop.models import Book


classBookSerializer(serializers.ModelSerializer):
    """本モデル用シリアライザ"""classMeta:
        model = Book
        fields = ['id', 'title', 'price']

ModelSerializer を継承したシリアライザクラスを用意して、Metaクラスの「model」属性に連携させるモデルクラス、「fields」属性に JSONの入出力フィールドとして利用するモデルのフィールド名を指定します。フィールドは、fields 属性以外に、除外するフィールドを excludes 属性で指定することもできます。

使い方は ModelFormとほぼ同じで、引数「data」にリクエストのパラメータ(DRF では JSON データ)、更新時には引数「instance」にモデルオブジェクトを指定します。バリデーションをおこなたいときは is_valid()、モデルオブジェクトを永続化したいときは save() メソッドを実行します。

>>> from api.serializers import BookSerializer
>>> serializer = BookSerializer(data={'title': 'DRFの本', 'price': 1000})
>>> serializer.is_valid()
True
>>> serializer.validated_data
OrderedDict([('title', 'DRFの本'), ('price', 1000)])
>>> book = serializer.save()

>>> serializer = BookSerializer(instance=book, data={'price': 2000}, partial=True)
>>> serializer.is_valid()
True
>>> serializer.save()
<Book: DRFの本>




ポイント②のまとめです。モデルと紐付いた REST API の場合は ModelSerializer を使うのが簡単で、その使い方は ModelForm とほぼ同じです。




 

ポイント③:起点は DRF 用ビュー

最後のポイント「起点は DRF 用ビュー」について。



DRF には「rest_framework.views.APIView」というクラスが用意されているのですが、「APIView」は DRF の起点となる基底ビュークラスで、これを継承することで、さまざまな前処理と後処理をやってくれます。

1. リクエストオブジェクトを DRF 用に変換
2. レンダラクラスとメディアタイプを決定
3. バージョニング
4. 認証チェック
5. パーミッションチェック
6. スロットリング
7. 例外ハンドリング

これらの処理は差し替え可能です。2. 〜 6. には代表的な処理クラスがいくつか用意されていて、デフォルトの設定を設定ファイル(settings.py)の「REST_FRAMEWORK」で上書きして全体的な設定をしたり、クラス変数をオーバーライドしてビュークラス単位での個別設定をしたりすることができます。






DRF 用のビューは大きく分けて3種類あり、用途に応じて

1)rest_framework.views.APIView
2)rest_framework.generics.CreateAPIView などの 汎用 APIView
3)rest_framework.viewsets.ModelViewSet 系ビュー

のいずれかを継承して作成します。どれを選択するかによってそれぞれカスタム性やコード量が異なってきますが、この後で具体的な実装を見ながら説明していきます(コードがたくさん出てきますが、雰囲気を理解してもらえれば大丈夫です)。

ちなみに下の図で示すように、2) と 3) のビューは「APIView」の子クラスで、APIView は「基本汎用ビュー」と呼ばれる Django の「View」クラスの子クラスです。







まず、1)の APIView を継承したビューの書き方について説明します。

例えば、更新 API で呼び出される post メソッドでは、モデルオブジェクトと入力データからシリアライザオブジェクトを作成し、is_valid() で入力データのバリデーションをおこない、save() でモデルオブジェクトを更新して、最後にシリアライザの data 属性の値を使ってレスポンスオブジェクトを作成して返しています。

api/views.py

from rest_framework import status, views
from rest_framework.generics import get_object_or_404
from rest_framework.response import Response

from shop.models import Book
from .serializers import BookSerializer


classBookRetrieveUpdateAPIView(views.APIView):
    """本モデルの取得(詳細)・更新APIクラス"""defget(self, request, pk, *args, **kwargs):
        """本モデルの取得(詳細)APIに対応するハンドラメソッド"""# モデルオブジェクトを取得
        book = get_object_or_404(Book, pk=pk)
        # シリアライザオブジェクトを作成
        serializer = BookSerializer(instance=book)
        return Response(serializer.data, status.HTTP_200_OK)

    defput(self, request, pk, *args, **kwargs):
        """本モデルの更新APIに対応するハンドラメソッド"""# モデルオブジェクトを取得
        book = get_object_or_404(Book, pk=pk)
        # シリアライザオブジェクトを作成
        serializer = BookSerializer(instance=book, data=request.data)
        # バリデーションを実行
        serializer.is_valid(raise_exception=True)
        # モデルオブジェクトを更新
        serializer.save()
        # レスポンスオブジェクトを作成して返すreturn Response(serializer.data, status.HTTP_200_OK)

コードは長くなりますが、好きなように書くことができます。しかし、単一リソースの CRUD 処理をする場合、モデルとシリアライザだけが違うビューを追加する場合に同じようなコードを何度も書くのはさすがに冗長ですよね。






そんな場合に利用したいのが、2)の「汎用 APIView」です。上の表に示したように、対応アクションごとにいろいろな汎用 APIView が用意されているので、用途に合わせてどれかを継承して、モデルオブジェクトを取得するためのクエリを指定するための「queryset」とシリアライザクラスを指定するための「serializer_class」をクラス変数を加えるだけです。

これでだいぶ短く書くことができますが、単一モデルのCRUD処理をするというのを逸脱した API を作ろうとすると、親クラスのメソッドをオーバーライドして拡張する必要があります。


api/views.py

from rest_framework import generics

from shop.models import Book
from .serializers import BookSerializer


classBookListCreateAPIView(generics.ListCreateAPIView):
    """本モデルの取得(一覧)・登録APIクラス"""

    queryset = Book.objects.all()
    serializer_class = BookSerializer


classBookRetrieveUpdateDestroyAPIView(generics.RetrieveUpdateDestroyAPIView):
    """本モデルの取得(詳細)・更新・一部更新・削除APIクラス"""

    queryset = Book.objects.all()
    serializer_class = BookSerializer

config/urls.py

from django.urls import path
from api import views

urlpatterns = [
    path('api/books/', views.BookListCreateAPIView.as_view()),
    path('api/books/<pk>/', views.BookRetrieveUpdateDestroyAPIView.as_view()),
]




最後に、3)ModelViewSet 系ビューを継承したビューの書き方です。冒頭で「単一リソースの CRUD を処理する REST API なら超簡単」で「20行くらいで API が全部書ける」と言っていたのはこちらです。

上の表に書いたように ModelViewSet は「一覧」「詳細」「登録」「更新」「一部更新」「削除」という6つの API を全部網羅しています。実装は、下に示したように ModelViewSet を継承して、「serializer_class」と「queryset」の指定をするだけです。

api/views.py

from rest_framework import viewsets

from shop.models import Book
from .serializers import BookSerializer


classBookViewSet(viewsets.ModelViewSet):
    """本モデルのCRUD用APIクラス"""

    serializer_class = BookSerializer
    queryset = Book.objects.all()


あとは、ViewSet 用の SimpleRouter(または DefaultRouter)を利用して URL パターンを URLconf に追加してあげれば、これで API の実装は完了です。シリアライザを含めて最短20行ほどで書くことができます。

config/urls.py

from django.urls import include, path
from rest_framework import routers
from api import views

router = routers.DefaultRouter()
router.register('books', api.BookViewSet)

urlpatterns = [
    # すべてのアクション(一覧・詳細・登録・更新・一部更新・削除)をまとめて追加
    path('api/', include(router.urls)),
]

なので、単一モデルの CRUD を処理する REST API をまるっと実装したいというニーズには「ModelViewSet」が一番最速で実装できるということになります。






ポイント③のまとめです。単一モデルの CRUD を処理する REST API をまるっと実装したい場合は ModelViewSet 系ビューを使うのが一番簡単で、複雑なことがしたい場合は APIView(または GenericAPIView)を継承したビュークラスを使うのがよいでしょう。



 

まとめ

最後のまとめです。



DRFを使う場合、Django に DRF の要素、コンポーネントが加わることになるので複雑になります。なのでまずは、全体像を把握しましょう。一見複雑にみえますが、新しい要素は「シリアライザ」と「DRF用ビュー」です。

「シリアライザ」は入力データのバリデーション、内部に保持したモデルオブジェクトの永続化、出力データの作成を担当するクラスで、Django のフォームと同じような使い方ができます。

「DRF用ビュー」は大きく3種類あり、まるっと API 全部を実装できてしまうものもあれば、複雑なカスタム API にも対応できるものもあるので、用途に合わせて親クラスと書き方を使い分けてください。


さてこれで、DRF がどんなものか完全に理解できたでしょうか。もっと DRF のことを深く知りたいという方は、「現場で使える Django REST Framework の教科書」という 220ページ以上まるまる DRF について書かれた素敵な本があるので、そちらを読んでみてください。ここで説明した内容をもっとディープに解説しています。そして何と、今月この本が改訂予定です。Django 3.2 LTS、DRF 3.14 対応で、Vue 3(+ Vite)と Vuetify 3 でチュートリアルを刷新しています。




トークのスライドはこちらにアップしています。

speakerdeck.com




 

宣伝

Django の技術同人誌をこれまでに4冊出しました。開発のお供にどうぞ。



現場で使える Django の教科書《基礎編》

「現場で使える Django の教科書」シリーズ第1弾となる Django の技術同人誌。Django を現場で使うための基礎知識やベストプラクティスについて、初心者・初級者向けに解説した本です。B5・本文192ページ。最新の Django 3.2 LTS に対応。


★ Amazon(電子版/ペーパーバック)


★ BOOTH(紙の本)



現場で使える Django の教科書《実践編》

《基礎編》の続編にあたる「現場で使える Django の教科書」シリーズの第2弾。認証まわり、ファイルアップロード、ユニットテスト、デプロイ、セキュリティ、高速化など、さらに実践的な内容に踏み込んでいます。現場で Django を本格的に活用したい、あるいはすでに活用している方にピッタリの一冊。B5・本文180ページ。


★ Amazon(電子版/ペーパーバック)


★ BOOTH(紙の本)



現場で使える Django REST Framework の教科書

Django で REST API を構築する際の鉄板ライブラリである「Django REST Framework」(通称「DRF」)にフォーカスした、「現場で使える Django の教科書」シリーズの第3弾。B5・本文220ページ。


★ Amazon(電子版/ペーパーバック)


★ BOOTH(紙の本)



現場で使える Django 管理サイトのつくり方

Django の管理サイト(Django Admin)だけに特化した、ニッチでオンリーワンな一冊。管理サイトをカスタマイズする前に絶対に読んでほしい本です。B5・本文152ページ。


★ Amazon(電子版)


★ BOOTH(紙の本)

既存テーブルに単一の主キーがない場合に Django モデルを使いたい場合の解決案(Django で複合主キーを使う方法)

$
0
0

この投稿は 「Calendar for Django | Advent Calendar 2022 - Qiita」 2日目の記事です。

最近こんなの見つけたよという緩い内容になっているので、「こんなのもあるんだな」という軽い気持ちで読んでいただければと思います。そしてもし、「現場で使ってるよ」という方がいれば教えていただければありがたいです。

課題

まず大前提ですが、Django モデルの仕様として、それぞれのモデルごとに単一の主キーを持たせる必要があり(「primary=True」オプションを持つフィールドをモデルに明示的に含めるか、さもなくば「id」という名前のフィールドが自動的にモデルに追加され、それに対応したカラムがテーブルに作成される)、複合主キー(composite primary key)はサポートされていません。*1


そのため、例えば、単一の主キーを持たない(が複合主キーを持っている)既存のテーブルを Django モデルで扱うことは困難です。



ところで Django で複合主キーっぽいことをしたければ通常は、「id」という名前のサロゲートキーを主キーとして別に用意しつつ、複合ユニーク制約を利用しますよね。具体的には、Django 2.2 から追加された UniqueConstraintを利用して次のように実装します。


models.py

from django.db import models


classEmployee(models.Model):
    """従業員モデル"""classMeta:
        db_table = 'employee'
        verbose_name = verbose_name_plural = '従業員'
        constraints = [
            models.UniqueConstraint(fields=['branch_code', 'employee_code'], name='unique_employee')
        ]

    branch_code = models.CharField('支店コード', max_length=3)
    employee_code = models.CharField('従業員コード', max_length=5)
    name = models.CharField('従業員名', max_length=255)

    def__str__(self):
        return f'{self.branch_code}-{self.employee_code}'


この従業員テーブルの仕様としては、支店ごとに従業員コードが振られていて、従業員コードはレコード全体としてはユニークになっていない(支店コードと従業員コードを合わせるとユニークになる)という想定です。

ちなみに複合ユニーク制約を実現するには unique_togetherを使うことも可能ですが、将来的に非推奨になる可能性があり、UniqueConstraint の方が多機能なので、UniqueConstraint の利用が推奨されます。



 

解決策

最近見つけたのですが、(次期リリースではありますが)「Viewflow」というライブラリの「CompositeKey」を使えば、既存テーブルに主キーがないテーブルを Django モデルで扱うことが可能になります。

使い方は次の通りです。

pip install django-viewflow --pre

# or

pip install django-viewflow==2.0.0a2


models.py

from django.db import models
from viewflow.fields import CompositeKey


classEmployee(models.Model):
    """従業員モデル"""classMeta:
        db_table = 'employee'
        managed = False
        verbose_name = verbose_name_plural = '従業員'id = CompositeKey(columns=['branch_code', 'employee_code'])
    branch_code = models.CharField('支店コード', max_length=3)
    employee_code = models.CharField('従業員コード', max_length=5)
    name = models.CharField('従業員名', max_length=255)

    def__str__(self):
        return f'{self.branch_code}-{self.employee_code}'


これで、CompositeKey の columns で指定した複数のフィールドの値を「{'branch_code': '001', 'employee_code': '00001'}」のような JSON 文字列で保持する「id」という名前の 仮想フィールドを持つことができます。

「managed = False」*2を指定しないとマイグレーションで「id」カラムが作成されてしまうのですが、CompositeKey が威力を発揮するのは「managed = False」を指定してモデルをマイグレーションの対象外にした場合で、主キーを持たない既存のテーブルに影響を及ぼさずに Django モデルを用意することができます。事例としては、次のように複合主キーを持った既存テーブルから Django ORM を使ってレコードを抽出(読み取り専用)したいというニーズに応えることができます。


Django 管理サイトでも動作するというのも高評価です。

*1:Django で複合主キーをサポートするかどうかは、17年前から議論が続いています… #373 (Add support for multiple-column primary keys) – Django

*2:https://docs.djangoproject.com/ja/3.2/ref/models/options/#managed


Django のビューから Django コマンドを実行する方法

$
0
0

この投稿は 「Calendar for Django | Advent Calendar 2022 - Qiita」 3日目の記事です。

小ネタです。


Django のビューから直接 Djangoコマンドを実行するには、django.core.management.call_command を使えば簡単です。*1


こんな感じで使います。

api/views.py

from django.core import management
from rest_framework.response import Response
from rest_framework.views import APIView


classCallCommandView(APIView):
    defpost(self, request, *args, **kwargs):
        # Djangoコマンドを実行
        management.call_command('loaddata', 'test_data', verbosity=0)
        # レスポンスオブジェクトを作成して返すreturn Response({'success': True})


これは Django REST Framework(通称 DRF)を使用したビューの実装例ですが、Django 単体でも同じように利用することが可能です。



config/urls.py

from django.urls import path

from api import views

urlpatterns = [
    path('call-command/', views.CallCommandView.as_view()),
]


実行結果





ちなみに、Django コマンドの「--no-input」オプションは call_command() に「interactive=False」を指定することで利用可能です。

management.call_command('flush', verbosity=0, interactive=False)


標準出力をファイルに書き込むこともできます。*2

withopen('/path/to/command_output', 'w') as f:
    management.call_command('dumpdata', stdout=f)


cron などで定期実行している Django コマンドを、API で呼び出すなどといった利用方法が考えられますね。

*1:ビューからでなくても、モデルでもどこでも実行可能です。

*2:https://docs.djangoproject.com/ja/3.2/ref/django-admin/#output-redirection

Django パッケージ利用実態調査アンケート(2022年12月)について

$
0
0

この投稿は 「Calendar for Django | Advent Calendar 2022 - Qiita」 4日目の記事です。


Django Advent Calendar のネタ探しをしていて、ふと 「どんな Djangoパッケージがどれくらい使われているの?🤔」という利用実態調査みたいな記事なら自分もぜひ読んでみたいと思ったので、アンケートを採ってみることにしました。


ということで、ぜひアンケートにご協力よろしくお願いします 🙇‍♂️🙇‍♂️🙇‍♂️(回答者の情報は収集していません)

docs.google.com



アンケートの回答〆切は 12/15 23:59 までとさせていただき、12/16 以降の Django Advent Calendar のどこかで発表する予定です。


なお、Q2. の Django パッケージの選択肢を以下に列挙しますが、これは 2022 Django Developers Surveyというワールドワイドな Django 開発者向けアンケートの「お気に入りのサードパーティ Django パッケージは?」という質問の選択肢をそのまま利用しました(なぜこの選択肢になったのかは私にも分かりません)。


Django パッケージ説明文
dj-database-urlDjango を使用したアプリケーションで、データベースの接続文字列を簡単に扱うためのライブラリです。このライブラリを使うことで、データベースの接続情報を環境変数や設定ファイルから読み取り、Django の設定に渡すことができます。その結果、Django アプリケーションのデプロイやテストを容易に行うことができるようになります。また、dj-database-url は、様々なデータベース管理システムをサポートしているため、どのようなデータベースを使用しているかに関係なく利用できます。
dj-rest-knoxDjango の REST フレームワークである Django REST framework にて、トークンベースの認証を提供するためのものです。このパッケージを使用することで、Django アプリケーションでトークンベースの認証を実装することができます。また、dj-rest-knox は Django REST framework の上に構築されており、Django のログイン認証システムを使用している場合は、すぐに使用することができます。
dj-rest-authDjango の REST フレームワークである Django REST framework にて、ユーザー認証を提供するためのものです。このパッケージを使用することで、Django アプリケーションで簡単にユーザー認証を実装することができます。また、dj-rest-auth は Django のログイン認証システムを拡張しているため、既存の Django アプリケーションでも簡単に導入することができます。
django-allauthDjango にて、多要素認証を提供するためのものです。このパッケージを使用することで、Django アプリケーションで簡単に多要素認証を実装することができます。また、django-allauth は Django のログイン認証システムを拡張しており、既存の Django アプリケーションでも簡単に導入することができます。また、django-allauth は様々な外部サービス(Facebook、Google、Twitter など)との連携もサポートしています。
django-bracesDjango にて、様々な拡張機能を提供するためのものです。このパッケージを使用することで、Django アプリケーションで簡単にビューやミックスインなどの拡張機能を実装することができます。また、django-braces は他の Django パッケージとも組み合わせて使用することができ、Django プロジェクトで様々な拡張機能を組み合わせることができます。
django-celeryWeb フレームワークである Django と、Python のタスク駆動フレームワークである Celery を組み合わせるためのものです。このパッケージを使用することで、Django アプリケーション内で Celery を利用して、非同期タスクを管理することができます。
django-celery-beatDjango フレームワークと Celery というタスクキューライブラリを組み合わせて使用するためのツールを提供します。Celery は、バックグラウンドで処理を行うタスクを管理するためのライブラリです。django-celery-beat を使うことで、Django プロジェクトで Celery のスケジュール処理をより簡単に利用することができます。また、django-celery-beat には、Django のデータベースを使って Celery のスケジュールを管理するためのツールが提供されているため、Django プロジェクトで定期的なタスクを実行するためのインフラストラクチャを構築することができます。
django-channelsDjango にて、WebSocket や Server-Sent Events(SSE)を扱うためのものです。このパッケージを使用することで、Django アプリケーションで WebSocket や SSE を利用した通信を実装することができます。
django-clickPython の Webフレームワークである Django を使用したアプリケーションで、コマンドラインインターフェース(CLI)を簡単に構築するためのライブラリです。その結果、Django アプリケーションの管理や操作を、より便利でスムーズに行うことができるようになります。
django-compressorDjango にて、JavaScript や CSS のファイルを圧縮するためのものです。このパッケージを使用することで、Django アプリケーションで簡単に JavaScript や CSS のファイルを圧縮することができます。
django-configurationsDjango にて、複数の環境に対応した設定を管理するためのものです。このパッケージを使用することで、Django アプリケーションで簡単に複数の環境(開発環境、本番環境、ステージング環境など)に対応した設定を管理することができます。
django-cors-headersDjango にて、クロスオリジンリソースシェアリング(CORS)を扱うためのものです。CORS は、異なるオリジン(ドメイン)間での Web ページのやり取りを許可する仕組みです。
django-crispyformsこのパッケージを使用することで、Django アプリケーションで簡単にフォームを美しくレンダリングすることができます。また、django-crispy-forms は Bootstrap を使用したフォームのレンダリングをサポートしています。
django-dbbackupこのパッケージを使用することで、Django アプリケーションで簡単にデータベースのバックアップを管理することができます。また、django-dbbackup は様々なクラウドストレージサービス(Amazon S3、Dropbox、Google Cloud Storage など)との連携もサポートしています。
django-debug-toolbarDjango にて、開発時にデバッグを行うためのものです。このパッケージを使用することで、Django アプリケーションの開発時に、リクエストやレスポンスなどの詳細情報を表示することができます。
django-environDjango アプリケーションで環境変数を簡単に使用できるようにするためのライブラリです。このライブラリを使用することで、アプリケーションの設定値を環境変数から取得することができます。これにより、アプリケーションの設定値を外部から安全かつ簡単に管理することができます。
django-extensionsDjango フレームワークのためのサポートライブラリです。このライブラリには、Django アプリケーションを開発するためのさまざまな便利な機能が含まれています。例えば、コマンドラインから Django プロジェクトを作成するためのツール、データベースのスキーマを表示するためのツール、データベースのデータをダンプするためのツールなどがあります。このライブラリを使用することで、Django アプリケーションの開発プロセスをよりスムーズかつ効率的に行うことができます。
django-filterDjango アプリケーションでクエリセットをフィルタリングするためのライブラリです。このライブラリを使用することで、クエリセットを簡単に様々な条件でフィルタリングし、特定の条件にマッチするデータのみを取得することができます。 django-filter を使用することで、データベースからデータを抽出する際に、より細かいフィルタリングを行うことができるため、データをより正確かつ効率的に取得することができます。
django-import-exportDjangoフレームワークでのデータのインポートおよびエクスポートを簡単に行うためのライブラリです。このライブラリを使用することで、Djangoのモデルデータを様々な形式のファイル(CSV、JSON、Excelなど)とやりとりすることができます。django-import-export を使用することで、データのインポートおよびエクスポート操作を管理画面から直接実行することができます。
django-lifecyclePython の Web フレームワークである Django を使用したアプリケーションで、モデルの作成や更新時に実行される処理を定義するためのライブラリです。このライブラリを使うことで、Django のモデルを作成、更新する際に、特定の処理を自動的に実行することができます。例えば、モデルが作成されるときに、特定のメールを送信する処理や、モデルが更新されるときに、特定のログを記録する処理などを定義することができます。これにより、Django アプリケーションの動作をより自動化し、処理の追加や変更を容易に行うことができるようになります。
django-model-utilsDjango フレームワークでモデルを定義する際に便利なユーティリティクラスを提供するライブラリです。このライブラリを使用することで、モデルの作成やカスタマイズがより簡単かつ効率的に行えます。
django-moneyDjangoフレームワークで組み込みの「Money」フィールドを提供するライブラリです。このライブラリを使用することで、データベース内での金額を貨幣単位を考慮した形で保存することができます。django-moneyを使用することで、金額を扱うアプリケーションを作成する際に、正確かつ柔軟な金額処理が行えるようになります。また、django-moneyは、様々な通貨をサポートしており、通貨の相互変換も可能です。
django-redisDjango フレームワークで Redis を使用するためのライブラリです。Redis は、速度が非常に速いキー/値型のデータベースです。django-redis を使用することで、Django アプリケーションで Redis を使用してデータを保存することができます。これにより、Djangoアプリケーションのデータ保存や取得が高速化されます。また、django-redis は、Django のキャッシュフレームワークと組み合わせて使用することで、アプリケーションのパフォーマンスをさらに向上させることができます。
django-rest-swaggerDjango フレームワークで作成された RESTful API に Swagger を組み込むためのライブラリです。Swagger は、RESTful API を文書化して公開するためのツールです。django-rest-swagger を使用することで、Django アプリケーションで作成した RESTful API を Swagger を使用して文書化することができます。これにより、API を使用する開発者が API の仕様や使用方法を簡単に理解することができるようになります。また、django-rest-swagger を使用することで、API のテストやモックを作成することができます。
django-storagesDjango フレームワークでさまざまなクラウドストレージサービスを使用するためのライブラリです。このライブラリを使用することで、Django アプリケーションで Amazon S3 や Google Cloud Storage などのクラウドストレージサービスを簡単に使用することができます。 django-storages を使用することで、Django アプリケーションで保存されるデータや画像などのファイルをクラウドストレージ上に保存することができます。これにより、アプリケーションのデータを安全かつスケーラブルな方法で保存することができるようになります。
django-silkDjango フレームワークでリクエストとレスポンスをモニタリングするためのライブラリです。このライブラリを使用することで、Djangoアプリケーションで発生するHTTPリクエストとレスポンスを管理画面から簡単に閲覧することができます。django-silkを使用することで、アプリケーションのパフォーマンスやエラーを把握することができるため、デバッグやトラブルシューティングがより容易に行えます。また、django-silkは、リクエストとレスポンスの中身を表示するだけでなく、リクエストを再現したり、リクエストやレスポンスを検索したりすることができます。
django-taggitDjango フレームワークでタグを扱うためのライブラリです。このライブラリを使用することで、Djangoアプリケーションでタグを作成したり、タグを付けられたオブジェクトを取得したりすることができます。django-taggit を使用することで、アプリケーションにタグ付け機能を簡単に実装することができます。また、django-taggit は、既存の Django モデルにタグを追加することもできます。これにより、タグを使用してデータを管理したり、検索したりすることができます。
django-test-plusDjango フレームワークでのユニットテストを実行するためのライブラリです。このライブラリを使用することで、Django アプリケーションのユニットテストをより簡単かつ効率的に行うことができます。 django-test-plus には、ユニットテストを実行する際に便利なユーティリティ関数やアサーションが含まれています。例えば、ビューやURLのテストを行うためのヘルパー関数、フォームやモデルのテストを行うためのアサーションなどがあります。これらの機能を使用することで、Djangoアプリケーションのユニットテストをよりスムーズかつ効率的に実行することができます。
djangorestframeworkDjango フレームワークで RESTful な Web API を作成するためのライブラリです。このライブラリを使用することで、Django アプリケーションを通じて RESTful な Web API を提供することができます。 djangorestframework には、API のエンドポイントやリクエストとレスポンスのシリアライザ、API の認証や権限管理など、RESTful な Web API を実装するための必要な機能がすべて提供されています。このライブラリを使用することで、Django アプリケーションから RESTful な Web API を提供することができるようになります。
djangorestframework-simplejwtdjango-rest-framework を使用した RESTful な Web API にJSON Web Tokens(JWT)を使用した認証を実装するためのライブラリです。JWT は、トークンベースの認証方式の一種です。djangorestframework-simplejwt を使用することで、django-rest-framework で作成した RESTful な Web API に JWT を使用した認証を実装することができます。これにより、API を利用するアプリケーションがAPIに対してアクセスする際に、トークンを使用して認証を行うことができるようになります。
DjoserDjango フレームワークで作成された RESTful な Web API にユーザー管理機能を実装するためのライブラリです。このライブラリを使用することで、Djangoアプリケーションにユーザー登録、ユーザー情報の管理、パスワードの変更などの機能を簡単に実装することができます。また、Djoser は、JSON Web Tokens(JWT)を使用した認証をサポートしているため、JWT を使用した API の認証も実装することができます。Djoser を使用することで、Django アプリケーションにRESTful な API を作成し、ユーザー管理や認証を実装することができるようになります。
model-bakeryDjango プロジェクトで使用されるモデルを生成するための Python パッケージです。model-bakery は、モデルを生成し、操作し、評価するためのツールを提供します。
pylint-djangopylint-django は、Django フレームワークで作成された Python コードを検証するための Pylint プラグインです。Pylint は、Python のコード品質を測定するツールです。pylint-django を使用することで、Django フレームワークを使用した Python コードに対して、Pylint が提供する様々な品質指標を適用することができます。これにより、Django フレームワークを使用した Python コードの品質を向上させることができます。また、pylint-django は、Django フレームワーク固有の構文や API を理解しており、Django フレームワークを使用した Python コードをより正確かつ効率的に検証することができます。
pytest-djangoPython のテストフレームワークである pytest を用いて Django Web アプリケーションのテストを実行するためのプラグインです。このプラグインを使うことで、Django アプリケーションのテストを簡単に実行し、結果を比較することができます。また、pytest-django は、Django アプリケーションのデータベースを自動的にセットアップ・クリーンアップするため、データベースのテストも容易に行うことができます。
WagtailWagtail は、Django フレームワークを使用した CMS(Content Management System)です。このCMSを使用することで、WebサイトやWebアプリケーションのコンテンツの管理をより簡単かつスムーズに行うことができます。 Wagtail には、ページやブログ記事の管理、メディアライブラリ、ページのテンプレートやカスタマイズ、検索、ユーザー管理などの機能が備わっています。これらの機能を使用することで、WebサイトやWebアプリケーションのコンテンツを効率的に管理することができます。また、Wagtail はオープンソースであるため、カスタマイズや拡張も容易に行うことができます。


ちなみに、説明文は最近話題の「ChatGPT」の回答を使わせていただきました(なのでしれっと間違っている可能性もあるかもしれません)。冗長な説明があったのを削除して調整しましたが、スゴイですねこれ。

現場ではどんな Django パッケージがどれくらい使われているのか(Django パッケージ利用実態調査:2022年12月)

$
0
0

この投稿は 「Django Advent Calendar 2022」 23日目の記事です。


ふと、「実際の現場ではどんな Django パッケージがどれくらい使われているの?🤔」という疑問が湧いてきたので、自分で次のようなアンケートを採ってみることにしました。


docs.google.com



本記事では、この利用実態調査アンケートの結果発表に加えて、ちょっとした分析をしてみたいと思います。なお、本アンケートには40人の方から回答をいただくことができました。皆さま、ご協力ありがとうございました。





 

Q1.回答者の Django 経験年数

Q1.Django の経験はどのくらいですか?

  • 経験なし
  • 1年未満
  • 1〜3年くらい
  • 3〜5年くらい
  • 5年以上


回答者の Django 経験年数の分布は次のようになりました。



一番多かったグループは「1〜3年くらい」で 40%でした。なお、「1年未満」の階級値を「0.5」、「1〜3年くらい」を「2」、「3〜5年くらい」を「4」、「5年以上」を「7.5」として計算した 回答者の Django 経験年数の平均は 3.8 年でした。



 

Q2.Django パッケージ利用状況

代表的な Django パッケージ 35個について、それぞれの利用状況を「聞いたことがない」「聞いたことはあるが使ったことはない」「試しに使ったことがある」「現場で使っている(過去に使っていた)」の4択でアンケートしました。

パッケージの選定にあたっては、毎年数千名が参加する Django 開発者向けアンケート「2022 Django Developers Survey」の「お気に入りのサードパーティ Django パッケージは?」という質問の選択肢をそのまま利用しました。*1

Q2.次の Django パッケージをそれぞれどのくらい利用していますか?

  • 1: 聞いたことがない
  • 2: 聞いたことはあるが実際に使ったことはない
  • 3: 試しに使ったことがある
  • 4: 現場で使っている(過去に使っていた)

Django パッケージの選択肢


回答結果を 4 > 3 > 2 > 1 の優先順に並べ替えた利用度ランキングがこちらになります。






3人に2人が現場で使っているという「djangorestframework」(DRF)がダントツの1位でした。「試しに使ったことがある」を足し合わせると95%というのも衝撃の結果ですよね。「django-cors-headers」の利用度も高いことから、現場で DRF を使って API 開発をしている方が多いことが伺えます。

次点の 「django-debug-toolbar」は2人に1人くらいが現場で使っているという結果になりました。私は開発時にはどのプロジェクトにも「django-debug-toolbar」と「django-extensions」はとりあえず入れるようにしていますが、そんな感じで「とりあえず生!🍺」みたいに使われているのかもしれません。*2


「4: 現場で使っている」「3: 試しに使ったことがある」「2: 聞いたことはある」を足し合わせたものを「認知度」とすると、認知度が高いほど、現場で使っている割合が全体的に高い傾向にありますが、認知度の割には現場で利用されていない(現場で使っていない割には認知度が高いとも解釈できる?)パッケージとして「django-allauth」「django-crispyforms」「django-channels」などが挙げられるでしょうか。



なお、「django-celery」は開発が長年停滞していて現時点で公式サポートされているバージョンの Django には対応していないようなのですが、今回のアンケートでは過去に現場で使っていたということで票が入ったのでしょう。*3




 

経験年数による変化

ここで Q1. とQ2. の結果を組み合わせて、少し考察をしてみたいと思います。

まず、経験年数のグループごとに「現場で使っている」と回答したパッケージ数の平均を比較してみます。




3〜5年くらいを境にして、利用しているパッケージの数が跳ね上がっているように見えます。だいたい3年目くらいから複数の現場を経験するようになるということなのかもしれませんね。


次に、「Django 経験が長い人ほど利用度が高くなるパッケージはどれか?」を調べるために、経験年数とパッケージ利用度の相関を計算してみました。相関が高い Django パッケージランキングの上位5つを下に示します。


#Django パッケージ相関係数
1django-cors-headers0.64
2django-silk0.60
3django-extensions0.57
4django-channels0.55
5django-filter0.54


これらはベテランならではのニーズに答えてくれるパッケージということなのでしょうか。まだこれらのパッケージを試したことがないという方は、ぜひチェックしてみてください。



 

PyPI ダウンロード数のランキングとの比較

The 10 Most-Used Django Packages | LearnDjango.com」という記事にインスパイアされ、PyPI のダウンロード数を元にした別角度での利用度ランキングを作成してみました。PyPI のダウンロード数は「Top PyPI Packages」というサービスから取得しました。




パッケージ名の右側のカッコ内の数字は先に示した「現場で使っている」ランキングの順位ですが、多少のブレはあるものの今回の結果と比較して違和感のない結果になっている印象です。

直近のダウンロード数を元にしていることで、プロジェクトがすでにアーカイブ済みになっている「django-rest-swagger」のダウンロード数が少なかったり、開発が長年停滞している「django-celery」などのパッケージがランキング外になっていたりするのは、まさに現時点での「利用実態」を表していると言えるでしょう。


 

GitHub スター数のランキングとの比較

次に、12/14 時点の GitHub スター数で並べた人気ランキングを見てみます。




同じく、カッコ内の数字は先に示した「現場で使っている」ランキングの順位ですが、(PyPI のダウンロード数ランキングと比べて)少し乖離が大きいような感じがします。

例えば、上位にランクインしている「Wagtail」は8年以上、「django-allauth」は12年以上も継続しているリポジトリということでスター数が多くなっている可能性がありますし、「django-channels」は Django の公式コミュニティが開発しているということで注目度が高いためにスター数が多くなっている可能性があります。また、現在のスター数は多いけれども実際はもう使われなくなってしまったリポジトリもあったりするので、実際の利用実態と乖離していることもあるでしょう。


ということで、パッケージを選定する際には、GitHub スター数を「人気度」として参考にしつつ、PyPI の直近のダウンロード数を「利用度」として判断材料にするのがよいのではないでしょうか。なお、実際の選定にあたっては次の点も留意するのがよいでしょう。

  • 開発が停滞していないか(直近の PyPI ダウンロード数である程度把握できる)
  • Issue が放置されていないか
  • 公式ドキュメントが充実していること
  • コントリビュータが数人以上いること(ぼっち開発は危険)



 

まとめ

独自のアンケートを採って、「実際の現場ではどんな Django パッケージがどれくらい使われているのか?」を調査しました。

「Q2.Django パッケージ利用状況」の結果のグラフを見ると、特異的に認知度が高いものがいくつかあったものの、全体的に、認知度が高いほど現場で利用されている度合いが高い傾向にあることが分かりました。結果としては、「djangorestframework」が認知度・利用度ともダントツのトップでした。

今回は、私が全然知らなかった Django パッケージも含めてアンケートをすることができたので、大変興味深く調査することができました。アンケートにご協力いただいた皆さま、誠にありがとうございました。改めて御礼申し上げます。



参考までに、アンケート結果の可視化に利用したコードは ↓ にアップしています。

github.com




 

(おまけ)ChatGPT による Django パッケージの説明

今回のアンケートの選択肢として挙げたそれぞれの Django パッケージの説明文を載せておきます。ちなみにこの説明文は、最近話題の「ChatGPT」の回答を使わせていただきました。ざっとチェックしましたが、しれっと大嘘をついている可能性があるかもしれませんのでご注意ください。

Django パッケージ説明文
djangorestframeworkDjango フレームワークで RESTful な Web API を作成するためのライブラリです。このライブラリを使用することで、Django アプリケーションを通じて RESTful な Web API を提供することができます。 djangorestframework には、API のエンドポイントやリクエストとレスポンスのシリアライザ、API の認証や権限管理など、RESTful な Web API を実装するための必要な機能がすべて提供されています。このライブラリを使用することで、Django アプリケーションから RESTful な Web API を提供することができるようになります。
django-debug-toolbarDjango にて、開発時にデバッグを行うためのものです。このパッケージを使用することで、Django アプリケーションの開発時に、リクエストやレスポンスなどの詳細情報を表示することができます。
django-filterDjango アプリケーションでクエリセットをフィルタリングするためのライブラリです。このライブラリを使用することで、クエリセットを簡単に様々な条件でフィルタリングし、特定の条件にマッチするデータのみを取得することができます。 django-filter を使用することで、データベースからデータを抽出する際に、より細かいフィルタリングを行うことができるため、データをより正確かつ効率的に取得することができます。
django-storagesDjango フレームワークでさまざまなクラウドストレージサービスを使用するためのライブラリです。このライブラリを使用することで、Django アプリケーションで Amazon S3 や Google Cloud Storage などのクラウドストレージサービスを簡単に使用することができます。 django-storages を使用することで、Django アプリケーションで保存されるデータや画像などのファイルをクラウドストレージ上に保存することができます。これにより、アプリケーションのデータを安全かつスケーラブルな方法で保存することができるようになります。
django-cors-headersDjango にて、クロスオリジンリソースシェアリング(CORS)を扱うためのものです。CORS は、異なるオリジン(ドメイン)間での Web ページのやり取りを許可する仕組みです。
django-extensionsDjango フレームワークのためのサポートライブラリです。このライブラリには、Django アプリケーションを開発するためのさまざまな便利な機能が含まれています。例えば、コマンドラインから Django プロジェクトを作成するためのツール、データベースのスキーマを表示するためのツール、データベースのデータをダンプするためのツールなどがあります。このライブラリを使用することで、Django アプリケーションの開発プロセスをよりスムーズかつ効率的に行うことができます。
django-environDjango アプリケーションで環境変数を簡単に使用できるようにするためのライブラリです。このライブラリを使用することで、アプリケーションの設定値を環境変数から取得することができます。これにより、アプリケーションの設定値を外部から安全かつ簡単に管理することができます。
pytest-djangoPython のテストフレームワークである pytest を用いて Django Web アプリケーションのテストを実行するためのプラグインです。このプラグインを使うことで、Django アプリケーションのテストを簡単に実行し、結果を比較することができます。また、pytest-django は、Django アプリケーションのデータベースを自動的にセットアップ・クリーンアップするため、データベースのテストも容易に行うことができます。
django-import-exportDjangoフレームワークでのデータのインポートおよびエクスポートを簡単に行うためのライブラリです。このライブラリを使用することで、Djangoのモデルデータを様々な形式のファイル(CSV、JSON、Excelなど)とやりとりすることができます。django-import-export を使用することで、データのインポートおよびエクスポート操作を管理画面から直接実行することができます。
django-allauthDjango にて、多要素認証を提供するためのものです。このパッケージを使用することで、Django アプリケーションで簡単に多要素認証を実装することができます。また、django-allauth は Django のログイン認証システムを拡張しており、既存の Django アプリケーションでも簡単に導入することができます。また、django-allauth は様々な外部サービス(Facebook、Google、Twitter など)との連携もサポートしています。
djangorestframework-simplejwtdjango-rest-framework を使用した RESTful な Web API にJSON Web Tokens(JWT)を使用した認証を実装するためのライブラリです。JWT は、トークンベースの認証方式の一種です。djangorestframework-simplejwt を使用することで、django-rest-framework で作成した RESTful な Web API に JWT を使用した認証を実装することができます。これにより、API を利用するアプリケーションがAPIに対してアクセスする際に、トークンを使用して認証を行うことができるようになります。
django-redisDjango フレームワークで Redis を使用するためのライブラリです。Redis は、速度が非常に速いキー/値型のデータベースです。django-redis を使用することで、Django アプリケーションで Redis を使用してデータを保存することができます。これにより、Djangoアプリケーションのデータ保存や取得が高速化されます。また、django-redis は、Django のキャッシュフレームワークと組み合わせて使用することで、アプリケーションのパフォーマンスをさらに向上させることができます。
django-celeryWeb フレームワークである Django と、Python のタスク駆動フレームワークである Celery を組み合わせるためのものです。このパッケージを使用することで、Django アプリケーション内で Celery を利用して、非同期タスクを管理することができます。
dj-database-urlDjango を使用したアプリケーションで、データベースの接続文字列を簡単に扱うためのライブラリです。このライブラリを使うことで、データベースの接続情報を環境変数や設定ファイルから読み取り、Django の設定に渡すことができます。その結果、Django アプリケーションのデプロイやテストを容易に行うことができるようになります。また、dj-database-url は、様々なデータベース管理システムをサポートしているため、どのようなデータベースを使用しているかに関係なく利用できます。
django-silkDjango フレームワークでリクエストとレスポンスをモニタリングするためのライブラリです。このライブラリを使用することで、Djangoアプリケーションで発生するHTTPリクエストとレスポンスを管理画面から簡単に閲覧することができます。django-silkを使用することで、アプリケーションのパフォーマンスやエラーを把握することができるため、デバッグやトラブルシューティングがより容易に行えます。また、django-silkは、リクエストとレスポンスの中身を表示するだけでなく、リクエストを再現したり、リクエストやレスポンスを検索したりすることができます。
django-model-utilsDjango フレームワークでモデルを定義する際に便利なユーティリティクラスを提供するライブラリです。このライブラリを使用することで、モデルの作成やカスタマイズがより簡単かつ効率的に行えます。
django-crispyformsこのパッケージを使用することで、Django アプリケーションで簡単にフォームを美しくレンダリングすることができます。また、django-crispy-forms は Bootstrap を使用したフォームのレンダリングをサポートしています。
django-rest-swaggerDjango フレームワークで作成された RESTful API に Swagger を組み込むためのライブラリです。Swagger は、RESTful API を文書化して公開するためのツールです。django-rest-swagger を使用することで、Django アプリケーションで作成した RESTful API を Swagger を使用して文書化することができます。これにより、API を使用する開発者が API の仕様や使用方法を簡単に理解することができるようになります。また、django-rest-swagger を使用することで、API のテストやモックを作成することができます。
dj-rest-authDjango の REST フレームワークである Django REST framework にて、ユーザー認証を提供するためのものです。このパッケージを使用することで、Django アプリケーションで簡単にユーザー認証を実装することができます。また、dj-rest-auth は Django のログイン認証システムを拡張しているため、既存の Django アプリケーションでも簡単に導入することができます。
WagtailWagtail は、Django フレームワークを使用した CMS(Content Management System)です。このCMSを使用することで、WebサイトやWebアプリケーションのコンテンツの管理をより簡単かつスムーズに行うことができます。 Wagtail には、ページやブログ記事の管理、メディアライブラリ、ページのテンプレートやカスタマイズ、検索、ユーザー管理などの機能が備わっています。これらの機能を使用することで、WebサイトやWebアプリケーションのコンテンツを効率的に管理することができます。また、Wagtail はオープンソースであるため、カスタマイズや拡張も容易に行うことができます。
django-compressorDjango にて、JavaScript や CSS のファイルを圧縮するためのものです。このパッケージを使用することで、Django アプリケーションで簡単に JavaScript や CSS のファイルを圧縮することができます。
DjoserDjango フレームワークで作成された RESTful な Web API にユーザー管理機能を実装するためのライブラリです。このライブラリを使用することで、Djangoアプリケーションにユーザー登録、ユーザー情報の管理、パスワードの変更などの機能を簡単に実装することができます。また、Djoser は、JSON Web Tokens(JWT)を使用した認証をサポートしているため、JWT を使用した API の認証も実装することができます。Djoser を使用することで、Django アプリケーションにRESTful な API を作成し、ユーザー管理や認証を実装することができるようになります。
django-channelsDjango にて、WebSocket や Server-Sent Events(SSE)を扱うためのものです。このパッケージを使用することで、Django アプリケーションで WebSocket や SSE を利用した通信を実装することができます。
django-celery-beatDjango フレームワークと Celery というタスクキューライブラリを組み合わせて使用するためのツールを提供します。Celery は、バックグラウンドで処理を行うタスクを管理するためのライブラリです。django-celery-beat を使うことで、Django プロジェクトで Celery のスケジュール処理をより簡単に利用することができます。また、django-celery-beat には、Django のデータベースを使って Celery のスケジュールを管理するためのツールが提供されているため、Django プロジェクトで定期的なタスクを実行するためのインフラストラクチャを構築することができます。
django-taggitDjango フレームワークでタグを扱うためのライブラリです。このライブラリを使用することで、Djangoアプリケーションでタグを作成したり、タグを付けられたオブジェクトを取得したりすることができます。django-taggit を使用することで、アプリケーションにタグ付け機能を簡単に実装することができます。また、django-taggit は、既存の Django モデルにタグを追加することもできます。これにより、タグを使用してデータを管理したり、検索したりすることができます。
model-bakeryDjango プロジェクトで使用されるモデルを生成するための Python パッケージです。model-bakery は、モデルを生成し、操作し、評価するためのツールを提供します。
django-dbbackupこのパッケージを使用することで、Django アプリケーションで簡単にデータベースのバックアップを管理することができます。また、django-dbbackup は様々なクラウドストレージサービス(Amazon S3、Dropbox、Google Cloud Storage など)との連携もサポートしています。
pylint-djangopylint-django は、Django フレームワークで作成された Python コードを検証するための Pylint プラグインです。Pylint は、Python のコード品質を測定するツールです。pylint-django を使用することで、Django フレームワークを使用した Python コードに対して、Pylint が提供する様々な品質指標を適用することができます。これにより、Django フレームワークを使用した Python コードの品質を向上させることができます。また、pylint-django は、Django フレームワーク固有の構文や API を理解しており、Django フレームワークを使用した Python コードをより正確かつ効率的に検証することができます。
django-bracesDjango にて、様々な拡張機能を提供するためのものです。このパッケージを使用することで、Django アプリケーションで簡単にビューやミックスインなどの拡張機能を実装することができます。また、django-braces は他の Django パッケージとも組み合わせて使用することができ、Django プロジェクトで様々な拡張機能を組み合わせることができます。
django-configurationsDjango にて、複数の環境に対応した設定を管理するためのものです。このパッケージを使用することで、Django アプリケーションで簡単に複数の環境(開発環境、本番環境、ステージング環境など)に対応した設定を管理することができます。
django-clickPython の Webフレームワークである Django を使用したアプリケーションで、コマンドラインインターフェース(CLI)を簡単に構築するためのライブラリです。その結果、Django アプリケーションの管理や操作を、より便利でスムーズに行うことができるようになります。
django-test-plusDjango フレームワークでのユニットテストを実行するためのライブラリです。このライブラリを使用することで、Django アプリケーションのユニットテストをより簡単かつ効率的に行うことができます。 django-test-plus には、ユニットテストを実行する際に便利なユーティリティ関数やアサーションが含まれています。例えば、ビューやURLのテストを行うためのヘルパー関数、フォームやモデルのテストを行うためのアサーションなどがあります。これらの機能を使用することで、Djangoアプリケーションのユニットテストをよりスムーズかつ効率的に実行することができます。
django-moneyDjangoフレームワークで組み込みの「Money」フィールドを提供するライブラリです。このライブラリを使用することで、データベース内での金額を貨幣単位を考慮した形で保存することができます。django-money を使用することで、金額を扱うアプリケーションを作成する際に、正確かつ柔軟な金額処理が行えるようになります。また、django-money は、様々な通貨をサポートしており、通貨の相互変換も可能です。
dj-rest-knoxDjango の REST フレームワークである Django REST framework にて、トークンベースの認証を提供するためのものです。このパッケージを使用することで、Django アプリケーションでトークンベースの認証を実装することができます。また、dj-rest-knox は Django REST framework の上に構築されており、Django のログイン認証システムを使用している場合は、すぐに使用することができます。
django-lifecyclePython の Web フレームワークである Django を使用したアプリケーションで、モデルの作成や更新時に実行される処理を定義するためのライブラリです。このライブラリを使うことで、Django のモデルを作成、更新する際に、特定の処理を自動的に実行することができます。例えば、モデルが作成されるときに、特定のメールを送信する処理や、モデルが更新されるときに、特定のログを記録する処理などを定義することができます。これにより、Django アプリケーションの動作をより自動化し、処理の追加や変更を容易に行うことができるようになります。


 

宣伝

Django の技術同人誌をこれまでに4冊出しました。開発のお供にどうぞ。



現場で使える Django の教科書《基礎編》

「現場で使える Django の教科書」シリーズ第1弾となる Django の技術同人誌。Django を現場で使うための基礎知識やベストプラクティスについて、初心者・初級者向けに解説した本です。B5・本文192ページ。最新の Django 3.2 LTS に対応。


★ Amazon(電子版/ペーパーバック)


★ BOOTH(紙の本)



現場で使える Django の教科書《実践編》

《基礎編》の続編にあたる「現場で使える Django の教科書」シリーズの第2弾。認証まわり、ファイルアップロード、ユニットテスト、デプロイ、セキュリティ、高速化など、さらに実践的な内容に踏み込んでいます。現場で Django を本格的に活用したい、あるいはすでに活用している方にピッタリの一冊。B5・本文180ページ。


★ Amazon(電子版/ペーパーバック)


★ BOOTH(紙の本)



現場で使える Django REST Framework の教科書

Django で REST API を構築する際の鉄板ライブラリである「Django REST Framework」(通称「DRF」)にフォーカスした、「現場で使える Django の教科書」シリーズの第3弾。B5・本文220ページ。


★ Amazon(電子版/ペーパーバック)


★ BOOTH(紙の本)



現場で使える Django 管理サイトのつくり方

Django の管理サイト(Django Admin)だけに特化した、ニッチでオンリーワンな一冊。管理サイトをカスタマイズする前に絶対に読んでほしい本です。B5・本文152ページ。


★ Amazon(電子版)


★ BOOTH(紙の本)

*1:そもそもなぜこの35個が選ばれたのかというのは分かりませんでした。。orz

*2:私は飲めませんが。。

*3:Django 2.2 未対応という致命的な問題があるため、「django-celery-results」や「django-celery-beat」が代替となっているようです。

Django 5.0 で削除された「USE_L10N」とは何だったのか? そして今後はどうなるのか?

$
0
0

この投稿は 「Djangoのカレンダー | Advent Calendar 2023 - Qiita」 15日目の記事です。

小ネタです。


Django 5.0 で「USE_L10N」が削除されました。

https://docs.djangoproject.com/ja/5.0/releases/5.0/#features-removed-in-5-0より



そもそも Django には「USE_I18N 」と「USE_L10N」という設定があり、その変数名からそれぞれが「国際化 (internationalization) 」と「ローカル化 (localization) 」に対応するかと思いきや、Django においては、「USE_I18N 」は言語設定に合わせたテキストの多言語対応、「USE_L10N」は言語設定に合わせた日時や数字のフォーマット対応を扱います。これらの変数の微妙なネーミングには歴史的な経緯があるとのこと。

国際化 (internationalization)
ソフトウェアをローカル化に備えさせることです。通常、開発者によって行われます。
ローカル化 (localization)
翻訳およびローカルな表示形式を記述することです。通常、翻訳者によって行われます。


Translation is controlled by the USE_I18N setting. However, it involves internationalization and localization. The name of the setting is an unfortunate result of Django's history.
(翻訳はUSE_I18N設定によって制御されます。しかし、これは国際化とローカライゼーションに関わります。この設定の名前は Django の歴史が生んだ不幸な結果です。)


https://docs.djangoproject.com/ja/5.0/topics/i18n/より



なお、多言語対応については、芝田さんの「実践Django Pythonによる本格Webアプリケーション開発」に詳しく書かれているのでぜひどうぞ。

あとは、公式ドキュメントの 翻訳 | Django ドキュメント | Djangoあたりをご参照ください。




本題に入ります。

Django 4.2 まで使われている「USE_L10N」は、先に述べた通り、言語設定に合わせた日時や数字の表示形式を扱うための設定です。

もっと具体的に言えば、主にテンプレートに変数を表示する際、変数が数値型・日時型・日付型・時刻型の場合に、「USE_L10N」と「LANGUAGE_CODE」の設定内容にしたがったフォーマット変換をしてくれる設定ということになります。


もちろん、管理サイトのモデル一覧画面などの表示にも適用されます。




Django 4.2 以前は、「USE_L10N」は次のように動作します。

「USE_L10N」が False の場合、設定ファイルの「DATETIME_FORMAT」や「DATE_FORMAT」のフォーマットに変換される(設定が無ければ、global_settings.py のデフォルトのフォーマットに従って変換される)。


「USE_L10N」が True の場合、(設定ファイルや global_settings.py のフォーマット設定に関係なく)「LANGUAGE_CODE」で指定した言語ごとのフォーマットに変換される。


「LANGUAGE_CODE」で指定した言語ごとのフォーマットというのは、「conf/locale/ja/」のようにロケールごとに置かれた「formats.py」に書かれたフォーマットを指します。例えば、日本語の場合は次に示す formats.py がデフォルトで用意されています。

django/conf/locale/ja/formats.py

# This file is distributed under the same license as the Django package.## The *_FORMAT strings use the Django date format syntax,# see https://docs.djangoproject.com/en/dev/ref/templates/builtins/#date
DATE_FORMAT = "Y年n月j日"
TIME_FORMAT = "G:i"
DATETIME_FORMAT = "Y年n月j日G:i"
YEAR_MONTH_FORMAT = "Y年n月"
MONTH_DAY_FORMAT = "n月j日"
SHORT_DATE_FORMAT = "Y/m/d"
SHORT_DATETIME_FORMAT = "Y/m/d G:i"# FIRST_DAY_OF_WEEK =# The *_INPUT_FORMATS strings use the Python strftime format syntax,# see https://docs.python.org/library/datetime.html#strftime-strptime-behavior# DATE_INPUT_FORMATS =# TIME_INPUT_FORMATS =# DATETIME_INPUT_FORMATS =
DECIMAL_SEPARATOR = "."
THOUSAND_SEPARATOR = ","
NUMBER_GROUPING = 3


Django 5.0 以降で「USE_L10N」が削除されますが、言語設定に合わせて日時や数字の表示フォーマットを変更できなくなるわけではありません。「USE_L10N」が常に True 扱いになったと考えればよいだけです。ご安心ください。


Django 5.0 以降で日時や数字の表示フォーマットをデフォルトのものから変更するには、Django 4.2 以前と同じように「FORMAT_MODULE_PATH」*1を使って、言語に合わせた formats.py のフォーマット設定を書き換えてあげればよいです。


(ディレクトリ構成例)

.
|-- conf
|   |-- __init__.py
|   `-- locale
|       |-- __init__.py
|       `-- ja
|           |-- __init__.py
|           `-- formats.py
|-- config
|   |-- __init__.py
|   |-- asgi.py
|   |-- settings.py
|   |-- test.py
|   |-- urls.py
|   |-- views.py
|   `-- wsgi.py
・
・


config/settings.py

LANGUAGE_CODE = 'ja'

FORMAT_MODULE_PATH = 'conf.locale'

(Django 5.0 以降では「USE_L10N」の設定は不要です。むしろ、設定してもまったく意味がありません。)


conf/locale/ja/formats.py

DATE_FORMAT = 'Y-m-d'
DATETIME_FORMAT = 'Y-m-d H:i'


Viewing all 52 articles
Browse latest View live


<script src="https://jsc.adskeeper.com/r/s/rssing.com.1596347.js" async> </script>