Python Django チュートリアル(8)


勉強会用資料です．
今回のチュートリアルではDjangoのModelに対する補足説明と，
djangoの機能を拡張するライブラリを紹介し，shell上で色々と操作してみます．

→ [他のチュートリアル](http://qiita.com/maisuto/items/17653f344d1f64afa019)

# Model補足

Model，Field，インスタンスの関係がわかりにくいという意見があったので補足します．

## pythonコードとDB（テーブル）との関連

ModelはDBのテーブルを表しています．
table名をMetaに設定することで変更も可能ですが，デフォルトでは `アプリ名_model名` の名前が付きます．
pollsアプリ内にあるQuestionモデルの場合，テーブル名は `polls_question` になります．

Model内に定義した models.~Fieldがテーブル上のカラムに相当します．
idカラムはPrimaryKeyがとして設定されているフィールドがない場合，自動で追加されます．

ModelのインスタンスはDBテーブル上の1レコードを意味します．

```python:polls/models.py
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')
```


![Kobito.CVWNC3.png](https://qiita-image-store.s3.amazonaws.com/0/58048/31f401bc-b936-7dc9-c0b3-b2aef819d296.png "Kobito.CVWNC3.png")

## レコード（インスタンス）取得のためのあれこれ

DB上のテーブルからレコードを取得するにはSQL文を発行する必要があります．
これを行うために，ModelはManagerクラスを持っています．
実際のSQLの発行，生成はQuerySetクラスが担っており，sql発行後はベースとなったModelのインスタンスを返します．
レコードの取得や作成などの操作はこのQuerySetクラスを通して行われます．
ManagerクラスはModelとQuerySetを中継するような役割をしており，特にQuerySetと密に関わっています．

![Kobito.hB5eZg.png](https://qiita-image-store.s3.amazonaws.com/0/58048/ce38083e-e0d4-5280-f9a7-836f3c47dc4a.png "Kobito.hB5eZg.png")


QuerySetは`get`や`create`のようにピンポイントでレコードの取得や作成を行うメソッドも持っていますが，
iteratorを提供しているので，それ自身をforループで回してあげることで取得対象のレコード（＝インスタンス）を順番に取得できます．

```python
qs = Question.objects.all()
for q in qs:
   #  q <--- これが取得してきたレコードであり，Questionのインスタンスになる
```

## methodの切り分け

ソース→`8a9d88559ae94bea8bb706468eaa6459127c6f59`

Model＝テーブル，インスタンス＝レコードなので，レコードに対して行いたい処理はインスタンスメソッドで定義します．
逆にテーブルそのものに対して行いたい処理についてはクラスメソッドとして定義します．

例えばチュートリアルで作成した `was_published_recently` は 
”そのレコード（＝インスタンス）が最近公開されたかどうかの判定” なのでインスタンスメソッドなわけです．

これとは別に，”Questionテーブルの中から公開済みのものを取得” というメソッドの場合はクラスメソッドにします．

```python:polls/models/py
class Question(models.Model):
...
    @classmethod
    def get_published_data(cls):
        return cls.objects.filter(pub_date__lte=timezone.now())
```

例に出したような”公開済み”を取得するようなfilterの場合，
少し冗長にはなりますが，QuerySetを拡張するのもおすすめです．

```python:polls/models.py(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に追加する必要があります．

```python:tutorial/settings.py
...
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用のコマンドが実行できるようになります．

```bash
$ ./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文を見ることもできるようになりますので，ついでにつけてみましょう．

```bash
$ ./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` コマンドを実行してみます．

```bash
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に本日の日時を指定してあげます．

```bash
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に新しいレコードが登録されます．

```bash
In [5]: Question.objects.count()
SELECT COUNT(*) AS "__count" FROM "polls_question"

Execution time: 0.000154s [Database: default]

Out[5]: 4
```

次にインスタンスからの作成を試しましょう．
インスタンスの場合は作っただけではDBのレコードに反映されません．
インスタンスのsave()を実行することで，レコードが存在している場合は更新，存在していない場合は挿入の処理がされます．


```bash
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`などのような文字をつけることで完全一致ではなく以上，以下のように条件を変更できます．
どのような条件が使えるかは[公式ドキュメント](https://docs.djangoproject.com/ja/1.9/ref/models/querysets/#field-lookups)を参照してください．

### OR検索
→ [公式ドキュメント](https://docs.djangoproject.com/ja/1.9/topics/db/queries/#complex-lookups-with-q-objects])

最後に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?>]
```

<hr>
次のチュートリアルは未定です．

→ [他のチュートリアル](http://qiita.com/maisuto/items/17653f344d1f64afa019)