gotoshin

主に学んだ事の自分メモ用です。記事に書くまでも無いような事はhttps://scrapbox.io/study-diary/に書いてます。

grpcコースをやってみる〜その4 part1 ユーザ作成を実装する〜

www.youtube.com

今回何をやるか

未実装の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

[Go] 構造体の初期化方法まとめ - Qiita

実際に動作させてみる

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)などは同じだなという感覚。