【Go言語】WebフレームワークBeegoでTodoタスクを登録する方法(1対多のER編)

# サンプル
#### 登録画面から入力項目を入力して登録ボタンをクリックします。
![Beegoの登録画面.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1169664/5101e4f3-8757-a62b-1b71-d00f1658d7a9.png)

正常に登録できると、`PostgreSQL`に登録されます。
![登録後のPostgre.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1169664/6c731e14-f7d9-066c-7390-cae99782b84a.png)

上記の機能を作っていきます。

# ER図
モデルクラスは`Register`と`Task`の2種類です。
`Register`モデルには、ユーザ情報を、`Task`モデルにはタスク情報を入れています。

登録済みのユーザのみがタスクを登録できるので、`Register`モデルと`Task`モデルは<b>1対多</b>の関係となります。

![ER図.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1169664/f2ded4a1-574c-1ac0-be0c-2493e5b58994.png)

# ディレクトリ構成

```
MY-FIRST-BEEGO-PROJECT
├─ conf
│  └─ app.conf
├─ controllers
│  └─ default.go
├─ crypt
│  └─ crypt.go
├─ models
│  └─ todo.go
├─ routers
│  └─ router.go
└─ static
   ├─ css
   ├─ img
   ├─ js
   │  ├─ form.js
   │  ├─ login.js
   │  ├─ reload.min.js
   │  ├─ todoCreate.js
   │  ├─ todoCard.js
   │  └─ todoList.js
   ├─ lib
   │  └─ bootstrap
   │     ├─ css
   │     │  ├─ bootstrap-grid.min.css
   │     │  └─ bootstrap.min.css
   │     ├─ js
   │     │  ├─ bootstrap-bundle.min.js
   │     │  └─ bootstrap.min.js
   │     ├─ jquery
   │     │  └─ jquery.js
   │     └─ upload
   ├─ views
   │  ├─ mytodo
   │  │  ├─ addTodo.tpl
   │  │  └─ myList.tpl
   │  ├─ index.tpl
   │  ├─ subIndex.tpl
   │  └─ thirdIndex.tpl
   └─ main.go

```

# モデルクラスの修正

リレーショナルが<b>1対多</b>になるように、`Task`モデルの中に`Register`モデルをポインタ型で保持しておくようにしました。

`on_delete(do_nothing)`は、登録されたユーザー`（Register）`が削除された場合にそのユーザーに関連するタスクが削除されないように指定しています。

```models/todo.go
package models // ここでパッケージ名を指定

import (
	"time"

	"github.com/beego/beego/v2/client/orm"
	_ "github.com/lib/pq" // PostgreSQL ドライバをインポート
)

type Register struct {
	Id        int64     `orm:"auto"`
	Name      string    `orm:"size(20)"`
	Email     string    `orm:"size(50)"`
	Password  string    `orm:"size(100)"`
	CreatedAt time.Time `orm:"auto_now;type(datetime)"`
}

// 20250211追加
type Task struct {
	TaskId          int64     `orm:"auto"`
	TaskName        string    `orm:"size(20)"`
	TaskDescription string    `orm:"size(100)"`
	TaskPriority    string    `orm:"size(5)"`
	TaskImage       string    `orm:"size(100)"`
	Register        *Register `orm:"rel(fk);on_delete(do_nothing)"` // 外部キーリレーション
}

func init() {
	// PostgreSQLのドライバ登録
	orm.RegisterDriver("postgres", orm.DRPostgres)

	// データベース接続設定
	orm.RegisterDataBase("default", "postgres", "user=postgres password=postgres host=localhost port=5432 dbname=beegoDb sslmode=disable")

	// モデルの登録
	orm.RegisterModel(new(Register)) // Todo

	// Taskモデルの登録
	orm.RegisterModel(new(Task))

	// テーブルが存在しない場合に作成（強制的にマイグレーション）
	orm.RunSyncdb("default", false, true)
}

```

# フロント側のコード

```views/mytodo/addTodo.tpl
<!DOCTYPE>
<html>
    <head>
        <title>Todo登録</title>
        <!--共通-->
        <link rel="stylesheet" type="text/css" href="/static/lib/bootstrap/css/bootstrap.min.css"/>
        <script type="text/javascript" src="/static/lib/jquery/jquery.js"></script>
        <!--個別-->
        <script type="text/javascript" src="/static/js/taskCreate.js"></script>        
    </head>
    <body>
        <h1>Todo登録</h1>
        <div class="container">
            サンプル
            <form enctype="multipart/form-data">
                <div>
                    <input type="hidden" id="userId" name="userId" value=""/>
                </div>
                <div class="form-group mb-3">
                    <label>タスク名</label>
                    <input type="text" id="myTask" name="myTask" class="form-control border border-dark" required/>
                </div>
                <div class="form-group mb-3">
                    <label for="exampleFormControlTextarea1" class="form-label">タスク詳細</label>
                    <textarea class="form-control" id="myTaskDescription" name="myTaskDescription" rows="3"></textarea>
                </div>
                <div class="form-group mb-3">
                    <label>優先度</label>
                    <select id="myTaskPriority" name="myTaskPriority" class="form-select form-select-lg mb-3" aria-label=".form-select-lg">
                        <option>-- 選択してください --</option>
                        <option value="1">高</option>
                        <option value="2">中</option>
                        <option value="3">低</option>
                    </select>
                </div>
                <div class="form-group mb-3">
                    <label>タスク画像</label>
                    <input type="file" id="myTaskImage" name="myTaskImage" class="form-control border border-dark"/>
                </div>
                <div class="form-group mb-3">
                    <button id="todoCreate" class="btn btn-primary">
                        登録<br/><small>Create</small>
                    </button>
                    <button id="backToTopPage" class="btn btn-secondary">
                        一覧に戻る<br/><small>Back To TopPage</small>
                    </button>
                </div>
            </form>
        </div>
    </body>
</html>
```

```static/js/taskCreate.js
$(document).ready(function(){
    //Session StorageからユーザIDを取得する
    const userId = sessionStorage.getItem("userId");
    let inputValueInUserId = document.getElementById("userId");
    inputValueInUserId.value = userId;

    //一覧に戻るボタンをクリックしたときの処理
    $('#backToTopPage').on('click',function(){
        location.replace("/sub");
    });

    //登録ボタンをクリックしたときの処理
    $('#todoCreate').on('click',function(e){
        //POST通信するための設定
        e.preventDefault();

        //オプションタグの選択項目を取得
        const selectedTaskPriority = document.getElementById("myTaskPriority");

        //フォームデータの作成
        let formData = new FormData();
        formData.append('userId',$("#userId").val());
        formData.append('myTask',$("#myTask").val());
        formData.append('myTaskDescription',$("#myTaskDescription").val());
        formData.append('myTaskPriority',$("#myTaskPriority").val());   //  $("#myTaskPriority").val()
        formData.append('myTaskImage',$("#myTaskImage")[0].files[0]);
        const fileInfo = $("#myTaskImage").val();
        console.log(fileInfo);

        $.ajax({
            url:"/controllers/todoAdd",
            method:"POST",
            data:formData,
            processData:false,
            contentType:false,
            success:function(response){
                alert("タスクの登録に成功しました。");
            },
            error:function(response){
                alert("タスクの登録に失敗しました。");
            }
        });
    });

});
```

# サーバ側のコード

`targetTask.Register`は`models.Register型`のフィールドですが、これはポインタ型であり、初期化されていない状態ではnilになります。
nilの状態で`targetTask.Register.Id`にアクセスしようとすると、nilポインタの参照エラーが発生します。

### 解決方法：
`targetTask.Register`を初期化する必要があります。
以下のように、`targetTask.Register`に新しい`models.Register`のインスタンスを代入してください。

```
// Registerを初期化
targetTask.Register = &models.Register{}
```

以上を踏まえたコードがこちらです↓
```controllers/default.go
// タスク登録用コントローラ
func (c *TodoAddController) Post() {
	//フォームデータから入力情報を取得する
	userId := c.GetString("userId") //20250211
	task := c.GetString("myTask")
	taskDescription := c.GetString("myTaskDescription")
	taskPriority := c.GetString("myTaskPriority")
	fmt.Println("フォーム画面から取得したユーザIDは、", userId) //20250211
	fmt.Println("フォーム画面から取得したタスク名は、", task)
	fmt.Println("フォーム画面から取得したタスク説明は、", taskDescription)
	fmt.Println("フォーム画面から取得したタスク画像は、", taskPriority)

	//ファイルをアップロード
	f, h, err := c.GetFile("myTaskImage")
	if err != nil {
		fmt.Println("ファイルエラー", err)
		c.Ctx.ResponseWriter.WriteHeader(500)
		c.Ctx.WriteString("画像アップロード失敗")
		return
	}

	defer f.Close()
	// 画像を保存するパスを指定
	uploadPath := "static/upload/"
	err = os.MkdirAll(uploadPath, os.ModePerm) // ディレクトリがない場合に作成

	if err != nil {
		fmt.Println("ファイル保存エラー", err)
		c.Ctx.ResponseWriter.WriteHeader(500)
		c.Ctx.WriteString("画像保存失敗")
		return
	}

	// ファイル名を表示
	//fmt.Println("ファイル名は", fileName)

	// 画像ファイル名を取得して保存
	fileName := h.Filename
	savePath := filepath.Join(uploadPath, fileName)

	// ファイルを保存
	outFile, err := os.Create(savePath)
	if err != nil {
		fmt.Println("ファイル保存エラー", err)
		c.Ctx.ResponseWriter.WriteHeader(500)
		c.Ctx.WriteString("画像保存に失敗しました")
		return
	}

	defer outFile.Close()

	// アップロードされたファイルの内容を保存
	_, err = f.Seek(0, 0) // ファイルの最初に戻す
	if err != nil {
		fmt.Println("ファイル読み込みエラー", err)
		c.Ctx.ResponseWriter.WriteHeader(500)
		c.Ctx.WriteString("ファイル読み込みに失敗しました")
		return
	}

	_, err = outFile.ReadFrom(f)
	if err != nil {
		fmt.Println("ファイル書き込みエラー", err)
		c.Ctx.ResponseWriter.WriteHeader(500)
		c.Ctx.WriteString("ファイル書き込みに失敗しました")
		return
	}

	// ファイル名を表示
	fmt.Println("ファイル名は", fileName)

	//----Taskテーブルへ登録する処理----

	//ORMを取得する
	o := orm.NewOrm()

	//Taskモデルのインスタンスを生成する
	var targetTask models.Task
	// Registerを初期化
	targetTask.Register = &models.Register{}

	//トランザクション開始
	tx, err := o.Begin()

	if err != nil {
		fmt.Println("トランザクション開始エラー", err)
		c.Data["json"] = map[string]string{
			"status":  "失敗",
			"message": "トランザクションの開始に失敗しました。",
		}
	}
	//ユーザIDをStringからInt64型へ変換する
	numUserId, _ := strconv.ParseInt(userId, 10, 64)
	fmt.Println("整数型に変換したIDは、", numUserId)
	targetTask.Register.Id = numUserId //ユーザID
	fmt.Println("登録したRegisterId", targetTask.Register.Id)

	targetTask.TaskName = task
	fmt.Println("登録したTaskName", targetTask.TaskName)
	targetTask.TaskDescription = taskDescription
	fmt.Println("登録したTaskName", targetTask.TaskDescription)

	targetTask.TaskPriority = taskPriority
	fmt.Println("登録したTaskName", targetTask.TaskPriority)              //taskPriority
	targetTask.TaskImage = "http://localhost:8080/upload/" + fileName //fileName

	fmt.Println("登録したRegisterId", targetTask.Register.Id)
	fmt.Println("登録したTaskName", targetTask.TaskName)
	fmt.Println("登録したTaskName", targetTask.TaskDescription)
	fmt.Println("登録したTaskName", targetTask.TaskPriority)
	fmt.Println("登録したTaskName", targetTask.TaskImage)

	_, err = tx.Insert(&targetTask)

	if err != nil {
		tx.Rollback()
		fmt.Println("エラー発生", err)
		c.Data["json"] = map[string]string{
			"status":  "失敗",
			"message": "タスク登録に失敗しました。",
		}
		c.ServeJSON()
		return
	}

	//成功した場合コミットする
	err = tx.Commit()

	if err != nil {
		fmt.Println("トランザクションコミットに失敗しました。")
		c.Data["json"] = map[string]string{
			"status":  "失敗",
			"message": "トランザクションコミットに失敗しました。",
		}
		c.ServeJSON()
		return
	}

	// 成功レスポンスを返す
	c.Ctx.ResponseWriter.WriteHeader(200)
	c.Ctx.WriteString("タスク登録が成功しました")
	//c.SaveFile("uploadname", "static/upload/"+h.Filename)
}
```

全体のコードはこちら↓

```controllers/default.go
package controllers

import (
	"fmt"
	"time"

	"my-first-beego-project/models"
	_ "my-first-beego-project/models"

	"github.com/astaxie/beego"
	"github.com/beego/beego/v2/client/orm"

	"my-first-beego-project/crypto"

	"os"
	"path/filepath"

	"strconv"
)

type MainController struct {
	beego.Controller
}

func (c *MainController) Get() {
	c.Data["Website"] = "beego.me"
	c.Data["Email"] = "astaxie@gmail.com"
	c.TplName = "index.tpl"
}

// 20250205追加
type SubController struct {
	beego.Controller
}

type ThirdController struct {
	beego.Controller
}

type UserController struct {
	beego.Controller
}

// ログイン画面
type LoginController struct {
	beego.Controller
}

// タスクの登録画面
type TodoCreateController struct {
	beego.Controller
}

// タスク登録
type TodoAddController struct {
	beego.Controller
}

// 20250205
func (c *SubController) Get() {
	c.Data["Page"] = "SubPage"
	c.TplName = "subIndex.tpl"
}

// @router :id[get]
func (this *ThirdController) Get() {
	this.Data["ID"] = this.Ctx.Input.Param(":id")
	this.TplName = "thirdIndex.tpl"
}

// ログイン画面コントローラ
func (c *LoginController) Get() {
	c.TplName = "login.tpl"
}

func (c *UserController) Post() {
	//フォームデータを取得する
	name := c.GetString("name")
	email := c.GetString("email")
	password := c.GetString("password")

	//デバッグ用
	//fmt.Println("フォーム画面から取得したNameは：", name)
	//fmt.Println("フォーム画面から取得したEmailは：", email)
	//fmt.Println("フォーム画面から取得したPasswordは：", password)

	c.Data["json"] = map[string]interface{}{
		"status":  "成功",
		"message": "フォーム画面からデータを受け取りました。",
		"requestData": map[string]string{
			"Name":     name,
			"Email":    email,
			"Password": password,
		},
	}
	/*
		c.Data["json"] = map[string]string{
			"status":"成功",
			"message":"フォーム画面からデータを受け取りました。",
		}
	*/
	//パスワードのハッシュ化
	generatedHashedPassword, error := crypto.GeneratedPasswordHashed(password)

	if error != nil {
		fmt.Println("パスワードを暗号化中にエラーが発生しました。", error)
		c.Data["json"] = map[string]string{
			"status":  "失敗",
			"message": "パスワードのハッシュ化に失敗しました。",
		}
		c.ServeJSON()
		return
	}
	//ORMを取得する
	o := orm.NewOrm()
	//モデルTodoのインスタンスを生成
	var registerUser models.Register //var todo models.Todo
	//トランザクション開始
	tx, err := o.Begin()

	if err != nil {
		fmt.Println("トランザクション開始エラー", err)
		c.Data["json"] = map[string]string{
			"status":  "失敗",
			"message": "トランザクションの開始に失敗しました。",
		}
	}

	registerUser.Name = name
	registerUser.Email = email
	registerUser.Password = generatedHashedPassword
	registerUser.CreatedAt = time.Now()

	_, err = tx.Insert(&registerUser)
	if err != nil {
		tx.Rollback()
		fmt.Println("エラー発生", err)
		c.Data["json"] = map[string]string{
			"status":  "失敗",
			"message": "ユーザ登録に失敗しました。",
		}
		c.ServeJSON()
		return
	}
	//成功した場合コミット
	err = tx.Commit()

	if err != nil {
		fmt.Println("トランザクションのコミットに失敗しました。", err)
		c.Data["json"] = map[string]string{
			"status":  "失敗",
			"message": "トランザクションのコミットに失敗しました。",
		}
		c.ServeJSON()
		return
	}

	//登録したデータを取得する
	var registeredUser models.Register
	err = o.QueryTable("register").Filter("Email", email).One(&registeredUser)

	if err != nil {
		fmt.Println("ユーザ情報の取得に失敗しました。", err)
		c.Data["json"] = map[string]string{
			"status":  "失敗",
			"message": "ユーザー情報の取得に失敗しました。",
		}
		c.ServeJSON()
		return
	}

	fmt.Println("登録されたユーザは、", registeredUser.Name)
	fmt.Println("登録されたEmailは、", registeredUser.Email)

	//成功時のレスポンス
	c.Data["json"] = map[string]interface{}{
		"status":  "成功",
		"message": "ユーザーが正常に登録されました",
		"requestData": map[string]string{
			"Name":  registeredUser.Name,
			"Email": registeredUser.Email,
		},
	}

	//JSONデータを返却する
	c.ServeJSON()
}

func (c *LoginController) Post() {

	//フォームデータを取得する
	email := c.GetString("email")
	password := c.GetString("password")

	fmt.Println("ログイン画面から渡されたメールアドレスは、", email)
	fmt.Println("ログイン画面から渡されたパスワードは、", password)

	//ORMを定義する
	o := orm.NewOrm()

	//登録済みのデータを取得する
	var targetUser models.Register
	// `err` を宣言
	err := o.QueryTable("register").Filter("Email", email).One(&targetUser)
	fmt.Println("データベースから取得したユーザ情報は、", targetUser.Id)
	if err != nil {
		fmt.Println("ユーザ情報を取得できませんでした。", err)
		c.Data["json"] = map[string]string{
			"status":  "失敗",
			"message": "ユーザ情報をしゅとくできませんでした。",
		}
		c.ServeJSON()
		return
	}
	//fmt.Println("ここまで1", err)
	err = crypto.ComparedHashedPassword(targetUser.Password, password)
	//fmt.Println("ここまで2", err)
	if err != nil {
		fmt.Println("パスワードが一致しませんでした。", err)
		c.Data["json"] = map[string]string{
			"status":  "失敗",
			"message": "パスワードが一致しませんでした。",
		}
		c.ServeJSON()
		return
	}
	//fmt.Println("ここまで3")
	c.Data["json"] = map[string]interface{}{
		"status":  "成功",
		"message": "ログインに成功しました。",
		"user": map[string]interface{}{
			"id":    targetUser.Id,
			"name":  targetUser.Name,
			"email": targetUser.Email,
		},
	}
	c.ServeJSON()
}

func (c *TodoCreateController) Get() {
	//fmt.Println("TodoCreateControllerに来た")
	c.TplName = "mytodo/addTodo.tpl" //c.TplName = "mytodo/addTodo.tpl"
	//fmt.Println("TodoCreateControllerに来たよ")
}

// タスク登録用コントローラ
func (c *TodoAddController) Post() {
	//フォームデータから入力情報を取得する
	userId := c.GetString("userId") //20250211
	task := c.GetString("myTask")
	taskDescription := c.GetString("myTaskDescription")
	taskPriority := c.GetString("myTaskPriority")
	fmt.Println("フォーム画面から取得したユーザIDは、", userId) //20250211
	fmt.Println("フォーム画面から取得したタスク名は、", task)
	fmt.Println("フォーム画面から取得したタスク説明は、", taskDescription)
	fmt.Println("フォーム画面から取得したタスク画像は、", taskPriority)

	//ファイルをアップロード
	f, h, err := c.GetFile("myTaskImage")
	if err != nil {
		fmt.Println("ファイルエラー", err)
		c.Ctx.ResponseWriter.WriteHeader(500)
		c.Ctx.WriteString("画像アップロード失敗")
		return
	}

	defer f.Close()
	// 画像を保存するパスを指定
	uploadPath := "static/upload/"
	err = os.MkdirAll(uploadPath, os.ModePerm) // ディレクトリがない場合に作成

	if err != nil {
		fmt.Println("ファイル保存エラー", err)
		c.Ctx.ResponseWriter.WriteHeader(500)
		c.Ctx.WriteString("画像保存失敗")
		return
	}

	// ファイル名を表示
	//fmt.Println("ファイル名は", fileName)

	// 画像ファイル名を取得して保存
	fileName := h.Filename
	savePath := filepath.Join(uploadPath, fileName)

	// ファイルを保存
	outFile, err := os.Create(savePath)
	if err != nil {
		fmt.Println("ファイル保存エラー", err)
		c.Ctx.ResponseWriter.WriteHeader(500)
		c.Ctx.WriteString("画像保存に失敗しました")
		return
	}

	defer outFile.Close()

	// アップロードされたファイルの内容を保存
	_, err = f.Seek(0, 0) // ファイルの最初に戻す
	if err != nil {
		fmt.Println("ファイル読み込みエラー", err)
		c.Ctx.ResponseWriter.WriteHeader(500)
		c.Ctx.WriteString("ファイル読み込みに失敗しました")
		return
	}

	_, err = outFile.ReadFrom(f)
	if err != nil {
		fmt.Println("ファイル書き込みエラー", err)
		c.Ctx.ResponseWriter.WriteHeader(500)
		c.Ctx.WriteString("ファイル書き込みに失敗しました")
		return
	}

	// ファイル名を表示
	fmt.Println("ファイル名は", fileName)

	//----Taskテーブルへ登録する処理----

	//ORMを取得する
	o := orm.NewOrm()

	//Taskモデルのインスタンスを生成する
	var targetTask models.Task
	// Registerを初期化
	targetTask.Register = &models.Register{}

	//トランザクション開始
	tx, err := o.Begin()

	if err != nil {
		fmt.Println("トランザクション開始エラー", err)
		c.Data["json"] = map[string]string{
			"status":  "失敗",
			"message": "トランザクションの開始に失敗しました。",
		}
	}
	//ユーザIDをStringからInt64型へ変換する
	numUserId, _ := strconv.ParseInt(userId, 10, 64)
	fmt.Println("整数型に変換したIDは、", numUserId)
	targetTask.Register.Id = numUserId //ユーザID
	fmt.Println("登録したRegisterId", targetTask.Register.Id)

	targetTask.TaskName = task
	fmt.Println("登録したTaskName", targetTask.TaskName)
	targetTask.TaskDescription = taskDescription
	fmt.Println("登録したTaskName", targetTask.TaskDescription)

	targetTask.TaskPriority = taskPriority
	fmt.Println("登録したTaskName", targetTask.TaskPriority)              //taskPriority
	targetTask.TaskImage = "http://localhost:8080/upload/" + fileName //fileName

	fmt.Println("登録したRegisterId", targetTask.Register.Id)
	fmt.Println("登録したTaskName", targetTask.TaskName)
	fmt.Println("登録したTaskName", targetTask.TaskDescription)
	fmt.Println("登録したTaskName", targetTask.TaskPriority)
	fmt.Println("登録したTaskName", targetTask.TaskImage)

	_, err = tx.Insert(&targetTask)

	if err != nil {
		tx.Rollback()
		fmt.Println("エラー発生", err)
		c.Data["json"] = map[string]string{
			"status":  "失敗",
			"message": "タスク登録に失敗しました。",
		}
		c.ServeJSON()
		return
	}

	//成功した場合コミットする
	err = tx.Commit()

	if err != nil {
		fmt.Println("トランザクションコミットに失敗しました。")
		c.Data["json"] = map[string]string{
			"status":  "失敗",
			"message": "トランザクションコミットに失敗しました。",
		}
		c.ServeJSON()
		return
	}

	// 成功レスポンスを返す
	c.Ctx.ResponseWriter.WriteHeader(200)
	c.Ctx.WriteString("タスク登録が成功しました")
	//c.SaveFile("uploadname", "static/upload/"+h.Filename)
}

```