前回は、バリデーションとDAOについて実装を行いました。本日も引き続き、実装を進めていきます。
<目次>
前回
プロジェクト構成
最初に以下のディレクトリとファイルを作成してください。
1 2 |
mkdir -p api/datasources/mysql/products_db touch api/datasources/mysql/products_db/products_db.go |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 |
api ├── README.md ├── app │ ├── application.go │ └── url_mappings.go ├── controllers │ ├── ping_controller.go │ └── products │ ├── products_controller.go │ └── products_controller_test.go ├── datasources │ └── mysql │ └── products_db │ └── products_db.go ├── domain │ └── products │ ├── product_dao.go │ ├── product_dao_test.go │ ├── product_dto.go │ └── product_test.go ├── main.go ├── services │ └── products_service.go └── utils └── errors └── api_errors.go |
gormとMySQL
MySQLのDockerコンテナを環境構築編で作成していました。
今回は、これとGormを使ってDB操作を行います。
Gormは、ORMの一種です。通常、DBに対してデータを作成・読込・更新・削除するにはSQL文を発行しなければなりませんが、ORMはその違いを吸収し、通常のGo言語の記述でそれらの操作を行うことが可能になります。
詳しくは、この後のコードで触れていきましょう。
gorm ModelをProduct Structに追加
product_dto.goのProduct Structを以下のように変更してください。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 |
// product_dto.go package products import ( "strings" "github.com/jinzhu/gorm" "github.com/gouser/money-boy/api/utils/errors" ) // Product - defines product info uploaded by user type Product struct { gorm.Model // ID uint64 `json:"id"` Name string `json:"name"` Detail string `json:"detail"` Price uint64 `json:"price"` Img []byte `json:"img"` // CreatedAt time.Time // UpdatedAt time.Time // DeletedAt time.Time } |
ID, CreateAt, UpdatedAt, DeletedAtを削除して、その代わりにgorm.Modelを追加しました。
gorm.Modelの中身は以下のようになっています。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 |
package gorm import "time" // Model base model definition, including fields `ID`, `CreatedAt`, `UpdatedAt`, `DeletedAt`, which could be embedded in your models // type User struct { // gorm.Model // } type Model struct { ID uint `gorm:"primary_key"` CreatedAt time.Time UpdatedAt time.Time DeletedAt *time.Time `sql:"index"` } |
つまり、gorm.Modelを活用すれば、ID, CreatedAT, UpdatedAt, DeletedAtを継承することが可能です。
この変更に伴い、色々と直す必要がありますので、一気に修正してしまいましょう。
1 2 3 4 5 6 7 8 9 10 11 12 13 |
// product_dao.go package products import ( "fmt" "github.com/gouser/money-boy/api/utils/errors" ) var ( productsDB = make(map[uint]*Product) // uint64 -> uintに修正 ) |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 |
// product_test.go package products import ( "testing" "github.com/jinzhu/gorm" "github.com/stretchr/testify/assert" ) func TestProductValiateNoError(t *testing.T) { // Arrange --- p := Product{Model: gorm.Model{ID: 123}, Name: "coca cola"} // 修正 // Act --- err := p.Validate() // Assert --- assert.Nil(t, err) } func TestProductValiateBadRequestError(t *testing.T) { // Arrange --- p := Product{Model: gorm.Model{ID: 123}} // 修正 // Act --- err := p.Validate() // Assert --- assert.NotNil(t, err) assert.EqualValues(t, "invalid product name", err.Message) assert.EqualValues(t, 400, err.Status) assert.EqualValues(t, "bad_request", err.Error) } |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 |
// product_dao_test.go package products import ( "testing" "github.com/jinzhu/gorm" "github.com/stretchr/testify/assert" ) // 正常系テスト func TestProductSaveNoError(t *testing.T) { // Arrange --- p := Product{ Model: gorm.Model{ID: 1},// 修正 Name: "coca cola", Detail: "Wonderful Drink!!!", Price: 120, Img: []byte{1, 2, 3}, } // Act --- err := p.Save() // Assert --- assert.Nil(t, err) } // 同一名の商品が保存されたらエラーが発生 func TestProductSaveBadRequestErrorWithSameName(t *testing.T) { // Arrange --- p := Product{ Model: gorm.Model{ID: 1},// 修正 Name: "coca cola", Detail: "Wonderful Drink!!!", Price: 120, Img: []byte{1, 2, 3}, } p.Save() p2 := Product{ Model: gorm.Model{ID: 1},// 修正 Name: "coca cola", Detail: "Wonderful Drink!!!", Price: 120, Img: []byte{1, 2, 3}, } // Act --- err := p2.Save() // Assert --- assert.NotNil(t, err) assert.EqualValues(t, err.Message, "name coca cola already registered") assert.EqualValues(t, err.Status, 400) assert.EqualValues(t, err.Error, "bad_request") } // 同じIDを保存した場合エラーになる func TestProductSaveBadRequestErrorWithSameID(t *testing.T) { // Arrange --- p := Product{ Model: gorm.Model{ID: 1},// 修正 Name: "coca cola", Detail: "Wonderful Drink!!!", Price: 120, Img: []byte{1, 2, 3}, } p.Save() p2 := Product{ Model: gorm.Model{ID: 1},// 修正 Name: "orange", Detail: "Wonderful Drink!!!", Price: 100, Img: []byte{1, 2, 3}, } // Act --- err := p2.Save() // Assert --- assert.NotNil(t, err) assert.EqualValues(t, err.Message, "product 1 already exists") assert.EqualValues(t, err.Status, 400) assert.EqualValues(t, err.Error, "bad_request") } // 正常系テスト func TestProductGetNoError(t *testing.T) { // Arrange --- p := Product{ Model: gorm.Model{ID: 1}, Name: "coca cola", Detail: "Wonderful Drink!!!", Price: 120, Img: []byte{1, 2, 3}, } p.Save() newP := Product{Model: gorm.Model{ID: 1}} // 修正 // Act --- result := newP.Get() // Arrange --- assert.Nil(t, result) assert.EqualValues(t, p.Name, newP.Name) assert.EqualValues(t, p.Detail, newP.Detail) assert.EqualValues(t, p.Price, newP.Price) assert.EqualValues(t, p.Img, newP.Img) } // 商品が存在しない場合のテスト func TestProductNotFound(t *testing.T) { // Arrange --- p := Product{Model: gorm.Model{ID: 100}} // 修正 // Act --- err := p.Get() // Assert --- assert.NotNil(t, err) assert.EqualValues(t, err.Message, "product 100 not found") assert.EqualValues(t, err.Status, 404) assert.EqualValues(t, err.Error, "not_found") } |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 |
// products_service.go package services import ( "github.com/jinzhu/gorm" "github.com/gouser/money-boy/api/domain/products" "github.com/gouser/money-boy/api/utils/errors" ) // GetProduct - Service func GetProduct(productID uint) (*products.Product, *errors.ApiErr) { p := &products.Product{Model: gorm.Model{ID: productID}} // 修正 if err := p.Get(); err != nil { return nil, err } return p, nil } |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 |
// products_controller.go package products import ( "net/http" "strconv" "github.com/gin-gonic/gin" "github.com/gouser/money-boy/api/domain/products" "github.com/gouser/money-boy/api/services" "github.com/gouser/money-boy/api/utils/errors" ) // GetProduct - Get product by product id func GetProduct(c *gin.Context) { productID, productErr := strconv.ParseUint(c.Param("product_id"), 10, 64) // 修正 if productErr != nil { err := errors.NewBadRequestError("product id should be a number") c.JSON(err.Status, err) return } product, getErr := services.GetProduct(uint(productID)) // 修正 if getErr != nil { c.JSON(getErr.Status, getErr) return } c.JSON(http.StatusOK, product) } |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 |
// products_controller_test.go package products import ( "bytes" "encoding/json" "fmt" "net/http" "net/http/httptest" "testing" "github.com/gin-gonic/gin" "github.com/jinzhu/gorm" "github.com/gouser/money-boy/api/domain/products" "github.com/gouser/money-boy/api/utils/errors" "github.com/stretchr/testify/assert" ) func TestCreateProductNoError(t *testing.T) { // Arrange --- p := products.Product{ // 修正 Model: gorm.Model{ID: 123}, Name: "coca cola", } c, response := requestHandler(p) // Act --- CreateProduct(c) // Assert --- var product products.Product err := json.Unmarshal(response.Body.Bytes(), &product) assert.EqualValues(t, http.StatusCreated, response.Code) assert.Nil(t, err) fmt.Println(product) assert.EqualValues(t, uint64(123), product.ID) } func TestGetProductNoError(t *testing.T) { // Arrange p := products.Product{ // 修正 Model: gorm.Model{ID: 1}, Name: "coca cola", } c, _ := requestHandler(p) CreateProduct(c) c2, response := getRequestHandler("1") // Act --- GetProduct(c2) // Assert --- var product products.Product err := json.Unmarshal(response.Body.Bytes(), &product) assert.EqualValues(t, http.StatusOK, response.Code) assert.Nil(t, err) assert.EqualValues(t, uint64(1), product.ID) } |
たくさん修正しましたね。テストを実行して、passするか確認しましょう。
1 2 3 |
go test ./... ok github.com/gouser/money-boy/api/controllers/products 0.027s ok github.com/gouser/money-boy/api/domain/products (cached) |
OKですね。テストコードを書いていると、ソースコードの変更にも臆することが無くなります。
MySQLコンテナへの接続
mysqlコンテナに接続するコードを書きましょう。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 |
// products_db.go package products_db import ( "fmt" "log" "os" "github.com/jinzhu/gorm" _ "github.com/jinzhu/gorm/dialects/mysql" ) // CONSTANT VALUES const ( DBTYPE = "mysql" SCHEMA = "%s:%s@tcp(mysql:3306)/%s?charset=utf8&parseTime=True&loc=Local" ) var ( Client *gorm.DB username = os.Getenv("MYSQL_USER") password = os.Getenv("MYSQL_PASSWORD") dbName = os.Getenv("MYSQL_DATABASE") datasourceName = fmt.Sprintf(SCHEMA, username, password, dbName) ) // https://gorm.io/ja_JP/docs/connecting_to_the_database.html#MySQL func init() { var err error // user:password@/db_name -> docker.compose.yml - mysql service Client, err = gorm.Open(DBTYPE, datasourceName) if err != nil { log.Fatal(err) } log.Println("database successfully configure") } |
os.GetEnvでは、コンテナ内に設定した環境変数を取得することができます。Go言語のソースコードは、docker-compose.ymlに設定しているapiサービス内で動作し、そこに設定されているenvironmentの値を取得することができます。
1 2 3 4 5 6 7 |
# docker-compose.yml api: .... environment: MYSQL_USER: admin MYSQL_PASSWORD: admin MYSQL_DATABASE: money-boys |
これらがDBの接続キーになるので、gorm.OpenでMysqlコンテナと接続をします。
動作確認のため、product_dao.goのGetメソッドにPingを入れてみましょう。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 |
// product_dao.go package products import ( "fmt" "log" "github.com/gouser/money-boy/api/datasources/mysql/products_db" "github.com/gouser/money-boy/api/utils/errors" ) var ( productsDB = make(map[uint]*Product) ) // Get - product func (p *Product) Get() *errors.ApiErr { // 追加 // https://gorm.io/ja_JP/docs/generic_interface.html if err := products_db.Client.DB().Ping(); err != nil { log.Fatal(err) } result := productsDB[p.ID] if result == nil { return errors.NewNotFoundError(fmt.Sprintf("product %d not found", p.ID)) } p.ID = result.ID p.Name = result.Name p.Detail = result.Detail p.Price = result.Price p.Img = result.Img p.CreatedAt = result.CreatedAt p.UpdatedAt = result.UpdatedAt p.DeletedAt = result.DeletedAt return nil } |
続いて、以下のコマンドをterminal上に打ち込んで、コンテナを立ち上げます。
1 2 3 |
docker-compose up api | 2020/05/20 21:03:19 database successfully configure |
terminal上で、「database successfully configure」が表示されたら接続成功です。
または、Curlを打ち込んで、Ping系のエラーが出なかったら接続成功です。
1 |
curl -X GET http://localhost:8080/products/1 |
長くなって来たので、今回はここまでにしましょう。
次回
次回は、gormを使ってDB操作について触れていきます。
コメントを残す
コメントを投稿するにはログインしてください。