🍀

【Go】GinでCRUDなREST API作ってみた #3 (Read編)

2024/09/22に公開

はじめに

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

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

今回の記事ではCRUD操作のRead、読み込みを実装していきたいと思います。

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

プロジェクト構成

.
├── 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      <-  今回作業するファイル

全権取得を実装していく(FindAll)

Repository層(item_repository.go)

item_repository.go
package repositories

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

	"gorm.io/gorm"
)

type IItemRepository interface {
	Create(newItem models.Item) (*models.Item, error)
	FindAll() (*[]models.Item, 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
}

  1. IItemRepositoryインターフェース
item_repository.go
type IItemRepository interface {
	Create(newItem models.Item) (*models.Item, error)
	FindAll() (*[]models.Item, error)    // 追加部分
}

IItemRepositoryインターフェースで、FindAllメソッドの抽象化を行います。上位層(今回の場合だとService層)でFindAllメソッドを使用する際に役立ちます。

  1. FindAllメソッド(Repository層)
item_repsository.go
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
}

***r.db.Find(&items)***で引数に変数itemsのポインタを渡すことによって、データベースに保存されているオブジェクトを変数itemsに格納。
変数itemsを返す。

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)    // 追加部分
}

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()
}

  1. IItemServiceインターフェース
item_service.go
type IItemService interface {
	Create(createItemInput dto.CreateItemInput) (*models.Item, error)
	FindAll() (*[]models.Item, error)    // 追加部分
}

こちらでもFindAllメソッドの抽象化。Controller層でのFindAllメソッド利用に役立つ。

  1. FindAllメソッド(Service層)
item_service.go
func (s *ItemService) FindAll() (*[]models.Item, error) {
	return s.repository.FindAll()
}

repository層のFindAllメソッドを実行。

Controller層(item_controller.go)

item_controller.go
package controllers

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

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

type IItemController interface {
	Create(ctx *gin.Context)
	FindAll(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})
}

  1. IItemControllerインターフェース
item_controller.go
type IItemController interface {
	Create(ctx *gin.Context)
	FindAll(ctx *gin.Context)     // 追加部分
}

main.goでルーティングする際、コントローラー関数として設定するために、ここでFindAllを指定。

  1. FindAllメソッド(Controller層)
item_controller.go
// 追加部分
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})
}

変数itemsにService層のFindAllメソッドを実行した結果である、オブジェクトの配列を受け取り、最後にJSON形式でステータスコードとともに返す。

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.Run("localhost:8080")
}

  • ルーティング
main.go
r.GET("/items", itemController.FindAll)   //追加部分

第一引数のエンドポイントにアクセスすることによって、Controller層のFindAll関数が発火する。

実際の動作確認

airコマンドでホットリロード

1〜2はairコマンドの導入手順です。

  1. Airのインストール
sh
$ go install github.com/air-verse/air@latest

こちらはAirの公式のgithubページです。
https://github.com/air-verse/air

  1. airの初期化
sh
air init

こちらのコマンドで.air.tomlファイルが作成されている。

パスの通り方で困っている方は以下のサイトが参考になるかもしれません。
https://maku77.github.io/p/s258beh/#google_vignette

ちなみに私は以下のコマンドでパスを通しています。

sh
export PATH=$PATH:$(go env GOPATH)/bin 
  1. airコマンドでホットリロードを行う
sh
air

これで以下のようなログが表示されるはずです。

Postmanで動作確認

http://localhost:8080/itemsにGETリクエストすると、

しっかり動作しています。

IDによる特定オブジェクトの検索を実装していく(FindById)

Repository層(item_repostiroy.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)    //追加部分
}

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
}
  1. IItemRepositoryインターフェースにFindByIdメソッドを追加
item_repository.go
type IItemRepository interface {
	Create(newItem models.Item) (*models.Item, error)
	FindAll() (*[]models.Item, error)
	FindById(itemId uint) (*models.Item, error)    //追加部分
}
  1. FindById(Repository層)を記述
item_repository.go
// 追加部分
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
}

***r.db.First(&item, "id = ?", itemId)***では、データベースにあるItemオブジェクトから、ID属性がitemIdと等しいオブジェクトの内、IDが最小の値のものを返す。

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)      // 追加部分
}

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)
}

  1. IItemServiceインターフェースにFindByIdメソッドを追加
item_service.go
type IItemService interface {
	Create(createItemInput dto.CreateItemInput) (*models.Item, error)
	FindAll() (*[]models.Item, error)
	FindById(itemId uint) (*models.Item, error)      // 追加部分
}
  1. FindByIdメソッドを記述
item_service.go
// 追加部分
func (s *ItemService) FindById(itemId uint) (*models.Item, error) {
	return s.repository.FindById(itemId)
}

Controller層(item_controller.go)

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)    // 追加部分
}

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})
}

  1. strconvパッケージのインポート
item_controller.go
import (
	"<プロジェクト名>/dto"
	"<プロジェクト名>/services"
	"net/http"
	"strconv"   // 追加部分

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

strconvパッケージは型変換するためのパッケージです。
今回は、パスパラメーターを受け取って、uint型に変換する際に使用します。

  1. IItemControllerインターフェースにFindByIdメソッドを追加
item_controller.go
type IItemController interface {
	Create(ctx *gin.Context)
	FindAll(ctx *gin.Context)
	FindById(ctx *gin.Context)    // 追加部分
}
  1. FindByIdメソッドの記述
item_controller.go
// 追加部分
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})
}
  • strconv.ParseUint(ctx.Param("id"), 10, 64)
    第二引数では、第一引数(ctx.Param("id"))の数値の基数を指定することができます。今回は10としているため、第一引数を10進数で受け取ることができます。
    第三引数では、結果を格納するビットサイズを指定します。例えば、0, 8, 16, 32, 64のように指定可能です。このコードでは64が指定されているため、64ビットのuint64型として結果が返されます。

  • c.service.FindById(uint(itemId))の引数uint(itemId)について
    itemIdはuint64型となってしまっているのですが、FindByIdではuint型が期待されているため、uint型に変換する必要があります。

main.go

main.go
package main

import (
	"gin_crud/controllers"
	"gin_crud/infra"
	"gin_crud/repositories"
	"gin_crud/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.Run("localhost:8080")
}

エンドポイントを***/items/:id***と設定することによって、パスパラメータidを受け取ることができる。

Postmanで動作確認

http://localhost:8080/items/2にGETリクエストすると、

しっかり動作しています。

終わりに

次回はUpdateとDeleteを実装していきたいと思います。

参考

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

Discussion