Djangoで作る新規Webサービス

新規Webサービス開発プロジェクト振り返りメモ

Photo by rawpixel on Unsplash

いつも開発するシステムは、データ量が多かったり、検索要件も複雑なシステムが多く、必ずバックエンドのElasticsearch(検索エンジン)にお世話になっています。

しかーし、今回は、久しぶりに検索エンジンを使わないシンプルな3-Tier ArchitectureのWebアプリケーションを開発。特に変わったことはしていませんが、振り返りも兼ねて、気をつけたことや、ポイントなどメモしておきます。参考になれば幸いです。

1.管理サイトファースト

今回の開発は、新規サービスのため、システムで使用するコンテンツも、どこかのシステムでマスターデータが管理されているわけではありません。設計と開発、コンテンツの情報収集も同時進行で行われます。そのため、公開サイトのデザインとデータモデルのコンセプトから、データモデル設計・開発と同時に管理サイトを開発するのが最初のステップです。

管理サイトを最初にリリースすることで、本物のデータをプロジェクトの早い段階で登録することができます。その後、公開サイトの開発が徐々に進んでいく過程で、公開サイトのデザインを本物のデータを使って確認できるメリットは大きいと考えています。

開発するメンバーは、データ構造と画面の関連も理解しているので、テスト用のダミーデータを使って開発を進めることができます。データ同士の関連とフィールドの型、フォーマット、文字列の長さや入力制限が主な関心ごとです。

一方で、開発以外のメンバーは、画面のデザインと表示される内容そのものに関心があります。そのためダミーデータでは興味を持ってもらえません。興味を持ってもらえないと言うことは、「思ってたのと違う!」と言うことがプロジェクトの後のフェーズに先送りされます。

管理サイトを先行リリースすることで、開発以外のメンバーもプロジェクトの早い段階で興味を持ってもらえ、後戻りを最小限に抑えることができる。

これが、「管理サイトファースト」の目的です。

Djangoで管理サイト開発

「管理サイトファースト」の目的を達成するため、Djangoはとても良い選択肢のひとつです。デザイナーいらずで、モデルクラスを元に数行で完璧な管理サイトを公開する事ができます。

カスタマイズしづらいのでは?と思うかもしれませんが、管理サイト自体がクラス化されていて、あらかじめ用意されているオプションの他、カスタマイズしたければクラスを継承して機能を上書きすれば良いため、かなり柔軟にカスタマイズする事ができます。

管理サイトのデザインを気にせずにロジックにフォーカスできるのも開発者にとっては魅力的です。

そのほか、既存のシステムでも、テーブルをリバースしてモデルを作成し、プロキシモデルとして定義すれば、簡単・安全に管理サイトだけをDjangoで提供することもできます。

また、Djangoには、データベースのルーティング機能も定義できるので、全く異なるRDBのデータ管理を1つの管理サイトで提供することだってできる優れものです。

2.データ識別IDの設計

データの識別IDは、シンプルにSlugを採用しました。Slugは「ヒューマンリーダブルID」という理解です。半角の英数字とハイフンで構成します。

例えば、山田太郎さんのプロフィールページのSlugは、taro-yamada のように登録します。人がIDを見てどんな情報なのか想像できるIDです。

データモデルのコンセプトから想定されるURLパターンを全て定義し、Slugが必要なテーブルはどれか?を洗い出します。正規化されたテーブル全てにSlugを定義してしまうと、データを管理する人の運用負荷も大きくなってしまいますし、API開発を想定した場合も、いたずらにCURD用のAPIが増えてしまう事で複雑化します(好みの問題?)。

{lang}/               # Home
{lang}/guides/ # ガイド一覧
{lang}/guides/{slug}/ # ガイド詳細
{lang}/tags/ # タグ一覧
{lang}/tags/{slug}/ # タグ詳細(タグに関連するガイド一覧)

SlugはURLに使用されるのでSEOやサイト分析時のデータの把握など、様々なメリットがあります。

3.データモデルの多言語設計

データモデルの多言語対応もちょっとだけ工夫してます。コードを見てもらった方がイメージしやすいと思うので、見てみましょう。

以下の例はツアーガイドさんの情報を管理するモデルの例です。

from django.utils.translation import activate
from swan.contrib.guides.models import Guide
# ガイドさんの日本語名称を表示したい時
activate(‘ja’)
obj = Guide.objects.get(pk=1)
print(obj.name)
# ガイドさんの英語名称を表示したい時
activate('en')
obj = Guide.objects.get(pk=1)
print(obj.name)

このように、アプリケーションの言語情報を切り替えるだけで、データモデルの言語を切り替えられるように設計しました。

値の更新も同じようにできます。

from django.utils.translation import activate
from swan.contrib.guides.models import Guide
# 日本語の名称を更新
activate('ja')
obj = Guide.objects.get(pk=1)
obj.name = '山田太郎'
obj.save()

多言語対応と言っても、メインのデータだけでなく、カテゴリ名称など関連するマスター情報も多言語対応する必要があります。この問題を解決するために、モデルのオプションで言語切り替えを行うのではなく、アプリケーションの言語情報を元に切り替えられるようにしています。

(予定はないけど、、、)Djangoで作った管理サイトもこのデータモデル設計は便利に機能します(するはず)。例えば、英語の管理サイトでは、関連するカテゴリ名称の選択肢は英語で表示され、日本語の管理サイトであれば、日本語で表示する事が容易に可能です。

4.ビューはとにかくシンプルに

Djangoの場合、ビジネスロジックはモデルまたはモデルのマネージャークライスに実装するのが鉄則です。ただ、画面で必要なデータの収集はモデルの役割ではないので、ビューに書いてしまいがちではないでしょうか?

例えば、ツアーガイドさんのプロフィールページをイメージしてください。プロフィール情報以外に、関連する口コミや、催行している様々なツアー情報、そして同じエリアでガイドをしている他のガイドさんなど、付属の情報がたくさんあります。

Djangoのドキュメントの例を見ると、ビュー内でページに必要な情報を全て集めてページのテンプレートに渡したくなりますが、そこはちょっと待ってください!なるべく、汎用ビューを使ってビューにロジックを書かない方が、画面の仕様変更にも対応しやすくメンテナンスもし易いです。

では、どうやって関連情報を表示すれば良いのか?

カスタムテンプレートタグで部品化する

その答えは、カスタムのテンプレートタグの活用です。

以下の例は、掲載中のツアーガイドさんに関連する「他のガイドさん」を表示する例です。

<div class="related-guides">
{% show_related_guides object size=10 %}
</div>

例えば、「他のガイドさんを表示しない。」という仕様に変わった場合、テンプレートの記述を削除するだけですみます。

また、「アクセス条件に応じて非表示にしたい。」という場合でも、テンプレートに条件を追加するだけです。

画面で必要な情報を収集する処理をビュー側で実装してしまうと、テンプレートでは表示しないように変更しても、ビュー側で他のガイドさん情報を収集する処理が走ってしまうなど、無駄な処理が残ってしまいます。

他にも、テンプレートタグをうまく活用する事で、部品として他のページでも使いまわせたり、ページ内の部分的なキャッシュ機能をうまく使いこなせたりできます。

ビューにロジックを書いたら負けですw

5.パフォーマンスへの考慮

パフォーマンスの考慮では、大きく3つの層で考えています。システム、サーバーサイド、クライアントサイドです。

システムはGoogleにおんぶに抱っこ

今回システムは深くは触れませんが、静的ファイルを置く場所とアプリケーションサーバーのオートスケーリングです。幸い Google Cloud Platform を採用しているため、静的ファイルは Google Cloud Storage へ配置し、アプリケーションは Google AppEngine でホストしているため、それほど手間はかけてません。

サーバーサイドはキャッシュが命

サーバーサイドは主にキャッシュです。キャッシュといってもいろんなレイヤーのキャッシュ方法があったり、範囲や期間、タイミングも重要です。

テンプレートレイヤー

管理サイトへログインしているユーザーが、公開サイトにアクセスしているときは、管理サイトとの行き来がしやすいように、ページヘッダーに管理メニューを表示しています。

そのため、ページ全体をキャッシュしてしまうと、いろいろ複雑なことをしなければならないので、部分的なキャッシュを採用しています。ガイドさんのプロフィールページでは、その関連情報表示箇所がキャッシュの対象です。

キャッシュされるタイミングは、一番最初に誰かが、そのページにアクセスした時です。

また、キャッシュが消えるタイミングは、ページの種類ごとに異なる設計をしています。例えば、一覧ページは24時間、詳細ページ7日間です。リリースしたばかりはアクセスが少ないと想定しているため、キャッシュ効率を考慮して長めの期間にしています。

プロフィール写真などの画像は、圧縮効率の良いjpegへの変換と各種サイズのサムネイルの作成です。最近はCDNで、画像のフォーマット変換やサイズ変換を提供してくれるサービスもありますが、今回は自力で実装しています。

データーレイヤー

RDB以外にも外部のAPIから情報取得することがあるので、期限を設定して、キャッシュしています。

クライアントサイドはツールに頼る

クライアントサイドは、ページのレスポンスやその中で読み込んでいる静的ファイルなど多岐に渡ります。ツールを使って問題箇所を特定して効率的に改善するアプローチです。今回使用したのPageSpeed Insightsです。

パソコン、モバイル毎にページの速度の評価と改善点を提案してくれます。評価スコア90を越えれば早いページスピードという評価です。

ちなみ、今回のサービスの評価スコアは、パソコンは100、モバイル96まであげました。

クライアントにHTTPのレスポンスで正しく状況を伝える

その他クライアントサイドのポイントは、レスポンスコードとヘッダー情報で、ページの状態を正しくクライアントに伝えることです。

静的なコンテンツ

画像やCSSなどの静的コンテンツは、Last modifiedヘッダーやExpiresヘッダーなどを設定して、2度目以降はコンテンツへアクセスせずに、クライアント側のキャッシュを使うように支持します。サーバーへのアクセスはなくなるので、コンテンツの配信負荷を減らし、表示も高速化します。

動的なページ(詳細ページ)

一方、動的ページでは、管理サイトでデータを更新し、すぐに最新のデータでページを確認したい。ということは普通にあることです。そのため、静的ページと同じキャッシュ戦略を採用してしまうと、クライアントのキャッシュ期限に依存してしまうため、キャッシュのコントロールが難しくなります。

そこで、Last ModifiedヘッダーやEtagを設定して、HTTP 304 Not Modified のレスポンスをうまく活用します。この方法では、2度目以降もクライアントはサーバーへアクセスします。サーバーはリクエストされたヘッダー情報を元に 304 のレスポンスを返します。クライアントは304のレスポンスの場合、クライアント側のキャッシュを使用する仕組みです。

コンテンツに変更がない場合は、ヘッダー情報のやりとりだけになるので、サーバーのロジックも最小限にすみます。

Googleボットにも有効

Google ボットなどのいわゆるWebクローラーもこのレスポンスコードにしたがってくれるため、効率よく巡回してくれます。

画像のようにレスポンスのサイズは0 Bとなっているのが確認できます。ヘッダーだけのやりとりになるので、サーバー負荷も減りますね。

動的なページ(一覧ページ/トップページ)

一覧ページやトップページなどの動的ページのキャッシュ戦略は、サーバーサイド側の対応しかしていません。

詳細ページ同様のキャッシュ戦略をとってしまうと、Last ModifideやEtagを何を基準にするのか?表示するデータを元に決めるのであれば、プライマリーキー以外でアクセスしなくてはならないデータにアクセスする必要があるなど、効率的でない気がしているためです。もしかしたら、詳細ページほどのキャッシュコントロールは不要なので、静的ページ同様のキャッシュ戦略の方が、理にかなっているかもしれません。

さいごに

いかがでしたでしょうか?私の得意な技術領域は検索エンジンの活用分野なので、フロントエンド含めた開発はいろんなこと考えないといけないからちゃんと作ると大変だなぁ。と、つくづく思います。Djangoさま様ですね。