前回は、MySQLコンテナに対して、Productの登録と検索処理を実装しました。
今回は、MySQLのエラーハンドリングを実装していきます。
<目次>
前回
プロジェクト構成
以下のディレクトリとファイルを作成してください。
1 2 |
mkdir api/utils/mysqlutils touch api/utils/mysqlutils/mysql_utils.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 27 28 |
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 └── mysqlutils # new └── mysql_utils.go # new |
MySQLエラーハンドリング
処理を実装する過程で、発生したエラーの種類ごとにエラーハンドリングをしたくなる時があります。
例えば、Productテーブルのnameカラムにはユニークキーが設定されており、同じ名前の商品を登録した場合は重複エラー(400)を、それ以外はサーバーエラー(500)を出したい、などがユースケースとして考えられます。
そんな時は、「github.com/go-sql-driver/mysql」パッケージのMySQLError Structが使えます。
1 2 3 4 5 |
// MySQLError is an error type which represents a single MySQL error type MySQLError struct { Number uint16 Message string } |
MySQLErrorのNumberには、発生したエラーに紐付くエラーコードが自動で入ります。
例えば、重複エラーの場合は、1062番が入ります。
これを使えば、発生したエラーの種別ごとに処理が実装できそうです。
実際に試して見ましょう。
Save ~MySQLエラーハンドリング ~
product_dao.goに実装したSaveメソッドを以下のように修正します。
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 |
// product_dao.go package products import ( "fmt" "github.com/go-sql-driver/mysql" "github.com/gouser/money-boy/api/datasources/mysql/products_db" "github.com/gouser/money-boy/api/utils/errors" ) // Save - product func (p *Product) Save() *errors.ApiErr { // https://gorm.io/ja_JP/docs/error_handling.html if result := products_db.Client.Create(&p); result.Error != nil { sqlErr, ok := result.Error.(*mysql.MySQLError) if !ok { return errors.NewInternalServerError( fmt.Sprintf("error when trying to save product: %s", result.GetErrors()), ) } // Check error number -> 1062 fmt.Println("Mysql Error Key", sqlErr.Number) switch sqlErr.Number { case 1062: // duplicate key return errors.NewBadRequestError(fmt.Sprintf("name '%s' is already exists", p.Name)) } return errors.NewInternalServerError( fmt.Sprintf("error when trying to save product: %s", result.GetErrors()), ) } return nil } |
まず、「sqlErr, ok := result.Error.(*mysql.MySQLError)」にて、発生したエラーをMySQLError Structにキャストしました。
ok変数には、キャストが成功した場合はtrue、失敗した場合はfalseが入ります。※okは、Go言語の慣習です
エラーコードを取り出すには、単純にsqlErr.Numberにアクセスするだけです。
エラーコードを調べるために、「fmt.Println(“Mysql Error Key”, sqlErr.Number)」を実装し、出力内容を確認してます。
確認した結果、nameが重複した場合のエラーコードは、「1062」番だったので、switch文で処理をしました。
Talend API Testerで動作確認をしてみましょう。
下記のコマンドで、dockerコンテナを立ち上げてください。
1 |
docker-compose up |
また、テストに使うデータは以下のようになっています。
1 2 3 4 5 |
{ "name": "coca cola", "detail": "The coca cola is very very delicious drink", "price": 200 } |
nameに設定しているcoca colaは、既にMySQLに登録ずみです。
この状態で「http://localhost:8080/products」に対してリクエストを送ると、400エラーが発生するはずです。

1 2 3 4 5 |
{ "message": "name 'coca cola' is already exists", "status": 400, "error": "bad_request" } |
OKですね。
Get ~MySQLエラーハンドリング ~
続いて、Getメソッドにもエラーハンドリングを追加しましょう。
Getの場合は、レコードが存在しない場合のエラーハンドリングになります。
存在しないIDを渡した時にエラーが発生することになりますが、Saveのエラーハンドリングのようにエラーコードを拾うことができませんでした。
単に「record not found」のエラーメッセージが返ってくるだけです。
そのため、以下のように実装しました。
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 |
// product_dao.go package products import ( "fmt" "strings" "github.com/go-sql-driver/mysql" "github.com/gouser/money-boy/api/datasources/mysql/products_db" "github.com/gouser/money-boy/api/utils/errors" ) const ( noSearchResult = "record not found" ) // Get - product func (p *Product) Get() *errors.ApiErr { if result := products_db.Client.Where("id = ?", p.Model.ID).Find(&p); result.Error != nil { errMsg := fmt.Sprintf("%s", result.Error) if strings.Contains(errMsg, noSearchResult) { return errors.NewNotFoundError( fmt.Sprintf("product %d not found", p.ID), ) } return errors.NewInternalServerError( fmt.Sprintf("error when trying to get product: %s", result.Error), ) } return nil } |
stringsパッケージのContainsメソッドで、取得したエラーメッセージがソースコード内に定義したエラーメッセージに含まれているかチェックしました。
「record not found」が含まれていた場合は、404エラーが発生します。
Talend API Testerから存在しないid(10000)でリクエストして見ましょう。
「http://localhost:8080/products/10000」

1 2 3 4 5 |
{ "message": "product 10000 not found", "status": 404, "error": "not_found" } |
想定内のメッセージが返却されました。
リファクタリング~MySQLエラーハンドリング~
SaveとGetメソッドに実装したエラーハンドリングの処理ですが、リファクタリングができそうです。
mysql_utils.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 27 28 29 30 31 32 33 |
// mysql_utils.go package mysqlutils import ( "fmt" "strings" "github.com/go-sql-driver/mysql" "github.com/gouser/money-boy/api/utils/errors" ) const ( noSearchResult = "record not found" ) // ParseError - parse mysql error to api error func ParseError(err error) *errors.ApiErr { sqlErr, ok := err.(*mysql.MySQLError) if !ok { if strings.Contains(err.Error(), noSearchResult) { return errors.NewNotFoundError("no record matching given id") } return errors.NewInternalServerError("error parsing database response") } switch sqlErr.Number { case 1062: // duplicate key return errors.NewBadRequestError("invalid data") } return errors.NewInternalServerError("error processing request") } |
上記は、Save、Getメソッドのエラーハンドリングを一纏めにしたイメージです。
ParseError関数ををproduct_dao.goのSave、Getメソッドから呼び出しましょう。
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 |
// product_dao.go package products import ( "github.com/gouser/money-boy/api/datasources/mysql/products_db" "github.com/gouser/money-boy/api/utils/errors" "github.com/gouser/money-boy/api/utils/mysqlutils" ) // Get - product func (p *Product) Get() *errors.ApiErr { if result := products_db.Client.Where("id = ?", p.Model.ID).Find(&p); result.Error != nil { return mysqlutils.ParseError(result.Error) } return nil } // Save - product func (p *Product) Save() *errors.ApiErr { // https://gorm.io/ja_JP/docs/error_handling.html if result := products_db.Client.Create(&p); result.Error != nil { return mysqlutils.ParseError(result.Error) } return nil } |
だいぶスッキリしましたね。
次回
次回は、MySQLのupdate処理について学びましょう。
コメントを残す
コメントを投稿するにはログインしてください。