akiyoko blog

akiyoko の IT技術系ブログです

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

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


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






検証環境

  • 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 の画面例




Swagger Inspector の画面例




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

openapi.tools


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



 

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

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



しかし先述のように、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ドキュメント画面が表示できます。




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



 

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

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






 

カスタマイズ

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

例えば、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


class BookListCreateAPIView(views.APIView):
    """本モデルの取得(一覧)・登録APIクラス"""

    @extend_schema(
        responses={200: BookSerializer},
    )
    def get(self, request, *args, **kwargs):
        """本モデルの取得(一覧)APIに対応するハンドラメソッド"""

        ...(略)...

        return Response(serializer.data, status.HTTP_200_OK)

    @extend_schema(
        request=BookSerializer,
        responses={201: BookSerializer},
    )
    def post(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・本文228ページ。Django 3.2 LTS に対応。


★ 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 でデータベースビューを扱う方法(初級者向け)

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


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







検証環境

  • 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


class SalesResultPerMonth(models.Model):
    class Meta:
        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)コマンドを実行したときにマイグレーションファイルの内容がデータベースのテーブルに反映されず、テーブルが新たに作成されたり、テーブル構造が変更されたりすることがなくなります。





 

具体例

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




通常のモデルを作成する

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

sales/models.py(モデル)

from django.db import models


class SalesTarget(models.Model):
    """売上目標モデル"""

    class Meta:
        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月}'


class SalesResult(models.Model):
    """売上実績モデル"""

    class Meta:
        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




データベースビュー作成用の 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:34

from django.db import migrations


class Migration(migrations.Migration):

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

    operations = [
    ]


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

# Generated by Django 3.1.3 on 2020-12-04 12:34

from django.db import migrations


class Migration(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 上で確認すると、このようになっています。





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

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


sales/models.py(モデル)

from django.db import models


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


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


class SalesResultPerMonth(models.Model):  # 追加
    """月別合計売上金額モデル"""

    class Meta:
        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


class SalesResultPerMonth(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)

    class Meta:
        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


class SalesResultAdmin(admin.ModelAdmin):
    """売上実績モデル用 ModelAdmin"""

    ###############################
    # モデル一覧画面のカスタマイズ
    ###############################
    list_display = ('sales_date', 'format_amount', 'subject')
    ordering = ('sales_date', 'created_at')

    def format_amount(self, obj):
        return format_yen(obj.amount)

    format_amount.short_description = '売上金額'
    format_amount.admin_order_field = 'amount'

    ###############################
    # モデル追加・変更画面のカスタマイズ
    ###############################
    readonly_fields = ('id', 'created_at')


class SalesTargetAdmin(admin.ModelAdmin):
    """売上目標モデル用 ModelAdmin"""

    ###############################
    # モデル一覧画面のカスタマイズ
    ###############################
    list_display = ('format_sales_month', 'format_amount')
    ordering = ('sales_month',)

    def format_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'

    def format_amount(self, obj):
        return format_yen(obj.amount)

    format_amount.short_description = '目標金額'
    format_amount.admin_order_field = 'amount'

    ###############################
    # モデル追加・変更画面のカスタマイズ
    ###############################
    readonly_fields = ('id', 'created_at')


class SalesResultPerMonthAdmin(admin.ModelAdmin):
    """月別合計売上金額モデル用 ModelAdmin"""

    ###############################
    # モデル一覧画面のカスタマイズ
    ###############################
    list_display = ('format_sales_month', 'format_target_amount', 'format_total_amount')
    ordering = ('sales_month',)

    def format_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'

    def format_target_amount(self, obj):
        return format_yen(obj.target_amount)

    format_target_amount.short_description = '目標売上金額'
    format_target_amount.admin_order_field = 'target_amount'

    def format_total_amount(self, obj):
        return format_yen(obj.total_amount)

    format_total_amount.short_description = '合計売上金額'
    format_total_amount.admin_order_field = 'total_amount'

    ###############################
    # その他のカスタマイズ
    ###############################
    def has_add_permission(self, request):
        return False

    def has_change_permission(self, request, obj=None):
        return False

    def has_delete_permission(self, request, obj=None):
        return False


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



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




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

akiyoko.hatenablog.jp



 

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

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

# Generated by Django 3.1.3 on 2020-12-04 12:34

from django.db import migrations
from django.db.migrations import exceptions


def code(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.')


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


class Migration(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・本文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・本文228ページ。Django 3.2 LTS に対応。


★ 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 で楽観的排他制御を簡単に実装する方法(初級者向け)

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


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




検証環境

  • Windows 10 Home
  • Django 3.1
  • PostgreSQL 13.1



 

はじめに

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

次の図を見てください。


一例として、決済処理の中で在庫テーブルを 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」とは異なり、他のトランザクションのコミットによる変更が参照できます。例えば、次の図のように、他のユーザのトランザクションが終了した瞬間から最新の値が参照できるようになっています。



つまり、「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


class Book(models.Model):
    """本モデル"""

    class Meta:
        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


class BookStock(models.Model):
    """在庫モデル"""

    class Meta:
        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さんの処理がエラーになり、更新ができなくなります。


「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 %}





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


def conflict(request, target=None, template_name='409.html'):
    if isinstance(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 %}






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



 

まとめ

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

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



 

宣伝

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・本文228ページ。Django 3.2 LTS に対応。


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


★ BOOTH(紙の本)※在庫なし



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

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


★ Amazon(電子版)


★ BOOTH(紙の本)

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

この投稿は 「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

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


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

デフォルトでは、「① 注文情報を登録」「③ 在庫数を 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()


class Book(models.Model):
    """本モデル"""

    class Meta:
        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


class BookStock(models.Model):
    """在庫モデル"""

    class Meta:
        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})'


class Order(models.Model):
    """注文モデル"""

    class Meta:
        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


class CheckoutView(View):
    ...(略)...

    def post(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 ですよね。



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


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


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


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,  # 追加
    }
}


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



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



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


class CheckoutView(View):
    ...(略)...

    def post(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


《 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')  # 追加
class CheckoutView(View):
    ...(略)...

    def post(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・本文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・本文228ページ。Django 3.2 LTS に対応。


★ 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 管理サイトのつくり方』頒布のお知らせ

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



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


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


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


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


f:id:akiyoko:20200824093755p: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:20200824093846p:plain

《 紙面サンプル ② 》

f:id:akiyoko:20200824093917p: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:20200824094004p:plain

《 紙面サンプル ④ 》

f:id:akiyoko:20200824094036p: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:20200824094147p:plain

《 紙面サンプル ⑥ 》

f:id:akiyoko:20200824094231p:plain



頒布本情報

現在、BOOTH で紙版、Amazon で電子版が購入可能です。


f:id:akiyoko:20200926195858p:plain:w450


■ BOOTH
現場で使える Django 管理サイトのつくり方(技術書典9バージョン) | BOOTH
booth.pm


■ Amazon
現場で使える Django 管理サイトのつくり方 | Kindleストア | Amazon
www.amazon.co.jp



技術書典マーケットでは技術書典9 期間終了後、販売を一時停止しています。



書評

Django のイベント関係でいつもお世話になっている方々に献本させていただきました。その書評をいくつか紹介させていただきます。



まさに「管理サイトでどこまでできるのか?」を把握するためにも有用な本だと自負しています。今回の本は図表が多いので、152ページだけど読み流しやすいはず。セキュリティ対策のところは最後の最後に追記したところなので追記しておいてよかったです 😄



「管理サイト」は本当に便利なんですよね。スクレイピングしたデータを保存して画面で操作するとか、そのためだけに Django を使うのもアリだと思ってます。



管理サイトはちょっとしたカスタマイズまでは簡単なのですが、それ以上のことをやろうとすると少し大変なんですよね。そんなときにこの本があれば安心ですよね 😉



electricsheep.hatenadiary.jp

id:electricSheep さんによる新刊の感想ブログです。

>「え、管理サイトのソースどこ??この2行????」

分かります…😅 これだけで動いちゃうので、いろいろカスタマイズできるとか気付かないですよね。管理サイトの網羅的な情報ってまだまだ少ないですよねぇ。



nikkie-ftnext.hatenablog.com

id:nikkie-ftnext さんの感想ブログです。

本にたくさん付箋紙が貼ってあったのが嬉しかったです!!

確かに DjangoGirls チュートリアル では管理サイトについてあまり触れられていないのですが、公式チュートリアルの「はじめての Django アプリ作成、その2」や「はじめての Django アプリ作成、その 7」では少し触れられています(が、ちょっと分かりにくいかもですね)。



tokibito.hatenablog.com

Django の管理サイトを10年以上も前からカスタマイズしている tokibito さんの書評ブログでは、いろいろなオススメポイントを紹介していただきました。おっしゃる通り、「Djangoのチュートリアルをこなした後、次にやることを探してるひと」にもぜひ読んでいただきたいですね!




最後に

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


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


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


f:id:akiyoko:20200824094307p: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 モデルのフィールドの「auto_now_add」「auto_now」オプションの挙動を詳しく調べてみた

Django モデルに登録日時や更新日時のフィールドを付加する場合、「auto_now_add」オプションや「auto_now」オプションを利用すると便利です。それらのオプションの挙動について詳しく調べてみたので、まとめておきます。

f:id:akiyoko:20200521233958p:plain

TL;DR


挙動

たとえば、

shop/models.py

from django.db import models


class Book(models.Model):
    """本モデル"""

    class Meta:
        db_table = 'book'
        verbose_name = verbose_name_plural = '本'

    title = models.CharField('タイトル', max_length=255)
    price = models.PositiveIntegerField('価格', null=True, blank=True)
    created_at = models.DateTimeField('登録日時', auto_now_add=True)
    updated_at = models.DateTimeField('更新日時', auto_now=True)

    def __str__(self):
        return self.title

というフィールドがあるとすると、

登録時

  • 「auto_now_add=True」のフィールド(created_at)に現在日時が自動的にセットされる
  • 「auto_now=True」のフィールド(updated_at)に現在日時が自動的にセットされる

更新時

  • 「auto_now=True」のフィールド(updated_at)に現在日時が自動的にセットされる

という挙動になります。


つまり、テストを書くとこういうことになります(参考までに freezegununittest.mock を使ったものを併記)。


shop/tests/test_models.py

from datetime import datetime
from unittest.mock import patch

from django.test import TestCase
from django.utils import timezone
from freezegun import freeze_time

from ..models import Book


class TestBook(TestCase):

    @freeze_time('2020-01-01 01:01:01')
    def test_save_with_freezegun(self):
        book = Book(title='Django本', price=1500)
        # 登録
        book.save()

        # 登録日時・更新日時のどちらも値がセットされる
        self.assertEqual(book.created_at, datetime(2020, 1, 1, 1, 1, 1, tzinfo=timezone.utc))
        self.assertEqual(book.updated_at, datetime(2020, 1, 1, 1, 1, 1, tzinfo=timezone.utc))

        with(freeze_time('2020-02-02 02:02:02')):
            # 更新
            book.save()

        # 更新日時のみが更新される
        self.assertEqual(book.created_at, datetime(2020, 1, 1, 1, 1, 1, tzinfo=timezone.utc))
        self.assertEqual(book.updated_at, datetime(2020, 2, 2, 2, 2, 2, tzinfo=timezone.utc))

    @patch('django.utils.timezone.now',
           return_value=datetime(2020, 1, 1, 1, 1, 1, tzinfo=timezone.utc))
    def test_save_with_mock(self, _mock_now):
        book = Book(title='Django本', price=1500)
        # 登録
        book.save()

        # 登録日時・更新日時のどちらも値がセットされる
        self.assertEqual(book.created_at, datetime(2020, 1, 1, 1, 1, 1, tzinfo=timezone.utc))
        self.assertEqual(book.updated_at, datetime(2020, 1, 1, 1, 1, 1, tzinfo=timezone.utc))

        _mock_now.return_value = datetime(2020, 2, 2, 2, 2, 2, tzinfo=timezone.utc)
        # 更新
        book.save()

        # 更新日時のみが更新される
        self.assertEqual(book.created_at, datetime(2020, 1, 1, 1, 1, 1, tzinfo=timezone.utc))
        self.assertEqual(book.updated_at, datetime(2020, 2, 2, 2, 2, 2, tzinfo=timezone.utc))



 
フィールドに現在日時が自動的にセットされる仕組みを見てみると、次のように、pre_save() で django.utils.timezone.now() が呼ばれています。

django.db.models.fields.DateTimeField.pre_save *1

    def pre_save(self, model_instance, add):
        if self.auto_now or (self.auto_now_add and add):
            value = timezone.now()
            setattr(model_instance, self.attname, value)
            return value
        else:
            return super().pre_save(model_instance, add)


そして django.utils.timezone.now() では、Django の設定ファイルの「USE_TZ」が True の場合は datetime.datetime.utcnow() が呼ばれています。

django.utils.timezone.now *2

def now():
    """
    Return an aware or naive datetime.datetime, depending on settings.USE_TZ.
    """
    if settings.USE_TZ:
        # timeit shows that datetime.now(tz=utc) is 24% slower
        return datetime.utcnow().replace(tzinfo=utc)
    else:
        return datetime.now()


freezegun の freezegun.api.FakeDatetime が次のように datetime.datetime.now() や utcnow() などをモックしてくれているので、「auto_now_add」や「auto_now」にも適用できるのですね。よかった。

freezegun.api.FakeDatetime *3

class FakeDatetime(with_metaclass(FakeDatetimeMeta, real_datetime, FakeDate)):

    ...(略)...

    @classmethod
    def now(cls, tz=None):
        now = cls._time_to_freeze() or real_datetime.now()
        if tz:
            result = tz.fromutc(now.replace(tzinfo=tz)) + cls._tz_offset()
        else:
            result = now + cls._tz_offset()
        return datetime_to_fakedatetime(result)

    ...(略)...

    @classmethod
    def utcnow(cls):
        result = cls._time_to_freeze() or real_datetime.utcnow()
        return datetime_to_fakedatetime(result)

    ...(略)...


 

「auto_now_add」や「auto_now」を使うことによる弊害

便利でよいのですが、二点ほど弊害があります。

1.任意の値で更新できない

実例を挙げると、

>>> from shop.models import Book
>>> from datetime import datetime
>>> from django.utils import timezone
>>> book = Book()
>>> book.updated_at = datetime(2020, 1, 1, 1, 1, 1, tzinfo=timezone.utc)
>>> book.save()
>>> book.updated_at
datetime.datetime(2020, 5, 21, 6, 41, 7, 219328, tzinfo=<UTC>)

という感じで、保存時に現在日時で上書きされてしまいます。

(参考)www.ianlewis.org


しかしながら、登録日時や更新日時フィールドに「auto_now_add」および「auto_now」オプションを利用する場合、任意の値で更新したいユースケースとしてはユニットテストのときくらいでしょうから、実害はあまり無いと言えるかもしれません。
 

2.管理サイトの詳細画面で表示されない

「auto_now_add」もしくは「auto_now」オプションを利用すると、自動的に editable オプションが False になるので、管理サイトの詳細画面で表示されなくなります。

f:id:akiyoko:20200521232712p:plain

django.db.models.fields.DateField.__init__ *4

class DateField(DateTimeCheckMixin, Field):
    ...(略)...

    def __init__(self, verbose_name=None, name=None, auto_now=False,
                 auto_now_add=False, **kwargs):
        self.auto_now, self.auto_now_add = auto_now, auto_now_add
        if auto_now or auto_now_add:
            kwargs['editable'] = False
            kwargs['blank'] = True
        super().__init__(verbose_name, name, **kwargs)

    ...(略)...

class DateTimeField(DateField):
    ...(略)...

    # __init__ is inherited from DateField


詳細画面にフィールドを表示するだけであれば(任意の値で登録・更新できないが)、ModelAdmin の「fields」と「readonly_fields」に同時に列挙することで対応は可能です。

shop/admin.py

from django.contrib import admin

from .models import Book


class BookAdmin(admin.ModelAdmin):
    fields = ('title', 'price', 'created_at', 'updated_at')
    readonly_fields = ('created_at', 'updated_at')


admin.site.register(Book, BookAdmin)

f:id:akiyoko:20200521233140p:plain



これらの弊害については一応解決策がありますが、ちょっと面倒です。あまりオススメしません。

stackoverflow.com



 

宣伝

Django の本を3冊書きました。Django 開発のお供にどうぞ。

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

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


★ BOOTH(ペーパー版)


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

★ Amazon(電子版)


★ BOOTH(ペーパー版)


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

★ Amazon(電子版)


★ BOOTH(ペーパー版)

PyCharm のオレオレ最強設定(2020.1 バージョン)

PyCharm 大好き akiyoko です。

以前、「PyCharm のオレオレ最強設定」という記事を書いたのですが、あれから3年経ち、PyCharm もどんどん新しいバージョンがリリースされる中で記事の内容が少し古くなっていたので、今回、最新バージョンの「PyCharm 2020.1」に合わせて記事を書き直してみました。


f:id:akiyoko:20200508040938p:plain


PyCharm を含めた JetBrains 社の IDE は「out of the box(箱から取り出してすぐに使える、難しい設定などは一切なしで使える)」というのが大きな魅力のひとつですが、今回紹介する環境設定をすることでさらなる実力を発揮させることができます。


なお、PyCharm には大きく分けて、無償版の「PyCharm CE(Community Edition)」と有償版の「PyCharm Professional」がありますが *1、「PyCharm CE」の設定は「PyCharm Professional」でもそのまま利用可能です。



今回の設定については

  • バージョン:PyCharm CE 2020.1
  • OS:Windows 10 Home / macOS 10.15 Catalina

で動作確認しています(画面キャプチャは Windows 版のもの)。


なお、今回紹介するのはあくまで筆者の(オレオレ)最強設定ですので、異論・反論は緩やかな範囲でお願いします。


 

PyCharm CE のインストール手順

まずは PyCharm CE 2020.1 のインストール手順について軽く説明します。


PyCharm 公式ダウンロードページ から、「コミュニティ」(PyCharm CE)の方の「ダウンロード」ボタンをクリックして、インストーラをダウンロードします。

f:id:akiyoko:20200508034016p:plain


あとはインストーラをダブルクリックして、表示される内容に従ってインストールを実行してください。


f:id:akiyoko:20200501114537p:plain:w400


デフォルトでは、Windows の場合は

  • C:\Program Files\JetBrains\PyCharm Community Edition 2020.1

macOS の場合は

  • ~/Library/Application Support/JetBrains/PyCharmCE2020.1

にインストールされます。


 

環境設定

上部メニューの[File] > [Settings](macOS 版では[PyCharm] > [Preferences])から、PyCharm の環境設定が変更できます。

1.テーマ設定

[Appearance & Behavior] > [Appearance]

[Theme] をデフォルトの「IntelliJ Light」から、「Darcura」(少し暗めのテーマ)に変更します。あくまで任意で。 *2

f:id:akiyoko:20200501125359p:plain

理由:かっこいいから。


ちなみに、「Darcula」テーマはこんな感じの画面になります。

f:id:akiyoko:20200507035200p:plain


 

2.エディタ外観

[Editor] > [General] > [Appearance]

[Show whitespaces]にチェックを入れます。

f:id:akiyoko:20200501115211p:plain

「Leading(行頭)」「Inner(行内)」「Trailing(行末)」にもチェックが入っていることを確認してください。


理由:半角スペース、全角スペースを可視化したいから。


 

3.コード補完

[Editor] > [General] > [Code Completion]

[Match case]のチェックを外します。

f:id:akiyoko:20200501115517p:plain

理由:コード補完(Ctrl + スペース)の際に、大文字小文字を区別しないで補完してほしいから。

(参考)コード補完 — PyCharm


 

4.コードの折りたたみ

[Editor] > [General] > [Code Folding]

[Fold by default]から、「Imports」のチェックを外します。

f:id:akiyoko:20200501173118p:plain

理由:import 文がデフォルトで折りたたまれていると、レビューしにくいから。


 

5.タブの表示数

[Editor] > [General] > [Editor Tabs]

[Closing Policy]の[Tab limit]を「10」から「50」に変更します。

f:id:akiyoko:20200501172330p:plain

理由:タブを 10 個以上開くと勝手に閉じちゃう設定だと使いにくいから。


 

6.フォントサイズ設定

[Editor] > [Font]

[Size]を「12」(デフォルトは「13」)くらいにしておきます。

f:id:akiyoko:20200501120518p:plain

理由:できるだけ広い範囲でコードを見たいから。


 

7.改行コード設定

[Editor] > [Code Style]

[General]タブの[Line separator]を「Unix and macOS (\n)」に変更します。macOS 版の場合は「System-Dependent」のままでOKです。

f:id:akiyoko:20200501120725p:plain

理由:macOS で編集したコードとの差異を無くしたいから。


 

8.Python コードスタイル設定

[Editor] > [Code Style]> [Python]

8.1.一行あたりの文字数設定

[Wrapping and Braces]タブの[Hard wrap at]を「100」に変更します(デフォルトは「120」)。なお、一行あたりの文字数はプロジェクトのルールに適宜合わせてください。

f:id:akiyoko:20200501123713p:plain

理由:PyCharm の自動フォーマットを使ったときに規定文字数で自動的に折り返ししてほしいから。

8.2.import文のソート

[Imports]タブの[Sort import statements]で、「Sort imported names in "from" imports」(from 節でインポートした名前をソート)にチェックを入れます。

同じく、[Imports]タブの[Structure of "from" imports]は、デフォルトの「Leave as is」から「Join imports with the same source」(同じソースでインポートを結合)に変更します。

f:id:akiyoko:20200501124326p:plain

理由:

  • import文の自動フォーマットを使用したときに、from 節内の複数 import を自動ソートしてほしいから。
  • import文の自動フォーマットを使用したときに、from 節が同一の import 文を自動で一行にまとめてほしいから。


(参考)コードスタイル : Python — PyCharm


 

9.PEP8 違反の警告

[Editor] > [Inspections]

  • [PEP8 coding style violation]
  • [PEP8 naming conversion violation]

の「Severity」を「Week warning」から「Warning」に変更。

f:id:akiyoko:20200501184523p:plain

理由:PEP8 違反の警告をワーニングに上げたいから。


(参考)最強のPython統合開発環境PyCharm - Qiita


 

10.プラグイン

[Plugins]

から、完全に好みで プラグインをインストールします。

  • CodeGlance(Sublime Text のミニマップ風プラグイン)
  • black-pycharm(black フォーマッタプラグイン)
  • IdeaVim(Vim エミュレータプラグイン)

インストールしていないプラグインは、検索して「Install」ボタンをクリックするとオンラインのリポジトリからダウンロードしてインストールすることができます。


f:id:akiyoko:20200503160741p:plain


 

VMオプション設定

[Help] > [Edit Custom VM Options...]

を選択すると、VMオプションの設定をカスタマイズするためのファイルを編集することができます。 *3

11.メモリ設定

デフォルトのヒープメモリサイズを 2GB くらいに増やしておきます。たとえば、

-Xms128m
-Xmx750m
-XX:ReservedCodeCacheSize=240m
...(略)...

となっているのを、

-Xms128m
-Xmx2048m
-XX:ReservedCodeCacheSize=240m
...(略)...

と書き換えて、PyCharm を再起動します。


(参考)高度な構成 — PyCharm


 

プロジェクトツールウィンドウ設定

プロジェクト・ビューの歯車アイコンをクリックして、ツールウィンドウの設定をおこないます。
 

12.プロジェクト・ビューの表示設定

  • [Open Files with Single Click](プロジェクト・ビューからシングルクリックでソースビューを開く)
  • [Always Select Opened File](ソースビューを開くとプロジェクト・ビューに該当モジュールがフォーカスする)

にチェックを入れておきます。

f:id:akiyoko:20200501174811p:plain

理由:プロジェクト・ビューとエディタを連動させたいから。


 

日本語化

PyCharm の「2020.1」バージョン以降、JetBrains 社が提供している純正の「Japanese Language Pack / 日本語言語パック」が利用できるようになりました。「Pleiades 日本語化プラグイン」を利用すれば、PyCharm の UI を日本語化することも可能です。ただし JetBrains 公式のものではないため、動作は保証されていないのでご注意ください。

samuraism.com

 

13.Japanese Language Pack / 日本語言語パックの導入手順

[Plugins]

[Marketplace]タブで「Japanese Language Pack / 日本語言語パック」を検索し、「Install」ボタンをクリックしてインストールします。


f:id:akiyoko:20201216130841p:plain


あとは、PyCharm を再起動すれば OK です。



 

便利なショートカット集

最後に非常に便利なショートカットをいくつか紹介します。

JetBrains 公式の PyCharm チートシート

もありますが、さすがに全部覚えきれませんよね。そこで、私が特に重要と考えるショートカットを抜粋してみました。黄色で強調したショートカットだけでも覚えておくと、効率が劇的に上がりますのでぜひ活用してください。

 

コードフォーマット
Windows
macOS
説明
Ctrl + Alt + L option + ⌘ + L コードの自動フォーマット
Ctrl + Alt + O control + option + O import文の自動フォーマット

 

検索
Windows
macOS
説明
Ctrl + F ⌘ + F ファイル内検索
(Shift +) F3 ⌘ + (shift +) G ファイル内検索結果の前後を表示
Ctrl + Shift + F ⌘ + shift + F パス内検索
Alt + F7 option + F7 関数・メソッドを使用している箇所を検索して Find ウィンドウに表示
Ctrl + Alt + ↓ (↑) ⌘ + option + ↓ (↑) Findウィンドウの検索結果の前後を表示
Ctrl + Alt + ← (→) ⌘ + option + ← (→) 履歴の前後を表示
Ctrl + B ⌘ + B 関数・メソッドの宣言箇所にジャンプ

 

ファイルを開く
Windows
macOS
説明
Shift x 2 (素早く2回) shift x 2 (素早く2回) クイック検索
Ctrl + E ⌘ + E 最近開いたファイルを開く
Ctrl + Shift + N ⌘ + shift + O ファイル名でファイルを開く

 

タブ移動
Windows
macOS
説明
Alt + ← (→) control + ← (→) タブ移動

 

差分表示
Windows
macOS
説明
(プロジェクト・ビューで) Ctrl + D (プロジェクト・ビューで) ⌘ + D 別ファイルとの Diff

 

その他
Windows
macOS
説明
Ctrl + Shift + S ⌘ + ,(カンマ) 環境設定を開く
Ctrl + Shift + A ⌘ + shift + A 利用できるアクションを検索

 

まとめ

個人的に、PyCharm は Python の統合開発環境(IDE)として最強だと考えています。デフォルトの環境設定を何もイジらなくても快適に動いてくれるというのも大きな魅力のひとつが、今回のような環境設定をすると PyCharm はさらに使いやすくなります。


それでは、素敵な PyCharm ライフを!



 

宣伝

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:その他にも Educational 版があります

*2:macOS 版ではデフォルトが「Darcura」になっているようです。少なくとも私の環境では。

*3:Windows の場合は「~\AppData\Roaming\JetBrains\PyCharmCE2020.1\pycharm64.exe.vmoptions」、macOS の場合は「~/Library/Application Support/JetBrains/PyCharmCE2020.1/pycharm.vmoptions」がデフォルトです。