🎮

ゲームのサーバを作ろう with Django REST Framework

2021/07/09に公開

この記事の目的

このスライドは、ゲームとかサーバサイドのプログラムを作ったことがない人(主に学生)に向けて、実践を通じてゲームのサーバサイドのエンジニアがどんなお仕事をするのかを体験してもらうための資料です。コレ1つでバックエンドエンジニアになれるわけではないですが、なるために必要なエッセンスをできるだけ入れていこうと思います。

今回はゲームにおけるランキング機能をサーバとして用意するという目標で、サーバサイドでの設計や実装を体験してもらいます。

そもそもゲームのサーバってなんなん?

まずは、ゲームにおけるサーバってなにするものなのか?という話をします。そもそもゲームにおいてサーバは昔からあったわけではありません。私が少年時代にハマっていたファミコンは、エンジと白の本体に、カセットを挿してゲームを遊んだものです。そのときになにかと通信をすることなんてありませんでしたし、そもそもインターネットなんてものが存在しませんでした。しかし、ゲームはそれで面白かったし、何の不満もありませんでした。

さて、ゲーム業界においてサーバが生まれた背景としては

  • インターネットとそれにつながる携帯電話が普及した
  • ソーシャルゲームの誕生

があると思います。まず、インターネットと、携帯電話の普及により誰かとつながっていることに価値を見出す人が増えました。このため、ゲームが単に個人で楽しむだけに終わるものでなく、ネットワーク上にいる誰かとつながり、助け合ったり競い合ったりすることが価値となっていきます。

この現象と並行して、ソーシャルゲームが誕生します。日本のソーシャルゲームの主舞台となった携帯電話(いわゆるガラケー)では端末側のスペック(CPU、メモリ、ストレージなど)が非力だったことも有り、端末の機能だけで商用に堪えうるゲーム体験の提供が困難なため、ユーザ同士のメッセージのやり取りだけではなく、追加コンテンツの提供やある程度の計算が必要な処理をサーバサイドで提供する仕組みが作られることになります。

このように、ゲーム業界におけるサーバ(バックエンド)では

  • ユーザ同士の交流のための機能
  • 追加コンテンツの配信
  • (必要に応じて)ガチャやダメージ計算などゲームロジックの提供

が、行われています。

さて、オンラインでつながることを前提にしたゲームにおいて、ゲーム機側とサーバ側でどのようなやりとりをするかは要件により千差万別です。今回は初心者向けの体験ということもあり、サーバを比較的簡単に実装することを重視して、ゲームロジックはゲーム機側に置くことを前提にしてランキング機能を実装することにします。

前提条件

今回の講義ではサーバ作成に注力するため、次の前提を置きます。

  • pythonでプログラムがかけること
  • Linuxでシェルが使えること (Linuxで動かすことを前提にします。Windowsマシンでやりたい人はWSL2を使ってUbuntu Linuxを入れてみてください。)
  • Visual Studio Codeを入れる (無料のエディタとして極めて優秀です)
    • Python、Django extensionあたりは入れておきましょう。

ゲームサーバを立ち上げるまで

Python、poetryのインストール

まず、Django REST Frameworkを動かすために必要なPythonをインストールします。Pythonは直にパッケージマネージャ(apt)等で入れてもよいですが、pyenvというバージョン管理ツールを通じて入れます。pyenvを介すことで、

  • 任意のバージョンを選択してインストール可能
  • プロジェクトごとにバージョンを切り替えて運用可能

といったメリットがあるのでpyenv経由で入れることをオススメします。

pyenvはHomeBrew経由で入れる方法が簡単です。

$ brew install pyenv

pyenvコマンドを使うためには、シェルにいくつかの環境変数の設定が必要になります。

https://github.com/pyenv/pyenv#basic-github-checkout

を参考に~/.bashrc等々に設定を保存してシェルを再起動します。

pyenvをインストールしたら、必要なPythonバージョンをインストールします。今回はPython 3.7.9をインストールします。

$ pyenv install 3.7.9

pythonをインストールしたら、pythonをコマンドで動かせるようにします。設定にはlocalとglobalがあり、このDjangoプロジェクト限定で使う場合は、プロジェクトのディレクトリで

$ pyenv local 3.7.9

と入力すると、.python-versionというファイルが作られ、以降、このプロジェクトでは3.7.9のバージョンのpythonが実行されることになります。

Django REST Frameworkのインストール

今回、pythonのパッケージ管理ツールとしてpoetryを使います。poetryはpip経由でインストールできます。


$ pip install poetry

pyenvを使った環境の場合、poetryへのpathが通らずにpoetryコマンドがエラーになってしまうので、初回はpyenv rehashを実行します。

$ pyenv rehash

$ poetry init

色々聞かれますが、てきとうにEnterを押しておけばよいです。

$ poetry add django
$ poetry add djangorestframework

Django プロジェクトおよびアプリの作成

$ poetry run django-admin startproject practice
$ cd practice
$ poetry run python manage.py startapp ranking

実行すると、次のようなファイルが出来上がるはずです。

├── poetry.lock
├── practice
│   ├── manage.py
│   ├── practice
│   │   ├── __init__.py
│   │   ├── asgi.py
│   │   ├── settings.py
│   │   ├── urls.py
│   │   └── wsgi.py
│   └── ranking
│       ├── __init__.py
│       ├── admin.py
│       ├── apps.py
│       ├── migrations
│       │   └── __init__.py
│       ├── models.py
│       ├── tests.py
│       └── views.py
└── pyproject.toml

設計

インストールが終わったら、いよいよランキングサーバを実装していきます。しかし、これはバックエンドに限らずですが、コードを書き始める前にきちんと設計をすることが大切です。

要件の整理

設計の前に、まずランキングサーバに求められる要件を整理します。今回はサンプルとして

ランキングサーバに必要な操作

  • ランキングとして使うスコアを登録する
  • ランキングを表示するためのデータを取得する

ランキングに保存するデータ

  • ユーザ名
  • スコア(整数値)
  • 達成日時

この様な要件を満たすサーバを設計します。サーバにおける設計とは

  • 保存するデータをSQLで扱えるテーブルを考える
  • データを操作、参照するための手続き(API)を考える

ということに置き換えることができます。では、まずどの様なテーブルを作ればよいか考えてみてください。

テーブル設計例1

ランキングを管理するデータとしてこんな感じの1つのテーブルで表現するというのはどうでしょうか。

name score created_at
aaa 100 2021/07/01 xx:yy:zz
bbb 120 2021/07/01 xx:yy:zz
aaa 140 2021/07/02 xx:yy:zz
abc 110 2021/07/02 xx:yy:zz

先の要件で出されたものを直接テーブルに置き換えたもので、確かに要件は満たしていそうです。しかし、少し想像力を働かすと、このテーブル設計には問題があることがわかります。何でしょうか?

解答例1の問題

前章で出された要件だけであれば、例1のテーブルでも不都合は生じないかもしれません。しかし、その後、

ユーザにメールアドレスの項目を付けてダイレクトメールを打ちたい

という要件が増えたらどうなるでしょうか?はいはいーと新人くんは二つ返事で先のテーブルにemailカラムを追加しました。

name score created_at email
aaa 100 2021/07/01 xx:yy:zz aaa@hoge.co.jp
bbb 120 2021/07/01 xx:yy:zz bbb@foo.com
aaa 140 2021/07/02 xx:yy:zz abc@hoge.co.jp
abc 110 2021/07/02 xx:yy:zz abc@abc.net

これでよし、、っておかしなことになりましたね。aaaというユーザのスコアは2つ存在していますが、片方の行には

aaa@hoge.co.jp

もう片方には

abc@hoge.co.jp

となっています。こうなってしまうとどちらが正しいアドレスかわかりません。いや、もしかするとaaaという名前の人は実は2人存在するかもしれません。しかし、このテーブルからは何が真実かわかりません。これをバックエンドの世界では 整合性が失われた といいます。

もう一つ要件を加えて

ユーザ名を変更可能にしたい

ということになったらどうなるでしょうか。さきほどの例題のテーブルではaaaさんのスコアは2つ存在しました。ではaaaさんがabbさんに名前を変更する場合、

  • score=100の行のnameをabbに変える
  • score=140の行のnameをabbに変える
  • score=100,140の行のnameをabbに変える

という3つの解が存在します。しかし、これもどれを選択すればよいか、テーブルの情報では判断することができません。

設計の改善

解答例1における問題の本質は

  • ユーザという存在が、名前だけで表されていて一意性がない
  • ユーザとスコアという別の存在を1つのテーブルに載せてしまった

ことにあります。つまり、ユーザとスコアを別々のテーブルに切り離し、それぞれに一意性をもたせることが必要となります。

改善すると、このようなテーブル設計になります。

Users

id name
1 aaa
2 bbb
3 abc

Scores

id user_id score created_at
1 1 100 2021/07/01 xx:yy:zz
2 2 120 2021/07/01 xx:yy:zz
3 1 140 2021/07/02 xx:yy:zz
4 3 110 2021/07/02 xx:yy:zz

ここで、id という新たなカラムが生まれましたが、これはユーザやスコアに対して一意性をもたせるために存在するカラムで、主キー(Primary Key)と呼ばれます。また、Scoresテーブルにuser_idというカラムを付けて、Usersテーブルのidと関連づけています。user_idのようなカラムを外部キー(Foreign Key)と呼びます。

図で表すとこのようになります。この図のことをER図(Entity Relationship Diagram)と呼びます。

ER図

APIを設計する

テーブル設計が終わったら、次はテーブルを操作するAPIの設計を行います。APIの設計方針やsyntaxには様々ありますが、今回は表題にもあるREST(Representational State Transfer)を使って行いたいと思います。

RESTとは違ったAPIの例としては

  • RPCといわれる手続き呼び出し型のAPI
    • gRPC、JSON-RPC、SOAPなど
  • GraphQL(https://graphql.org/)

があります。

RESTとは

RESTはHTTPプロトコル作成者の一人であるRoy Fieldingによって2000年ごろに提案された設計原則で、次の4つの原則があります。

  • アドレス可能性
    アドレス指定可能なURIで公開されており全ての情報が一意なURIで表現されていること
  • ステートレス性
    すべてのHTTPリクエストが完全に分離している性質であること。
  • 接続性
    ある情報に「別の情報へのリンク」を含めることができること。そして、リンクを含めることで「別の情報に接続すること」ができる。
  • 統一インターフェース
    リソースに対して操作するためのメソッドはHTTPで定義されているGET、POST、PUT、DELETE、HEAD、OPTIONS、TRACEのみを使用する。

RESTに基づくAPIの作成

RESTではサーバが扱うリソースを

http://localhost:8000/users/{id}

といったようにリソース名、および主キーで表現することが一般的です。これにより、REST原則のアドレス可能性、ステートレス性を満たします。また、リソースに対する操作を

  • GET: リソースを取得
  • POST: リソースを作成
  • PUT、PATCH: リソースの更新、差分更新
  • DELETE: リソースの削除

で定義することで統一したインタフェースで扱うようにします。

最後の接続性はレスポンスに対して、関連する情報に対してのリンクを用意することで実現します。リンクについては、直接的にURLを示す方法もありますし、主キーを返す方法など様々です。

drfを使う場合、(https://www.django-rest-framework.org/api-guide/relations/) のような関連テーブルを出力するクラスが用意されています。

RESTに基づくAPI設計例

今回のランキングサーバの例では、テーブル設計に基づき、

API pattern 操作
GET /users/ ユーザ一覧取得
POST /users/ ユーザ作成
GET /users/{id}/ ユーザ個別取得
PUT /users/{id}/ ユーザ更新
DELETE /users/{id}/ ユーザ削除
GET /scores/ スコア一覧取得
POST /scores/ スコア作成
GET /scores/{id}/ スコア個別取得
PUT /scores/{id}/ スコア更新
DELETE /scores/{id}/ スコア削除

といった手続きにまとめるのが自然かと思います。

Django REST Frameworkを使ったModel、APIの実装

RESTに基づいたAPI実装はDjango REST Framework(通称drf)を使うと簡単に作成できます。pythonでREST APIをつくるならまずdrfからtryしましょう。

drfを使う

まず、drfを使うための設定をsettings.pyに追加しましょう。また、これから実装するアプリもINSTALLED_APPSに加えておきます。

practice/practice/settings.py
INSTALLED_APPS = [
    'ranking.apps.RankingConfig',
    'django.contrib.admin',
    'django.contrib.auth',
    'django.contrib.contenttypes',
    'django.contrib.sessions',
    'django.contrib.messages',
    'django.contrib.staticfiles',
    'rest_framework'
]

modelの実装

次にmodelを実装します。djangoにおけるモデルはSQLのテーブルに相当する概念です。設計に基づいてdjango.db.models.Modelクラスを継承したmodelクラスを実装します。

practice/ranking/models.py
from django.db import models
from django.utils import timezone

# Create your models here.

class RankingUser(models.Model):
    name = models.CharField(null=False, max_length=256)


class Score(models.Model):
    score = models.PositiveIntegerField()
    user = models.ForeignKey(RankingUser, on_delete=models.CASCADE)
    created_at = models.DateTimeField(null=False, default=timezone.now)

※ クラスの定義に設計で用意した主キーが存在しませんが、Djangoでは何も書かなくても、idという主キーが自動で作成されます。

serializerの実装

次にserializerを実装します。serializerは、APIの応答の書式(スキーマ)を定義する概念です。drfにはmodelの情報を自動で整形してJSONとして出力するModelSerializerというクラスが用意されています。ModelSerializerではMetaクラスを内部に持つことで出力するモデルを記述します。fieldsにはJSONとして出力したいmodelのfield(カラム)を列挙します。__all__は特別なキーワードで、modelのカラムを全て出力することを表します。

practice/ranking/serializers.py
from rest_framework import serializers
from ranking.models import Score, RankingUser

class UserSerializer(serializers.ModelSerializer):
    class Meta:
        model = RankingUser
        fields = '__all__'

class ScoreSerializer(serializers.ModelSerializer):
    class Meta:
        model = Score
        fields = '__all__'

view(API)の実装

次にview(API)を実装します。これもdrfではModelViewSetというクラスが用意されていて、これを使うとmodel、serializerを使って自動で先程設計したGET、POST、PUT、PATCH、DELETEが作れますのでほぼノーコードで実現できます。

practice/ranking/views.py
from rest_framework import viewsets
from ranking.models import Score, RankingUser
from ranking.serializers import ScoreSerializer, UserSerializer

# Create your views here.

class UserViewSet(viewsets.ModelViewSet):
    """API endpoint for users"""
    queryset = RankingUser.objects.all()
    serializer_class = UserSerializer

class ScoreViewSet(viewsets.ModelViewSet):
    """API endpoint for scores"""
    queryset = Score.objects.all()
    serializer_class = ScoreSerializer

view(API)をURLにルーティングする

viewをWebサーバからアクセスできるようにするには、routerにviewを登録して、DjangoのURL設定に追加する必要が有ります。Routerには色々種類が有りますが、今回はデバッグに便利なDefaultRouterを利用しています。DjangoのURL設定はurls.pyになります。

practice/practice/urls.py
from django.contrib import admin
from django.urls import include,path
from rest_framework import routers
from ranking.views import UserViewSet, ScoreViewSet

router = routers.DefaultRouter()

# users/*にRankingUser系のAPIを追加
router.register(r'users', UserViewSet)
# scores/*にScore系のAPIを追加
router.register(r'scores', ScoreViewSet)

urlpatterns = [
    path('', include(router.urls)),
    path('admin/', admin.site.urls),
]

サーバを起動する

コードが実装できたら実際にサーバを起動して動作確認してみましょう。

テーブルを作成する

前章で実装したサーバを動かすには、まずデータベース上にmodelに対応したテーブルを作成する必要があります。このためにはcreate table XXXといったSQL文を実行する必要が有りますが、これらはDjangoのmigrationという機能で自動化されます。

まずは、djangoの機能を使ってmigrationを作成します。

$ poetry run python manage.py makemigrations

何気ない1文ですが、内部では非常に高度な処理が行われていて、アプリケーション内のmodel定義とデータベースを比較して、model定義に沿った差分となるSQL文(migration)を生成します。今回はテーブルが存在しないので、modelで定義されたカラムを持つテーブルを作成するSQL文が自動生成されます。

migrationを作成したら、migrationを実行します。

$ poetry run python manage.py migrate

これで、migrationディレクトリ内にあるmigrationが実行され、テーブルが作成されます。また、実行したmigrationは特別なテーブルに記録されていて、同じmigrationが複数回起動しないようになっています。

開発サーバの起動

テーブルを作成できたら、いよいよサーバを起動できます。

$ poetry run python manage.py runserver 0:5000

を実行して、サーバを起動してみましょう。ポート番号5000で待ち受けします。(ポート番号が埋まっている場合はパラメータを変えてください)

こんな画面が立ち上がった方、おめでとうございます👏ランキングサーバ起動完了です。

この画面はDefaultRouterが作成している開発用の画面です。ブラウザをポチポチしていくだけで、開発サーバの動作確認ができるスグレモノなので試してみましょう。

サーバの検証をする

ユーザを作成する

まず、ユーザの作成を試してみましょう。

http://localhost:5000/users/

にアクセスしてUsers APIを試します。

下の方にフォームがありますので、適当に名前を入れて『POST』を押します。(Raw Dataというタブで、JSON形式、HTML formというタブではHTML form形式でデータを投入できます。)

このようにユーザが表示されていれば成功です。

http://localhost:5000/users/1/

にアクセスすると、個別表示および編集、削除の検証が実施できます。

スコアを作成する

ユーザが作成できたら次はスコアを作成してみましょう。

http://localhost:5000/scores/

にアクセスして、ユーザと同様にFormにscore、created_at、userを入れてPOSTしてみましょう。

はい、こちらもちゃんとできましたね。

では、存在しないユーザIDを入れてみたらどうなるでしょうか。試しに

{
    "score": 10000,
    "user": 4
}

として、POSTしてみましょう。この場合は、

HTTP 400 Bad Request
Allow: GET, POST, HEAD, OPTIONS
Content-Type: application/json
Vary: Accept

{
    "user": [
        "Invalid pk \"4\" - object does not exist."
    ]
}

となり、エラーとして返却されることが確認できます。

ランキングサーバとしての機能追加

以上で、ランキングサーバとしての最小の機能は実装できましたが、少し物足りない部分もありますね。ゲーム機側の立場に立つと、たとえば

  • scores APIの列挙順序がID順になってるけど、ランキングというくらいだからスコア順にならんでいてほしい。。
  • やっぱり、scores APIからユーザ名も返ってきてほしい(今の仕様だとランキングを表示させるのにscoresとusersを両方叩く必要がある)

といった要望が出るでしょう。ではトライしてみましょう。

列挙順の変更

まず、列挙順の変更をしてみます。この対応には2種類の対応方法があります。

  • viewsetのquerysetにorder_byを追加する
  • OrderingFilter を導入してクエリパラメータに並べ替えたいフィールドを入れてもらう

今回はquerysetを変更して、簡便に列挙順を変更してみましょう。

practice/ranking/views.py

class ScoreViewSet(viewsets.ModelViewSet):
    """API endpoint for scores"""
    queryset = Score.objects.order_by('-score')
    serializer_class = ScoreSerializer

最初の実装でScore.objects.all()となっていたquerysetをScore.objects.order_by('-score')に変えるだけです。たったこれだけでさっきまでID順になっていたスコアが、スコア順に並び変わったと思います。ちなみに、order_byの先頭に入っているマイナス記号で降順を表しています。

score APIからユーザ名を返すようにする

こちらのほうがちょっとだけ難易度が高いです。やるべき操作を噛み砕いて説明すると

ScoreSerializerに読み取り専用のフィールドを作って、Scoreに紐付いたUserのnameフィールドを返す

となります。関連するモデルのフィールドを返すには、serializersのフィールドのsourceパラメータを使うことで実現できます。この場合、fieldのread_onlyパラメータは必ずTrueにして読み取り専用とする必要があります。

practice/ranking/serializers.py
class ScoreSerializer(serializers.ModelSerializer):
    username = serializers.CharField(read_only=True, source='user.name')
    class Meta:
        model = Score
        fields = ('id', 'score', 'created_at', 'user', 'username')

usernameという新たなフィールドを追加し、sourceにはuser.nameを指定しました。フィールドを新たに追加したため、__all__は使えなくなり、フィールドを列挙する必要があります。

これでscores APIを叩くと、usernameという新たなフィールドができて、そこにはユーザ名が返ってきているかと思います。

クエリの最適化

前スライドでSerializerを改良して関連テーブルの内容を取ってきましたが、このままではパフォーマンス上良くない点があります。これを確認するために、ログ出力にSQLを出力できるようにします。

practice/practice/settings.py
LOGGING = {
    'version': 1,
    'disable_existing_loggers': False,
    'handlers': {
        'console': {
            'level': 'DEBUG',
            'class': 'logging.StreamHandler',
        },
    },
    'loggers': {
        'django.db.backends': {
            'handlers': ['console'],
            'level': 'DEBUG',
        },
    },
}

この設定でdb.backendsからのSQL文を含むデバッグ出力が有効化されます。この状態で、http://localhost:5000/scores/にアクセスしてみてください。

Django version 3.2.5, using settings 'practice.settings'
Starting development server at http://0:5000/
Quit the server with CONTROL-C.
(0.000) SELECT "ranking_score"."id", "ranking_score"."score", "ranking_score"."user_id", "ranking_score"."created_at" FROM "ranking_score" ORDER BY "ranking_score"."score" DESC; args=()
(0.000) SELECT "ranking_rankinguser"."id", "ranking_rankinguser"."name" FROM "ranking_rankinguser" WHERE "ranking_rankinguser"."id" = 2 LIMIT 21; args=(2,)
(0.000) SELECT "ranking_rankinguser"."id", "ranking_rankinguser"."name" FROM "ranking_rankinguser" WHERE "ranking_rankinguser"."id" = 1 LIMIT 21; args=(1,)
(0.000) SELECT "ranking_rankinguser"."id", "ranking_rankinguser"."name" FROM "ranking_rankinguser" WHERE "ranking_rankinguser"."id" = 1 LIMIT 21; args=(1,)
(0.000) SELECT "ranking_rankinguser"."id", "ranking_rankinguser"."name" FROM "ranking_rankinguser" LIMIT 1000; args=()
[06/Jul/2021 00:17:28] "GET /scores/ HTTP/1.1" 200 9879

このログはスコアに3つ入った状態でアクセスしましたが、データベースのクエリが5回実行されていることがわかります。これは、

  • scoreを持ってくるために1回
  • usernameを持ってくるために3回
  • userを持ってくるために1回

クエリを実行しています。もしスコアが1000個あったらクエリは1002回になってしまい、レスポンスを返すのに非常に時間がかかってしまいます。これをバックエンドの世界ではN+1問題と呼びます。

この問題を解消するには、scoreを持ってくる最初のクエリで、userテーブルの情報も一緒に持ってくることができれば解消します。RDBではjoinが使えるはずです。この手法をeager loadingと呼びます。

Djangoでeager loadingを実行するには、querysetにselect_relatedを挿入します。

practice/ranking/views.py
class ScoreViewSet(viewsets.ModelViewSet):
    """API endpoint for scores"""
    queryset = Score.objects.select_related('user').order_by('-score')
#    queryset = Score.objects.order_by('-score')
    serializer_class = ScoreSerializer

再びhttp://localhost:5000/scores/にアクセスします。

(0.001) SELECT "ranking_score"."id", "ranking_score"."score", "ranking_score"."user_id", "ranking_score"."created_at", "ranking_rankinguser"."id", "ranking_rankinguser"."name" FROM "ranking_score" INNER JOIN "ranking_rankinguser" ON ("ranking_score"."user_id" = "ranking_rankinguser"."id") ORDER BY "ranking_score"."score" DESC; args=()
(0.000) SELECT "ranking_rankinguser"."id", "ranking_rankinguser"."name" FROM "ranking_rankinguser" LIMIT 1000; args=()

前のコードで呼ばれていた3回のselectがなくなって、scoreを持ってくるときにINNER JOINでuserテーブルを持ってきていることが確認できました。

[06/Jul/2021 00:53:55] "GET /scores/?format=json HTTP/1.1" 200 271
(0.001) SELECT "ranking_score"."id", "ranking_score"."score", "ranking_score"."user_id", "ranking_score"."created_at", "ranking_rankinguser"."id", "ranking_rankinguser"."name" FROM "ranking_score" INNER JOIN "ranking_rankinguser" ON ("ranking_score"."user_id" = "ranking_rankinguser"."id") ORDER BY "ranking_score"."score" DESC; args=()
[06/Jul/2021 00:58:01] "GET /scores/?format=json HTTP/1.1" 200 271

まとめ

今回の演習では

  • Python、drfのセットアップ
  • テーブル(ER)の設計
  • APIの設計、RESTの理解
  • drfを使ったmodel、view、serializerの実装
  • サーバの起動と検証
  • 機能追加への対応
  • クエリの最適化

を実践しました。内容がもりだくさんでしたが、drfでは非常にシンプルにやりたいことが実装、検証までできることが実感できたのではないでしょうか。

演習のソースコードは
https://github.com/adacotech/adacotech-game-practice-server

こちらに用意しています。ブランチは

  • 00-new Djangoのappを作った時点
  • 01-app model、view、serializerを実装した時点
  • 02-migration migrationファイルを作成した時点
  • 03-score-order-and-serialize-name, master 本演習完了時点

を指しています。

宿題

今回は学習目的でランキングサーバを実装しましたが、もし実際に運用してみるとしたら(主にセキュリティ面で)足りていない部分がたくさんあります。実際にネット上で運用するとどのような不都合が起きるか考えてみてください。

解答例
  • ユーザIDの検証がないため、他人になりすましてスコアを送信できてしまう (認証機能が必要)
  • スコアに対して確認をしていないため、どんなスコアでも送信できてチートし放題 (リクエストの妥当性確認が必要)
  • ユーザを無尽蔵に作成できるため、不要なユーザで埋め尽くされる。(bot対策、サインアップが必要)
  • ユーザ名にSPAMを埋め込まれる (ユーザ名の検証が必要)

Discussion