grpcコースをやってみる〜その4 part1 ユーザ作成を実装する〜
今回何をやるか
未実装のAPIを実装する
実装する
完成形のコードは以下
func (server *Server) CreateUser(ctx context.Context, req *pb.CreateUserRequest) (*pb.CreateUserResponse, error) { hashedPassword, err := util.HashPassword(req.GetPassword()) if err != nil { return nil, status.Errorf(codes.Internal, "failed to hash password: %s", err) } arg := db.CreateUserParams{ Username: req.GetUsername(), HashedPassword: hashedPassword, FullName: req.GetFullName(), Email: req.GetEmail(), } user, err := server.store.CreateUser(ctx, arg) if err != nil { if pqErr, ok := err.(*pq.Error); ok { errName := pqErr.Code.Name() switch errName { case "unique_violation": return nil, status.Errorf(codes.AlreadyExists, "username already exists: %s", err) } } return nil, status.Errorf(codes.Internal, "failed to create user: %s", err) } rsp := &pb.CreateUserResponse{ User: convertUser(user), } return rsp, nil }
以下に実装内容の補足を書いていく
リクエストパラメータのバインドが不要
ginでは以下の様にパラメータをバインドする必要があったが、grpcではフレームワークで実施してくれるためバインドが不要
// api/user.go // jsonのキー名とマッピングさせたリクエスト type createUserRequest struct { Username string `json:"username" binding:"required,alphanum"` Password string `json:"password" binding:"required,min=6"` FullName string `json:"full_name" binding:"required"` Email string `json:"email" binding:"required,email"` } // メソッド func (server *Server) createUser(ctx *gin.Context) { // jsonとバインドできているか検査 var req createUserRequest if err := ctx.ShouldBindJSON(&req); err != nil { ctx.JSON(http.StatusBadRequest, errorResponse(err)) return }
grpcのエラーハンドリング方法
httpの場合jsonにhttpのステータスコードを設定していたが、grpcの場合status
(grpcのサブパッケージ)にエラーコードを設定する
// http ctx.JSON(http.StatusInternalServerError, errorResponse(err)) // grpc return nil, status.Errorf(codes.Unimplemented, "method CreateUser not implemented") // 呼び出し先 // Errorf returns Error(c, fmt.Sprintf(format, a...)). func Errorf(c codes.Code, format string, a ...interface{}) error { return Error(c, fmt.Sprintf(format, a...)) }
dbで生成したデータをproto型へ変換する
- dbで生成したユーザを直接クライアントへ返却しないのが一般的である
- ユーザテーブルのフィールドをそのまま返却した場合パスワード情報など不要でかつ公開したくない情報まで返却することになるため
// レスポンスとして使用したいデータ型 // pb/rpc_create_user.pb.go = protoで自動生成したデータ型 type CreateUserResponse struct { state protoimpl.MessageState sizeCache protoimpl.SizeCache unknownFields protoimpl.UnknownFields User *User `protobuf:"bytes,1,opt,name=user,proto3" json:"user,omitempty"` }
以下の様に変換用の処理を作成し変換する
// gapi/converter.go func convertUser(user db.User) *pb.User { return &pb.User{ Username: user.Username, FullName: user.FullName, Email: user.Email, PasswordChangedAt: timestamppb.New(user.PasswordChangedAt), CreatedAt: timestamppb.New(user.CreatedAt), } }
// レスポンス生成処理 抜粋 rsp := &pb.CreateUserResponse{ // 引数で受け取っているユーザはdbで生成したユーザ User: convertUser(user), } return rsp, nil
実際に動作させてみる
evansから呼び出し方法を確認したところ成功!
pb.SimpleBank@localhost:9090> call CreateUser username (TYPE_STRING) => a full_name (TYPE_STRING) => a email (TYPE_STRING) => a@gmail.com password (TYPE_STRING) => password { "user": { "createdAt": "2023-05-05T01:49:38.907170Z", "email": "a@gmail.com", "fullName": "a", "passwordChangedAt": "0001-01-01T00:00:00Z", "username": "a" } }
感想
- これまでgrpcの概要から入り実際に呼び出すところまでやったが、httpと手続きが異なるだけで呼び出し後の処理(処理を書く、エラーハンドリングをする・・・etc)などは同じだなという感覚。