【Go】GinでCRUDなREST API作ってみた #3 (Read編)
はじめに
この記事は前回の記事の続きとなっているので、まだみていない方は以下の記事から見てください。
今回の記事では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)
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
}
- IItemRepositoryインターフェース
type IItemRepository interface {
Create(newItem models.Item) (*models.Item, error)
FindAll() (*[]models.Item, error) // 追加部分
}
IItemRepositoryインターフェースで、FindAllメソッドの抽象化を行います。上位層(今回の場合だとService層)でFindAllメソッドを使用する際に役立ちます。
- FindAllメソッド(Repository層)
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)
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()
}
- IItemServiceインターフェース
type IItemService interface {
Create(createItemInput dto.CreateItemInput) (*models.Item, error)
FindAll() (*[]models.Item, error) // 追加部分
}
こちらでもFindAllメソッドの抽象化。Controller層でのFindAllメソッド利用に役立つ。
- FindAllメソッド(Service層)
func (s *ItemService) FindAll() (*[]models.Item, error) {
return s.repository.FindAll()
}
repository層のFindAllメソッドを実行。
Controller層(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})
}
- IItemControllerインターフェース
type IItemController interface {
Create(ctx *gin.Context)
FindAll(ctx *gin.Context) // 追加部分
}
main.goでルーティングする際、コントローラー関数として設定するために、ここでFindAllを指定。
- FindAllメソッド(Controller層)
// 追加部分
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
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")
}
- ルーティング
r.GET("/items", itemController.FindAll) //追加部分
第一引数のエンドポイントにアクセスすることによって、Controller層のFindAll関数が発火する。
実際の動作確認
airコマンドでホットリロード
1〜2はairコマンドの導入手順です。
- Airのインストール
$ go install github.com/air-verse/air@latest
こちらはAirの公式のgithubページです。
- airの初期化
air init
こちらのコマンドで.air.tomlファイルが作成されている。
パスの通り方で困っている方は以下のサイトが参考になるかもしれません。
ちなみに私は以下のコマンドでパスを通しています。
export PATH=$PATH:$(go env GOPATH)/bin
- airコマンドでホットリロードを行う
air
これで以下のようなログが表示されるはずです。
Postmanで動作確認
http://localhost:8080/itemsにGETリクエストすると、
しっかり動作しています。
IDによる特定オブジェクトの検索を実装していく(FindById)
Repository層(item_repostiroy.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
}
- IItemRepositoryインターフェースにFindByIdメソッドを追加
type IItemRepository interface {
Create(newItem models.Item) (*models.Item, error)
FindAll() (*[]models.Item, error)
FindById(itemId uint) (*models.Item, error) //追加部分
}
- FindById(Repository層)を記述
// 追加部分
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)
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)
}
- IItemServiceインターフェースにFindByIdメソッドを追加
type IItemService interface {
Create(createItemInput dto.CreateItemInput) (*models.Item, error)
FindAll() (*[]models.Item, error)
FindById(itemId uint) (*models.Item, error) // 追加部分
}
- FindByIdメソッドを記述
// 追加部分
func (s *ItemService) FindById(itemId uint) (*models.Item, error) {
return s.repository.FindById(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) // 追加部分
}
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})
}
- strconvパッケージのインポート
import (
"<プロジェクト名>/dto"
"<プロジェクト名>/services"
"net/http"
"strconv" // 追加部分
"github.com/gin-gonic/gin"
)
strconvパッケージは型変換するためのパッケージです。
今回は、パスパラメーターを受け取って、uint型に変換する際に使用します。
- IItemControllerインターフェースにFindByIdメソッドを追加
type IItemController interface {
Create(ctx *gin.Context)
FindAll(ctx *gin.Context)
FindById(ctx *gin.Context) // 追加部分
}
- FindByIdメソッドの記述
// 追加部分
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
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の講義を見ながら作成いたしました。とてもわかりやすくておすすめです!
ginフレームワークの公式ドキュメント
Discussion