巳年じゃないけど Python やろうぜ(その 15)
初期データの一括投入
今回から、当ブログではすっかりお馴染みの「政令指定都市一覧」を使って、Django の様々な機能を見ていきます。
$ ./manage.py startapp major_city
例によってアプリを作成するところからスタート。
Model を作ります。
from django.db import models # Create your models here. class District(models.Model): name = models.CharField(max_length=30) class Prefecture(models.Model): name = models.CharField(max_length=30) district = models.ForeignKey(District, on_delete=models.CASCADE) class City(models.Model): name = models.CharField(max_length=30) prefecture = models.ForeignKey(Prefecture, on_delete=models.CASCADE) designated = models.DateField() area = models.DecimalField(max_digits=7, decimal_places=2) population = models.IntegerField()
ForeignKey
が初登場しました。これは参照する親モデルのクラス名と、親クラスを削除した時の挙動(ON DELETE ~)を指定します。ちなみに on_delete
は Django 2.0 以降で必須になるそうです*1ので、デフォルトに甘えずに指定しておくと移行が楽になるでしょう。
マイグレーションを作成します。
$ ./manage.py makemigrations major_city (0.001) SELECT name, type FROM sqlite_master WHERE type in ('table', 'view') AND NOT name='sqlite_sequence' ORDER BY name; args=None (0.000) SELECT "django_migrations"."app", "django_migrations"."name" FROM "django_migrations"; args=() Migrations for 'major_city': major_city/migrations/0001_initial.py: - Create model City - Create model District - Create model Prefecture - Add field prefecture to city
SQL を確認してみましょう。
$ ./manage.py sqlmigrate major_city 0001 (中略) BEGIN; -- -- Create model City -- CREATE TABLE "major_city_city" ( "id" integer NOT NULL PRIMARY KEY AUTOINCREMENT, "name" varchar(30) NOT NULL, "designated" date NOT NULL, "area" decimal NOT NULL, "population" integer NOT NULL ); -- -- Create model District -- CREATE TABLE "major_city_district" ( "id" integer NOT NULL PRIMARY KEY AUTOINCREMENT, "name" varchar(30) NOT NULL ); -- -- Create model Prefecture -- CREATE TABLE "major_city_prefecture" ( "id" integer NOT NULL PRIMARY KEY AUTOINCREMENT, "name" varchar(30) NOT NULL, "district_id" integer NOT NULL REFERENCES "major_city_district" ("id") ); -- -- Add field prefecture to city -- ALTER TABLE "major_city_city" RENAME TO "major_city_city__old"; CREATE TABLE "major_city_city" ( "id" integer NOT NULL PRIMARY KEY AUTOINCREMENT, "name" varchar(30) NOT NULL, "designated" date NOT NULL, "area" decimal NOT NULL, "population" integer NOT NULL, "prefecture_id" integer NOT NULL REFERENCES "major_city_prefecture" ("id") ); INSERT INTO "major_city_city" ( "id", "name", "designated", "area", "population", "prefecture_id" ) SELECT "id", "name", "designated", "area", "population", NULL FROM "major_city_city__old"; DROP TABLE "major_city_city__old"; CREATE INDEX "major_city_prefecture_a34a99d3" ON "major_city_prefecture" ("district_id"); CREATE INDEX "major_city_city_71a71d54" ON "major_city_city" ("prefecture_id"); COMMIT;
若干回りくどい処理をしていますが、これはモデル名のアルファベット順に処理をしようとしているからでしょうか。
最後はマイグレーションファイルを適用するのを忘れずに。
$ ./manage.py migrate
巳年じゃないけど Python やろうぜ(その 14)
エラー表示の話
フォームのバリデーションエラーの表示場所をカスタマイズする方法があっさりとわかったので、次なるお題に移る前にさくっと書いておきます。
{% load static %} <!doctype html> <html> <head> <title>顧客検索</title> <link rel="stylesheet" href="{% static 'customer_search/css/search.css' %}"> </head> <body> <h1>顧客検索</h1> <form method="post" action="."> {% csrf_token %} <table> {% for field in form %} <tr> <th>{{ field.label_tag }}</th> <td> {{ field }} {% if field.errors %} <span class="error"> {% for error in field.errors %} {{ error }} {% endfor %} </span> {% endif %} </td> </tr> {% endfor %} </table> <input type="submit" value="検索"> </form> </body> </html>
.error { color: red; } .error::before { content: 'Error!'; }
フォームの表示を簡略化せずにフィールドごとにやれば良かったんですね。
エラーが無いときは通常どおり。
巳年じゃないけど Python やろうぜ(その 13)
ロギングの話
デフォルトでもある程度のログは出力されるのですが、データベースとの連携においては、少なくとも開発の段階では SQL の発行状況などを確認したいと思うはずです。これは settings.py
に設定を記述することで可能になります。
LOGGING = { 'version': 1, 'disable_existing_loggers': False, 'handlers': { 'console': { 'level': 'DEBUG', 'class': 'logging.StreamHandler', }, }, 'loggers': { 'django.db.backends': { 'handlers': ['console'], 'level': 'DEBUG', }, }, }
最初の二つの設定は、ほぼこれで固定と思って間違いありません。'version': 1
は dictConfig のフォーマットの現時点で唯一のバージョンを指定するもので、'disable_existing_loggers': False
は、既にある Logger を無効化しない設定です。デフォルトで True
になっているので、書かないと既存の Logger が全て無効化されてしまいます。
それ以降は個別の用途に合わせて自由に書ける項目です。handlers
はログの出力先で、用途に応じてファイルへの保存や管理者宛のメールなども設定できます。今回はコンソールへの出力を設定しています。level
は最も強い(= デバッグ用の低レベルなシステム情報も出力する) DEBUG
に設定しています。loggers
で、データベース関連のバックエンドに関して DEBUG
レベルでコンソールにログを出力させる設定をしています。これによりデータベースに対して発行される SQL をコンソールで見られるようになります。
巳年じゃないけど Python やろうぜ(その 12)
フォームとビューの作成
まずはビューの作成に必要なフォームから作ります。既に Django バリバリ使ってる人なら「ModelForm 使えば楽やろ」って言うと思いますが、実は ModelForm には「(自動生成される id などの)AutoField はフォーム部品化の対象外」だったりするのと、今回はいろいろと属性をいじりたいということもあって、「それだったら一から作った方がいいんじゃね ?」ということで見送りとなりました… orz
というわけで forms.py
から。
from django import forms from .models import Customer class CustomerForm(forms.Form): customer_id = forms.IntegerField( label='顧客ID', widget=forms.TextInput(attrs={'size': 5}), min_value=1, ) customer_name = forms.CharField( required=False, label='顧客名', widget=forms.TextInput(attrs={'size': 20, 'readonly': True}), ) customer_phone = forms.CharField( required=False, label='連絡先', widget=forms.TextInput(attrs={'size': 20, 'readonly': True}), )
label
は以前ご説明の通りですが、それ以外は初お目見えですね。
IntegerField
は整数値を入力するべきフォームになります。widget
はデフォルトでは NumberInput
*1なのですが、ご指名(?)で TextInput
に登場していただきます。attrs
には辞書形式で input
要素に指定する属性と属性値の組み合わせを指定します。このコードの例だと
<input id="id_customer_id" name="customer_id" size="5" type="text" required />
と同等になります。min_value
は後でバリデーションチェックに使われます。テーブルの ID なので正の数でないとおかしいですよね。だから最小値は 1 です。
customer_name
と customer_phone
は label
以外は同じですね。widget
はデフォルトの TextInput
なのですが、属性を付与したいので敢えて指定します。required
はデフォルトは True
なのですが、この二つは検索結果を埋め込むためのものなので、送信時に空になっていても良いように False
にします。また readonly
属性を付けて、このフォームからは編集できない*2ようにします。
*1:localize がデフォルトで False のため。localize を True にすると TextInputになるようです。
*2:あくまでもユーザーインターフェース上の話であって、これによってデータが完全に守られるというわけではないことには注意が必要です。
巳年じゃないけど Python やろうぜ(その 11)
管理画面を使う
管理者ユーザーの作成
管理画面を使うにはまず管理者ユーザーを作成しなければなりません。
$ ./manage.py createsuperuser Username (leave blank to use '(snip)'): admin Email address: admin@example.com Password: Password (again): This password is too short. It must contain at least 8 characters. This password is too common. Password: Password (again): Superuser created successfully.
メールアドレスはでっち上げで公式のチュートリアルと同じものを入れてますが、実際に入力するときはちゃんと使えるアドレスを入れておいた方が良いかと思います(自分も実際にはそうしています)。
ちなみにパスワードは 8 文字以上にしないと怒られる、ということをお見せするために敢えてそのままコピー & ペーストしました。
巳年じゃないけど Python やろうぜ(その 10)
だいぶシリーズが長期化してきました(苦笑)
Model で遊ぶ
今回使用するデータは「現場で使えるSQL 第2版」のものを使用することにします。
現場で使えるSQL 第2版 (DB Magazine SELECTION)
- 作者: 小野哲,藤本亮
- 出版社/メーカー: 翔泳社
- 発売日: 2006/11/16
- メディア: 単行本(ソフトカバー)
- 購入: 2人 クリック: 22回
- この商品を含むブログ (12件) を見る
$ ./manage.py shell Python 3.6.0 (default, Feb 4 2017, 00:11:30) [GCC 6.3.1 20161221 (Red Hat 6.3.1-1)] on linux Type "help", "copyright", "credits" or "license" for more information. (InteractiveConsole) >>> from customer_search.models import Customer >>> Customer.objects.all() <QuerySet []> >>> c = Customer(name='(株)ワイキキソフト', phone='090-AAAA-AAAA') >>> c.save() >>> c.id 1 >>> c.name '(株)ワイキキソフト' >>> c.phone '090-AAAA-AAAA' >>> Customer.objects.all() <QuerySet [<Customer: Customer object>]>
save
はテーブルにデータを保存するメソッドです。実際に DB Browser for SQLite で確認した画面がこちら。
確かにデータは保存されています…が、チュートリアルでは objects.all()
の出力を見て「これは困りました*1ね」というお話になります。これはオブジェクトを表示するときに文字列化する models.Model.__str__
メソッドをオーバーライドしていないので、デフォルトのものが使用されている*2ことによります。
このままだと、後程説明する管理画面での表示にも差し障るので、models.py
を改造しましょう。
from django.db import models # Create your models here. class Customer(models.Model): name = models.CharField(max_length=20) phone = models.CharField(max_length=20) def __str__(self): return self.name
自身の name
の内容を表示するように変更しました。もう一度 shell を起動して確認してみましょう。
$ ./manage.py shell Python 3.6.0 (default, Feb 4 2017, 00:11:30) [GCC 6.3.1 20161221 (Red Hat 6.3.1-1)] on linux Type "help", "copyright", "credits" or "license" for more information. (InteractiveConsole) >>> from customer_search.models import Customer >>> Customer.objects.all() <QuerySet [<Customer: (株)ワイキキソフト>]> >>> Customer.objects.filter(id=1) <QuerySet [<Customer: (株)ワイキキソフト>]> >>> Customer.objects.get(pk=1) <Customer: (株)ワイキキソフト>
filter
は条件に合致するデータを全て取得するもので、出力の型は QuerySet
になっています。一方の get
は主キーを指定してそれに該当する単一のデータを取得するものなので、出力の型は Customer
になります。
表示に顧客名が表示されていて、いい感じですね。次回は管理画面を利用して Model を管理する方法について書きます。
巳年じゃないけど Python やろうぜ(その 9)
Model を扱う
いよいよ今回から Model を扱います。
…の前に。
新しいアプリの作成
端末からコマンドを入力して新しいアプリを作成します。
$ ./manage.py startapp customer_search
今回はデータベース内のレコードを ID を入力して顧客を検索する customer_search アプリを作ります。なお今回から Linux 上で作業していますが、基本となる手順はほぼ同じ*1です。
settings.py
の編集を忘れずに。
# Application definition INSTALLED_APPS = [ 'django.contrib.admin', 'django.contrib.auth', 'django.contrib.contenttypes', 'django.contrib.sessions', 'django.contrib.messages', 'django.contrib.staticfiles', 'hello', 'customer_search', # 追加 ]
Model を作成する
Model の作成は、Model に対応する class を models.py
に書くだけです。と言っても、Rails みたいにコマンド一つで自動生成、とは行かないのが難点ですが…。
from django.db import models # Create your models here. class Customer(models.Model): name = models.CharField(max_length=20) phone = models.CharField(max_length=20)
今回は Customer (顧客)という単一のモデルを扱うことにします。モデル同士の関連性についてはまた後程ということで。
CharField
は VARCHAR
型に対応するもので、max_length
にバイト数を指定できます。このコードは SQL の VARCHAR(20)
という型宣言と同等になります。なお、主キーは特に指定しなければデフォルトで id
というフィールドが勝手に作成されます。
Model を有効にする
作った Model を有効にするための作業が必要です。まずは
$ ./manage.py makemigrations customer_search Migrations for 'customer_search': search/migrations/0001_initial.py: - Create model Customer
sqlmigrate
コマンドを使うと、migrate
コマンドを実行した時に実際に発行される SQL が読めます*2。
$ ./manage.py sqlmigrate search 0001 BEGIN; -- -- Create model Customer -- CREATE TABLE "customer_search_customer" ( "id" integer NOT NULL PRIMARY KEY AUTOINCREMENT, "name" varchar(20) NOT NULL, "phone" varchar(20) NOT NULL ); COMMIT;
customer_search_customer
という新たなテーブルが作成されるようですね。では migrate
を実行しましょう。
$ ./manage.py migrate Operations to perform: Apply all migrations: admin, auth, contenttypes, customer_search, sessions Running migrations: Applying customer_search.0001_initial... OK
システムに必要なテーブルは既に作成されているので、今回は新しく追加したものだけが実行されました。