Notes

はじめに

  • これまで、HTMLとCSSを使って簡単なWebページを構築する方法、GitとGitHubを使ってコードの変更を追跡し、他の人と共同作業するための方法について説明してきました。また、Pythonプログラミング言語にも慣れ、Webアプリケーションの作成にDjangoを使い始めました。
  • 本日は、SQLモデルとDjangoモデルを使用してデータを効率的に保存し、アクセスする方法について学習します。

SQL

SQL (Structured Query Language) は、データベースの更新とクエリを可能にするプログラミング言語です。

SQL logo

データベース

SQL言語の使用方法に入る前に、データの格納方法について説明します。SQLを使用する場合は、多数のテーブルに格納されているすべてのデータを検索できるリレーショナル・データベースを使用します。これらの各テーブルは、設定した列数と任意の行数で構成されています。

SQLを使用する方法を説明するために、フライトと乗客を管理するために使用される航空会社のWebサイトの例を使用します。次のテーブルでは、複数のフライトを管理しています。各フライトには、出発地 origin、目的地 destination、および所要時間 duration があります。

Flights 0

情報の格納に一般的に使用され、SQLコマンドで簡単に利用できるリレーショナル・データベース管理システムには、次のようなものがあります。

最初の2つのMySQLとPostgreSQLは、通常はWebサイトを実行するサーバーとは別のサーバーで実行される、髙い負荷に耐えられるデータベース管理システムです。一方、SQLiteはすべてのデータを1つのファイルに格納できる軽量のシステムです。このコースでは、Djangoが使用するデフォルト・システムであるSQLiteを使用します。

列の型

Pythonでいくつかの異なる変数型を扱ったように、SQLiteには異なる形式の情報を表すがあります。他の管理システムは異なるデータ型を持っているかもしれませんが、どれもSQLiteとかなり似ています。

  • TEXT:文字列の場合(例:人の名前)
  • NUMERIC:数値データのより一般的な形式(例:日付またはブール値)
  • INTEGER:任意の整数(例:年齢)
  • REAL:任意の実数(例:人の体重)
  • BLOB (Binary Large Object):データベースに格納するその他のバイナリデータ(例:画像)

テーブル

SQLを使用したデータベースへの接続を実際に開始するには、まず新しいテーブルを作成します。新しいテーブルを作成するコマンドは、次のようになります。

CREATE TABLE flights(
    id INTEGER PRIMARY KEY AUTOINCREMENT,
    origin TEXT NOT NULL,
    destination TEXT NOT NULL,
    duration INTEGER NOT NULL
);

上のコマンドでは、flights という名前の新しいテーブルを作成し、このテーブルに4つの列を追加します。

  1. id:テーブル内の各行を一意に識別できる番号を設定すると便利です。ここでは、id が整数であることと、それが主キーであること、つまり一意の識別子であることを指定しました。さらに AUTOINCREMENT を指定しました。これは、テーブルに追加するたびにIDを指定する必要がないことを意味します。IDは自動的に指定されるためです。
  2. origin:ここでは、これがテキストフィールドであることを指定し、NOT NULL を記述することにより、値が必須になります。
  3. destination:ここでも、これがテキストフィールドになるように指定し、NULLを禁止します。
  4. duration:この値もNULLにはできませんが、今回はテキストではなく整数で表されます。

列を作成する際に NOT NULLPRIMARY KEY 制約を見ましたが、他の制約も利用できます。

  • CHECK:行の追加/変更を許可する前に、指定された制約が満たされていなければなりません。
  • DEFAULT:値が指定されていない場合にデフォルト値を提供します。
  • NOT NULL:値が指定されていることが必要です。
  • PRIMARY KEY:データベース内の行を検索する主な方法であることを示します。
  • UNIQUE:その列で同じ値を持つ行があってはいけません。

ここまではテーブルの作成方法について説明しました。次に、テーブルに行を追加する方法について説明します。SQLでは、INSERT コマンドを使用してこれを行います。

INSERT INTO flights
    (origin, destination, duration)
    VALUES ("New York", "London", 415);

上記のコマンドでは、挿入するテーブル名を指定し、情報を提供するカラム名のリストを指定してから、テーブルのその行に入力する VALUES を指定して、VALUES が対応するカラムのリストと同じ順序になるようにしています。id は自動的にインクリメントされるため、値を指定する必要はありません。

SELECT

テーブルに行を追加したら、そのテーブル内のデータにアクセスする方法が必要になるでしょう。これは、SQLのSELECTクエリーを使用して行います。flightsテーブルへの最も簡単な SELECT クエリは、次のようになります。

SELECT * FROM flights;

上記のコマンド (*) は、フライトテーブルからすべてのデータを取得します。

all

しかし、実際にはデータベースのすべてのカラムが必要なわけではなく、originとdestinationだけが必要な場合もあります。これらの列のみにアクセスするには、*をアクセス先の列名に置き換えます。次のクエリは、すべての出発地と目的地を返します。

SELECT origin, destination FROM flights;
Just two cols

テーブルが大きくなるにつれて、クエリが返す行を絞り込む必要も出てきます。これを行うには、WHEREの後に条件を追加します。たとえば、次のコマンドは、id が 3 の行のみを選択します。

SELECT * FROM flights WHERE id = 3;
only one row

id だけでなく、任意の列でもフィルタリングできます。

SELECT * FROM flights WHERE origin = "New York";
Origin is New York

ターミナルでのSQLの操作

基本的なSQLコマンドがいくつかわかったので、ターミナルでテストしてみましょう。コンピュータでSQLiteを使用するには、まず SQLiteをダウンロードする必要があります (レッスンでは使用しませんが、SQLクエリーをより使いやすく実行するためにDB Browserをダウンロードすることもできます) 。

まず、新しいファイルを手動で作成するか、ターミナルで touch flights.sql を実行して、データベースのファイルを作成します。次に、ターミナルで sqlite3 flights.sql を実行すると、SQLiteプロンプトが表示され、SQLコマンドを実行できます。


# Entering into the SQLite Prompt
(base) % sqlite3 flights.sql
SQLite version 3.26.0 2018-12-01 12:34:55
Enter ".help" for usage hints.

# Creating a new Table
sqlite> CREATE TABLE flights(
   ...>     id INTEGER PRIMARY KEY AUTOINCREMENT,
   ...>     origin TEXT NOT NULL,
   ...>     destination TEXT NOT NULL,
   ...>     duration INTEGER NOT NULL
   ...> );

# Listing all current tables (Just flights for now)
sqlite> .tables
flights

# Querying for everything within flights (Which is now empty)
sqlite> SELECT * FROM flights;

# Adding one flight
sqlite> INSERT INTO flights
   ...>     (origin, destination, duration)
   ...>     VALUES ("New York", "London", 415);

# Checking for new information, which we can now see
sqlite> SELECT * FROM flights;
1|New York|London|415

# Adding some more flights
sqlite> INSERT INTO flights (origin, destination, duration) VALUES ("Shanghai", "Paris", 760);
sqlite> INSERT INTO flights (origin, destination, duration) VALUES ("Istanbul", "Tokyo", 700);
sqlite> INSERT INTO flights (origin, destination, duration) VALUES ("New York", "Paris", 435);
sqlite> INSERT INTO flights (origin, destination, duration) VALUES ("Moscow", "Paris", 245);
sqlite> INSERT INTO flights (origin, destination, duration) VALUES ("Lima", "New York", 455);

# Querying this new information
sqlite> SELECT * FROM flights;
1|New York|London|415
2|Shanghai|Paris|760
3|Istanbul|Tokyo|700
4|New York|Paris|435
5|Moscow|Paris|245
6|Lima|New York|455

# Changing the settings to make output more readable
sqlite> .mode columns
sqlite> .headers yes

# Querying all information again
sqlite> SELECT * FROM flights;
id          origin      destination  duration
----------  ----------  -----------  ----------
1           New York    London       415
2           Shanghai    Paris        760
3           Istanbul    Tokyo        700
4           New York    Paris        435
5           Moscow      Paris        245
6           Lima        New York     455

# Searching for just those flights originating in New York
sqlite> SELECT * FROM flights WHERE origin = "New York";
id          origin      destination  duration
----------  ----------  -----------  ----------
1           New York    London       415
4           New York    Paris        435

また、フライトを除外するために、等号だけでなく、不等号も使うことができます。整数値と実数値には、>または<を使用できます。

SELECT * FROM flights WHERE duration > 500;
> 500

また、Pythonのように他のロジック(AND, OR)を使用することもできます。

SELECT * FROM flights WHERE duration > 500 AND destination = "Paris";
> 500 and paris
SELECT * FROM flights WHERE duration > 500 OR destination = "Paris";
> 500 or paris

また、キーワードINを使用して、データの一部が次のいずれかにあてはまるかどうかを確認することもできます。

SELECT * FROM flights WHERE origin IN ("New York", "Lima");
in

キーワードLIKEを使用して単語をより広範に検索するために、正規表現を使用することもできます。次のクエリは、ワイルドカード文字として % を使用して、出発地(origin)に a を持つすべての結果を検索します。

SELECT * FROM flights WHERE origin LIKE "%a%";
Origin has an 'a'

関数

クエリの結果に適用できるSQL関数もいくつかあります。これらは、クエリによって返されるすべてのデータではなく、データの要約統計のみが必要な場合に便利です。

UPDATE

テーブルを追加して検索する方法を見てきましたが、すでに存在するテーブルの行を更新したい場合もあります。これを行うには、次に示すようにUPDATEコマンドを使用します。単語の意味からコマンドの意味がわかると思いますが、このコマンドはニューヨークからロンドンに向かうフライトをすべて見つけ、その所要時間を430に設定します。

UPDATE flights
    SET duration = 430
    WHERE origin = "New York"
    AND destination = "London";

DELETE

また、データベースから行を削除する機能が必要な場合もあります。これにはDELETEコマンドを使用します。次のコードは、東京に着陸するすべての便を削除します。

DELETE FROM flights WHERE destination = "Tokyo";

その他の文

クエリの結果を制御するために使用できる追加の句がいくつかあります。

  • LIMIT:クエリによって戻される結果の数を制限します。
  • ORDER BY:指定した列に基づいて結果を並べ替えます。
  • GROUP BY:指定した列で結果をグループ化します。
  • HAVING:結果の数に基づいて追加の条件を与えます。

テーブルの結合

これまでは、一度に1つのテーブルしか操作していませんでしたが、実際には多くのデータベースが、何らかの方法で相互に関連する複数のテーブルで構成されています。フライトの例では、都市に合わせて空港コードを追加するとします。現在のテーブルの設定では、各行に2つの列を追加する必要があります。また、都市XがコードYに関連付けられていることを複数の場所に記述する必要があるため、情報を繰り返す必要があります。

この問題を解決する1つの方法は、フライトを記録するテーブルと、空港を記録するテーブルを作成することです。2番目のテーブルは次のようになります。

Airport Table

これで、コードと都市を関連づけるテーブルができました。フライトテーブルに都市名全体を保存するのではなく、その空港の id だけを保存できれば、ストレージスペースを節約できます。そのため、フライトテーブルを書き換える必要があります。origin_iddestination_id は、airports表の id 列を指定しているため、これらの値を外部キーと呼びます。

New Flights

航空会社は、フライトや空港だけでなく、各乗客がどのフライトに搭乗するかなど、乗客に関するデータを保存することもできます。リレーショナル・データベースの機能を使用して、姓と名を格納する別のテーブルと、それらが実行されているフライトを表す外部キーを追加できます。

Simple Passengers Table

しかし、同じ人が複数のフライトに乗っているかもしれないので、もう一工夫するべきでしょう。これをチェックするために、名前と姓を格納する people テーブルと、人とフライトをペアにする passengers テーブルを作成できます。

People
Passengers

この場合、1人の人間が複数のフライトに搭乗でき、1つのフライトに複数の人間が搭乗できることから、flightspeople の関係を多対多の関係と呼びます。この2つを結ぶ passengers テーブルは、関連テーブルと呼ばれます。

JOINクエリ

データはより効率的に保存されるようになりましたが、データを照会するのはより困難なようです。ありがたいことに、SQLにはJOINクエリがあり、別のクエリのために2つのテーブルを結合することができます。

たとえば、乗客が旅行するたびに、出発地、目的地、名前を検索するとします。また、この表を簡単にするために、フライトID、名、姓を含む最適化されていない passengers テーブルを使用します。このクエリの最初の部分は、すでに親しんでいるものです。

SELECT first, origin, destination
FROM ...

しかし、first は乗客 passengers テーブルに格納され、出発地 origin と目的地 destination はフライトテーブルに格納されているため、ここで問題が発生します。この問題を解決するには、passengers テーブルの flight_idflights テーブルの id に対応していることを使用して、2つのテーブルを結合します。

SELECT first, origin, destination
FROM flights JOIN passengers
ON passengers.flight_id = flights.id;
Join blurry

ここではINNER JOINと呼ばれる方法を使用しました。これは、テーブル間で一致しない行を無視することを意味しますが、LEFT JOINRIGHT JOIN, FULL OUTER JOINなどの他のタイプの結合もあります。ここでは詳しく説明しません。

インデックスの作成:

大きなテーブルを扱うときにクエリをより効率的にする1つの方法は、テキストの後ろに表示されるインデックスに似たインデックスを作成することです。たとえば、乗客を苗字で検索することが多いことがわかっている場合は、次のコマンドを使用して、苗字からidまでのインデックスを作成できます。

CREATE INDEX name_index ON passengers (last);

SQLの脆弱性

SQLを使用したデータ処理の基本を理解したので、SQLの使用に関連する主な脆弱性を指摘することが重要です。 SQLインジェクションから始めます。

SQLインジェクション攻撃とは、悪意のあるユーザーがサイトのセキュリティ対策を回避するためにSQLコードを入力することです。たとえば、ユーザ名とパスワードを格納するテーブルがあり、ページのホームサイトにログインフォームがあるとします。次のようなクエリを使用してユーザーを検索できます。

SELECT * FROM users
WHERE username = username AND password = password;

Harryという名前のユーザーがこのサイトにアクセスし、ユーザー名として harry 、パスワードとして 12345 を入力すると、クエリは次のようになります:

SELECT * FROM users
WHERE username = "harry" AND password = "12345";

一方、ハッカーはユーザ名として  harry" -- と入力し、パスワードとしては何も入力しません。SQLでは -- はコメントを表し、クエリは次のようになります。

SELECT * FROM users
WHERE username = "harry"--" AND password = "12345";

このクエリではパスワードチェックがコメント化されているため、ハッカーはパスワードを知らなくてもHarryのアカウントにログインできます。この問題を解決するには、次の方法があります。

  • エスケープ文字は、SQLが入力をSQLコードではなくプレーン・テキストとして扱うようにするために使用します。
  • 独自のエスケープシーケンスを含むSQL上の抽象レイヤなので、自分でSQLクエリを記述する必要はありません。

SQLに関するもう1つの主な脆弱性は、競合状態として知られています。

競合状態とは、データベースに対する複数のクエリが同時に発生する状況です。これらが適切に処理されないと、データベースが更新される正確な時間に問題が発生する可能性があります。たとえば、銀行口座に150ドルあるとします。スマートフォンとラップトップPCの両方で自分の銀行口座にログインし、各デバイスで$100を引き出そうとすると、競合状態が発生する可能性があります。銀行のソフトウェア開発者が競合条件に正しく対処していなければ、150ドルしか入っていない口座から200ドルを引き出すことができるかもしれません。この問題を解決する1つの方法は、データベースをロックすることです。1つのトランザクションが完了するまで、データベースとの他の対話を許可できませんでした。銀行の例では、私のコンピュータの「引き出しをする」ページに移動するためにクリックした後、銀行は私のスマートフォンからそのページに移動することを許可しないかもしれません。

Djangoモデル

DjangoモデルはSQLを抽象化 したもので、直接SQLクエリを使うのではなく、Pythonのクラスやオブジェクトを使ってデータベースを操作することができます。

航空会社用のdjangoプロジェクトを作成し、そのプロジェクト内にアプリケーションを作成して、モデルの使用を開始しましょう。

django-admin startproject airline
cd airline
python manage.py startapp flights

次に、通常どおりにアプリケーションを追加する手順を実行します。

  1. settings.pyINSTALLED_APPS リストに flights を追加します。
  2. urls.pyflights のルートを追加します。
path("flights/", include("flights.urls")),
  1. flights アプリケーション内で urls.py ファイルを作成します。そして、標準的な urls.py のインポートとリストを入力します。

ここでは、実際のパスを作成して views.py を始めるのではなく、models.py ファイルにモデルをいくつか作成します。このファイルでは、アプリケーションに保存するデータの概要を説明します。次に、Djangoは各モデルの情報を保存するために必要なSQL構文を決定します。1つの flight のモデルがどのようなものかを見てみましょう。

class Flight(models.Model):
    origin = models.CharField(max_length=64)
    destination = models.CharField(max_length=64)
    duration = models.IntegerField()

このモデル定義で何が起こっているかを見てみましょう。

  • 最初の行では、Djangoのモデル・クラスを拡張する新しいモデルを作成します。
  • 以下では、出発地origin、目的地destination、および所要時間durationのフィールドを追加します。最初の2つは文字フィールドで、文字列を格納します。3番目は整数フィールドです。これらは、多くの組み込みDjango Fieldクラスのうちの2つにすぎません。
  • 2つの文字フィールドに対して64の最大長を指定します。ドキュメントを参照して、特定のフィールドで使用可能な仕様を確認できます。

マイグレーション

モデルを作成しましたが、この情報を保存するデータベースはまだありません。モデルからデータベースを作成するには、プロジェクトのメインディレクトリに移動して以下のコマンドを実行します。

python manage.py makemigrations

このコマンドは、データベースを作成または編集してモデルに保存できるようにするためのPythonファイルを作成します。次のような出力が表示されるはずです。migrations ディレクトリに移動すると、新しいファイルが作成されています。

migrations output 0

次に、これらのマイグレーションをデータベースに適用するために、次のコマンドを実行します。

python manage.py migrate

ここでは、いくつかのデフォルトのマイグレーションが独自のマイグレーションと一緒に適用されています。また、プロジェクトのディレクトリに db.sqlite3 というファイルがあります。

migrate output

シェル

これで、このデータベースへの情報の追加と操作を始めるために、Djangoのシェルを入力して、プロジェクト内でPythonコマンドを実行することができます。

python manage.py shell
Python 3.7.2 (default, Dec 29 2018, 00:00:04)
Type 'copyright', 'credits' or 'license' for more information
IPython 6.5.0 -- An enhanced Interactive Python. Type '?' for help.
# Import our flight model
In [1]: from flights.models import Flight

# Create a new flight
In [2]: f = Flight(origin="New York", destination="London", duration=415)

# Instert that flight into our database
In [3]: f.save()

# Query for all flights stored in the database
In [4]: Flight.objects.all()
Out[4]: <QuerySet [<Flight: Flight object (1)>]>

データベースを照会すると、Flight object (1) という flight が1つだけあることがわかります。これはあまりわかりやすい名前ではありませんが、修正できます。models.py 内で、Flightオブジェクトを文字列に変換する方法を示す __str__ 関数を定義します。

class Flight(models.Model):
    origin = models.CharField(max_length=64)
    destination = models.CharField(max_length=64)
    duration = models.IntegerField()

    def __str__(self):
        return f"{self.id}: {self.origin} to {self.destination}"

ここでシェルに戻ると、出力が少し読みやすくなります。

# Create a variable called flights to store the results of a query
In [7]: flights = Flight.objects.all()

# Displaying all flights
In [8]: flights
Out[8]: <QuerySet [<Flight: 1: New York to London>]>

# Isolating just the first flight
In [9]: flight = flights.first()

# Printing flight information
In [10]: flight
Out[10]: <Flight: 1: New York to London>

# Display flight id
In [11]: flight.id
Out[11]: 1

# Display flight origin
In [12]: flight.origin
Out[12]: 'New York'

# Display flight destination
In [13]: flight.destination
Out[13]: 'London'

# Display flight duration
In [14]: flight.duration
Out[14]: 415

これは良いスタートですが、前に戻って考えてみると、すべてのフライトの出発地と目的地として都市名を保存する必要はないので、おそらくフライトモデルに何らかの関連性のある空港の別のモデルが必要です。

class Airport(models.Model):
    code = models.CharField(max_length=3)
    city = models.CharField(max_length=64)

    def __str__(self):
        return f"{self.city} ({self.code})"

class Flight(models.Model):
    origin = models.ForeignKey(Airport, on_delete=models.CASCADE, related_name="departures")
    destination = models.ForeignKey(Airport, on_delete=models.CASCADE, related_name="arrivals")
    duration = models.IntegerField()

    def __str__(self):
        return f"{self.id}: {self.origin} to {self.destination}"

新しい Airport クラスの内容は以前にも見たことがありますが、Flight クラス内の origin と destination フィールドの変更は新しいものです。

  • origin フィールドと destination フィールドがそれぞれ外部キーであることを指定します。これは、外部キーが別のオブジェクトを参照していることを意味します。
  • 最初の引数として Airport を入力することで、このフィールドが参照するオブジェクトのタイプを指定します。
  • 次の引数 on_delete=models.CASCADE は、空港が削除された場合の対処方法を示します。この場合、空港が削除されると、それに関連付けられているすべてのフライトも削除されるように指定します。CASCADE 以外にもいくつかのオプションがあります。
  • 関連する名前を指定すると、特定の空港を起点または目的地とするすべてのフライトを検索できます。

models.py で変更を行うたびに、マイグレーションを行ってからマイグレーションを適用する必要があります。ニューヨークからロンドンへの既存のフライトは、新しいデータベース構造に適合しないため、削除しなければならない場合があります。

# Create New Migrations
python manage.py makemigration

# Migrate
python manage.py migrate

では、Djangoシェルでこれらの新しいモデルを試してみましょう。

# Import all models
In [1]: from flights.models import *

# Create some new airports
In [2]: jfk = Airport(code="JFK", city="New York")
In [4]: lhr = Airport(code="LHR", city="London")
In [6]: cdg = Airport(code="CDG", city="Paris")
In [9]: nrt = Airport(code="NRT", city="Tokyo")

# Save the airports to the database
In [3]: jfk.save()
In [5]: lhr.save()
In [8]: cdg.save()
In [10]: nrt.save()

# Add a flight and save it to the database
f = Flight(origin=jfk, destination=lhr, duration=414)
f.save()

# Display some info about the flight
In [14]: f
Out[14]: <Flight: 1: New York (JFK) to London (LHR)>
In [15]: f.origin
Out[15]: <Airport: New York (JFK)>

# Using the related name to query by airport of arrival:
In [17]: lhr.arrivals.all()
Out[17]: <QuerySet [<Flight: 1: New York (JFK) to London (LHR)>]>

アプリケーションの開始

これで、モデルを使用してデータベースと対話するこのプロセスを中心としたアプリケーションの構築を開始できます。まず、航空会社のインデックスルートを作成します。

urls.py

urlpatterns = [
    path('', views.index, name="index"),
]

views.py

from django.shortcuts import render
from .models import Flight, Airport

# Create your views here.

def index(request):
    return render(request, "flights/index.html", {
        "flights": Flight.objects.all()
    })

新しいlayout.html


<!DOCTYPE html>
<html lang="en">
    <head>
        <title>Flights</title>
    </head>
    <body>
        {% block body %}
        {% endblock %}
    </body>
</html>

新しいindex.html


{% extends "flights/layout.html" %}

{% block body %}
    <h1>Flights:</h1>
    <ul>
        {% for flight in flights %}
            <li>Flight {{ flight.id }}: {{ flight.origin }} to {{ flight.destination }}</li>
        {% endfor %}
    </ul>
{% endblock %}

ここでは、これまでに作成したすべてのフライトのリストを表示するデフォルトページを作成しました。今ページを開くとこのような感じです。

Just one on the list

次に、Djangoシェルに戻って、アプリケーションにフライトを追加します。

# Using the filter command to find all airports based in New York
In [3]: Airport.objects.filter(city="New York")
Out[3]: <QuerySet [<Airport: New York (JFK)>]>

# Using the get command to get only one airport in New York
In [5]: Airport.objects.get(city="New York")
Out[5]: <Airport: New York (JFK)>

# Assigning some airports to variable names:
In [6]: jfk = Airport.objects.get(city="New York")
In [7]: cdg = Airport.objects.get(city="Paris")

# Creating and saving a new flight:
In [8]: f = Flight(origin=jfk, destination=cdg, duration=435)
In [9]: f.save()

再びサイトを訪れると以下のようになります。

Two flights

Django管理

Djangoにはデフォルトの管理インターフェースが用意されていて、これを使うと簡単に新しいオブジェクトを作成できます。このツールを使用するには、最初に管理ユーザーを作成する必要があります。

(base) cleggett@Connors-MacBook-Pro airline % python manage.py createsuperuser
Username: user_a
Email address: a@a.com
Password:
Password (again):
Superuser created successfully.

次に、adminアプリケーションに admin.py ファイルを入力し、モデルをインポートして登録して、adminアプリケーションにモデルを追加します。これはDjangoに、adminアプリケーションでどのモデルにアクセスしたいかを伝えます。

from django.contrib import admin
from .models import Flight, Airport

# Register your models here.
admin.site.register(Flight)
admin.site.register(Airport)

サイトにアクセスしてURLに /admin を追加すると、次のようなページにログインできます。

login

ログインすると、次のようなページが表示され、データベースに保存されているオブジェクトを作成、編集、削除できます。

admin page

それでは、サイトにページを追加しましょう。まず、フライトをクリックして詳細情報を表示する機能を追加します。これを行うには、フライトの id を含むURLパスを作成します。

path("<int:flight_id>", views.flight, name="flight")

次に、views.pyにフライトIDを取り込み、新しいhtmlページをレンダリングするflight関数を作成します。

def flight(request, flight_id):
    flight = Flight.objects.get(id=flight_id)
    return render(request, "flights/flight.html", {
        "flight": flight
    })

テンプレートを作成して、このフライト情報とホームページへのリンクを表示します。


{% extends "flights/layout.html" %}

{% block body %}
    <h1>Flight {{ flight.id }}</h1>
    <ul>
        <li>Origin: {{ flight.origin }}</li>
        <li>Destination: {{ flight.destination }}</li>
        <li>Duration: {{ flight.duration }} minutes</li>
    </ul>
    <a href="{% url 'index' %}">All Flights</a>
{% endblock %}

最後に、1つのページから別のページにリンクする機能を追加する必要があるので、インデックスページを変更してリンクを含めます。


{% extends "flights/layout.html" %}

{% block body %}
    <h1>Flights:</h1>
    <ul>
        {% for flight in flights %}
            <li><a href="{% url 'flight' flight.id %}">Flight {{ flight.id }}</a>: {{ flight.origin }} to {{ flight.destination }}</li>
        {% endfor %}
    </ul>
{% endblock %}

今私たちのホームページはこのようになっています。

new home

例えば、フライト5をクリックすると、このページが表示されます。

One flight

多対多の関係

では、乗客をモデルに組み込む作業をしましょう。まず、Passengerモデルを作成します。

class Passenger(models.Model):
    first = models.CharField(max_length=64)
    last = models.CharField(max_length=64)
    flights = models.ManyToManyField(Flight, blank=True, related_name="passengers")

    def __str__(self):
        return f"{self.first} {self.last}"
  • これまで述べてきたように、乗客はフライトと多対多の関係があり、これについては、DjangoでManyToManyFieldを使って指定します。
  • このフィールドの最初の引数は、このオブジェクトが関連するオブジェクトのクラスです。
  • ここでは引数 blank=True を指定しています。これは、ある乗客に関連したフライトが存在しない場合もありうる、ということを意味します。以前と同じ目的で related_name を追加しました。これにより、特定のフライトのすべての乗客を検索できるようになります。

これらの変更を実際に行うには、マイグレーションを行う必要があります。次に、admin.py にPassengerモデルを登録し、管理ページにアクセスして乗客を作成します。

乗客を追加したので、フライトページを更新して、フライトのすべての乗客が表示されるようにします。まず views.py にアクセスしてフライトのビューを更新し、コンテキストとして乗客のリストを提供します。前に定義した関連する名前を使用してリストにアクセスします。

def flight(request, flight_id):
    flight = Flight.objects.get(id=flight_id)
    passengers = flight.passengers.all()
    return render(request, "flights/flight.html", {
        "flight": flight,
        "passengers": passengers
    })

flight.html に乗客のリストを追加します。


<h2>Passengers:</h2>
<ul>
    {% for passenger in passengers %}
        <li>{{ passenger }}</li>
    {% empty %}
        <li>No Passengers.</li>
    {% endfor %}
</ul>

この時点で、flight 5をクリックすると、以下のようになります。

new flight 5

さて、私たちのサイトの訪問者にフライトを予約する機能を提供する作業をしてみましょう。これを行うには、urls.py に予約ルートを追加します。

path("<int:flight_id>/book", views.book, name="book")

views.py に、フライトに乗客を追加するbook関数を追加します。

def book(request, flight_id):

    # For a post request, add a new flight
    if request.method == "POST":

        # Accessing the flight
        flight = Flight.objects.get(pk=flight_id)

        # Finding the passenger id from the submitted form data
        passenger_id = int(request.POST["passenger"])

        # Finding the passenger based on the id
        passenger = Passenger.objects.get(pk=passenger_id)

        # Add passenger to the flight
        passenger.flights.add(flight)

        # Redirect user to flight page
        return HttpResponseRedirect(reverse("flight", args=(flight.id,)))

次に、Djangoのクエリから特定のオブジェクトを除外する機能を使用して、現在フライトに搭乗していないすべてのユーザーがページからアクセスできるように、フライトテンプレートにコンテキストを追加します。

def flight(request, flight_id):
    flight = Flight.objects.get(id=flight_id)
    passengers = flight.passengers.all()
    non_passengers = Passenger.objects.exclude(flights=flight).all()
    return render(request, "flights/flight.html", {
        "flight": flight,
        "passengers": passengers,
        "non_passengers": non_passengers
    })

次に、選択入力フィールドを使用して、フライトページのHTMLにフォームを追加します。


<form action="{% url 'book' flight.id %}" method="post">
    {% csrf_token %}
    <select name="passenger" id="">
        {% for passenger in non_passengers %}
            <option value="{{ passenger.id }}">{{ passenger }}</option>
        {% endfor %}
    </select>
    <input type="submit">
</form>

では、フライトページに移動して乗客を追加したときのサイトの外観を見てみましょう。

form
submitted

Django管理アプリケーションを使用するもう1つの利点は、カスタマイズできることです。たとえば、フライトのすべての側面を管理インターフェイスで表示する場合、admin.py 内に新しいクラスを作成し、Flight モデルの登録時に引数として追加できます。

class FlightAdmin(admin.ModelAdmin):
    list_display = ("id", "origin", "destination", "duration")

# Register your models here.
admin.site.register(Flight, FlightAdmin)

これで、フライトの管理ページにアクセスすると、id も表示されるようになりました。

admin table

Djangoの管理アプリケーションのカスタマイズ方法については、Django管理者ドキュメントを参照してください。

ユーザの管理

今日のレッスンで最後に取り上げるのは、認証、つまりユーザがWebサイトにログインしたり、Webサイトからログアウトしたりできるようにする方法です。幸いなことに、Djangoはこれを非常に簡単にしてくれるので、その方法の例を見ていきましょう。まず、 users という新しいアプリケーションを作成します。ここでは、新しいアプリを作成するための通常の手順をすべて説明しますが、新しい urls.py ファイルには、さらにいくつかのルートを追加します。

urlpatterns = [
    path('', views.index, name="index"),
    path("login", views.login_view, name="login"),
    path("logout", views.logout_view, name="logout")
]

まず、ユーザがログインできるフォームを作成します。いつものように layout.html ファイルを作成してから、フォームを含み、メッセージを表示する login.html ファイルを作成します。


{% extends "users/layout.html" %}

{% block body %}
    {% if message %}
        <div>{{ message }}</div>
    {% endif %}

    <form action="{% url 'login' %}" method="post">
        {% csrf_token %}
        <input type="text", name="username", placeholder="Username">
        <input type="password", name="password", placeholder="Password">
        <input type="submit", value="Login">
    </form>
{% endblock %}

ここで、views.py に3つの関数を追加します。

def index(request):
    # If no user is signed in, return to login page:
    if not request.user.is_authenticated:
        return HttpResponseRedirect(reverse("login"))
    return render(request, "users/user.html")

def login_view(request):
    return render(request, "users/login.html")

def logout_view(request):
    # Pass is a simple way to tell python to do nothing.
    pass

次に、管理サイトに移動し、ユーザを追加します。その後、views.py に戻り、login_view 関数を更新して、ユーザ名とパスワードを持つ POSTリクエストを処理します。

# Additional imports we'll need:
from django.contrib.auth import authenticate, login, logout

def login_view(request):
    if request.method == "POST":
        # Accessing username and password from form data
        username = request.POST["username"]
        password = request.POST["password"]

        # Check if username and password are correct, returning User object if so
        user = authenticate(request, username=username, password=password)

        # If user object is returned, log in and route to index page:
        if user:
            login(request, user)
            return HttpResponseRedirect(reverse("index"))
        # Otherwise, return login page again with new context
        else:
            return render(request, "users/login.html", {
                "message": "Invalid Credentials"
            })
    return render(request, "users/login.html")

次に、ユーザの認証時に index 関数がレンダリングする user.html ファイルを作成します。


{% extends "users/layout.html" %}

{% block body %}
    <h1>Welcome, {{ request.user.first_name }}</h1>
    <ul>
        <li>Username: {{ request.user.username }}</li>
        <li>Email: {{ request.user.email }}</li>
    </ul>

    <a href="{% url 'logout' %}">Log Out</a>
{% endblock %}

最後に、ユーザがログアウトできるように、Djangoの組み込み logout 関数を使用するように logout_view 関数を更新します。

def logout_view(request):
    logout(request)
    return render(request, "users/login.html", {
                "message": "Logged Out"
            })

これで設定が完了しましたので、ウェブサイトを見てみましょう。

Demo

これでこのレッスンは終わりです!次回は、このコースの2番目のプログラミング言語であるJavaScriptについて学びます。