【Go】GinでCRUDなREST API作ってみた #4 (Update, Delete編)
はじめに
この記事は前回の記事の続きとなっているので、まだみていない方は以下の記事から見てください。
今回の記事では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)
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
}
- IItemRepositoryインターフェースにUpdateメソッド、Deleteメソッドを追加
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 //追加部分
}
- Updateメソッドを記述
//追加部分
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)***で変更データに塗り替える。
- Deleteメソッドを記述
//追加部分
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)
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)
}
- IItemServiceインターフェースにUpdateメソッド、Deleteメソッドを追加
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バインドした値が引数となります。
- Updateメソッド
//追加部分
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の適当な属性に格納
- Deleteメソッド
//追加部分
func (s *ItemService) Delete(itemId uint) error {
return s.repository.Delete(itemId)
}
Controller層(item_controller.go)
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)
}
- IItemControllerインターフェースにUpdateメソッド、Deleteメソッドを追加
type IItemController interface {
Create(ctx *gin.Context)
FindAll(ctx *gin.Context)
FindById(ctx *gin.Context)
Update(ctx *gin.Context) // 追加部分
Delete(ctx *gin.Context) // 追加部分
}
- Updateメソッドの実装
// 追加部分
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メソッドを実行し、更新を行います。
- Deleteメソッドを記述
// 追加部分
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)
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
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の講義を見ながら作成いたしました。とてもわかりやすくておすすめです!
ginフレームワークの公式ドキュメント
Discussion