DjangoでCRUDする際のメモ書きです。
いろいろなサイトを参考に、自分に合った内容を抜粋しています。
なお、開発に先立ち、Djangoでの開発の基本フローや概念をまとめたこちらも参考にしてください(ちょっと内容かぶってますが)。
環境
OSとか
私はMac環境なのでMac上で試していますが、基本Windows等でも同じかと思います。
Pythonと仮想環境
Pythonは3.6.1です。Python3.xに標準でついてくるvenvを利用して仮想環境を作っていますが、別に仮想環境じゃなくてもいいと思います。環境構築についてはこちらをご覧ください。
pipでインストールしているもの
pip freezeの結果は下記の通り。
Django==1.11
django-bootstrap-form==3.2.1
PyMySQL==0.7.11
pytz==2017.2
pytzはDjango入れたら合わせて入るので、明示的にいれているのはDjango, django-bootstrap-form, PyMySQLの3つ。必要に応じてインストールして下さい。
pip install Django
pip install django-bootstrap-form
pip install PyMySQL
django-bootstrap-fromはBootstrapのスタイルでフォームを自動生成するためのパッケージです。
プロジェクトの作成と準備
Djangoには「プロジェクト」と「アプリケーション」という概念があります。
まずプロジェクトを作成し、それにアプリケーション(以下アプリ)を追加する感じです。
VisualStudioのソリューションとプロジェクトの感じでしょうかね。ASP.NET MVCのAraとか。
プロジェクトの作成
では早速プロジェクトを作成してみます。ここではjango_testというプロジェクトを作成しています。
django-admin.py startproject django_test
作成が完了すると、下記のようなファイル構造が生成されます。
django_test/
manage.py
django_test/
__init__.py
settings.py
urls.py
wsgi.py
プロジェクトディレクトリ直下にプロジェクトと同名のディレクトリが存在します。この中にプロジェクト共通の設定ファイルが存在します(が、説明上まぎらわしい)。なお、ここで説明する作業は原則として、プロジェクトディレクトリ直下での作業を想定しています。
settings.pyの設定
プロジェクトが作成されたら、settings.py内の設定を変更します。
データベース
Djangoでは標準でSQLiteを利用する設定になっていますが、私はMySQLを利用するのでDATABASESをそれに合わせ編集します。
DATABASES = {
'default': {
'ENGINE': 'django.db.backends.mysql',
'NAME': 'djangodb', #書き換え
'USER': 'root', #書き換え
'PASSWORD': 'root', #書き換え
'HOST': '',
'PORT': '',
}
}
なお、これに先立ちMySQLの設定が必要なことは言うまでもありません。
パッケージのimport
冒頭にて、MySQLとの接続のためにPyMySQLをインストールしましたが、それだけでは利用できないのでパッケージをプロジェクトに読み込みます。私はsettings.pyの冒頭に記述しました。
manage.pyに記述するという記事もありましたが、設定関連なのでsettings.pyにとりあえず書きました。
import pymysql
pymysql.install_as_MySQLdb()
その他
その他の設定項目として、取り急ぎ、LANGUAGE_CODEとTIME_ZONEを編集しておきます。
LANGUAGE_CODE = 'ja'
TIME_ZONE = 'Asia/Tokyo'
マイグレーションの実行
DBの設置も完了したので、ここで一度マイグレーションをしておきます。
python manage.py migrate
これにより、Djangoを利用するために必要なテーブルが指定したDBに生成されます。
開発用サーバの起動と動作確認
ここで一度、開発用Webサーバを起動させて、Djangoの画面が正しく閲覧できるか見ておきましょう。
起動は下記コマンドで実行します。
python manage.py runserver
起動が完了したらい、下記URLはにアクセスしてみましょう。
「うまくいった!」といいう、それっぽいのが出ていればOKです。
アプリケーションの作成と準備
プロジェクトの設定が完了したらのでアプリを作成してみます。
アプリケーションの作成
ここではcrudという名前のアプリを作成してみます。
python manage.py startapp crud
作業ディレクトリと同レベルにcrudというディレクトリが作成されました。
アプリ関連のファイルはこれ以下に生成されています。
プロジェクトへのアプリケーションの追加
アプリを生成するだけでは利用できないようなので、プロジェクトに読み込むための記述を行います。
設定はsettings.pyに対して行います。あと、ついでに、後で利用するdjango-bootstrap-formも読み込んでおきます。
INSTALLED_APPS = [
'django.contrib.admin',
'django.contrib.auth',
'django.contrib.contenttypes',
'django.contrib.sessions',
'django.contrib.messages',
'django.contrib.staticfiles',
+ 'bootstrapform',
+ 'crud',
]
モデルの定義とマイグレーション
DjangoはMVCではなく、MTVモデルというのを採用しています。とはいえ、MVCのVがT(emplate)、VMCのCがV(iew)という名称に変わっただけというイメージのものであるため特に新しい概念を会得する必要はありません。
モデルを記述
会員情報の保存をイメージしたMemberモデルを作成します。
crudディレクトリ以下のmodels.pyに下記のように記述します。
from django.db import models
# Register your models here
class Member(models.Model):
name = models.CharField('氏名', max_length=255)
email = models.CharField('E-Mail', max_length=255)
age = models.IntegerField('年齢', blank=True, default=0)
def __str__(self):
return self.name
特に説明はしませんが、なんとなく想像がつく範囲かと思います。
マイグレーションファイルを作成
モデルを記述したら、モデルを保持するテーブル生成用のマイグレーションファイルを生成します。
いわゆるcodefirst的なものです。アプリ名を指定してmakemigrationsを実行することで、対象アプリ内のモデルの変更を走査し、マイグレーションファイルが作られるようです。
python manage.py makemigrations crud
既存テーブルでも利用は可能なようです。
マイグレーション
マイグレーションファイルが生成されたらmigrateを実行します。
python manage.py migrate
実開発では「モデルの編集」⇒「mekemigration」⇒「migrate」を繰り返し行うことになります。
管理サイトの有効化とテストデータの追加
Djangoでは http://localhost:8000/admin にアクセスすることで最初から(モデルの)管理サイトを利用することができます。しかし、管理サイトにログインするためのユーザーとパスワードが設定されて無いので、まずはそれを生成・取得します。
ユーザーの追加
以下のコマンドでユーザーを追加します。
python manage.py createsuperuser
適当なパスワード、メールアドレス等を設定します。
管理対象モデル(Member)の追加
なお、管理サイトにログインしてみると、標準では「グループ」と「ユーザー」しか管理対象として表示されていません。
自分で作成したモデルを管理するには、アプリディレクトリ以下にあるadmin.pyに必要な記述を追加する必要があります。
from django.contrib import admin
from crud.models import Member
# Register your models here.
admin.site.register(Member)
記述を追加したら更新するか、再ログインして変更を確認してください。
データの追加
管理サイトでMemberが管理できるようになったら、後々のテスト用に数行データを追加しておきましょう。
なお、Djangoにはfirexureというデータをインポートする機能があります。
Viewとルーティングの設定
MVTの順序でいけば、次はTempleteをいじりたいところですが、アプリの構造を理解するために先にView(Controller相当)とルーティングを設定しておきます。今回は、下記のURLパターンで、各機能にアクセスできるよう設定してみます。
- http://localhost/crud/members/ (一覧表示)
- http://localhost/crud/members/add/ (追加)
- http://localhost/crud/members/edit/2/ (編集)
- http://localhost/crud/members/delete/2/ (削除)
- http://localhost/crud/members/detail/2/ (詳細)
Viewの雛形を作成
では、まずViewを記述してみたいと思います。
ただ、具体的な機能を実装する前に、まずは「一覧」、「編集」、「削除」といった感じで、ただ文字列を返すメソッドを実装し、URLと機能のマッピングを確認したいと思います。
なお、「新規」が無いのは他のフレームワークと同様、POSTかつIDがあれば編集、ただのPOSTだと新規追加と判断させるためです(「編集」を共通で利用するからです)。
雛形のviews.pyの作成
crud以下のviews.pyを以下のようにします。
from django.shortcuts import render
from django.http import HttpResponse
#一覧
def index(request):
return HttpResponse("一覧")
#新規と編集
def edit(request, id=None):
return HttpResponse("編集")
#削除
def delete(request, id=None):
return HttpResponse("削除")
#詳細(おまけ)
def detail(request, id=None):
return HttpResponse("詳細")
ただ、文字列を返すだけの処理です。
ルーティングを設定する
Djangoでのルーティングの設定は、アプリディレクトリ以下にurls.pyを生成し、アプリ内でのルティング設定をした後、それをプロジェクト全体のurls.pyにincludeさせ、設定するというのがお作法のようです。
アプリ内でのルーティング
上記に示したURLと機能マップのルールに従いルーティングを設定します。
from django.conf.urls import url
from crud import views
urlpatterns = [
url(r'^members/$', views.index, name='index'),
url(r'^members/add/$', views.edit, name='add'),
url(r'^members/edit/(?P<id>\d+)/$', views.edit, name='edit'),
url(r'^members/delete/(?P<id>\d+)/$', views.delete, name='delete'),
url(r'^members/detail/(?P<id>\d+)/$', views.detail, name='detail'),
]
ルーティングは、
url(r'URLのパターンの正規表現', views.pyの対応メソッド, name='ルート名')
という形式です。
r''という記述は''の中では特殊文字をエスケープしないという意味のようです。ルート名はテンプレートでリンク先を指定する際等に利用します。
プロジェクト全体でのルーティング
アプリディレクトリで設定したurls.pyをインクルードします。
これにより http://localhost:8000/crud/members/ といったようなURLとなります。
from django.conf.urls import url, include
from django.contrib import admin
urlpatterns = [
url(r'^admin/', admin.site.urls),
url(r'^crud/', include('crud.urls', namespace='crud'))
]
動作を確認してみる
では、runserver を実行し、それぞれのURLパターンにアクセスし、動作(適切な文字が表示されるか)を確認してみてください。
- http://localhost:8000/crud/members/ (一覧表示)
- http://localhost:8000/crud/members/add/ (追加)
- http://localhost:8000/crud/members/edit/2/ (編集)
- http://localhost:8000/crud/members/delete/2/ (削除)
- http://localhost:8000/crud/members/detail/2/ (詳細)
一覧機能の実装
では、仮実装したviews.py内の実装を本実装?します。
views.pyの編集(一覧の実装)
index()を下記のように記述します。
from django.shortcuts import render
from django.http import HttpResponse
from crud.models import Member #追加
#一覧
def index(request):
members = Member.objects.all().order_by('id') #値を取得
return render(request, 'members/index.html', {'members':members}) #Templateに値を渡す
membersに値を取得し、それをmembers/index.htmlにmembersという名前で渡しています。
共通テンプレートの準備
受け取り側のindex.htmlを作成する前に、各ページで共通となる部分を共通テンプレート(base.html)として作成しておきます。なお、今回はCSSフレームワークとしてBootstrapを利用するので、CDNから必要なCSSやJSを読み込んでいます。
これを、アプリディレクトリ(crud)以下にTemplatesというディレクトリを作成し、base.htmlとして保存します。
{% load staticfiles %}
<!DOCTYPE html>
<html lang="ja">
<head>
<meta charset="utf-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>{% block title %}My books{% endblock %}</title>
<!-- Bootstrap -->
<link href="https://maxcdn.bootstrapcdn.com/bootstrap/3.3.7/css/bootstrap.min.css" rel="stylesheet">
</head>
<body>
<div class="container">
{% block content %}
{{ content }}
{% endblock %}
</div>
<!-- jQuery (necessary for Bootstrap's JavaScript plugins) -->
<script src="https://ajax.googleapis.com/ajax/libs/jquery/1.12.4/jquery.min.js"></script>
<!-- Include all compiled plugins (below), or include individual files as needed -->
<script src="https://maxcdn.bootstrapcdn.com/bootstrap/3.3.7/js/bootstrap.min.js"></script>
<!-- jsを書く場所 -->
{% block script %}
{% endblock %}
</body>
</html>
これで共通テンプレートは完成しました。テンプレート中の{% %}に対応するコンテンツを個別ページで記述します。
個別ページ(index.html)の作成
では、個別ページを作成します。先ほど作成したtemplatesディレクトリ以下にmembersディレクトリを作成し、その中にindex.htmlとして保存します。
Djangoではテンプレートを保存するディレクトリ名の制約がそれほどきつくないようです。ネット上の例ではアプリ名としているもの(ここではcrud)が多く見られました。公式チュートリアルでもそうなっているようです。
{% extends "base.html" %}
{% block title %}
一覧を表示する予定
{% endblock title %}
{% block content %}
<h3>一覧表示</h3>
<a href="{% url 'crud:add' %}" class="btn btn-primary btn-sm">追加</a>
<table class="table table-striped table-bordered">
<thead>
<tr>
<th>ID</th>
<th>氏名</th>
<th>E-Mail</th>
<th>年齢</th>
<th>操作</th>
</tr>
</thead>
<tbody>
{% for member in members %}
<tr>
<td>{{ member.id }}</td>
<td><a href="{% url 'crud:detail' id=member.id %}">{{ member.name }}</a></td>
<td>{{ member.email }}</td>
<td>{{ member.age }}</td>
<td>
<a href="{% url 'crud:edit' id=member.id %}" class="btn btn-primary btn-sm">編集</a>
<a href="{% url 'crud:delete' id=member.id %}" class="btn btn-primary btn-sm" id="btn_del">削除</a>
</td>
</tr>
{% endfor %}
</tbody>
</table>
<!-- ページネーションをここに入れる -->
{% endblock content %}
<!-- jsを入れる -->
{% block script %}
{% endblock %}
動作を確認してみる
各実装が完了したら正しく一覧画面が表示されるか確認してください。
「登録」、「編集」、「削除」ボタンのリンクが適切かクリックして確認してみてください。
新規追加・編集機能を実装する
続いて新規追加・編集機能を実装します。新規と編集ではviews.pyとテンプレートは同じものを利用します。
なお、Djangoでは、form(のHTML情報)を自動生成するFormおよびModelFormクラスというものを利用するのが一般的なようなので利用してみます。
FormクラスはModelが伴わないform(例えば検索とか)の作成に利用するようです。
では、アプリディレクトリ以下にforms.pyを作成し、下記のように記述します。
from django.forms import ModelForm
from crud.models import Member
class MemberForm(ModelForm):
class Meta:
model = Member
fields = ('name','email','age',)
詳細はさておき、Memberモデルを利用し、nameとemailとageを使うぞ!と宣言という感じでしょうか。
views.pyの編集(新規・編集の実装)
from django.shortcuts import render, get_object_or_404, redirect #追加
from django.http import HttpResponse
from crud.models import Member
from crud.forms import MemberForm #追加
#(抜粋)
#新規と編集
def edit(request, id=None):
if id: #idがあるとき(編集の時)
#idで検索して、結果を戻すか、404エラー
member = get_object_or_404(Member, pk=id)
else: #idが無いとき(新規の時)
#Memberを作成
member = Member()
#POSTの時(新規であれ編集であれ登録ボタンが押されたとき)
if request.method == 'POST':
#フォームを生成
form = MemberForm(request.POST, instance=member)
if form.is_valid(): #バリデーションがOKなら保存
member = form.save(commit=False)
member.save()
return redirect('crud:index')
else: #GETの時(フォームを生成)
form = MemberForm(instance=member)
#新規・編集画面を表示
return render(request, 'members/edit.html', dict(form=form, id=id))
新規・編集画面の準備
新規・編集時に利用する画面(edit.html)を作成します。
{% extends "base.html" %}
{% load bootstrap %}
{% block title %}会員の編集{% endblock title %}
{% block content %}
{% if id %}
<h3 class="page-header">会員の編集</h3>
<form action="{% url 'crud:edit' id=id %}" method="post" class="form-horizontal" role="form">
{% else %}
<h3 class="page-header">会員の登録</h3>
<form action="{% url 'crud:add' %}" method="post" class="form-horizontal" role="form">
{% endif %}
{% csrf_token %}
{{ form|bootstrap_horizontal }}
<div class="form-group">
<div class="col-sm-offset-2 col-sm-10">
<button type="submit" class="btn btn-primary">送信</button>
</div>
</div>
</form>
<a href="{% url 'crud:index' %}" class="btn btn-default btn-sm">戻る</a>
{% endblock content %}
タイトルも条件分岐して変更したほうがいいかもしれませんね。
では、動作を確認してみてください。
削除機能を実装する
では、最後に削除機能を実装します。
とりあえず消す
とりあえず消してみます。本当はPOSTで削除処理を実装しないと怒られますが、それはまた別の機会に。
def delete(request, id):
# return HttpResponse("削除")
member = get_object_or_404(Member, pk=id)
member.delete()
return redirect('crud:index')
削除時に確認する
せめて削除時に確認するようにします。index.htmlに下記の記述を追加します。
{% endblock content %}の手前あたりでいいでしょう。
{% block script %}
<script>
$(function(){
$("#btn_del").click(function(){
if(confirm("削除しますか?")){
//yesの処理(何もぜず進む)
}else{
//cancelの処理
return false;
}
});
})
</script>
{% endblock %}
詳細表示機能を実装する
本当におまけ程度ですが、詳細画面を実装してみます。
views.pyの編集
編集、削除と同様idを受け取り検索し、テンプレートへ結果を渡します。
#詳細
def detail(request, id=id):
member = get_object_or_404(Member, pk=id)
return render(request, 'members/detail.html', {'member':member})
詳細画面の準備
受け取ったmemberを展開します。本当はテーブルを組んだ方がいいと思いますが、そこが要点ではないので、とりあえず表示するだけ。
{% extends "base.html" %}
{% load bootstrap %}
{% block title %}会員の詳細{% endblock title %}
{% block content %}
<h3>会員の詳細情報</h3>
<h5>名前</h5>
{{ member.name }}
<h5>E-Mail</h5>
{{ member.email }}
<h5>年齢</h5>
{{ member.age }}
<br>
<br>
<a href="{% url 'crud:index' %}" class="btn btn-default btn-sm">戻る</a>
{% endblock content %}
以上でCRUD機能を一通り実装してみました。
応用
ページネーション
どんなWebアプリでもページネーションはつきものです。また、各フレームワークではそれを簡単にする方法が準備さています。
Djangoでは、LitViewを利用するのが一般的なようなので、それを利用してみます。ListViewとはクラスベース汎用ビューというものの一種で、各種目的に応じた汎用的な機能を提供するViewクラスのようです。
views.pyの編集(indexメソッドの書き換え)
indexメソッドを書き換えてもいいのですが、比較のため、indexメソッドはそのままにMemberList()クラスを追加します。
from django.views.generic import ListView #追加
#一覧
def index(request):
members = Member.objects.all().order_by('id') #値を取得
return render(request, 'members/index.html', {'members':members}) #Templateに値を渡す
#一覧(ページネーション用に追加)
class MemberList(ListView):
model = Member #利用するモデル
context_object_name='members' #オブジェクト名の設定(標準ではobject_listとなってしまう)
template_name='members/index.html' #テンプレートページの指定
paginate_by = 1 #1ページあたりのページ数
ルーティングの編集
追加したら、ページング処理が無いindexではなく、MemberList()を利用するようにルーティングを編集します。
from django.conf.urls import url
from crud import views
urlpatterns = [
#url(r'^members/$', views.index, name='index'), #コメントアウト
url(r'^members/$', views.MemberList.as_view(), name='index'), #追加
url(r'^members/add/$', views.edit, name='add'),
url(r'^members/edit/(?P<id>\d+)/$', views.edit, name='edit'),
url(r'^members/delete/(?P<id>\d+)/$', views.delete, name='delete'),
]
この時点で見ると、1行のみ表示されているはずです。(送り・戻り機能なしの状態)。
ページング部分の追加
ページング用のHTML要素を記述します。なお、ここではBootstrapのページング要素が正しくレンダリングできるようclass要素を追加しています。
index.htmlの<!-- ページネーションをここに入れる -->に下記の内容を入れます。
<!-- ページネーション(以下追加) -->
{% if is_paginated %}
<ul class="pagination">
<!-- 戻る<<の表示処理 -->
{% if page_obj.has_previous %}
<li><a href="?page={{ page_obj.previous_page_number }}">«</a></li>
{% else %}
<li class="disabled"><a href="#">«</a></li>
{% endif %}
<!-- ページの表示(多いと別処理が必要) -->
{% for linkpage in page_obj.paginator.page_range %}
{% ifequal linkpage page_obj.number %}
<li class="active"><a href="#">{{ linkpage }}</a></li>
{% else %}
<li><a href="?page={{ linkpage }}">{{ linkpage }}</a></li>
{% endifequal %}
{% endfor %}
<!-- 次へ>>の表示処理 -->
{% if page_obj.has_next %}
<li><a href="?page={{ page_obj.next_page_number }}">»</a></li>
{% else %}
<li class="disabled"><a href="#">»</a></li>
{% endif %}
</ul>
{% endif %}
バリデーション
続いてバリデーションについて簡単に見てみます。
標準では、モデルで設定したカラム?情報に準じて最低限度のバリデーション(max_lengthなど)が設定されるようです。また、基本入力が必須(required)となっているようです。
これではちょっと足りないので追加します。追加は、forms.pyをいじるようです。
下記の例では、
- nameを必須ではなくする。
- emailは必須のまま、かつ、@が入ってなければエラーとする。
という感じです。
from django.forms import ModelForm
from crud.models import Member
from django import forms
import re
class MemberForm(ModelForm):
#モデルで定義された条件をオーバーライド?する(Metaの記述より先に書く)
name = forms.CharField(required=False,max_length=8)
class Meta:
model = Member
fields = ('name','email','age',)
#各要素のバリデーション
def clean_email(self):
email = self.cleaned_data['email']
#的にだがどりあえず
if re.match(r'.+@+',email) == None:
raise forms.ValidationError("メールアドレスではありません。")
return email
各要素に対するバリデーションは、clear_要素名()というメソッドを定義することで追加できるようです。
clear~の意味は基本的なバリデーションを通過した(クリアーな)データという意味のようです。
その他
- 検索機能の追加
- 検索結果のページネーション
などもそのうち追加できればと思っていますが。とりあえず。