勉強会用資料です.
今回のチュートリアルではDjangoのModelに対する補足説明と,
djangoの機能を拡張するライブラリを紹介し,shell上で色々と操作してみます.
Model補足
Model,Field,インスタンスの関係がわかりにくいという意見があったので補足します.
pythonコードとDB(テーブル)との関連
ModelはDBのテーブルを表しています.
table名をMetaに設定することで変更も可能ですが,デフォルトでは アプリ名_model名
の名前が付きます.
pollsアプリ内にあるQuestionモデルの場合,テーブル名は polls_question
になります.
Model内に定義した models.~Fieldがテーブル上のカラムに相当します.
idカラムはPrimaryKeyがとして設定されているフィールドがない場合,自動で追加されます.
ModelのインスタンスはDBテーブル上の1レコードを意味します.
class Question(models.Model):
class Meta:
verbose_name = '質問'
verbose_name_plural = '質問の複数形'
ordering = ['-pub_date']
question_text = models.CharField(max_length=200)
pub_date = models.DateTimeField('date published')
レコード(インスタンス)取得のためのあれこれ
DB上のテーブルからレコードを取得するにはSQL文を発行する必要があります.
これを行うために,ModelはManagerクラスを持っています.
実際のSQLの発行,生成はQuerySetクラスが担っており,sql発行後はベースとなったModelのインスタンスを返します.
レコードの取得や作成などの操作はこのQuerySetクラスを通して行われます.
ManagerクラスはModelとQuerySetを中継するような役割をしており,特にQuerySetと密に関わっています.
QuerySetはget
やcreate
のようにピンポイントでレコードの取得や作成を行うメソッドも持っていますが,
iteratorを提供しているので,それ自身をforループで回してあげることで取得対象のレコード(=インスタンス)を順番に取得できます.
qs = Question.objects.all()
for q in qs:
# q <--- これが取得してきたレコードであり,Questionのインスタンスになる
methodの切り分け
ソース→8a9d88559ae94bea8bb706468eaa6459127c6f59
Model=テーブル,インスタンス=レコードなので,レコードに対して行いたい処理はインスタンスメソッドで定義します.
逆にテーブルそのものに対して行いたい処理についてはクラスメソッドとして定義します.
例えばチュートリアルで作成した was_published_recently
は
”そのレコード(=インスタンス)が最近公開されたかどうかの判定” なのでインスタンスメソッドなわけです.
これとは別に,”Questionテーブルの中から公開済みのものを取得” というメソッドの場合はクラスメソッドにします.
class Question(models.Model):
...
@classmethod
def get_published_data(cls):
return cls.objects.filter(pub_date__lte=timezone.now())
例に出したような”公開済み”を取得するようなfilterの場合,
少し冗長にはなりますが,QuerySetを拡張するのもおすすめです.
import datetime
from django.db import models
from django.utils import timezone
class QuestionQuerySet(models.query.QuerySet):
def is_published(self):
return self.filter(pub_date__lte=timezone.now())
class Question(models.Model):
...
objects = models.Manager.from_queryset(QuestionQuerySet)()
...
@classmethod
def get_published_data(cls):
return cls.objects.is_published()
いずれの場合も,
Question.get_published_data()
することで公開済みのquerysetが取れます.
ただし,Modelを直に拡張した場合, pkが10以下のもので,公開済みのもの
というふうに
途中に条件を入れることができません.
Question.get_published_date().filter(pk__lte=10)
のように,”公開済みの中で,pkが10以下のもの” という条件ならかけます.
一方,QuerySetの拡張の場合は自分の好きなところで "公開済みのもの" を取得するためのfilterをかけることができます.
Question.objects.filter(pk__lte=10).is_published()
shellで遊ぶ
djangoでは manage.py shell
コマンドで動作させることでModel等を直接操作可能になりますが,
そのshellをもう少し便利に使用するためのライブラリを紹介します.
まずは ipython
. これを入れるとshellのカラー化やコマンドの補完などをしてくれるようになります.
次に django-extensions
.
これはshellだけでなく,名前の通りdjangoに様々な拡張機能を提供します.
両方共pipで簡単にインストールできますので入れてみてください.
$ pip install ipython
$ pip install django-extensions
ipythonのほうは $ ./manage.py shell
を実行するだけで自動的に見た目が変化します.
django-extensionsのほうはsettingsのINSTALL_APPSに追加する必要があります.
...
INSTALLED_APPS = (
'django.contrib.admin',
'django.contrib.auth',
'django.contrib.contenttypes',
'django.contrib.sessions',
'django.contrib.messages',
'django.contrib.staticfiles',
'django_extensions', # ←←← これを追加
'bootstrap3',
'polls',
)
...
INSTALLED_APPS
に設定すると,manage.py
コマンドでdjango_extensions用のコマンドが実行できるようになります.
$ ./manage.py
Type 'manage.py help <subcommand>' for help on a specific subcommand.
Available subcommands:
...
[django_extensions]
admin_generator
clean_pyc
clear_cache
compile_pyc
create_app
create_command
create_jobs
create_template_tags
describe_form
drop_test_database
dumpscript
export_emails
find_template
generate_secret_key
graph_models
mail_debug
notes
passwd
pipchecker
print_settings
print_user_for_session
reset_db
runjob
runjobs
runprofileserver
runscript
runserver_plus
set_default_site
set_fake_emails
set_fake_passwords
shell_plus
show_template_tags
show_templatetags
show_urls
sqlcreate
sqldiff
sqldsn
sync_s3
syncdata
unreferenced_files
update_permissions
validate_templates
...
今回はこの中で shell
コマンドの拡張である shell_plus
を使います.
起動時に --print-sql
オプションを付けることでインスタンス取得時のSQL文を見ることもできるようになりますので,ついでにつけてみましょう.
$ ./manage.py shell_plus --print-sql
# Shell Plus Model Imports
from django.contrib.admin.models import LogEntry
from django.contrib.auth.models import Group, Permission, User
from django.contrib.contenttypes.models import ContentType
from django.contrib.sessions.models import Session
from polls.models import Choice, Question
# Shell Plus Django Imports
from django.db import transaction
from django.core.urlresolvers import reverse
from django.utils import timezone
from django.core.cache import cache
from django.db.models import Avg, Count, F, Max, Min, Sum, Q, Prefetch, Case, When
from django.conf import settings
Python 3.5.1 (default, Jan 23 2016, 02:16:23)
Type "copyright", "credits" or "license" for more information.
IPython 4.2.0 -- An enhanced Interactive Python.
? -> Introduction and overview of IPython's features.
%quickref -> Quick reference.
help -> Python's own help system.
object? -> Details about 'object', use 'object??' for extra details.
In [1]:
shellでは使いたいModelを手動でimportする必要がありましたが,
shell_plusでは起動すると自動でmodelを読み込んでくれます.
先ほど作成した get_published_data
コマンドを実行してみます.
In [1]: Question.get_published_data()
Out[1]: SELECT "polls_question"."id", "polls_question"."question_text", "polls_question"."pub_date" FROM "polls_question" WHERE "polls_question"."pub_date" <= '2016-06-23 06:45:46.716854' ORDER BY "polls_question"."pub_date" DESC LIMIT 21
Execution time: 0.001740s [Database: default]
[<Question: 2つ目の質問>, <Question: what's up?>, <Question: 3つ目の質問>]
In [2]:
今回は --print-sql
オプションをつけているので,このように実行されるSQL文も確認できます.
データを登録してみる
データの登録方法はManager(queryset)から行う方法と,instanceを直接操作する方法があります.
まずは create
から試してみましょう.
pub_dateに日付を入れる必要があるので,予めdjango.utils.timezone
をimportし,pud_dateに本日の日時を指定してあげます.
In [3]: from django.utils import timezone
In [4]: Question.objects.create(pub_date=timezone.now())
BEGIN
Execution time: 0.000028s [Database: default]
INSERT INTO "polls_question" ("question_text", "pub_date") VALUES ('', '2016-06-23 07:02:01.013534')
Execution time: 0.000638s [Database: default]
Out[4]: <Question: >
これでDBに新しいレコードが登録されます.
In [5]: Question.objects.count()
SELECT COUNT(*) AS "__count" FROM "polls_question"
Execution time: 0.000154s [Database: default]
Out[5]: 4
次にインスタンスからの作成を試しましょう.
インスタンスの場合は作っただけではDBのレコードに反映されません.
インスタンスのsave()を実行することで,レコードが存在している場合は更新,存在していない場合は挿入の処理がされます.
In [6]: ins = Question(pub_date=timezone.now(), question_text='インスタンスからの作成')
In [7]: ins
Out[7]: <Question: インスタンスからの作成>
In [8]: Question.objects.count()
SELECT COUNT(*) AS "__count" FROM "polls_question"
Execution time: 0.000177s [Database: default]
Out[8]: 4 # ←←←←←←← この時点ではまだ作られてない.
In [9]: ins.save() # ←←←←←← ここでレコードの更新メソッド実行
BEGIN
Execution time: 0.000032s [Database: default]
INSERT INTO "polls_question" ("question_text", "pub_date") VALUES ('インスタンスからの作成', '2016-06-23 07:07:46.485479')
Execution time: 0.001240s [Database: default]
In [10]: Question.objects.count()
SELECT COUNT(*) AS "__count" FROM "polls_question"
Execution time: 0.000167s [Database: default]
Out[10]: 5 # ←←←←←←← insertされている
filter色々
データをいくつか登録したら今度はレコードの取得で色々やってみましょう.
いくつか方法がありますが,とりあえず条件で絞り込むための filter
と exclude
を覚えとけば問題ありません.
filterは条件に一致したレコードが残っていきます.
excludeはその逆で,条件に一致しなかったレコードが残っていきます.
filterの引数には フィールド名=条件
を渡します.
フィールド名の後ろに__lte
などのような文字をつけることで完全一致ではなく以上,以下のように条件を変更できます.
どのような条件が使えるかは公式ドキュメントを参照してください.
OR検索
→ 公式ドキュメント
最後にOR条件の指定方法について記述します.
filter(exclude)は全てANDとなるので,条件をOR検索するにはQクラスを使用します.
Qはfilterで指定したのと同じように フィールド名=条件
でインスタンスを作成します.
そこで作成した条件を,QuerySetのfilterに渡すことでOR検索を実現できます.
Qは &
(and) |
(or) ~
(not) の論理記号が使用可能です.
In [12]: from django.db.models import Q
In [13]: q1 = Q(pk=1)
In [14]: q2 = Q(pk=2)
In [15]: Question.objects.filter(q1|q2)
Out[15]: SELECT "polls_question"."id", "polls_question"."question_text", "polls_question"."pub_date" FROM "polls_question" WHERE ("polls_question"."id" = 1 OR "polls_question"."id" = 2) ORDER BY "polls_question"."pub_date" DESC LIMIT 21
Execution time: 0.000390s [Database: default]
[<Question: 2つ目の質問>, <Question: what's up?>]
次のチュートリアルは未定です.