【Django】forms.pyを使わずに送信データを複数テーブルに保存させる方法

　以前、1度のフォーム送信で複数テーブルにデータを保存する方法を記事にしたのですが、forms.pyやinlineformset_factory()のようなメソッドは、複雑な実装をする場合に、逆に使い方がややこしくなるなと感じてしまいました。

[【Django】1度のフォーム送信で複数テーブルにデータを保存する方法](https://qiita.com/kim-shun/items/04fdce686c725927d599)

　そこで今回、forms.pyを使用せずにフォーム送信データを複数テーブルに保存する方法を考えたので、記事にしたいと思います。
　参考にしたのは下記の記事です。

[【django】モデルのDBにデータを追加する create() と save()](https://kuma-server.com/create-save/)
[Djangoのget_object_or_404について](https://zenn.dev/gaipoi/articles/ab66a8f91835ce)
[【Django】formの値を取得する方法をたったの１行で解説](https://itc.tokyo/django/form-to-get-value/)
[【Django】フォームForm：データベースへの新規登録・更新機能を組み込む（instance変数）](https://office54.net/python/django/form-database-instance)

　説明に不要なコードは適宜省いて説明しています。

## 使用したモデル
　1度のフォーム送信で、漫画の名前を登録するテーブルと、漫画の評価を登録するテーブルの2つに、同時にデータを登録させていきます。

models.py
```python
class Comic(models.Model):
    comic_pk = models.UUIDField(primary_key=True, default=uuid.uuid4,editable=False)
    comic_name = models.CharField(verbose_name="漫画名",max_length=128,null=False,unique=True)
    created_at = models.DateTimeField(verbose_name='作成日時', auto_now_add=True)
    updated_at = models.DateTimeField(verbose_name='更新日時', auto_now=True)

class ComicEvaluation(models.Model):
    comic_evaluation_pk = models.UUIDField(primary_key=True, default=uuid.uuid4,editable=False)
    comic_name = models.ForeignKey(Comic,verbose_name="漫画名",to_field="comic_name",db_column="comic_name",related_name="c_name",max_length=128,null=False,on_delete=models.PROTECT)
    comic_score = models.PositiveSmallIntegerField(verbose_name="評点",validators=[MaxValueValidator(100)],null=False)
    comment = models.TextField(verbose_name="コメント",null=True)
    created_at = models.DateTimeField(verbose_name='作成日時', auto_now_add=True)
    updated_at = models.DateTimeField(verbose_name='更新日時', auto_now=True)
```

## HTMLとView
　htmlに関してですが、{% csrf_token %}の記載以外は一般的なhtmlと変わらない記述です。

templates/test.html
```django
{% block content %}
<form method="POST">{% csrf_token %}
  <div>漫画名<input name="comic_name" type="text"/></div>
  <div>評点<input type="number" name="score"/></div>
    <div>コメント<input type="text" name="comment"/></div>
  <button type="submit">送信</button>
</form>
{% endblock %}
```

　そして肝心のViewですが、こちらはrequest.POST[]でformの入力値を取得して、その取得した値を引数にしてsave()を行なっているだけです。create()でも保存は可能です。
　またComicEvaluationモデルのcomic_nameに関してはComicモデルのcomic_nameの外部キーとなるため、get_object_or_404メソッドを使用して取得した値(オブジェクト？)を引数にしています。

views.py
```python
def test(request):
    if request.method == 'POST':
        input_comic_name = request.POST["comic_name"]
        input_score = request.POST["score"]
        input_comment = request.POST["comment"]
        comic = Comic(comic_name=input_comic_name)
        comic.save()
        saved_comic_name = get_object_or_404(Comic,comic_name=input_comic_name)
        comic_evaluation = ComicEvaluation(comic_score=input_score,comment=input_comment,comic_name=saved_comic_name)
        comic_evaluation.save()
        return HttpResponseRedirect('../')
    return render(request,'test.html')
```

## 挙動


[![Image from Gyazo](https://i.gyazo.com/96b0f591e44afd5a4571b4ce0f05a7b3.gif)](https://gyazo.com/96b0f591e44afd5a4571b4ce0f05a7b3)

[![Image from Gyazo](https://i.gyazo.com/e8a0c64e8275baabff4e04bd3747f46a.gif)](https://gyazo.com/e8a0c64e8275baabff4e04bd3747f46a)

## 追記
　あくまでも複数テーブルにデータを保存させることを目的としたため、データベース設計やコードの書き方に問題はあると思いますが、事前にデータを入れている2テーブルを用いて選択項目を作り、3テーブルにデータを同時保存できるようにコードを書き加えたため追記しておきます。

[![Image from Gyazo](https://i.gyazo.com/25e118a39894c2a5f6c4cde04762d831.gif)](https://gyazo.com/25e118a39894c2a5f6c4cde04762d831)

[![Image from Gyazo](https://i.gyazo.com/84d3d3834f58ab573c4e622b0b59d78f.gif)](https://gyazo.com/84d3d3834f58ab573c4e622b0b59d78f)

models.py
```python
import uuid
from django.db import models
from django.core.validators import MaxValueValidator

class Comic(models.Model):
    comic_pk = models.UUIDField(primary_key=True, default=uuid.uuid4,editable=False)
    comic_name = models.CharField(verbose_name="漫画名",max_length=128,null=False,unique=True)
    created_at = models.DateTimeField(verbose_name='作成日時', auto_now_add=True)
    updated_at = models.DateTimeField(verbose_name='更新日時', auto_now=True)

class EvaluationItem(models.Model):
    evaluation_item_pk = models.UUIDField(primary_key=True, default=uuid.uuid4,editable=False)
    evaluation_item_id = models.PositiveSmallIntegerField(verbose_name="評価項目ID",null=False,unique=True)
    evaluation_item_name = models.CharField(verbose_name="評価項目名",max_length=50,null=False)

class EvaluationItemContents(models.Model):
    evaluation_item_contents_pk = models.UUIDField(primary_key=True, default=uuid.uuid4,editable=False)
    parent_fk = models.ForeignKey(EvaluationItem,verbose_name="親キー",db_column="parent_fk",related_name="contents",on_delete=models.PROTECT)
    evaluation_item_id = models.ForeignKey(EvaluationItem,to_field="evaluation_item_id",db_column="evaluation_item_id",verbose_name="評価項目ID",related_name="items",null=False,on_delete=models.PROTECT)
    item_content = models.CharField(verbose_name="項目内容名",max_length=50,null=False)

class ComicEvaluation(models.Model):
    comic_evaluation_pk = models.UUIDField(primary_key=True, default=uuid.uuid4,editable=False)
    comic_name = models.ForeignKey(Comic,verbose_name="漫画名",to_field="comic_name",db_column="comic_name",related_name="c_names",max_length=128,null=False,on_delete=models.PROTECT)
    comic_score = models.PositiveSmallIntegerField(verbose_name="評点",validators=[MaxValueValidator(100)],null=False)
    comment = models.TextField(verbose_name="コメント",null=True)
    nickname = models.ForeignKey(User,to_field="nickname",verbose_name="作成者",db_column="created_by",max_length=100,null=True,on_delete=models.PROTECT)
    created_at = models.DateTimeField(verbose_name='作成日時', auto_now_add=True)
    updated_at = models.DateTimeField(verbose_name='更新日時', auto_now=True)

class ComicEvaluationDetail(models.Model):
    comic_evaluation_detail_pk = models.UUIDField(primary_key=True, default=uuid.uuid4,editable=False)
    parent_fk = models.ForeignKey(ComicEvaluation,verbose_name="親キー",db_column="parent_fk",on_delete=models.PROTECT)
    comic_name = models.ForeignKey(Comic,verbose_name="漫画名",to_field="comic_name",db_column="comic_name",related_name="c_names_detail",max_length=128,null=False,on_delete=models.PROTECT)
    evaluation_item_id = models.PositiveSmallIntegerField(verbose_name="評価項目ID",null=False)
    item_content = models.CharField(verbose_name="評価項目内容",max_length=50,null=True)
    created_at = models.DateTimeField(verbose_name='作成日時', auto_now_add=True)
    updated_at = models.DateTimeField(verbose_name='更新日時', auto_now=True)
```

templates/test.html
```django
{% extends "base.html" %}
{% block title %}テストページ{% endblock %}
{% block header%}
<div>
  <a class="top_page" href="{% url 'e_comic:index' %}">トップページ</a>
  <a class="user_registration" href="{% url 'e_comic:users' %}">ユーザー登録</a>
</div>
<br>
{% endblock %}
{% block content %}
<form method="POST">{% csrf_token %}
  <div>漫画名<input name="comic_name" type="text"/></div>
  <div>評点<input type="number" name="score"/></div>
  <div>コメント<input type="text" name="comment"/></div>
  <div>
    {% for evaluation_item in evaluation_items %}
      <div>{{evaluation_item.evaluation_item_name}}
        <select name="{{evaluation_item.evaluation_item_id }}">
          <option selected>選択してください</option>
          {% for content in evaluation_item.contents.all %}
            <option value="{{content.item_content}}">{{content.item_content}}</option>
          {% endfor %}
        </select>
      </div>
    {% endfor %}
  </div>
  <button type="submit">送信</button>
</form>

{% endblock %}
```

views.py
```python
from django.http import HttpResponseRedirect
from django.shortcuts import render
from e_comic.services.SaveFormService import getChoiceItem,saveForm
from e_comic.DAO.SaveFormDao import countChoiceItem,saveComicEvaluationDetail

def test(request):
    choice_items = getChoiceItem()
    if request.method == 'POST':
        input_comic_name = request.POST["comic_name"]
        input_score = request.POST["score"]
        input_comment = request.POST["comment"]
        saveForm(input_comic_name,input_score,input_comment)
        count = countChoiceItem()
        for i in range(count):
            i += 1
            input_item = request.POST[str(i)]
            saveComicEvaluationDetail(input_comic_name,input_item,i)
        return HttpResponseRedirect('../')
    return render(request,'test.html',choice_items)
```
　views.pyのコードが長くなりすぎるため、新たに2ファイル作成して処理を分割させました。処理の分け方に関しては今後修正していきます。

SaveFormService.py
```python
from e_comic.DAO.SaveFormDao import choiceItem,saveComic,saveComicEvaluation

def getChoiceItem():
  evaluation_items = choiceItem()
  context = {
    "evaluation_items" : evaluation_items
  }
  return context

def saveForm(input_comic_name,input_score,input_comment):
  saveComic(input_comic_name)
  saveComicEvaluation(input_comic_name,input_score,input_comment)

```

SaveFormDao.py
```python
from django.shortcuts import get_object_or_404
from e_comic.models import Comic,ComicEvaluation,EvaluationItem,ComicEvaluationDetail

def choiceItem():
  evaluation_items = EvaluationItem.objects.all()
  return evaluation_items

def countChoiceItem():
  item_count = EvaluationItem.objects.all().count()
  return item_count

def saveComic(input_comic_name):
  comic = Comic(comic_name=input_comic_name)
  comic.save()
  
def saveComicEvaluation(input_comic_name,input_score,input_comment):
  saved_comic_name = get_object_or_404(Comic,comic_name=input_comic_name)
  comic_evaluation = ComicEvaluation(comic_score=input_score,comment=input_comment,comic_name=saved_comic_name)
  comic_evaluation.save()

def saveComicEvaluationDetail(input_comic_name,input_item,i):
  saved_comic_name = get_object_or_404(Comic,comic_name=input_comic_name)
  comic_evaluation_pk = get_object_or_404(ComicEvaluation,comic_name=input_comic_name)
  comic_evaluation_detail = ComicEvaluationDetail(evaluation_item_id=i,comic_name=saved_comic_name,parent_fk=comic_evaluation_pk,item_content=input_item)
  comic_evaluation_detail.save()
```



　以上です。
　まだまだプログラミング経験が浅いため、ご意見いただけるととても勉強になります。