【Go】APIサーバの構築 ~データの作成(Create)/更新(Update)/削除(Delete)編~

#はじめに
前記事の続きでデータの作成、更新、削除の処理を追加しました。

https://qiita.com/suzuki0430/items/f4396a337209cd1e37b7

#実装
各処理の実装内容について記述します。

##削除(Delete)
まずは`routes.go`にルートハンドラーを追加します。

```go:routes.go
router.HandlerFunc(http.MethodGet, "/v1/admin/deletemovie/:id", app.deleteMovie)
```
処理の内容を`movie-handlers.go`に記述します。
パスパラメータを取得するとき、`strconv.Atoi(params.ByName("id"))`でidの型をStringからIntに変換することに注意します。

```go:movie-handlers.go
func (app *application) deleteMovie(w http.ResponseWriter, r *http.Request) {
	params := httprouter.ParamsFromContext(r.Context())

	// パスパラメータのidをIntに変換する
	id, err := strconv.Atoi(params.ByName("id"))
	if err != nil {
		app.errorJSON(w, err)
		return
	}

	// データの削除処理を行う
	err = app.models.DB.DeleteMovie(id)
	if err != nil {
		app.errorJSON(w, err)
		return
	}

	ok := jsonResp{
		OK: true,
	}

	err = app.writeJSON(w, http.StatusOK, ok, "response")
	if err != nil {
		app.errorJSON(w, err)
		return
	}
}
```
DBデータの削除処理`DeleteMovie()`は`movies-db.go`に記述します。
データの取得処理には`QueryContext()`を使用していましたが、データを変更する処理(削除、更新など)を行う場合には`execContext()`を使用します。

```go:movies-db.go
func (m *DBModel) DeleteMovie(id int) error {
	ctx, cancel := context.WithTimeout(context.Background(), 3*time.Second)
	defer cancel()

	stmt := "delete from movies where id = $1"

	_, err := m.DB.ExecContext(ctx, stmt, id)
	if err != nil {
		return err
	}

	return nil
}
```

##作成(Create)/更新(Update)
データの作成と更新については、既存のデータを編集するかどうかの違いだけで処理自体はほぼ同じです。
そのため、ルートハンドラーは一つにまとめてしまいます。

```go:routes.go
router.HandlerFunc(http.MethodPost, "/v1/admin/editmovie", app.editMovie)
```
削除のときと同様に、処理の中身は`movie-handler.go`に記述します。

まずはリクエストデータの型を`MoviePayload`という構造体で定義します。
`payload`という構造体を定義し、`json.NewDecoder(r.Body).Decode(&payload)`でリクエストデータの中身`r.Body`を`payload`に入れます。

リクエストデータのレコードをDBに登録(作成)するために、`movie.ID, _ = strconv.Atoi(payload.ID)`のように、`payload`のプロパティをmovieテーブルのカラムと同じ型に変換していきます。
そして、`movie.ID == 0`のときにレコード作成処理`InsertMovie()`、それ以外のときに更新処理`UpdateMovie()`を行うようにします。

```go:movie-handler.go
// JSONと同じ構造の構造体の型を定義する(入力値の型とDBのカラムの型は異なる)
type MoviePayload struct {
	ID string `json:"id"`
	Title string `json:"title"`
	Description string `json:"description"`
	Year string `json:"year"`
	ReleaseDate string `json:"release_date"`
	Runtime string `json:"runtime"`
	Rating string `json:"rating"`
	MPAARating string `json:"mpaa_rating"`
}

func (app *application) editMovie(w http.ResponseWriter, r *http.Request) {
	// リクエストデータの型をもつ構造体を定義する
	var payload MoviePayload

	// JSONオブジェクトを読み込んでpayloadに代入する
	err := json.NewDecoder(r.Body).Decode(&payload)
	if err != nil {
		log.Println(err)
		app.errorJSON(w, err)
		return
	}

	// DBデータの型をもつ構造体を定義する
	var movie models.Movie

	// データ更新時にUpdatedAtを更新する
	if payload.ID != "0" {
		id, _ := strconv.Atoi(payload.ID)
		m, _ := app.models.DB.Get(id)
		movie = *m
		movie.UpdatedAt = time.Now()
	}

	// payloadの各プロパティの型を変換してmovieに代入する
	movie.ID, _ = strconv.Atoi(payload.ID)
	movie.Title = payload.Title
	movie.Description = payload.Description
	movie.ReleaseDate, _ = time.Parse("2006-01-02", payload.ReleaseDate)
	movie.Year = movie.ReleaseDate.Year()
	movie.Runtime, _ = strconv.Atoi(payload.Runtime)
	movie.Rating, _ = strconv.Atoi(payload.Rating)
	movie.MPAARating = payload.MPAARating
	movie.CreatedAt = time.Now()
	movie.UpdatedAt = time.Now()

	if movie.ID == 0 { // データ作成時の処理
		err = app.models.DB.InsertMovie(movie)
		if err != nil {
			app.errorJSON(w, err)
			return
		}
	} else { // データ更新時の処理
		err = app.models.DB.UpdateMovie(movie)
		if err != nil {
			app.errorJSON(w, err)
			return
		}
	}
	
	ok := jsonResp{
		OK: true,
	}

	err = app.writeJSON(w, http.StatusOK, ok, "response")
	if err != nil {
		app.errorJSON(w, err)
		return
	}
}
```
各処理の中身は以下のようになります。

```go:movie-db.go
func (m *DBModel) InsertMovie(movie Movie) error {
	ctx, cancel := context.WithTimeout(context.Background(), 3*time.Second)
	defer cancel()

	stmt := `insert into movies (title, description, year, release_date, runtime, rating, mpaa_rating, created_at, updated_at) values ($1, $2, $3, $4, $5, $6, $7, $8, $9)`

	_, err := m.DB.ExecContext(ctx, stmt,
		movie.Title,
		movie.Description,
		movie.Year,
		movie.ReleaseDate,
		movie.Runtime,
		movie.Rating,
		movie.MPAARating,
		movie.CreatedAt,
		movie.UpdatedAt,
	)

	if err != nil {
		return err
	}

	return nil
}
```

```go:movie-db.go
func (m *DBModel) UpdateMovie(movie Movie) error {
	ctx, cancel := context.WithTimeout(context.Background(), 3*time.Second)
	defer cancel()

	stmt := `update movies set title = $1, description = $2, year = $3, release_date = $4, 
						runtime = $5, rating = $6, mpaa_rating = $7, 
						updated_at = $8 where id = $9`

	_, err := m.DB.ExecContext(ctx, stmt,
		movie.Title,
		movie.Description,
		movie.Year,
		movie.ReleaseDate,
		movie.Runtime,
		movie.Rating,
		movie.MPAARating,
		movie.UpdatedAt,
		movie.ID,
	)

	if err != nil {
		return err
	}

	return nil
}
```

##コード全文
以下をご覧ください。

https://github.com/suzuki0430/backend-go-movies

#参考資料

https://www.udemy.com/share/104HTy3@mQTNuzCAa-KvFpwCAEsdWE8u7_LO922BvrMXQh1T25Pp8KNda2tBjx-nxdh5HpaD/

https://qiita.com/Sekky0905/items/5a65602dce83551184b3

https://qiita.com/Coolucky/items/44f2bc6e32ca8e9baa96