素人基盤エンジニアがDockerでDjangoを触るシリーズ②：モデルの作成

# TL;DR
[素人基盤エンジニアがDockerでDjangoを触るシリーズ①:DockerでDjangoを作る](https://qiita.com/LittleGreenMen/items/660c27669214da1c9613)の続き。
前回作ったdjangoにモデルを作成していく。
[Django Girls](https://tutorial.djangogirls.org/ja/)というdjangoのチュートリアルを掲載しているサイトがあるので、今回はそれの「Djangoモデル」以降の章に沿ってblogのサンプルを作成してみながら、djangoと戯れる。
（このサイトはとてもわかりやすくて、正直この投稿見るよりはそちらのほうがいいかもしれないが、あくまで素人が触ったときの学習の過程と調べたことを公開することで同じようなレベルの人の理解促進を狙って書いている。）

# Djangoの設計方針
### Model - Template - View
DjangoはMTV(モデル・テンプレート・ビュー)という考え方で設計されているらしい。MVC(モデル・ビュー・コントローラ)なら（名前だけは）聞いたことあるが。。
ひとつずつ確認していく。

#### Model
モデルは、データベースとのやり取りを行う。
DjangoGirlsのblogの例では、オブジェクト指向について言及した後、blogに必要なデータとして下記を挙げている。

```
Post
--------
title
text
author
created_date
published_date
```
このような、必要になるであろうデータ形式をモデルとして定義しておいて、リクエストが来たら必要なデータをDBから取得して返す。
これはMVCにおけるModelの役割とほぼ同じだと思われる。（多分）

#### Template
Djangoにおけるテンプレートは、見栄えの部分を指す。
htmlや、それを補完するためのCSSやjavascriptがそれにあたる。
MVCでいうと、Viewの役割。ややこしい。。
MVCではControllerがModelとViewの橋渡しをするのと同様で、MTVにおけるTemplateは直接Modelからデータを取得することはない。必ず、Viewを通してデータを扱う。

#### View
Templateでも少し触れたが、ViewではModelを使って取得した情報をどう見せるかという部分を記述する。その上で、関数化しTemplateとの紐づけを行う。
つまり、役割としては、
①Modelに対しデータ取得の命令を出す
②ユーザのリクエストに返すTemplateを決定する
③TemplateにModelから取得したデータを関数化して渡す
という３つがある。（だいぶ自信がないが間違えていたら直すので指摘ください。。）
ユーザがdjangoでできたwebサイトを訪問した際、まずはurls.pyの記述に従ってどのView関数で処理を行うかが決定し、選択されたView関数はModelを使いデータを取得し、選択したテンプレートにデータを渡した上でwebページを表示させる。

なんとなくのイメージは下記のような感じだろうか
　U 　⇒ urls.py ⇒ view.py ⇔ Model ⇔ DB
　S　　　　　　　　⇓
　E　　　　　　　　⇓
　R 　⇐　　　　Templates

この3つの役割をふんわりと意識した上で、構築に進む。

# 構築

### ①アプリケーションのひな型作成
#### manage.py startapp
```shell
docker-compose run web python manage.py startapp blog
```
前回作成したdocker-compose.yamlを利用して、ローカル環境にアプリケーションのひな型を作成する。前回はweb serverを起動するためのrunserverに利用したmanage.pyだが、startappとするとモデルを作るために必要なファイル群を作成してくれる。
前回の実行分を含めて、このコマンドを実行した後のディレクトリ構成は下記のようになっているはず。

```
.
∟ blog
　 ∟ admin.py
　 ∟ apps.py
　 ∟ __init__.py
　 ∟ migrations
　 ∟ models.py
　 ∟ tests.py
　 ∟ views.py
∟ composeexample
　 ∟ __init__.py
　 ∟ __pycache__
　 ∟ settings.py
　 ∟ urls.py
　 ∟ wsgi.py
∟ docker-compose.yaml
∟ Dockerfile
∟ manage.py
∟ migrations
∟ requirements.txt
```
前回同様、linuxマシンで実行した場合はフォルダの権限がrootになるので、下記を実行する。

```shell
sudo chown -R $USER:$USER .
```


### ②settings.pyによる紐づけ
#### settings.py
```python:settings.py
INSTALLED_APPS = [
    'django.contrib.admin',
    'django.contrib.auth',
    'django.contrib.contenttypes',
    'django.contrib.sessions',
    'django.contrib.messages',
    'django.contrib.staticfiles',
    'blog',
]
```
INSTALLED_APPSに今回作成したアプリケーションを追記する。

### ③モデル作成
####models.py
```python:blog/models.py
from django.conf import settings
from django.db import models
from django.utils import timezone


class Post(models.Model):
    author = models.ForeignKey(settings.AUTH_USER_MODEL, on_delete=models.CASCADE)
    title = models.CharField(max_length=200)
    text = models.TextField()
    created_date = models.DateTimeField(default=timezone.now)
    published_date = models.DateTimeField(blank=True, null=True)

    def publish(self):
        self.published_date = timezone.now()
        self.save()

    def __str__(self):
        return self.title
```
モデルの定義を作成する。startappで作成されたblogフォルダの中に、models.pyがあるのでそれを編集する。最初からあるのは二行目の `from django.db import models`のみ、あとはDjango Girlsのサンプルを拝借している。

####よくわかる解説
```python
from django.conf import settings
```
`project/settings.py`の変数を参照する際に利用するモジュール。

```python
from django.db import models
```
modelに関する様々なメソッドを提供するモジュール。
外部キーとか、データ型とか。

```python
from django.utils import timezone
```
タイムゾーンに関するモジュール。いろんな言語で提供されているユーティリティのものとまぁだいたいおなじもの。今回は投稿時の時間を取得するために使っている。
timezoneの設定はsettings.pyで行うので、何もしていない現状ではUTCになるはず。

```python
class Post(models.Model):
```
Postというモデルを定義している部分。
定義するモデルは、django.db.models.Modelを継承して作成する。
さらに子クラスを作成することも可能。例えばcreate_dateは汎用的に他のモデルでも使う可能性があるので、create_dateデータを定義しているTimestampクラスを作成しておいて、Post(Timestamp)という形で継承して利用する等。

```python
    author = models.ForeignKey(settings.AUTH_USER_MODEL, on_delete=models.CASCADE)
```
ForeignKey()は、外部キーとなるモデルを定義する。今回は、djangoがデフォルトで持っているUserモデルを外部キーとして指定するためsettingsモジュールを利用してMODEL定義を取得しているが、普通に自分が作成したモデルを外部キーとして利用する場合はモデル名を指定する。on_deleteオプションはdjango2.0以降では必須となっており、外部キーのオブジェクトを削除した際の挙動を指定している。CASCADEは、外部キーのオブジェクトを削除すると紐づくオブジェクトをすべて削除する。

```python
    title = models.CharField(max_length=200)
    text = models.TextField()
    created_date = models.DateTimeField(default=timezone.now)
    published_date = models.DateTimeField(blank=True, null=True)
```
blogを構成するそれぞれのデータ型と制約を指定する。created_dateは、オブジェクトが生成された時間が入力されるようになっている。つまり、記事が書かれた時間。それに対し、published_dateは現時点では空欄になっており、後述するpublishメソッド内で値が入るようになっている。

```python
    def publish(self):
        self.published_date = timezone.now()
        self.save()
```
blog記事を公開するメソッド。公開した時間であるpublished_dateに今の時間を代入し、save()メソッドを呼び出している。save()メソッドなど書いた覚えがないので、もちろん継承元のmodels.Model内のメソッドで、テーブルに主キーが存在する場合はUPDATEで存在しない場合はCREATEをする。

```python
    def __str__(self):
        return self.title
```
magicメソッド。(magicメソッドについては本筋ではないので各自調べてください。)インスタンス化したときにblog記事のタイトルがリターンされるようにしている。

### ④データベースのマイグレーション
djangoでは、models.pyの内容で接続しているデータベースにテーブルを作成することをマイグレーションと呼ぶらしい。
`manage.py makemigrations`というコマンドを実行すると、マイグレーションのためのファイルが作成され、`manage.py migrate`というコマンドで実際にデータベース上にテーブルを作成する。
早速`docker-compose exec`でwebサーバに入ってコマンドを実行し、dbサーバに入ってテーブルができているか確認してみる。(これらの操作は後々docker-compose内のcommandに追記して、`docker-compose up`でマイグレーションまで実施されるようにするが、今は確認のためコンテナに入ってコマンド実行をしている)

```shell
docker-compose exec web bash
```
(以下webコンテナ内)

```shell
manage.py makemigrations blog
```
下記のようなoutputが出る。
blog/migrations/0001_initial.py
 - Create model Post
アプリケーションのディレクトリ下のmigrationsに連番でマイグレーションファイルが作成され、2番目以降は差分が入るらしい。

```shell
manaeg.py migrate blog
```
  Applying contenttypes.0001_initial... OK
  Applying auth.0001_initial... OK
  Applying blog.0001_initial... OK
上のように出力されたら、マイグレーションは完了している。exitでwebサーバを抜けてdbサーバに入って確認してみる。

```shell
docker-compose exec db bash
```
(以下dbコンテナ内)

```shell
psql -U postgres
\d
```
```:\d実行結果
                        List of relations
 Schema |               Name                |   Type   |  Owner
--------+-----------------------------------+----------+----------
 public | auth_group                        | table    | postgres
 public | auth_group_id_seq                 | sequence | postgres
 public | auth_group_permissions            | table    | postgres
 public | auth_group_permissions_id_seq     | sequence | postgres
 public | auth_permission                   | table    | postgres
 public | auth_permission_id_seq            | sequence | postgres
 public | auth_user                         | table    | postgres
 public | auth_user_groups                  | table    | postgres
 public | auth_user_groups_id_seq           | sequence | postgres
 public | auth_user_id_seq                  | sequence | postgres
 public | auth_user_user_permissions        | table    | postgres
 public | auth_user_user_permissions_id_seq | sequence | postgres
 public | blog_post                         | table    | postgres
 public | blog_post_id_seq                  | sequence | postgres
 public | django_content_type               | table    | postgres
 public | django_content_type_id_seq        | sequence | postgres
 public | django_migrations                 | table    | postgres
 public | django_migrations_id_seq          | sequence | postgres
```
テーブルがたくさんできている。
authなんちゃらとdjangoなんちゃらはおそらくデフォルトで作成されるものだろう（多分）。
models.pyに書いたのはblog_postなので、blog_postテーブルの定義を確認する。

```shell
\d blog_post
```
```:\d実行結果
                                         Table "public.blog_post"
     Column     |           Type           | Collation | Nullable |                Default
----------------+--------------------------+-----------+----------+---------------------------------------
 id             | integer                  |           | not null | nextval('blog_post_id_seq'::regclass)
 title          | character varying(200)   |           | not null |
 text           | text                     |           | not null |
 created_date   | timestamp with time zone |           | not null |
 published_date | timestamp with time zone |           |          |
 author_id      | integer                  |           | not null |
Indexes:
    "blog_post_pkey" PRIMARY KEY, btree (id)
    "blog_post_author_id_dd7a8485" btree (author_id)
Foreign-key constraints:
    "blog_post_author_id_dd7a8485_fk_auth_user_id" FOREIGN KEY (author_id) REFERENCES auth_user(id) DEFERRABLE INITIALLY DEFERRED
```
idは、djangoがモデルを定義する際、デフォルトで付与する連番で、プライマリキーになる。その他はmodels.pyで定義した通りになっている。これで、モデルの準備は完了。

ようやく2歩目といった感じだが、めげずにやっていこう。。

[つづく](https://qiita.com/LittleGreenMen/items/85d41c1fa6bb9bf4da42)