🐺

【Go】GinでCRUDなREST API作ってみた #4 (Update, Delete編)

2024/09/22に公開

はじめに

この記事は前回の記事の続きとなっているので、まだみていない方は以下の記事から見てください。
https://zenn.dev/daino/articles/41524068ce68bf

https://zenn.dev/daino/articles/6b1598366aaabf

https://zenn.dev/daino/articles/7b45026bd16217

今回の記事ではCRUD操作のUpdate, Deleteを実装していきたいと思います。

今回作業するファイルの説明

プロジェクト構成

.
├── controllers
│   └── item_controller.go   <-  今回作業するファイル
├── docker-compose.yaml
├── dto
│   └── item_dto.go          <- 今回作業するファイル
├── go.mod
├── go.sum
├── infra
│   ├── db.go
│   └── initializer.go
├── main.go                  <-  今回作業するファイル
├── migrations
│   └── migration.go
├── models
│   └── item.go
├── repositories
│   └── item_repository.go   <-  今回作業するファイル
└── services
    └── item_service.go      <-  今回作業するファイル

作っていく

Repository層(item_repository.go)

item_repository.go
package repositories

import (
	"errors"
	"<プロジェクト名>/models"

	"gorm.io/gorm"
)

type IItemRepository interface {
	Create(newItem models.Item) (*models.Item, error)
	FindAll() (*[]models.Item, error)
	FindById(itemId uint) (*models.Item, error)
	Update(updateItem models.Item) (*models.Item, error) //追加部分
	Delete(itemId uint) error  //追加部分
}

type ItemRepository struct {
	db *gorm.DB
}

func NewItemRepository(db *gorm.DB) IItemRepository {
	return &ItemRepository{db: db}
}

func (r *ItemRepository) Create(newItem models.Item) (*models.Item, error) {
	result := r.db.Create(&newItem)
	if result.Error != nil {
		return nil, result.Error
	}
	return &newItem, nil
}

func (r *ItemRepository) FindAll() (*[]models.Item, error) {
	var items []models.Item
	result := r.db.Find(&items)
	if result.Error != nil {
		return nil, result.Error
	}
	return &items, nil
}

func (r *ItemRepository) FindById(itemId uint) (*models.Item, error) {
	var item models.Item
	result := r.db.First(&item, "id = ?", itemId)
	if result.Error != nil {
		if result.Error.Error() == "record not found" {
			return nil, errors.New("item not found")
		}
		return nil, result.Error
	}
	return &item, nil
}

//追加部分
func (r *ItemRepository) Update(updateItem models.Item) (*models.Item, error) {
	result := r.db.Save(&updateItem)
	if result.Error != nil {
		return nil, result.Error
	}
	return &updateItem, nil
}

//追加部分
func (r *ItemRepository) Delete(itemId uint) error {
	deleteItem, err := r.FindById(itemId)
	if err != nil {
		return err
	}

	result := r.db.Delete(&deleteItem)
	if result.Error != nil {
		return result.Error
	}
	return nil
}
  1. IItemRepositoryインターフェースにUpdateメソッド、Deleteメソッドを追加
item_repository.go
type IItemRepository interface {
	Create(newItem models.Item) (*models.Item, error)
	FindAll() (*[]models.Item, error)
	FindById(itemId uint) (*models.Item, error)
	Update(updateItem models.Item) (*models.Item, error) //追加部分
	Delete(itemId uint) error  //追加部分
}
  1. Updateメソッドを記述
item_repository.go
//追加部分
func (r *ItemRepository) Update(updateItem models.Item) (*models.Item, error) {
	result := r.db.Save(&updateItem)
	if result.Error != nil {
		return nil, result.Error
	}
	return &updateItem, nil
}

***r.db.Save(&updateItem)***で変更データに塗り替える。

  1. Deleteメソッドを記述
item_repository.go
//追加部分
func (r *ItemRepository) Delete(itemId uint) error {
	deleteItem, err := r.FindById(itemId)
	if err != nil {
		return err
	}

	result := r.db.Delete(&deleteItem)
	if result.Error != nil {
		return result.Error
	}
	return nil
}
  • deleteItem, err := r.FindById(itemId)
    FindByIdで削除したいデータを取得。

  • result := r.db.Delete(&deleteItem)
    r.db.Delete関数を使用して、目的のデータの削除

Service層(item_service.go)

item_service.go
package services

import (
	"<プロジェクト名>/dto"
	"<プロジェクト名>/models"
	"<プロジェクト名>/repositories"
)

type IItemService interface {
	Create(createItemInput dto.CreateItemInput) (*models.Item, error)
	FindAll() (*[]models.Item, error)
	FindById(itemId uint) (*models.Item, error)
	Update(itemId uint, updateItemInput dto.UpdateItemInput) (*models.Item, error) //追加部分
	Delete(itemId uint) error //追加部分
}

type ItemService struct {
	repository repositories.IItemRepository
}

func NewItemService(repository repositories.IItemRepository) IItemService {
	return &ItemService{repository: repository}
}

func (s *ItemService) Create(createItemInput dto.CreateItemInput) (*models.Item, error) {
	newItem := models.Item{
		Name:        createItemInput.Name,
		Price:       createItemInput.Price,
		Description: createItemInput.Description,
		SoldOut:     false,
	}
	return s.repository.Create(newItem)
}

func (s *ItemService) FindAll() (*[]models.Item, error) {
	return s.repository.FindAll()
}

func (s *ItemService) FindById(itemId uint) (*models.Item, error) {
	return s.repository.FindById(itemId)
}

//追加部分
func (s *ItemService) Update(itemId uint, updateItemInput dto.UpdateItemInput) (*models.Item, error) {
	targetItem, err := s.FindById(itemId)
	if err != nil {
		return nil, err
	}

	if updateItemInput.Name != nil {
		targetItem.Name = *updateItemInput.Name
	}
	if updateItemInput.Price != nil {
		targetItem.Price = *updateItemInput.Price
	}
	if updateItemInput.Description != nil {
		targetItem.Description = *updateItemInput.Description
	}
	if updateItemInput.SoldOut != nil {
		targetItem.SoldOut = *updateItemInput.SoldOut
	}

	return s.repository.Update(*targetItem)

}

//追加部分
func (s *ItemService) Delete(itemId uint) error {
	return s.repository.Delete(itemId)
}

  1. IItemServiceインターフェースにUpdateメソッド、Deleteメソッドを追加
item_service.go
type IItemService interface {
	Create(createItemInput dto.CreateItemInput) (*models.Item, error)
	FindAll() (*[]models.Item, error)
	FindById(itemId uint) (*models.Item, error)
	Update(itemId uint, updateItemInput dto.UpdateItemInput) (*models.Item, error) //追加部分
	Delete(itemId uint) error //追加部分
}

Updateメソッドに関しては、ユーザーのリクエストデータをdtoバインドした値が引数となります。

  1. Updateメソッド
item_service.go
//追加部分
func (s *ItemService) Update(itemId uint, updateItemInput dto.UpdateItemInput) (*models.Item, error) {
	targetItem, err := s.FindById(itemId)
	if err != nil {
		return nil, err
	}

	if updateItemInput.Name != nil {
		targetItem.Name = *updateItemInput.Name
	}
	if updateItemInput.Price != nil {
		targetItem.Price = *updateItemInput.Price
	}
	if updateItemInput.Description != nil {
		targetItem.Description = *updateItemInput.Description
	}
	if updateItemInput.SoldOut != nil {
		targetItem.SoldOut = *updateItemInput.SoldOut
	}

	return s.repository.Update(*targetItem)

}
  • targetItem, err := s.FindById(itemId)
    targetItemには更新したいデータを格納

  • 各属性のデータを更新
    updateItemInputの各属性の値でnilでないものに関しては、targetItemの適当な属性に格納

  1. Deleteメソッド
item_service.go
//追加部分
func (s *ItemService) Delete(itemId uint) error {
	return s.repository.Delete(itemId)
}

Controller層(item_controller.go)

item_controller.lgo
package controllers

import (
	"<プロジェクト名>/dto"
	"<プロジェクト名>/services"
	"net/http"
	"strconv"

	"github.com/gin-gonic/gin"
)

type IItemController interface {
	Create(ctx *gin.Context)
	FindAll(ctx *gin.Context)
	FindById(ctx *gin.Context)
	Update(ctx *gin.Context)   // 追加部分
	Delete(ctx *gin.Context)   // 追加部分
}

type ItemController struct {
	service services.IItemService
}

func NewItemController(service services.IItemService) IItemController {
	return &ItemController{service: service}
}

func (c *ItemController) Create(ctx *gin.Context) {
	var input dto.CreateItemInput
	if err := ctx.ShouldBindJSON(&input); err != nil {
		ctx.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
		return
	}

	newItem, err := c.service.Create(input)
	if err != nil {
		ctx.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
		return
	}

	ctx.JSON(http.StatusCreated, gin.H{"data": newItem})
}

func (c ItemController) FindAll(ctx *gin.Context) {
	items, err := c.service.FindAll()
	if err != nil {
		ctx.JSON(http.StatusInternalServerError, gin.H{"error": "Unexpected error"})
		return
	}

	ctx.JSON(http.StatusOK, gin.H{"data": items})
}

func (c ItemController) FindById(ctx *gin.Context) {
	itemId, err := strconv.ParseUint(ctx.Param("id"), 10, 64)
	if err != nil {
		ctx.JSON(http.StatusBadRequest, gin.H{"error": "Invalid id"})
		return
	}
	item, err := c.service.FindById(uint(itemId))
	if err != nil {
		if err.Error() == "Item not found" {
			ctx.JSON(http.StatusNotFound, gin.H{"error": err.Error()})
			return
		}
		ctx.JSON(http.StatusInternalServerError, gin.H{"error": "Unexpected error"})
		return
	}
	ctx.JSON(http.StatusOK, gin.H{"data": item})
}

// 追加部分
func (c ItemController) Update(ctx *gin.Context) {
	itemId, err := strconv.ParseUint(ctx.Param("id"), 10, 64)
	if err != nil {
		ctx.JSON(http.StatusBadRequest, gin.H{"error": "Invalid id"})
		return
	}

	var input dto.UpdateItemInput
	if err := ctx.ShouldBindJSON(&input); err != nil {
		ctx.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
		return
	}

	updateItem, err := c.service.Update(uint(itemId), input)
	if err != nil {
		if err.Error() == "Item not found" {
			ctx.JSON(http.StatusNotFound, gin.H{"error": err.Error()})
			return
		}
		ctx.JSON(http.StatusInternalServerError, gin.H{"error": "Unexpected error"})
		return
	}
	ctx.JSON(http.StatusOK, gin.H{"data": updateItem})
}

// 追加部分
func (c *ItemController) Delete(ctx *gin.Context) {
	itemId, err := strconv.ParseUint(ctx.Param("id"), 10, 64)
	if err != nil {
		ctx.JSON(http.StatusBadRequest, gin.H{"error": "Invalid id"})
		return
	}

	err = c.service.Delete(uint(itemId))
	if err != nil {
		if err.Error() == "Item not found" {
			ctx.JSON(http.StatusNotFound, gin.H{"error": err.Error()})
			return
		}
		ctx.JSON(http.StatusInternalServerError, gin.H{"error": "Unexpected error"})
		return
	}

	ctx.Status(http.StatusOK)
}

  1. IItemControllerインターフェースにUpdateメソッド、Deleteメソッドを追加
item_controller.go
type IItemController interface {
	Create(ctx *gin.Context)
	FindAll(ctx *gin.Context)
	FindById(ctx *gin.Context)
	Update(ctx *gin.Context)   // 追加部分
	Delete(ctx *gin.Context)   // 追加部分
}
  1. Updateメソッドの実装
item_controller.go
// 追加部分
func (c ItemController) Update(ctx *gin.Context) {
	itemId, err := strconv.ParseUint(ctx.Param("id"), 10, 64)
	if err != nil {
		ctx.JSON(http.StatusBadRequest, gin.H{"error": "Invalid id"})
		return
	}

	var input dto.UpdateItemInput
	if err := ctx.ShouldBindJSON(&input); err != nil {
		ctx.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
		return
	}

	updateItem, err := c.service.Update(uint(itemId), input)
	if err != nil {
		if err.Error() == "Item not found" {
			ctx.JSON(http.StatusNotFound, gin.H{"error": err.Error()})
			return
		}
		ctx.JSON(http.StatusInternalServerError, gin.H{"error": "Unexpected error"})
		return
	}
	ctx.JSON(http.StatusOK, gin.H{"data": updateItem})
}

  • itemId, err := strconv.ParseUint(ctx.Param("id"), 10, 64)
    第二引数では、第一引数(ctx.Param("id"))の数値の基数を指定することができます。今回は10としているため、第一引数を10進数で受け取ることができます。
    第三引数では、結果を格納するビットサイズを指定します。例えば、0, 8, 16, 32, 64のように指定可能です。このコードでは64が指定されているため、64ビットのuint64型として結果が返されます。
    これで更新したいオブジェクトのIDを取得しています。

  • ctx.ShouldBindJSON(&input)
    item_dto.goで記述したupdateItemInputの内容でバインドを行います。

  • updateItem, err := c.service.Update(uint(itemId), input)
    Service層のUpdateメソッドを実行し、更新を行います。

  1. Deleteメソッドを記述
item_controller.go
// 追加部分
func (c *ItemController) Delete(ctx *gin.Context) {
	itemId, err := strconv.ParseUint(ctx.Param("id"), 10, 64)
	if err != nil {
		ctx.JSON(http.StatusBadRequest, gin.H{"error": "Invalid id"})
		return
	}

	err = c.service.Delete(uint(itemId))
	if err != nil {
		if err.Error() == "Item not found" {
			ctx.JSON(http.StatusNotFound, gin.H{"error": err.Error()})
			return
		}
		ctx.JSON(http.StatusInternalServerError, gin.H{"error": "Unexpected error"})
		return
	}

	ctx.Status(http.StatusOK)
}
  • itemId, err := strconv.ParseUint(ctx.Param("id"), 10, 64)
    削除したいオブジェクトのIDをパスパラメータで受け取ります。

  • err = c.service.Delete(uint(itemId))
    Service層のDeleteメソッドを実行し、errを受け取ります。うまく動作していれば、errにはnilが入ります。

UpdateItemInput(item_dto.go)

item_dto.go
package dto

type CreateItemInput struct {
	Name        string `json:"name" biding:"required,min=2"`
	Price       uint   `json:"price" biding:"required,min=1,max=99999"`
	Description string `json:"description"`
}

// 追加部分
type UpdateItemInput struct {
	Name        *string `json:"name" biding:"omitnil,min=2"`
	Price       *uint   `json:"price" binding:"omitnil,min=1,max=999999"`
	Description *string `json:"description"`
	SoldOut     *bool   `json:"soldOut"`
}
  • omitnilについて
    omitnilに指定した属性に関しては、もしその属性の値がnilの時無視され、スキップしてくれる

main.go

main.go
package main

import (
	"<プロジェクト名>/controllers"
	"<プロジェクト名>/infra"
	"<プロジェクト名>/repositories"
	"<プロジェクト名>/services"

	"github.com/gin-gonic/gin"
)

func main() {
	infra.Initialize()
	db := infra.SetupDB()

	itemRepository := repositories.NewItemRepository(db)
	itemService := services.NewItemService(itemRepository)
	itemController := controllers.NewItemController(itemService)

	r := gin.Default()
	r.POST("/items", itemController.Create)
	r.GET("/items", itemController.FindAll)
	r.GET("/items/:id", itemController.FindById)
	r.PUT("/items/:id", itemController.Update)
	r.DELETE("/items/:id", itemController.Delete)
	r.Run("localhost:8080")
}

r.PUTとr.DELETEでルーティング。第一引数であるエンドポイントに適当なhttpメソッドでアクセスすることによって、第二引数の関数が発火される。

Postman動作確認

Updateの動作確認

しっかりと更新できています。

Deleteの動作確認


200 Okと出ているので、成功しています。

終わりに

ミドルウェアを実装したり、ユーザーのログイン機能などまだまだやることはあるので、また機会があれば記事を書いてみたいと思います。

参考

今回はYu Shinozakiさんの「Gin入門 Go言語ではじめるサーバーサイド開発」というUdemyの講義を見ながら作成いたしました。とてもわかりやすくておすすめです!
https://www.udemy.com/course/gin-golang/?couponCode=KEEPLEARNING
ginフレームワークの公式ドキュメント
https://gin-gonic.com/ja/

Discussion