LoginSignup
2
0

More than 1 year has passed since last update.

Go言語でREST APIを作ってみる⑥(Bookmark:ブックマーク)【Echo + GORM + MySQL】

Posted at

はじめに

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

この記事がAPI実装の最後です。

過去の記事はこちら
Go言語でREST APIを作ってみる【Echo + GORM + MySQL】
Go言語でREST APIを作ってみる②(User)【Echo + GORM + MySQL】
Go言語でREST APIを作ってみる③(Spot)【Echo + GORM + MySQL】
Go言語でREST APIを作ってみる④(Record:記録)【Echo + GORM + MySQL】
Go言語でREST APIを作ってみる⑤(Comment:コメント)【Echo + GORM + MySQL】

ディレクトリ構成

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

.
├── handler
│   └── bookmark.go
├── model
│   └── bookmark.go
├── repository
│   └── db.go
├── router
│   └── router.go
├── test
    ├── .env
│   └── bookmark_test.go
├── .env
└── main.go

実装

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

モデルの作成

model/bookmark.go
package model

import "time"

type Bookmark struct {
	ID        int64     `json:"id"`
	UserID    int64     `json:"user_id"`
	SpotID    int64     `json:"spot_id"`
	CreatedAt time.Time `json:"created_at"`
	UpdatedAt time.Time `json:"updated_at"`
}

DBの作成

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

repository/db.go
package repository

import (
	"log"
	"os"
	"time"

	. "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.Migrator().DropTable(&Comment{})
	DB.Migrator().DropTable(&Bookmark{}) // 追記

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

	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)

	comments := []Comment{
		{
			UserID:   users[0].ID,
			RecordID: 1,
			UserName: users[0].Name,
			Text:     "AAAAAAAAAAAAAAA",
		},
		{
			UserID:   users[0].ID,
			RecordID: 1,
			UserName: users[0].Name,
			Text:     "BBBBBBBBBBBBBBBBB",
		},
		{
			UserID:   users[1].ID,
			RecordID: 2,
			UserName: users[0].Name,
			Text:     "CCCCCCCCCCCCCCCCC",
		},
		{
			UserID:   users[1].ID,
			RecordID: 2,
			UserName: users[0].Name,
			Text:     "DDDDDDDDDDDDDDDDDD",
		},
	}
	DB.Create(&comments)

    // 以下追記
	bookmarks := []Bookmark{
		{
			UserID: users[0].ID,
			SpotID: spots[0].ID,
		},
		{
			UserID: users[0].ID,
			SpotID: spots[1].ID,
		},
		{
			UserID: users[1].ID,
			SpotID: spots[0].ID,
		},
		{
			UserID: users[1].ID,
			SpotID: spots[1].ID,
		},
	}
	DB.Create(&bookmarks)
}

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

Router

router.go
package router

import (
	"bike_noritai_api/handler"

	"github.com/labstack/echo/v4"
	"github.com/labstack/echo/v4/middleware"
)

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)

	e.GET("/api/users/:user_id/comments", handler.GetUserComments)
	e.GET("/api/records/:record_id/comments", handler.GetRecordComments)
	e.POST("/api/users/:user_id/records/:record_id/comments", handler.CreateComment)
	e.PATCH("/api/users/:user_id/records/:record_id/comments/:comment_id", handler.UpdateComment)
	e.DELETE("/api/users/:user_id/records/:record_id/comments/:comment_id", handler.DeleteComment)

    // 以下追記
	e.GET("/api/users/:user_id/bookmarks", handler.GetBookmarks)
	e.POST("/api/users/:user_id/spots/:spot_id/bookmarks", handler.CreateBookmark)
	e.DELETE("/api/users/:user_id/spots/:spot_id/bookmarks/:bookmark_id", handler.DeleteBookmark)

	return e
}

Handler

handler/bookmark.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 GetBookmarks(c echo.Context) error {
	bookmarks := []Bookmark{}

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

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

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

func CreateBookmark(c echo.Context) error {
	bookmark := Bookmark{}

	if err := c.Bind(&bookmark); 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")
	}

	bookmark.UserID = userID
	bookmark.SpotID = spotID

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

func DeleteBookmark(c echo.Context) error {
	bookmark := new(Bookmark)

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

	bookmarkID := c.Param("bookmark_id")
	if bookmarkID == "" {
		return c.JSON(http.StatusBadRequest, "bookmark ID is required")
	}

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

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

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

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

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

テスト

test/bookmark_test.go
package test

import (
	"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 TestGetBookmarks(t *testing.T) {
	router := NewRouter()
	req := httptest.NewRequest(http.MethodGet, "/api/users/1/bookmarks", 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`
	expectedBody2 := `"id":2,"user_id":1,"spot_id":2,`

	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 TestCreateBookmark(t *testing.T) {
	router := NewRouter()

	var userID int64 = 3
	var spotID int64 = 3

	req := httptest.NewRequest(http.MethodPost, "/api/users/"+strconv.Itoa(int(userID))+"/spots/"+strconv.Itoa(int(spotID))+"/bookmarks", nil)
	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 Bookmark
	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 comment user id to be %v but got %v", userID, resBody.UserID)
	}
	if resBody.SpotID != spotID {
		t.Errorf("expected comment record id to be %v but got %v", spotID, resBody.UserID)
	}
}

func TestDeleteBookmark(t *testing.T) {
	router := NewRouter()
	req := httptest.NewRequest(http.MethodDelete, "/api/users/1/spots/1/bookmarks/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 deletedBookmark *Bookmark
	err := DB.First(&deletedBookmark, "1").Error
	if !errors.Is(err, gorm.ErrRecordNotFound) {
		t.Errorf("expected spot record to be deleted, but found: %v", deletedBookmark)
	}
}

実行方法

実行方法はAPIの実行テストの実行を参照してください。

OPEN API

今回作成しているAPIです。

おわりに

今回はGolangでBookmarkのREST APIを作成しました。

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

2
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
2
0