todos テーブルを作成したうえで、小規模な JSON ビューで作成・読み取り・更新・削除のリクエストを処理します。すべての接続で、証明書の完全な検証を伴う TLS を使用します。
前提条件
- Python 3.12 以降 (Django 6 の要件) 。本ガイドは Python 3.13、Django 6.1.1、psycopg 3.3.6 で動作確認しています。
- ClickHouse Cloud アカウント
- データベース作成用の
psql。CREATE DATABASEステートメントは SQL コンソールで実行することもできます。
ClickHouse Managed Postgres サービスを作成する
ClickHouse Cloud コンソールで New service をクリックし、Postgres を選択します。インスタンスは数分で利用可能になります。詳しい手順については、クイックスタートを参照してください。接続情報を取得する
サービスの左サイドバーで Connect をクリックします。表示されるモーダルにはユーザー名、パスワード、サーバー、ポートが表示され、Directly / via PgBouncer を切り替えるトグルもあります。 このガイドでは、同梱の PgBouncer を経由し、ポート6432 で Django を接続します。本番環境の Django アプリケーションは通常、複数のマシン上で複数の worker プロセスを実行し、各プロセスがそれぞれ接続を開きます。PgBouncer はトランザクションプーリングモードで動作し、これらすべてのプロセスが少数の Postgres 接続を共有できるようにします。トランザクションプーリングを利用するには Django 側でいくつかの設定が必要です。これらの設定はこのガイドに含まれており、詳細は Django 向けの PgBouncer 設定で説明します。
via PgBouncer を選択すると、プーリング用の接続情報が表示されます。ポートが 6432 に変わり、接続文字列に sslmode=verify-full が含まれるようになります。
モーダルで Download CA certificate をクリックします。Settings → CA Certificate からもダウンロードできます。この証明書はインスタンスごとに固有のものであるため、ドライバーはこれを使って接続先が自分のサーバーであることを検証できます。
プロジェクトをセットアップする
仮想環境付きのプロジェクトディレクトリを作成し、Django と psycopg 3 をインストールしてから、todos アプリを含む Django プロジェクトを作成します。
psycopg[binary] には最新の libpq が同梱されているため、ローカルに PostgreSQL をインストールする必要はありません。
接続を設定する
ダウンロードした CA 証明書をプロジェクトディレクトリ内のmanage.py と同じ階層に移動し、ファイル名を ca-certificate.pem にリネームします。
アプリ用のデータベースを作成します。<PASSWORD> とホストは、Connect モーダルに表示される値に置き換えてください。
mysite/settings.py を開き、from pathlib import Path の下に import os を追加したうえで、DATABASES 設定を次の内容に置き換えます。
mysite/settings.py
OPTIONS 内のオプションを psycopg に渡し、psycopg はそれらを libpq に引き渡します。sslmode を verify-full に設定すると、libpq はサーバー証明書がサービスの CA によって署名されているか、またホスト名と一致するかを検証します。BASE_DIR を使うと証明書のパスが絶対パスになるため、Django をどのディレクトリから起動してもこの設定は機能します。
sslrootcert が誤った CA を指している場合、接続は SSL error: certificate verify failed で失敗します。sslrootcert を指定しない場合は、libpq がデフォルトの CA の場所にフォールバックするため、root certificate file ".../.postgresql/root.crt" does not exist で失敗します。ホスト名ではなく IP アドレスで接続した場合は、server certificate for "..." does not match host name で失敗します。モデルを作成して移行を実行する
mysite/settings.py の INSTALLED_APPS に todos アプリを追加します。
mysite/settings.py
todos/models.py に Todo モデルを定義します。
todos/models.py
migrate は todos_todo に加えて、auth_user や django_session など Django の組み込みアプリ用のテーブルも作成し、適用済みの移行を django_migrations に記録します。既存のアプリの場合は todos モデルの作成を省略し、python manage.py migrate だけを実行してください。これで、アプリのすべてのテーブルが新しいデータベースに作成されます。
migrate は session レベルのアドバイザリロックを取得しないため、PgBouncer 経由でも動作します。ただし、同時実行を防ぐ仕組みもないため、移行はデプロイ時の 1 ステップなど、1 か所からのみ実行するようにしてください。
JSON エンドポイントを追加する
todos/views.py の内容を、Django ORM で todo の作成・一覧取得・更新・削除を行う 2 つのビューに置き換えます。
todos/views.py
mysite/urls.py を次のように置き換えて、/todos と /todos/<id> をそれぞれのビューにルーティングします。
mysite/urls.py
実行と確認
開発サーバーを起動します。PATCH リクエストを実行すると、次のように更新された行が返されます。
http://localhost:3111/todos を開くと、残りの todo を一覧表示できます。レスポンスは次のようになります:
guide_django、public の順に展開して、todos_todo テーブルをクリックします。テーブルには、残った行が表示されます。
Django 向けの PgBouncer 設定
PgBouncer はトランザクションプーリングモードで動作します。つまり、各トランザクション (またはトランザクション外の各ステートメント) は、それぞれ異なる Postgres 接続上で実行される可能性があります。これが上記の設定やその他の Django の機能にどう影響するかを以下に説明します。DISABLE_SERVER_SIDE_CURSORS:QuerySet.iteratorは、サーバー側カーソルを介して行を chunk 単位で読み込みます。トランザクション外では、Django はカーソルをWITH HOLDとして宣言し、各 chunk を個別にフェッチします。そのため PgBouncer 経由では、フェッチがそのカーソルを持たない Postgres 接続に割り当てられ、cursor "_django_curs_..." does not existというエラーで失敗することがあります。この問題は他のクライアントが同時に pool を使用している場合にのみ発生するため、開発環境では表面化しないことがよくあります。この設定を有効にすると、.iterator()は通常のカーソルを使用し、psycopg は結果全体をメモリに読み込みます。非常に大きなテーブルをストリーミングする場合は、この設定を無効にしたうえで、ポート5432の直接接続を使ってそのコードを実行してください。CONN_MAX_AGE: デフォルト (0) では、Django はリクエストごとに新しい接続を開きます。TLS 接続の確立には、PgBouncer 経由でも直接接続でも複数回のネットワークラウンドトリップが必要なため、接続を再利用するとリクエストが大幅に高速化されます。ノート PC から 4 つの Gunicorn worker を使用して行ったテストでは、CONN_MAX_AGEを600に設定すると、リクエスト時間の中央値が約 670 ms から 95 ms に短縮されました。PgBouncer 経由の場合、開いたままの Django の接続はトランザクション間で Postgres 接続を保持しないため、永続接続によってmax_connectionsを使い切ることはありません。CONN_HEALTH_CHECKSを有効にすると、Django は各リクエストの開始時に再利用する接続をチェックし、切断されていれば再接続します。- 接続プール: Django の組み込みプール (
OPTIONS内の"pool": True。pip install "psycopg[binary,pool]"が必要) でも同程度の高速化が得られ、PgBouncer 経由でも動作します。ただしCONN_MAX_AGEを0に設定する必要があり、プロセスごとに最低 4 つの接続を開いたままにします。PgBouncer を使用する場合、ほとんどのアプリケーションではCONN_MAX_AGEで十分です。スレッド型サーバーや ASGI サーバーではプールの利用を検討してください。 - プリペアドステートメント: 設定は不要です。Django はデフォルトで psycopg の
prepare_thresholdをNoneに設定し、クエリパラメータをクライアント側でバインドするため、サーバー側のプリペアドステートメントは作成されません。OPTIONSで"server_side_binding": Trueを有効にして"prepare_threshold"を設定した場合でも、プリペアドステートメントは PgBouncer 経由で動作します。これは、psycopg[binary]にlibpq18 が同梱されており、プロトコルレベルのCloseメッセージでステートメントを解放するためです。libpq16 以前に対してビルドされた psycopg では、トランザクション内でステートメントを解放するとprepared statement "_pg3_0" does not existというエラーで失敗します。バージョンを確認するには、python -c "import psycopg; print(psycopg.pq.version())"を実行してください。170000以降が必要です。 - セッション状態:
SET search_pathなどのSETステートメントは、それを実行した Postgres 接続にのみ適用されます。そのため、後続のクエリには反映されない可能性がある一方で、他のクライアントに影響する可能性があります。PgBouncer は Django の"options": "-c search_path=..."設定をunsupported startup parameter in options: search_pathというエラーで拒否します。代わりに、ALTER DATABASE guide_django SET search_path = myschema, public;のように、データベース側でデフォルトを設定してください。SET ROLEを実行するassume_roleオプションは使用しないでください。Django 自体のTIME_ZONEの処理とisolation_levelオプションは、PgBouncer 経由でも問題なく動作します。
PORT を 5432 に設定し、DISABLE_SERVER_SIDE_CURSORS を削除してください。この場合、各 Django worker がそれぞれ独自の Postgres 接続を保持するため、worker の総数が max_connections を超えないようにしてください。