似非プログラマのうんちく

「似非プログラマの覚え書き」出張版

巳年じゃないけど 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_deleteDjango 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

*1:現状では警告を表示して CASCADE をデフォルトで設定している

続きを読む

巳年じゃないけど 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_namecustomer_phonelabel 以外は同じですね。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)

現場で使えるSQL 第2版 (DB Magazine SELECTION)

$ ./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 を管理する方法について書きます。

*1:オブジェクトの内容が表示されていないということです。

*2:デフォルトの挙動は公式ドキュメントからソースコードを参照するとわかります。

巳年じゃないけど Python やろうぜ(その 9)

Model を扱う

いよいよ今回から Model を扱います。

…の前に。

AtomDjango アプリの開発を快適に行うために

すっかり忘れていましたが、AtomDjango アプリの開発を行う際に入れておくと良いパッケージを二つ紹介しておきます。

新しいアプリの作成

端末からコマンドを入力して新しいアプリを作成します。

$ ./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 (顧客)という単一のモデルを扱うことにします。モデル同士の関連性についてはまた後程ということで。

CharFieldVARCHAR 型に対応するもので、max_lengthバイト数を指定できます。このコードは SQLVARCHAR(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

システムに必要なテーブルは既に作成されているので、今回は新しく追加したものだけが実行されました。

次回は公式のチュートリアルに沿って、これを使ってコマンドラインで少し遊んでみます。

*1:Windows 上での作業について特段の補足が必要な場合のみ別途補足します。

*2:下記の出力例は読みやすさのためにフォーマットを変えています。