はじめに
golang で簡単な CRUD 操作を行う REST API を作ってみました
go, gin, gorm, mysql を使った、簡単な TODO アプリです
環境
PC | Macbook Pro |
---|---|
OS | Big Sur |
docker-compose | 1.24.1, build 4667896b |
docker | 19.03.4, build 9013bf5 |
go | go1.20.4 linux/amd64 |
mysql | 5.7 |
作ったもの
今回作った API の仕様は以下の通りです
簡単な TODO アプリで、 CRUD 操作の為のエンドポイントを提供します
Method | Path | Description | Description |
---|---|---|---|
GET | /todo | 全件取得API | TODO リストの全件取得 |
GET | /todo/{id} | 取得API | 指定した TODO の取得 |
POST | /todo | 登録API | TODO の作成 |
PUT | /todo/{id} | 更新API | TODO の更新 |
DELETE | /todo/{id} | 削除API | TODO の削除 |
ソースコードはこちら
ディレクトリ構成
ディレクトリ構成はこんな感じ
一応 Clean Achitectute を意識しているつもり
.
├── Dockerfile.local
├── README.md
├── cmd
│ └── go-api-sample-todo
│ └── main.go
├── docker-compose.yml
├── domain
│ ├── model
│ │ └── todo.go
│ └── repository
│ └── todo.go
├── go.mod
├── go.sum
├── handler
│ ├── todo.go
│ ├── todo_test.go
│ └── validator
│ └── validator.go
├── infrastructure
│ ├── db.go
│ ├── todo.go
│ └── todo_test.go
├── migrations
│ └── 0001_create_tables.sql
└── usecase
├── todo.go
└── todo_test.go
各レイヤ・ディレクトリの役割
cmd
アプリのメインエントリが置かれるところ
handler
HTTP リクエストを受けとり usecase を呼び出し、処理結果を返す
usecase
ビジネスロジックを記述するところ
今回は特に複雑な処理はないので薄い実装になってます
domain
システムが扱う業務領域に関するコードを置くところ
repository
infrastructure 層を抽象化するための interface を定義
model
ドメインに関する値と振る舞いをもつところ
infrastructure
技術的な関心事を置くところ
今回だと、DB 操作周りを担っています
migrations
初期テーブルの作成や初期データの投入のためのSQLファイルを置くところ
今回使用するテーブルの DDL を配置しています
docker-compose.yml
に以下のような記述をすることで、初回起動時に/migrations
内のSQLファイルを実行してくれます。初期テーブルの作成や初期データの投入に使うと便利です。
※初回コンテナ作成時にしか実行されないので、あとから DDL や DML を追加するときは、手動で実行するか、コンテナを作り直す必要があります
mysql:
image: mysql:5.7
container_name: mysql-db
ports:
- 3306:3306
volumes:
- ./migrations:/docker-entrypoint-initdb.d
コード
それでは実際のソースコードを順々に見ていきます
cmd/go-api-sample-todo/main.go
今回のソースのエントリポイントです
DB の設定と、API のルーティング設定(setupRouter)を行います
web フレームワークには gin を使っています
また、DB 操作のための ORM フレームワークには gorm を使っています
package main
import (
"app/handler"
"app/infrastructure"
"app/usecase"
"fmt"
appvalidator "app/handler/validator"
"github.com/gin-gonic/gin"
"gorm.io/gorm"
)
func main() {
d, err := infrastructure.NewDB()
if err != nil {
fmt.Printf("failed to start server. db setup failed, err = %s", err.Error())
return
}
r := setupRouter(d)
if err := appvalidator.SetupValidator(); err != nil {
fmt.Printf("failed to start server. validator setup failed, err = %s", err.Error())
return
}
r.Run()
}
func setupRouter(d *gorm.DB) *gin.Engine {
r := gin.Default()
repository := infrastructure.NewTodo(d)
usecase := usecase.NewTodo(repository)
handler := handler.NewTodo(usecase)
todo := r.Group("/todo")
{
todo.POST("", handler.Create)
todo.GET("", handler.FindAll)
todo.GET("/:id", handler.Find)
todo.PUT("/:id", handler.Update)
todo.DELETE("/:id", handler.Delete)
}
return r
}
gin.Default()
gin.Default() は *gin.EngineをReturnする関数で、この Engine を使って、エンドポイントの設定やミドルウェアの登録を行うことができます
例えば、以下の様な記述をすることで、「pong」を返す/ping
というエンドポイントを作ることができます
r := gin.Default()
r.GET("/ping", func(c *gin.Context) {
c.JSON(http.StatusOK, gin.H{
"message": "pong",
})
})
今回の例では、更に r.Group("/todo")
を使って、パスのグルーピングを行っています
こうすることで、共通のパスプレフィックスを持つエンドポイントを作ることができます
それぞれ実際には以下のようなエンドポイントになります
// ルーティング設定
todo := r.Group("/todo")
{
todo.POST("", handler.Create) -> /todo
todo.GET("", handler.FindAll) -> /todo
todo.GET("/:id", handler.Find) -> /todo/:id
todo.PUT("/:id", handler.Update) -> /todo/:id
todo.DELETE("/:id", handler.Delete) -> /todo/:id
}
また、 todo.GET("", handler.FindAll)
の handler.FindAll
は、
対象のエンドポイントにリクエストが来た時に呼び出す handler の関数を登録しています
登録API の実装
以下は、 登録 API の handler の実装です
// リクエストデータを格納するための構造体
type CreateRequestParam struct {
Task string `json:"task" binding:"required,max=60"`
}
func (t *todoHandler) Create(c *gin.Context) {
var req CreateRequestParam
// リクエストパラメータを構造体(CreateRequestParam)にマッピング
if err := c.ShouldBindJSON(&req); err != nil {
// バリデーションエラーがあった場合はエラーを返す
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
return
}
// usecase の呼び出し
err := t.usecase.Create(req.Task)
if err != nil {
// エラーがあった場合はエラーレスポンスを返却
c.JSON(http.StatusInternalServerError, "")
return
}
// レスポンスを返却
c.JSON(http.StatusCreated, nil)
}
CreateRequestParam
で、リクエストを受け取るための構造体を定義し、
リクエストパラメータのタグ名の指定と「必須項目」、「桁数60桁まで」というバリデーション設定を行っています
type CreateRequestParam struct {
// json:"task" = リクエストパラメータのタグ名の指定(指定しないと構造体名になってしまう)
// required = 必須項目
// max=60 = 桁数60桁まで
Task string `json:"task" binding:"required,max=60"`
}
こうすることで、c.ShouldBindJSON(&req)で、構造体へのリクエストのパラメータのマッピングとバリデーションチェック行うことができます
var req CreateRequestParam
// リクエストパラメータを構造体(CreateRequestParam)にマッピング
if err := c.ShouldBindJSON(&req); err != nil {
// バリデーションエラーがあった場合はエラーを返す
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
return
}
バリデーションエラーになった場合の実際のレスポンスはこんな感じ
% curl -i localhost/todo -H "Content-Type: application/json" -X POST -d '{}'
HTTP/1.1 400 Bad Request
Content-Type: application/json; charset=utf-8
Date: Mon, 22 May 2023 16:37:26 GMT
Content-Length: 105
{"error":"Key: 'CreateRequestParam.Task' Error:Field validation for 'Task' failed on the 'required' tag"}
% curl -i localhost/todo -H "Content-Type: application/json" -X POST -d '{"task": "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"}'
HTTP/1.1 400 Bad Request
Content-Type: application/json; charset=utf-8
Date: Mon, 22 May 2023 16:38:04 GMT
Content-Length: 100
{"error":"Key: 'CreateRequestParam.Task' Error:Field validation for 'Task' failed on the 'max' tag"
続いて、usecase 層では、handler から task を受け取り、 DB へデータの登録を行います
func (t *todo) Create(task string) error {
// 登録するための構造体を作成
todo := model.NewTodo(task)
// DB にデータを登録
if err := t.todoRepository.Create(todo); err != nil {
return err
}
return nil
}
todo := model.NewTodo(task)
では、 domain/model/todo.go
のNewTodo(task string)
を呼び出し、DB にデータを登録するための構造体を作成します
実装はこんな感じ
type Todo struct {
ID int `gorm:"primaryKey"`
Task string
Status TaskStatus
CreatedAt time.Time `gorm:"<-:false"`
UpdatedAt time.Time `gorm:"<-:false"`
}
func NewTodo(task string) *Todo {
return &Todo{
Task: task,
Status: Created,
}
}
// Task Status の独自型の定義
type TaskStatus string
const (
Created = TaskStatus("created")
Processing = TaskStatus("processing")
Done = TaskStatus("done")
)
NewTodo(task string)
関数は Todo の構造体を作成して返します
初回作成なので、Status
は新規作成を表すCreated(created
)を固定で入れます
また、 Status
には事前に定義した値のみを扱うために、以下のようにtype TaskStatus string
で独自型を定義しています
// Task Status の独自型の定義
type TaskStatus string
const (
Created = TaskStatus("created")
Processing = TaskStatus("processing")
Done = TaskStatus("done")
)
Todo の構造体は DB のテーブル 定義と合わせるようにしています
※本来レイヤ構造において、domain が他のレイヤに依存するような実装はだめなんだけど、今回はサンプルなので。。。
type Todo struct {
ID int `gorm:"primaryKey"`
Task string
Status TaskStatus
CreatedAt time.Time `gorm:"<-:false"`
UpdatedAt time.Time `gorm:"<-:false"`
}
gorm:xxx
は gorm の用のタグで、テーブルの主キーの指定(gorm:"primaryKey"
)や、指定したカラムを書き込まないように(gorm:"<-:false"
)することができます
今回、created_at
とupdated_at
は、テーブル定義として自動的に値が入るようにしているので、アプリケーション側からは書き込まないようにしています
テーブル定義はこんな感じ
CREATE TABLE `todo` (
`id` BIGINT(20) NOT NULL AUTO_INCREMENT comment 'ID',
`task` VARCHAR (128) NOT NULL comment 'タスク',
`status` VARCHAR(20) NOT NULL comment 'タスクステータス',
`created_at` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP comment '作成日時',
`updated_at` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP comment '更新日時',
PRIMARY KEY(`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
最後に DB を操作する部分
t.todoRepository.Create(todo)
では、repository を介して、infrastructure/todo.go
のCreate(t *model.Todo)
を呼び出し、DB へのデータの登録を行います
func (td *Todo) Create(t *model.Todo) error {
if err := td.db.Create(t).Error; err != nil {
return err
}
return nil
}
td.db.Create(t)
では、 gorm の Create(value interface{})
を呼び出しデータの登録を行っています
gorm には 基本的な CURD 操作の為の関数が用意されているので用途によって使い分けます
以下は一例
操作 | 関数 |
---|---|
新規作成(Create) | Create |
参照(Read) | Take, Find |
更新(Update) | Update |
削除(Delete) | Delete |
※Update は 正確には upsert になります
主キーに該当するデータが有れば update、なければ insert
※参照用の関数に関しても、1件のみ取得であれば Take
、複数件取得したい場合は Find
を使うなど用途によって使い分けます
さて、これで登録API の実装完了したので、実際に API をコールしてデータが登録されるか見ていきたいと思います
まずは テーブル にデータが入っていないことを確認
mysql> select * from todo;
Empty set (0.00 sec)
はい、まだ空っぽです
それでは実際に API をコールしてデータの登録を行います
% curl -i localhost/todo -H "Content-Type: application/json" -X POST -d '{"task": "test"}'
HTTP/1.1 201 Created
Content-Type: application/json; charset=utf-8
Date: Mon, 22 May 2023 16:07:23 GMT
Content-Length: 4
成功したっぽい
テーブルにデータが入っているか確認します
mysql> select * from todo;
+----+------+---------+---------------------+---------------------+
| id | task | status | created_at | updated_at |
+----+------+---------+---------------------+---------------------+
| 1 | test | created | 2023-05-23 01:07:23 | 2023-05-23 01:07:23 |
+----+------+---------+---------------------+---------------------+
1 row in set (0.00 sec)
ちゃんとデータが登録されてます。よかった
これで登録API は完成です
更新API の実装
続いて、更新API の実装です
更新API では、 id を指定して対象データの task と status の更新を行うことができます
以下は更新API の handler の実装です
// パス(todo/:id)に指定されたパラメータを格納するための構造体
type UpdateRequestPathParam struct {
ID int `uri:"id"`
}
// リクエストデータ(body)を格納するための構造体
type UpdateRequestBodyParam struct {
Task string `json:"task" binding:"required,max=60"`
Status model.TaskStatus `json:"status" binding:"required,task_status"`
}
func (t *todoHandler) Update(c *gin.Context) {
var pathParam UpdateRequestPathParam
var bodyParam UpdateRequestBodyParam
// パスパラメータを構造体(UpdateRequestPathParam)にマッピング
if err := c.ShouldBindUri(&pathParam); err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
return
}
// リクエストパラメータ(body)を構造体(UpdateRequestBodyParam)にマッピング
if err := c.ShouldBindJSON(&bodyParam); err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
return
}
// usecase の呼び出し
if err := t.usecase.Update(pathParam.ID, bodyParam.Task, bodyParam.Status); err != nil {
c.JSON(http.StatusInternalServerError, "")
return
}
// レスポンスを返却
c.JSON(http.StatusNoContent, nil)
}
UpdateRequestPathParam
と UpdateRequestBodyParam
でそれぞれパスパラメータとボディに指定されたリクエストパラメータを受け取る構造体を定義しています
UpdateRequestBodyParam
では、登録API と同様にバリデーション設定を行っているのですが、ここではtask_status
というカスタムバリデータを作り独自のバリデーションチェックを行うようにしています
これは、更新用API ではステータスの更新もできるのですが、事前定義されたステータスの値以外を受付ないようにするためです
カスタムバリデータの実装は以下のようになっています
package validator
import (
"app/domain/model"
"github.com/gin-gonic/gin/binding"
"github.com/go-playground/validator/v10"
)
func SetupValidator() error {
if v, ok := binding.Validator.Engine().(*validator.Validate); ok {
if err := v.RegisterValidation("task_status", ValidateTaskStatus); err != nil {
return err
}
}
return nil
}
func ValidateTaskStatus(fl validator.FieldLevel) bool {
return model.TaskStatusMap[model.TaskStatus(fl.Field().String())]
}
ValidateTaskStatus(fl validator.FieldLevel)
が実際のバリデーションチェックを行う部分になっており、入力された値が事前定義された値に該当するかをチェックをし、 結果を bool で返します
チェックには、domain/model/todo.go
に定義してある以下の関数を使用しています
var TaskStatusMap = map[TaskStatus]bool{
Created: true,
Processing: true,
Done: true,
}
入力された値が TaskStatusMap
に存在すれば true
を返し、存在しなければ false
を返します
SetupValidator()
では作成したバリデーションチェックの関数の登録を行います
func SetupValidator() error {
if v, ok := binding.Validator.Engine().(*validator.Validate); ok {
// 第一引数に任意のタグ名を指定、第二引数にバリデーションチェックに使用する関数を指定
if err := v.RegisterValidation("task_status", ValidateTaskStatus); err != nil {
return err
}
}
return nil
}
func ValidateTaskStatus(fl validator.FieldLevel) bool {
return model.TaskStatusMap[model.TaskStatus(fl.Field().String())]
}
続いて usecase 層
こちらも登録API と同様に handler から task の情報を受け取りデータの更新を行います
func (t *todo) Update(id int, task string, status model.TaskStatus) error {
// 更新用の構造体を生成
todo := model.NewUpdateTodo(id, task, status)
// DBのデータの更新
if err := t.todoRepository.Update(todo); err != nil {
return err
}
return nil
}
todo := model.NewUpdateTodo(id, task ,status)
では、 domain/model/todo.go
のNewUpdateTodo(id int, task string, status TaskStatus)
を呼び出し、DB にデータを登録するための構造体を作成します
実装はこんな感じ
func NewUpdateTodo(id int, task string, status TaskStatus) *Todo {
return &Todo{
ID: id,
Task: task,
Status: status,
}
}
続いて DB 操作
登録時と同様に t.todoRepository.Update(todo)
で repository を介して
infrastructure/todo.go
の`Update(t *model.Todo)を呼び出し、DB へのデータの更新を行います
func (td *Todo) Update(t *model.Todo) error {
if err := td.db.Save(t).Error; err != nil {
return err
}
return nil
}
これで更新API の実装が完了したので、実際に API をコールしてデータの更新をしてみたいと思います
先程作成した以下のデータをのステータスを更新します
mysql> select * from todo where id = 1;
+----+------+---------+---------------------+---------------------+
| id | task | status | created_at | updated_at |
+----+------+---------+---------------------+---------------------+
| 1 | test | created | 2023-05-18 02:55:10 | 2023-05-18 02:55:10 |
+----+------+---------+---------------------+---------------------+
1 row in set (0.00 sec)
実際に API をコールして、タスクステータスを created -> processing に更新します。
% curl -i localhost/todo/1 -H "Content-Type: application/json" -X PUT -d '{"task": "test", "status": "processing"}'
HTTP/1.1 204 No Content
Content-Type: application/json; charset=utf-8
Date: Mon, 22 May 2023 16:08:36 GMT
データを確認します
mysql> select * from todo where id = 1;
+----+------+------------+---------------------+---------------------+
| id | task | status | created_at | updated_at |
+----+------+------------+---------------------+---------------------+
| 1 | test | processing | 2023-05-23 01:07:23 | 2023-05-23 01:08:36 |
+----+------+------------+---------------------+---------------------+
1 row in set (0.00 sec)
ちゃんと更新されてますね。よかった
(今となってはステータスだけ更新するAPI にすればよかったな。。。)
これで、更新API は完成です
取得API の実装
取得API では、 パスパラメータに設定した id に該当するデータを1件取得して返します
この部分
/todo/{id}
以下は 取得API の handler の実装です
// パス(todo/:id)に指定されたパラメータを格納するための構造体
type FindRequestParam struct {
ID int `uri:"id" binding:"required"`
}
func (t *todoHandler) Find(c *gin.Context) {
var req FindRequestParam
// パスパラメータを構造体(FindRequestParam)にマッピング
if err := c.ShouldBindUri(&req); err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
return
}
// usecase の呼び出し
res, err := t.usecase.Find(req.ID)
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
return
}
if res == nil {
// 検索結果(res = nil)が存在しなかった場合は 404 not found を返す
c.JSON(http.StatusNotFound, nil)
return
}
// レスポンスを返却
c.JSON(http.StatusOK, res)
}
続いて、usecase 層
usecase 層では、 handler から受け取った id 元に検索を行い結果を返します
func (t *todo) Find(id int) (*model.Todo, error) {
todo, err := t.todoRepository.Find(id)
if err != nil {
return nil, err
}
return todo, nil
}
infrastructure 層の実装は以下です
func (td *Todo) Find(id int) (*model.Todo, error) {
var todo *model.Todo
err := td.db.Where("id = ?", id).Take(&todo).Error
if err != nil {
if err == gorm.ErrRecordNotFound {
return nil, nil
}
return nil, err
}
return todo, nil
}
Take
関数を使った時、レコードが存在しないとgorm.ErrRecordNotFound
というエラーが返ってきます。レコードが存在しない場合にシステムエラーのような扱いをしたくなく、 404 でレスポンスを返したいので、ここでは err !=nil
じゃなかった場合、エラーの種類をチェックして エラーがgorm.ErrRecordNotFound
だった場合には nil を返すようにしています
if err != nil {
if err == gorm.ErrRecordNotFound {
return nil, nil
}
...
これで、取得API の実装が完了したので実際に API をコールしてデータが取得できるか確認してみたいと思います
先程利用した以下のデータを取得します
mysql> select * from todo where id = 1;
+----+------+------------+---------------------+---------------------+
| id | task | status | created_at | updated_at |
+----+------+------------+---------------------+---------------------+
| 1 | test | processing | 2023-05-18 02:55:10 | 2023-05-19 01:55:33 |
+----+------+------------+---------------------+---------------------+
API をコールします
% curl -i -XGET localhost/todo/1
HTTP/1.1 200 OK
Content-Type: application/json; charset=utf-8
Date: Mon, 22 May 2023 16:10:00 GMT
Content-Length: 114
{"ID":1,"Task":"test","Status":"processing","CreatedAt":"2023-05-23T01:07:23Z","UpdatedAt":"2023-05-23T01:08:36Z"}
ちゃんとデータが取得できますね。よかった
全件取得API の実装
続いて全件取得API の実装です
これは、取得API と違い、 id の指定をせずに、現在 DB に登録されている task を全件取得して返します
※実際に使うときは limit で制限かけるとかした方がいいです
以下は 全件取得API の handler の実装です
func (t *todoHandler) FindAll(c *gin.Context) {
res, err := t.usecase.FindAll()
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
return
}
c.JSON(http.StatusOK, res)
}
特に目新しいところはないですね
続いて、 usecase です
func (t *todo) FindAll() ([]*model.Todo, error) {
todo, err := t.todoRepository.FindAll()
if err != nil {
return nil, err
}
return todo, nil
}
infrastructure
func (td *Todo) FindAll() ([]*model.Todo, error) {
var todos []*model.Todo
err := td.db.Find(&todos).Error
if err != nil {
return nil, err
}
return todos, nil
}
gorm の Find 関数では結果が配列で返ってくるので、検索結果を格納するための構造体も配列として定義しています
var todos []*model.Todo
err := td.db.Find(&todos).Error
if err != nil {
return nil, err
}
これで、全件取得API の実装が完了したので、実際に API をコールしてデータを取得してみたいと思います
今はデータが1件しか登録されていないので、事前にもう1件登録しておきます
せっかくなので、API で登録しましょう
% curl -i localhost/todo -H "Content-Type: application/json" -X POST -d '{"task": "test2"}'
HTTP/1.1 201 Created
Content-Type: application/json; charset=utf-8
Date: Mon, 22 May 2023 16:10:30 GMT
Content-Length: 4
実際にデータが登録されたか確認します
mysql> select * from todo;
+----+-------+------------+---------------------+---------------------+
| id | task | status | created_at | updated_at |
+----+-------+------------+---------------------+---------------------+
| 1 | test | processing | 2023-05-23 01:07:23 | 2023-05-23 01:08:36 |
| 2 | test2 | created | 2023-05-23 01:10:30 | 2023-05-23 01:10:30 |
+----+-------+------------+---------------------+---------------------+
2 rows in set (0.00 sec)
ちゃんと登録されていますね
では、全件取得API でこの2件のデータが期待通り取得できるか試してみます
% curl -i -XGET localhost/todo
HTTP/1.1 200 OK
Content-Type: application/json; charset=utf-8
Date: Mon, 22 May 2023 16:11:19 GMT
Content-Length: 229
[{"ID":1,"Task":"test","Status":"processing","CreatedAt":"2023-05-23T01:07:23Z","UpdatedAt":"2023-05-23T01:08:36Z"},{"ID":2,"Task":"test2","Status":"created","CreatedAt":"2023-05-23T01:10:30Z","UpdatedAt":"2023-05-23T01:10:30Z"}]
ちゃんと2件、データが取得できてますね。良かった
見やすいように整形するとこんな感じです
[
{
"ID": 1,
"Task": "test",
"Status": "processing",
"CreatedAt": "2023-05-23T01:07:23Z",
"UpdatedAt": "2023-05-23T01:08:36Z"
},
{
"ID": 2,
"Task": "test2",
"Status": "created",
"CreatedAt": "2023-05-23T01:10:30Z",
"UpdatedAt": "2023-05-23T01:10:30Z"
}
]
削除API の実装
最後に、削除API の実装です
削除API は id を指定して対象データの削除を行います
以下は削除API の handler の実装です
// パス(todo/:id)に指定されたパラメータを格納するための構造体
type DeleteRequestParam struct {
ID int `uri:"id"`
}
func (t *todoHandler) Delete(c *gin.Context) {
var req DeleteRequestParam
// パスパラメータを構造体(DeleteRequestParam)にマッピング
if err := c.ShouldBindUri(&req); err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
return
}
// usecase の呼び出し
if err := t.usecase.Delete(req.ID); err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
return
}
// レスポンスを返却
c.JSON(http.StatusNoContent, nil)
}
続いて usecase
func (t *todo) Delete(id int) error {
if err := t.todoRepository.Delete(id); err != nil {
return err
}
return nil
}
infrastructure
func (td *Todo) Delete(id int) error {
if err := td.db.Where("id = ?", id).Delete(&model.Todo{}).Error; err != nil {
return err
}
return nil
}
gorm の Delete 関数を使いデータの削除を行っています
これで、削除API の実装が完了したので、実際にデータを削除してみます
以下のデータの削除を行います
mysql> select * from todo where id = 2;
+----+-------+---------+---------------------+---------------------+
| id | task | status | created_at | updated_at |
+----+-------+---------+---------------------+---------------------+
| 2 | test2 | created | 2023-05-23 01:10:30 | 2023-05-23 01:10:30 |
+----+-------+---------+---------------------+---------------------+
1 row in set (0.00 sec)
削除API の実行
% curl -i localhost/todo/2 -X DELETE
HTTP/1.1 200 OK
Content-Type: application/json; charset=utf-8
Date: Mon, 22 May 2023 16:12:41 GMT
Content-Length: 4
いけたっぽい。実際にデータが削除されたか確認します
mysql> select * from todo where id = 2;
Empty set (0.00 sec)
ちゃんと削除されてますね。よかった
以上で、API の実装は完了です
最後に
簡単ですが、基本的な CRUD 操作を行うための REST API を実装しました
今回作ったものは以下のリポジトリに配置しています
一応テストコードも書いているので、興味のある方は見てみてください
次回は githubactions を使って CI の構築でもしてみようと思います
ではでは。