0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 1 year has passed since last update.

Go言語でREST APIを作ってみる④(Record:記録)【Echo + GORM + MySQL】

Posted at

はじめに

前回の続きで、APIの詳細の実装をしていきます。
その中でも今回はRecord周りを記事にしていきます。

過去の記事はこちら
Go言語でREST APIを作ってみる【Echo + GORM + MySQL】
Go言語でREST APIを作ってみる②(User)【Echo + GORM + MySQL】
Go言語でREST APIを作ってみる③(Spot)【Echo + GORM + MySQL】

ディレクトリ構成

今回使用するファイルは以下です。

.
├── handler
│   └── record.go
├── model
│   └── record.go
├── repository
│   └── db.go
├── router
│   └── router.go
├── test
    ├── .env
│   └── record_test.go
├── .env
└── main.go

実装

Record周りの実装をしていきます。

モデルの作成

model/record.go
package model

import "time"

type Record struct {
	ID          int64     `json:"id"`
	UserID      int64     `json:"user_id"`
	SpotID      int64     `json:"spot_id"`
	Date        string    `json:"date"`
	Weather     string    `json:"weather"`
	Temperature float32   `json:"temperature"`
	RunningTime float32   `json:"running_time"`
	Distance    float32   `json:"distance"`
	Description string    `json:"description"`
	CreatedAt   time.Time `json:"created_at"`
	UpdatedAt   time.Time `json:"updated_at"`
}

DBの作成

過去に作成したrepository/db.goの追記していきます。

repository/db.go
package repository

import (
	"log"
	"os"

	. "bike_noritai_api/model"

	"github.com/joho/godotenv"
	"gorm.io/driver/mysql"
	"gorm.io/gorm"
)

var (
	DB  *gorm.DB
	err error
)

func init() {
	err := godotenv.Load()
	if err != nil {
		log.Fatal("Error loading .env file")
	}

	dsn := os.Getenv("DEV_DB_DNS")
	if os.Getenv("ENV") == "test" {
		dsn = os.Getenv("TEST_DB_DNS")
	}

	DB, err = gorm.Open(mysql.Open(dsn), &gorm.Config{})
	if err != nil {
		log.Fatalln(dsn + "database can't connect")
	}

	DB.Migrator().DropTable(&User{})
	DB.Migrator().DropTable(&Spot{})
    DB.Migrator().DropTable(&Record{}) // 追記

	DB.AutoMigrate(&User{})
	DB.AutoMigrate(&Spot{})
    DB.AutoMigrate(&Record{}) // 追記

	users := []User{
		{
			Name:       "tester1",
			Email:      "tester1@bike_noritai_dev",
			Password:   "password",
			Area:       "東海",
			Prefecture: "三重県",
			Url:        "http://test.com",
			BikeName:   "CBR650R",
			Experience: 5,
		},
		{
			Name:       "tester2",
			Email:      "tester2@bike_noritai_dev",
			Password:   "password",
			Area:       "関東",
			Prefecture: "東京都",
			Url:        "http://test.com",
			BikeName:   "CBR1000RR",
			Experience: 10,
		},
	}
	DB.Create(&users)

	spots := []Spot{
		{
			UserID:      1,
			Name:        "豊受大神宮 (伊勢神宮 外宮)",
			Image:       "http://test.com",
			Type:        "観光",
			Address:     "三重県伊勢市豊川町279",
			HpURL:       "https://www.isejingu.or.jp/about/geku/",
			OpenTime:    "5:00~18:00",
			OffDay:      "",
			Parking:     true,
			Description: "外宮から行くのが良いみたいですよ。",
			Lat:         34.48786428571363,
			Lng:         136.70372958477844,
		},
		{
			UserID:      1,
			Name:        "伊勢神宮(内宮)",
			Image:       "http://test.com",
			Type:        "観光",
			Address:     "三重県伊勢市宇治館町1",
			HpURL:       "https://www.isejingu.or.jp/",
			OpenTime:    "5:00~18:00",
			OffDay:      "",
			Parking:     true,
			Description: "日本最大の由緒正しき神社です。",
			Lat:         34.45616423029016,
			Lng:         136.7258739014393,
		},
	}
	DB.Create(&spots)

    // 以下追記
    records := []Record{
		{
			UserID:      users[0].ID,
			SpotID:      spots[0].ID,
			Date:        time.Now().Format("2023-01-01"),
			Weather:     "晴れ",
			Temperature: 23.4,
			RunningTime: 4,
			Distance:    120.4,
			Description: "最高のツーリング日和でした!",
		},
		{
			UserID:      users[0].ID,
			SpotID:      spots[1].ID,
			Date:        time.Now().Format("2023-01-01"),
			Weather:     "曇り",
			Temperature: 26.1,
			RunningTime: 7,
			Distance:    184.1,
			Description: "なんとか天気が持って良かったです!",
		},
		{
			UserID:      users[1].ID,
			SpotID:      spots[0].ID,
			Date:        time.Now().Format("2023-01-01"),
			Weather:     "雨",
			Temperature: 13.4,
			RunningTime: 2,
			Distance:    50.6,
			Description: "朝から雨で大変でした。",
		},
		{
			UserID:      users[1].ID,
			SpotID:      spots[1].ID,
			Date:        time.Now().Format("2023-01-01"),
			Weather:     "晴れ",
			Temperature: 33.4,
			RunningTime: 6,
			Distance:    220.4,
			Description: "バイク暑すぎます!!!",
		},
	}
	DB.Create(&records)
}

DBの細かな解説は前回の記事を参照してください

Router

router.go
func NewRouter() *echo.Echo {
	e := echo.New()
	e.Use(middleware.CORS())
	e.Use(middleware.Logger())

	e.GET("/api/users", handler.GetUsers)
	e.GET("/api/users/:user_id", handler.GetUser)
	e.POST("/api/users", handler.CreateUser)
	e.PATCH("/api/users/:user_id", handler.UpdateUser)
	e.DELETE("/api/users/:user_id", handler.DeleteUser)

	e.GET("/api/spots", handler.GetSpots)
	e.GET("/api/spots/:spot_id", handler.GetSpot)
	e.GET("/api/users/:user_id/spots", handler.GetUserSpot)
	e.POST("/api/users/:user_id/spots", handler.CreateSpot)
	e.PATCH("/api/users/:user_id/spots/:spot_id", handler.UpdateSpot)
	e.DELETE("/api/users/:user_id/spots/:spot_id", handler.DeleteSpot)

    // 以下追記
    e.GET("/api/users/:user_id/records", handler.GetUserRecords)
	e.GET("/api/spots/:spot_id/records", handler.GetSpotRecords)
	e.GET("/api/records/:record_id", handler.GetRecord)
	e.POST("/api/users/:user_id/spots/:spot_id/records", handler.CreateRecord)
	e.PATCH("/api/users/:user_id/spots/:spot_id/records/:record_id", handler.UpdateRecord)
	e.DELETE("/api/users/:user_id/spots/:spot_id/records/:record_id", handler.DeleteRecord)
}

Handler

handler/record.go
package handler

import (
	"errors"
	"net/http"
	"strconv"

	"github.com/labstack/echo/v4"
	"gorm.io/gorm"

	. "bike_noritai_api/model"
	. "bike_noritai_api/repository"
)

func GetUserRecords(c echo.Context) error {
	records := []Record{}

	userID := c.Param("user_id")
	if userID == "" {
		return c.JSON(http.StatusBadRequest, "user ID is required")
	}

	if err := DB.Where("user_id = ?", userID).Find(&records).Error; err != nil {
		if errors.Is(err, gorm.ErrRecordNotFound) {
			return c.JSON(http.StatusNotFound, "records not found")
		}
		return err
	}

	return c.JSON(http.StatusOK, records)
}

func GetSpotRecords(c echo.Context) error {
	records := []Record{}

	spotID := c.Param("spot_id")
	if spotID == "" {
		return c.JSON(http.StatusBadRequest, "spot ID is required")
	}

	if err := DB.Where("spot_id = ?", spotID).Find(&records).Error; err != nil {
		if errors.Is(err, gorm.ErrRecordNotFound) {
			return c.JSON(http.StatusNotFound, "records not found")
		}
		return err
	}

	return c.JSON(http.StatusOK, records)
}

func GetRecord(c echo.Context) error {
	record := []Record{}

	recordID := c.Param("record_id")
	if recordID == "" {
		return c.JSON(http.StatusBadRequest, "record ID is required")
	}

	if err := DB.Where("id = ?", recordID).Find(&record).Error; err != nil {
		if errors.Is(err, gorm.ErrRecordNotFound) {
			return c.JSON(http.StatusNotFound, "record not found")
		}
		return err
	}

	return c.JSON(http.StatusOK, record)
}

func CreateRecord(c echo.Context) error {
	record := Record{}

	if err := c.Bind(&record); err != nil {
		return err
	}

	userID, _ := strconv.ParseInt(c.Param("user_id"), 10, 64)
	if userID == 0 {
		return c.JSON(http.StatusBadRequest, "user ID is required")
	}

	spotID, _ := strconv.ParseInt(c.Param("spot_id"), 10, 64)
	if spotID == 0 {
		return c.JSON(http.StatusBadRequest, "spot ID is required")
	}

	record.UserID = userID
	record.SpotID = spotID

	DB.Create(&record)
	return c.JSON(http.StatusCreated, record)
}

func UpdateRecord(c echo.Context) error {
	record := new(Record)

	userID, _ := strconv.ParseInt(c.Param("user_id"), 10, 64)
	if userID == 0 {
		return c.JSON(http.StatusBadRequest, "user ID is required")
	}

	spotID, _ := strconv.ParseInt(c.Param("spot_id"), 10, 64)
	if spotID == 0 {
		return c.JSON(http.StatusBadRequest, "spot ID is required")
	}

	recordID := c.Param("record_id")
	if recordID == "" {
		return c.JSON(http.StatusBadRequest, "record ID is required")
	}

	if err := DB.First(&record, recordID).Error; err != nil {
		return c.JSON(http.StatusBadRequest, err.Error())
	}

	if record.UserID != userID {
		return c.JSON(http.StatusBadRequest, "user and record do not match")
	}

	if record.SpotID != spotID {
		return c.JSON(http.StatusBadRequest, "spot and record do not match")
	}

	if err := c.Bind(&record); err != nil {
		return c.JSON(http.StatusBadRequest, err.Error())
	}

	if err := DB.Model(&record).Where("id=?", record.ID).Updates(&record).Error; err != nil {
		return c.JSON(http.StatusBadRequest, err.Error())
	}

	return c.JSON(http.StatusCreated, record)
}

func DeleteRecord(c echo.Context) error {
	record := new(Record)

	userID, _ := strconv.ParseInt(c.Param("user_id"), 10, 64)
	if userID == 0 {
		return c.JSON(http.StatusBadRequest, "user ID is required")
	}

	spotID, _ := strconv.ParseInt(c.Param("spot_id"), 10, 64)
	if spotID == 0 {
		return c.JSON(http.StatusBadRequest, "spot ID is required")
	}

	recordID := c.Param("record_id")
	if recordID == "" {
		return c.JSON(http.StatusBadRequest, "spot ID is required")
	}

	if err := DB.First(&record, recordID).Error; err != nil {
		return c.JSON(http.StatusBadRequest, err.Error())
	}

	if record.UserID != userID {
		return c.JSON(http.StatusBadRequest, "user and record do not match")
	}

	if record.SpotID != spotID {
		return c.JSON(http.StatusBadRequest, "spot and record do not match")
	}

	if err := DB.Where("id = ?", recordID).Delete(&record).Error; err != nil {
		return c.JSON(http.StatusBadRequest, err.Error())
	}

	return c.JSON(http.StatusNoContent, record)
}

テスト

test/record_test.go
package test

import (
	"bytes"
	"encoding/json"
	"errors"
	"gorm.io/gorm"
	"net/http"
	"net/http/httptest"
	"strconv"
	"strings"
	"testing"

	. "bike_noritai_api/model"
	. "bike_noritai_api/repository"
	. "bike_noritai_api/router"
)

func TestGetUserRecords(t *testing.T) {
	router := NewRouter()
	req := httptest.NewRequest(http.MethodGet, "/api/users/1/records", nil)
	res := httptest.NewRecorder()
	router.ServeHTTP(res, req)

	if res.Code != http.StatusOK {
		t.Errorf("unexpected status code: got %v, want %v", res.Code, http.StatusOK)
	}

	expectedBody := `"id":1,"user_id":1,"spot_id":1,"date":"70710-05-05","weather":"晴れ","temperature":23.4,"running_time":4,"distance":120.4,"description":"最高のツーリング日和でした!"`

	expectedBody2 := `"id":2,"user_id":1,"spot_id":2,"date":"70710-05-05","weather":"曇り","temperature":26.1,"running_time":7,"distance":184.1,"description":"なんとか天気が持って良かったです!"`

	if !strings.Contains(res.Body.String(), expectedBody) {
		t.Errorf("unexpected response body: got %v, want %v", res.Body.String(), expectedBody)
	}

	if !strings.Contains(res.Body.String(), expectedBody2) {
		t.Errorf("unexpected response body: got %v, want %v", res.Body.String(), expectedBody2)
	}
}

func TestGetSpotRecords(t *testing.T) {
	router := NewRouter()
	req := httptest.NewRequest(http.MethodGet, "/api/spots/1/records", nil)
	res := httptest.NewRecorder()
	router.ServeHTTP(res, req)

	if res.Code != http.StatusOK {
		t.Errorf("unexpected status code: got %v, want %v", res.Code, http.StatusOK)
	}

	expectedBody := `"id":1,"user_id":1,"spot_id":1,"date":"70710-05-05","weather":"晴れ","temperature":23.4,"running_time":4,"distance":120.4,"description":"最高のツーリング日和でした!"`

	expectedBody2 := `"id":3,"user_id":2,"spot_id":1,"date":"70710-05-05","weather":"雨","temperature":13.4,"running_time":2,"distance":50.6,"description":"朝から雨で大変でした。"`

	if !strings.Contains(res.Body.String(), expectedBody) {
		t.Errorf("unexpected response body: got %v, want %v", res.Body.String(), expectedBody)
	}

	if !strings.Contains(res.Body.String(), expectedBody2) {
		t.Errorf("unexpected response body: got %v, want %v", res.Body.String(), expectedBody2)
	}
}

func TestGetRecord(t *testing.T) {
	router := NewRouter()
	req := httptest.NewRequest(http.MethodGet, "/api/records/1", nil)
	res := httptest.NewRecorder()
	router.ServeHTTP(res, req)

	if res.Code != http.StatusOK {
		t.Errorf("unexpected status code: got %v, want %v", res.Code, http.StatusOK)
	}

	expectedBody := `"id":1,"user_id":1,"spot_id":1,"date":"70710-05-05","weather":"晴れ","temperature":23.4,"running_time":4,"distance":120.4,"description":"最高のツーリング日和でした!"`

	if !strings.Contains(res.Body.String(), expectedBody) {
		t.Errorf("unexpected response body: got %v, want %v", res.Body.String(), expectedBody)
	}
}

func TestCreateRecord(t *testing.T) {
	router := NewRouter()

	record := Record{
		Date:        "2023-01-01",
		Weather:     "曇り",
		Temperature: 23.4,
		RunningTime: 4.5,
		Distance:    201.6,
		Description: "AAAAAAAAAAAAAA",
	}

	reqBody, err := json.Marshal(record)
	if err != nil {
		t.Fatalf("failed to marshal request body: %v", err)
	}

	var userID int64 = 1
	var spotID int64 = 1

	req := httptest.NewRequest(http.MethodPost, "/api/users/"+strconv.Itoa(int(userID))+"/spots/"+strconv.Itoa(int(spotID))+"/records", bytes.NewBuffer(reqBody))
	req.Header.Set("Content-Type", "application/json")
	res := httptest.NewRecorder()
	router.ServeHTTP(res, req)

	if res.Code != http.StatusCreated {
		t.Errorf("expected status code %v but got %v", http.StatusCreated, res.Code)
	}

	var resBody Record
	if err := json.Unmarshal(res.Body.Bytes(), &resBody); err != nil {
		t.Fatalf("failed to unmarshal response body: %v", err)
	}
	if resBody.ID == 0 {
		t.Errorf("expected spot ID to be non-zero but got %v", resBody.ID)
	}
	if resBody.UserID != userID {
		t.Errorf("expected record user id to be %v but got %v", userID, resBody.UserID)
	}
	if resBody.SpotID != spotID {
		t.Errorf("expected record record id to be %v but got %v", spotID, resBody.SpotID)
	}
	if resBody.Date != record.Date {
		t.Errorf("expected record date to be %v but got %v", record.Date, resBody.Date)
	}
	if resBody.Weather != record.Weather {
		t.Errorf("expected record Weather to be %v but got %v", record.Weather, resBody.Weather)
	}
	if resBody.Temperature != record.Temperature {
		t.Errorf("expected record Temperature to be %v but got %v", record.Temperature, resBody.Temperature)
	}
	if resBody.RunningTime != record.RunningTime {
		t.Errorf("expected record RunningTime to be %v but got %v", record.RunningTime, resBody.RunningTime)
	}
	if resBody.Distance != record.Distance {
		t.Errorf("expected record Distance to be %v but got %v", record.Distance, resBody.Distance)
	}
	if resBody.Description != record.Description {
		t.Errorf("expected record Description to be %v but got %v", record.Description, resBody.Description)
	}
}

func TestUpdateRecord(t *testing.T) {
	record := Record{
		UserID:      1,
		SpotID:      1,
		Date:        "2023-01-01",
		Weather:     "曇り",
		Temperature: 23.4,
		RunningTime: 4.5,
		Distance:    201.6,
		Description: "AAAAAAAAAAAAAA",
	}
	if err := DB.Create(&record).Error; err != nil {
		t.Fatalf("failed to create test user: %v", err)
	}

	updatedRecord := Record{
		ID:          record.ID,
		UserID:      1,
		SpotID:      1,
		Date:        "2023-02-01",
		Weather:     "晴れ",
		Temperature: 33.4,
		RunningTime: 6.5,
		Distance:    211.5,
		Description: "BBBBBBBBBBBBB",
	}
	reqBody, err := json.Marshal(updatedRecord)
	if err != nil {
		t.Fatalf("failed to marshal request body: %v", err)
	}

	router := NewRouter()
	req := httptest.NewRequest(http.MethodPatch, "/api/users/"+strconv.Itoa(int(record.UserID))+"/spots/"+strconv.Itoa(int(record.SpotID))+"/records/"+strconv.Itoa(int(record.ID)), bytes.NewBuffer(reqBody))
	req.Header.Set("Content-Type", "application/json")
	res := httptest.NewRecorder()
	router.ServeHTTP(res, req)

	if res.Code != http.StatusCreated {
		t.Errorf("expected status code %v but got %v", http.StatusCreated, res.Code)
	}

	var resBody Record
	if err := json.Unmarshal(res.Body.Bytes(), &resBody); err != nil {
		t.Fatalf("failed to unmarshal response body: %v", err)
	}
	if resBody.ID == 0 {
		t.Errorf("expected spot ID to be non-zero but got %v", resBody.ID)
	}
	if resBody.UserID != updatedRecord.UserID {
		t.Errorf("expected record user id to be %v but got %v", updatedRecord.UserID, resBody.UserID)
	}
	if resBody.SpotID != updatedRecord.SpotID {
		t.Errorf("expected record record id to be %v but got %v", updatedRecord.SpotID, resBody.SpotID)
	}
	if resBody.Date != updatedRecord.Date {
		t.Errorf("expected record date to be %v but got %v", updatedRecord.Date, resBody.Date)
	}
	if resBody.Weather != updatedRecord.Weather {
		t.Errorf("expected record Weather to be %v but got %v", updatedRecord.Weather, resBody.Weather)
	}
	if resBody.Temperature != updatedRecord.Temperature {
		t.Errorf("expected record Temperature to be %v but got %v", updatedRecord.Temperature, resBody.Temperature)
	}
	if resBody.RunningTime != updatedRecord.RunningTime {
		t.Errorf("expected record RunningTime to be %v but got %v", updatedRecord.RunningTime, resBody.RunningTime)
	}
	if resBody.Distance != updatedRecord.Distance {
		t.Errorf("expected record Distance to be %v but got %v", updatedRecord.Distance, resBody.Distance)
	}
	if resBody.Description != updatedRecord.Description {
		t.Errorf("expected record Description to be %v but got %v", updatedRecord.Description, resBody.Description)
	}
}

func TestDeleteRecord(t *testing.T) {
	router := NewRouter()
	req := httptest.NewRequest(http.MethodDelete, "/api/users/1/spots/1/records/1", nil)
	res := httptest.NewRecorder()
	router.ServeHTTP(res, req)

	if res.Code != http.StatusNoContent {
		t.Errorf("expected status code %v, but got %v", http.StatusNoContent, res.Code)
	}

	var deletedRecord *Record
	err := DB.First(&deletedRecord, "1").Error
	if !errors.Is(err, gorm.ErrRecordNotFound) {
		t.Errorf("expected record to be deleted, but found: %v", deletedRecord)
	}
}

おわりに

今回はGolangでRecordのREST APIを作成しました。
次回はCommentのAPIを実装していこうと思います。

はじめてのGolangのAPI作成の役に立てれば幸いです。

0
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?