DjangoのCRUD処理について

# はじめに
DjangoのCRUD処理について勉強した内容を記録に残しました。[Djangoビギナーズブック](https://www.amazon.co.jp/Django%E3%83%93%E3%82%AE%E3%83%8A%E3%83%BC%E3%82%BA%E3%83%96%E3%83%83%E3%82%AF-%E6%BB%9D%E6%BE%A4-%E6%88%90%E4%BA%BA/dp/4877834699)を参考人させていただきました。余談ですが、本記事を書いている時点でプレミア価格になってました…！大変お世話になっているので納得です。電子版が欲しいなあ。

# プロジェクトファイルを作成
下準備です。

```
$ django-admin startproject conf .
```
```python:conf/settings.py
# 変更した部分のみ抜粋
LANGUAGE_CODE = 'ja'
TIME_ZONE = 'Asia/Tokyo'
```

# homeアプリを作成
CRUD処理に入る前にホーム画面を表示するアプリを作ります。

```
$ python manage.py startapp home
``` 
```python:conf/settings.py
INSTALLED_APPS = [
    'home.apps.HomeConfig', # これを追加

# 省略
TEMPLATES = [
    {
        'BACKEND': 'django.template.backends.django.DjangoTemplates',
        'DIRS': [os.path.join(BASE_DIR, 'templates')], #ここを修正
```

ルーティングします。

```python:conf/urls.py
from django.contrib import admin
from django.urls import path, include # 追加

urlpatterns = [
    path('', include('home.urls')), # 追加
    path('admin/', admin.site.urls),
]

```
```python:home/urls.py
from  django.urls import path
from . import views

app_name = 'home'

urlpatterns = [
    path('', views.top, name='top')
]
```
```python:home/views.py
from django.shortcuts import render


def top(request):
    return render(request, 'home/top.html')
```
htmlを作成します。まずはベースとなるファイルを作ります。

```html:templates/base.html
{% load static %}
<!doctype html>
<html lang="ja">
  <head>
    <!-- Required meta tags -->
    <meta charset="utf-8">
    <meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no">

    <!-- 互換表示の解除 -->
    <meta http-equiv="X-UA-Compatible" content="IE=edge">

    <!-- Bootstrap CSS -->
    <link rel="stylesheet" href="https://stackpath.bootstrapcdn.com/bootstrap/4.3.1/css/bootstrap.min.css" integrity="sha384-ggOyR0iXCbMQv3Xipma34MD+dH/1fQ784/j6cY/iJTQUOhcWr7x9JvoRxT2MZw1T" crossorigin="anonymous">

    <title>ユーザー登録・更新</title>
  </head>
  <body>
    <!-- ナビバー -->
    <nav class="navbar navbar-expand-lg navbar-dark bg-primary">
      <a class="navbar-brand" href="{% url 'home:top' %}">テストサイト</a>
      <button class="navbar-toggler" type="button" data-toggle="collapse" data-target="#navbarSupportedContent" aria-controls="navbarSupportedContent" aria-expanded="false" aria-label="Toggle navigation">
        <span class="navbar-toggler-icon"></span>
      </button>

      <div class="collapse navbar-collapse" id="navbarSupportedContent">
        <ul class="navbar-nav mr-auto">
          <li class="nav-item">
            <a class="nav-link" href="{% url 'home:top' %}">Home</a>
          </li>
        </ul>
      </div>
    </nav>

    <!-- コンテンツ -->
    {% block content %}{% endblock %}

    <!-- Optional JavaScript -->
    <!-- jQuery first, then Popper.js, then Bootstrap JS -->
    <script src="https://code.jquery.com/jquery-3.3.1.slim.min.js" integrity="sha384-q8i/X+965DzO0rT7abK41JStQIAqVgRVzpbzo5smXKp4YfRvH+8abtTE1Pi6jizo" crossorigin="anonymous"></script>
    <script src="https://cdnjs.cloudflare.com/ajax/libs/popper.js/1.14.7/umd/popper.min.js" integrity="sha384-UO2eT0CpHqdSJQ6hJty5KVphtPhzWj9WO1clHTMGa3JDZwrnQq4sF86dIHNDz0W1" crossorigin="anonymous"></script>
    <script src="https://stackpath.bootstrapcdn.com/bootstrap/4.3.1/js/bootstrap.min.js" integrity="sha384-JjSmVgyd0p3pXB1rRibZUAYoIIy6OrQ6VrjIEaFf/nJGzIxFDsf4x0xIM+B07jRM" crossorigin="anonymous"></script>
  </body>
</html>
```
```html:home/top.html
{% extends "base.html" %}

<!-- コンテンツ -->
{% block content %}
<div class="container">
    <h1>home</h1>
</div>
{% endblock %}
```
開発用サーバーを起動して、ブラウザからサーバーにアクセスすると、以下のようなページが表示されるはずです。
![img_1.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/689015/fb4ef1fe-fc4c-6d8b-fe8d-92bf0683e5d0.png)


# レビューアプリを作成

前置きが長くなりましたが、CRUD処理のためのアプリを作成します。ご飯屋さんをレビューするアプリをモチーフにします。

```
$ python manage.py startapp reviews
``` 
```python:conf/settings.py
INSTALLED_APPS = [
    'home.apps.HomeConfig',
    'reviews.apps.ReviewsConfig', # これを追加
]
```
```python:conf/urls.py
urlpatterns = [
    path('', include('home.urls')),
    path('reviews/', include('reviews.urls')), # 追加
]
```
```python:reviews/urls.py
from django.urls import path
from . import views


app_name = 'reviews'

urlpatterns = [
]
```

モデルを作成します。店名とタイトルとレビュー本文、星の数、作成日時をフィールドとします。

```python:reviews/models.py
from django.db import models
from django.utils import timezone

class Review(models.Model):
    STARS = (
        (1, '★'),
        (2, '★★'),
        (3, '★★★'),
        (4, '★★★★'),
        (5, '★★★★★'),
    )

    store_name = models.CharField('店名', max_length=255)
    title = models.TextField('タイトル', max_length=255)
    text = models.TextField('口コミテキスト', blank=True)
    stars = models.IntegerField('星の数', choices=STARS)
    created_at = models.DateTimeField('作成日', default=timezone.now)

    def __str__(self):
        return self.title
```

モデルの定義が終わったら、マイグレーションします。

```
$ python manage.py makemigrations reviews
```
```
$ python manage.py migrate
```

管理サイトで表示できるようにします。

```python:reviews/admin.py
from django.contrib import admin
from .models import Review


admin.site.register(Review)
```

# データを追加

スーパーユーザーを作成します。

```
$ python manage.py createsuperuser
```
ブラウザから管理サイト(http://***/admin)を開き、ログインします。
![img_2.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/689015/46e054e8-89e8-f180-3be2-88d20d25f01b.png)

適当にデータを追加します。
![img_3.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/689015/1327166b-e1f7-1ec5-e6a3-a34d0c925526.png)

# 一覧と詳細ページ(Read)

一覧ページ(/)と詳細ページ(/detail/int)を作成します。int:pkは後でreview_list.htmlで指定することになります。

```python:reviews/urls.py
urlpatterns = [
    path('', views.review_list, name='review_list'), 
    path('detail/<int:pk>/', views.review_detail, name='review_detail'),
]
```
views.pyを作成します。detail/intのintにデータベースのPrimary Keyに存在しない数字を入れたら、Not Foundになるようにしています。

```python:reviews/views.py
from django.shortcuts import render, get_object_or_404
from .models import Review


def review_list(request):
    context = {
        'review_list': Review.objects.all().order_by('-created_at'),
    }
    return render(request, 'reviews/review_list.html', context)

def review_detail(request, pk): # pkはreview_idのこと
    #review = Review.objects.get(id=pk) # pkはreview_idのこと
    context = {
        #'review': review,
        'review': get_object_or_404(Review, pk=pk),
    }
    return render(request, 'reviews/review_detail.html', context)
```
htmlを作成します。

```html:templates/base.html
<div class="collapse navbar-collapse" id="navbarSupportedContent">
  <ul class="navbar-nav mr-auto">
    <li class="nav-item">
      <a class="nav-link" href="{% url 'home:top' %}">Home</a>
    </li>
    <!-- レビューページへのリンク -->
    <li class="nav-item">
      <a class="nav-link" href="{% url 'reviews:review_list' %}">Reviews</a>
    </li>
  </ul>
</div>
```
review_list.html（一覧）を作成します。int:pkはurlタグで指定します。'reviews:review_detail'の後に半角スペースを入れて、review.pkのように埋め込みたいデータを書きます。

```html:templates/review_list.html
{% extends "base.html" %}

<!-- コンテンツ -->
{% block content %}
<div class="container">
  <h1>Reviews</h1>
  <hr>
  {% for review in review_list %}
    <article>
      <h3><a href="{% url 'reviews:review_detail' review.pk %}">{{ review.store_name }}</a></h3>
      <p>{{ review.get_stars_display }} - {{ review.title }}</p>
    </article>
  {% endfor %}
</div>
{% endblock %}
```
詳細ページのhtmlを作成します。


```html:templates/review_detail.html
{% extends "base.html" %}

<!-- コンテンツ -->
{% block content %}
<div class="container">
  <article>
    <h1>{{ review.store_name }}</h1>
    <p>{{ review.get_stars_display }} - {{ review.title }}</p>
    {{ review.text | linebreaks }}
  </article>
</div>
{% endblock %}
```
開発用サーバーを起動し、ブラウザでアクセスすると、以下のページが表示されます。
![img_4.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/689015/bcd54e5c-3948-862b-012d-d58de965a893.png)

![img_5.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/689015/4ad0ae3b-dab6-1ac3-a8c7-b94aed56d6a1.png)

# レビュー作成ページ(Create)
レビュー作成ページ(create/)を追加します。

```python:reviews/urls.py
urlpatterns = [
    path('', views.review_list, name='review_list'), 
    path('detail/<int:pk>/', views.review_detail, name='review_detail'), 
    path('create/', views.review_create, name='review_create'), # 追加：作成ページと送信処理
    #path('create/send/', views.review_create_send, name='review_create_send'), # 参考：送信処理
]
```
レビュー作成と送信処理を一つのビューに書くと以下のようになります。

```python:reviews/views.py
from .forms import ReviewCreateForm # フォーム機能
from .models import Review
from django.shortcuts import render, get_object_or_404, redirect

# 省略

'''追加：作成&送信'''
def review_create(request):
    form = ReviewCreateForm(request.POST or None)

    # request.method == 'POST' : Submitボタンが押された場合
    # form.is_valid() : 入力内容が問題ない
    # 上記を両方満たせば、データを保存して一覧ページへリダイレクトする。
    if request.method == 'POST' and form.is_valid():
        form.save()
        return redirect('reviews:review_list')

    # 通常のページアクセス時はmethodがGETになる
    # 入力内容に問題がある場合は、form.is_validがFalseになり、ReviewCreateFormにエラー情報が付与される
    context = {
        'form': ReviewCreateForm()
    }
    return render(request, 'reviews/review_form.html', context)
```
分かりやすくするため、レビュー作成ページの表示と送信後の処理を分けて書くと以下のようになります。

```python:reviews/views.py

'''作成'''
def review_create(request):
    context = {
        'form': ReviewCreateForm()
    }
    return render(request, 'reviews/review_form.html', context)


'''送信ボタンを押したときの処理'''
def review_create_send(request):
    #name = request.POST.get('store_name')
    #print('送信されたデータ→{}'.format(name))
    form = ReviewCreateForm(request.POST)
    
    # 入力内容に問題が無いかチェック
    if form.is_valid():
        form.save()
        return redirect('reviews:review_list')
        #return render(request, 'reviews/review_list.html') # renderを使うと二重送信の恐れあり。

    # 問題があれば、入力画面のテンプレートファイルにエラーメッセージ入りのformオブジェクトを渡す
    else:
        context = {
            'form': form,
        }
        return render(request, 'reviews/review_form.html', context) # saveしてないので、renderでも二重送信の恐れはない。



```

forms.pyを作成します。フォームの役割は大まかに二つあります。

- form内の各入力欄、選択欄を自動的に生成する
- 送信されてきたデータをまとめる

```python:reviews/forms.py
from django import forms
from . models import Review


class ReviewCreateForm(forms.ModelForm):

    class Meta:
        model = Review
        fields = '__all__'
        #fields = ('store_name',)
        #exclude = ('created_at',)

    # bootstrap対応のため、classを指定する。
    def __init__(self, *args, **kwargs):
        super().__init__(*args, **kwargs)
        for field in self.fields.values():
            field.widget.attrs['class'] = 'form-control'
```
review_list.htmlに作成ページへのリンクを追加します。

```html:templates/reviews/review_list.html
{% extends "base.html" %}

<!-- コンテンツ -->
{% block content %}
<div class="container">
  <!-- 省略 -->
  <a href="{% url 'reviews:review_create' %}">to create page</a>
</div>
{% endblock %}
```
レビュー作成ページのhtmlを作成します。action属性を空文字にした場合、「現在のURL=review/create」がクリック後のアクセス先になります。こうすることでコードが再利用しやすくなります。

```html:templates/reviews/review_form.html
{% extends "base.html" %}

<!-- コンテンツ -->
{% block content %}
<div class="container">
  <!-- フォーム利用。 -->
  <form class="form-group mt-2" action="" method="POST">
    {{ form.as_p }}
    {% csrf_token %}
    <button type="submit" class="btn btn-primary ml-1">Submit</button>
  </form>
</div>
{% endblock %}
```
実際の作成ページは以下のようになります。一覧ページに作成ページへのリンクが追加されています。

![img_6.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/689015/3b85800c-71ff-ac3e-f2f7-0ed13231e72d.png)

作成ページはこのような形です。（本当は作成日は入力可能です。。）

![img_7.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/689015/783b1225-4025-c0c5-be47-1853736d0d2a.png)

# 更新ページ(Update)
作成(Create)ができるようになったので、次は更新(Update)を実装します。

```python:reviews/urls.py
urlpatterns = [
    # 省略
    path('update/<int:pk>/', views.review_update, name='review_update'), # 更新
]
```

関数ビューを追加します。ポイントは、作成(Create) で作ったreview_form.htmlを再利用している点です。また、３行目でReviewCreateFormをインスタンス化する際に、instance引数に、Reviewモデルインスタンスを渡しています。これによって、入力欄に現在の値（店名やレビュー本文など）が入った状態で表示されます。

```python:reviews/views.py
def review_update(request, pk):
    review = get_object_or_404(Review, pk=pk)
    form = ReviewCreateForm(request.POST or None, instance=review)
    if request.method == 'POST' and form.is_valid():
        form.save()
        return redirect('reviews:review_list')

    context = {
        'form': form
    }
    return render(request, 'reviews/review_form.html', context)
```

更新ページへのリンクを、詳細ページに追加します。

```html:templates/reviews/review_detail.html
<a href="{% url 'reviews:review_update' review.pk %}">更新</a>
```

forms.pyを少し変更します。作成日フィールド(created_at)を除外します。

```python:reviews/forms.py
class ReviewCreateForm(forms.ModelForm):

    class Meta:
        model = Review
        #fields = '__all__'
        exclude = ('created_at',)
```

作成日は表示はしたいので、review_form.htmlを修正します。

```html:templates/reviews/review_form.html
<form class="form-group mt-2" action="" method="POST">
    {{ form.as_p }}
    <p>作成日 : {{ form.instance.created_at }}</p>
    {% csrf_token %}
    <button type="submit" class="btn btn-primary ml-1">Submit</button>
</form>
```

詳細ページに更新ページへのリンクが追加されています。

![img_8.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/689015/deacbb65-8424-c35f-3dad-e10f84d65edd.png)

リンクをクリックすると、更新ページになります。作成ページと同様にreview_form.htmlを使用しているのでデザインは同じですが、店名など現在の値が入っています。

![img_9.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/689015/6e16ce62-99a6-c374-35b3-95d9cbf5b5b5.png)

# 削除(Delete)
読み込み(Read)、作成(Create)、更新(Update)ができたので、最後に削除(Delete)を実装します。まずはURL定義から。どのレビューを削除するか指定する必要があるので、pkが要ります。

```python:reviews/urls.py
urlpatterns = [
    # 省略
    path('delete/<int:pk>/', viwes.review_delete, name='review_delete'), # 削除
]
```

続いて関数ビューを定義します。削除ボタンを押すと、リクエストメソッドはPOSTになるので、ifの中に入流ので、データを削除し、一覧ページにリダイレクトします（二重送信防止のため）。通常のアクセスでは削除確認用のページに遷移します。その際、Reviewモデルインスタンスをテンプレートファイルに渡します。

```python:review/views.py
def review_delete(request, pk):
    review = get_object_or_404(Review, pk=pk)
    if request.method == 'POST':
        review.delete()
        return redirect('reviews:review_list')
    
    context = {
        'review': review
    }
    return render(request, 'reviews/review_confirm_delete.html', context)
```

テンプレートファイルを作成します。削除するだけなので、ユーザーが入力する欄はありませんが、CSRF対策とリクエストメソッドをPOSTにするためform要素を使っています。

```html:templates/reviews/review_confirm_delete.html
{% extends "base.html" %}

<!-- コンテンツ -->
{% block content %}
<div class="container">
  <p>{{ review.store_name }}のレビューを削除します。よろしいですか？</p>
  <form action="", method='POST'>
    {% csrf_token %} 
    <button type="submit">削除</button>
  </form>
</div>
{% endblock %}
```

詳細ページから削除確認ページへのリンクを貼ります。

```html:templates/reviews/review_detail.html
<a href="{% url 'reviews:review_delete' review.pk %}">削除</a>
```

削除ページへのリンクが追加になっています。

![img_10.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/689015/b45c0f2c-5497-d039-b6f9-99eec702fa55.png)

削除リンクをクリックすると、削除確認ページへジャンプします。

![img_11.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/689015/2a6f118a-a4ca-a302-af6d-a1e3fda63541.png)