gRPC入门系列之5-添加DB操作

今天我们继续完善gRPC服务,在服务中添加数据库操作。

  • 操作系统:MacOS 14.2。
  • 数据库:MySQL8.0.20。

功能简介

这本篇文章里,我们新建一张用户表,实现简单的用户操作功能。表结构设计如下:

sql 复制代码
create database if not exists `grpcstudy`;

CREATE TABLE grpcstudy.`user` (
  `id` int unsigned NOT NULL AUTO_INCREMENT,
  `username` varchar(20) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NOT NULL COMMENT '用户名',
  `mobile` char(11) NOT NULL COMMENT '手机号',
  `email` varchar(128) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NOT NULL DEFAULT '' COMMENT '邮箱',
  `created_at` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '添加时间',
  `modified_at` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间',
  `password` varbinary(60) NOT NULL DEFAULT '' COMMENT '密码hash',
  PRIMARY KEY (`id`),
  UNIQUE KEY `udx_mobile` (`mobile`) USING BTREE,
  UNIQUE KEY `udx_email` (`email`) USING BTREE
) ENGINE=InnoDB AUTO_INCREMENT=2 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci;

我们将实现如下接口:

  • 添加用户
  • 根据用户id查询用户信息
  • 校验密码

gRPC接口设计

我们在上一章节的基础上进行修改,添加相关接口,修改后的proto文件如下:
proto文件定义

protobuf 复制代码
syntax= "proto3";

package proto;

option go_package = "git.gqnotes.com/guoqiang/grpcexercises/consuldemo/pb";

service GreetService {
    rpc Greet(GreetRequest) returns (GreetResponse) {}
    rpc GreetManyTimes(GreetRequest) returns (stream GreetResponse) {}
    rpc GetUser(GetUserRequest) returns (GetUserResponse) {}
    rpc CreateUser(CreateUserRequest) returns (CreateUserResponse) {}
    rpc CheckPassword(CheckPasswordRequest) returns (CheckPasswordResponse) {}
}

message GreetRequest {
    string greeting = 1;
}

message GreetResponse {
    string result = 1;
}




message CreateUserRequest {
    string username = 1;
    string mobile = 2;
    string password = 3;
    string email = 4;
}

message CreateUserResponse {
    int64 id = 1;
}

message GetUserRequest {
    int64 id = 1;
}

message GetUserResponse {
    int64 id = 1;
    string username = 2;
    string mobile = 3;
    string email = 4;
}

message CheckPasswordRequest {
    string username = 1;
    string password = 2;
}

message CheckPasswordResponse {
    bool ok = 1;
}

在consuldemo目录下执行相关protoc命令,生成对应的go文件:

bash 复制代码
protoc -Iproto/ --go_out=. --go_opt=module=git.gqnotes.com/guoqiang/grpcexercises/consuldemo --go-grpc_out=. --go-grpc_opt=module=git.gqnotes.com/guoqiang/grpcexercises/consuldemo proto/*.proto

密码处理

在存储密码时,我们不存储明文密码,而是存储密码的hash值,并且进行加盐处理。密码加盐有如下好处:

  • 增加密码破解难度
  • 防止撞库攻击
  • 提高密码安全性

具体算法上,我们使用go语言的bcrypt库来。bcrypt库生成的密码hash值,包含了加盐信息,所以我们不需要额外字段来存储盐(salt)值。

数据库代码生成工具sqlc介绍

sqlc是一款数据库代码生成工具库,它可以根据sql语句生成相关的数据库操作代码(Go)。更多信息,可以查阅官方文档:docs.sqlc.dev/

sqlc的安装

如果你和我一样,是mac OS操作系统,可以直接执行brew install sqlc 命令来安装sqlc。其它操作系统,请参考官方文档安装:安装方法

生成数据库操作代码

我们在consuldemo/db目录下新建如下三个文件::

  • schema.sql:数据库表结构
  • query.sql:查询语句
  • sqlc.yaml:sqlc配置文件

其中表结构我们之前已经介绍过,不再赘述。另外两个文件内容如下:
query.sql

sql 复制代码
-- name: CreateUser :execresult
INSERT INTO grpcstudy.user (username, mobile, email, password) VALUES (?, ?, ?, ?);

-- name: GetUserById :one
SELECT * FROM grpcstudy.user WHERE id = ? LIMIT 1;

-- name: GetUserByUsername :one
SELECT * FROM grpcstudy.user WHERE username = ? LIMIT 1;

-- name: GetUserByMobile :one
SELECT * FROM grpcstudy.user WHERE mobile = ? LIMIT 1;

-- name: ListUsers :many
SELECT * FROM grpcstudy.user ORDER BY id DESC LIMIT ? OFFSET ?;

sqlc.yaml

yaml 复制代码
version: "2"
sql:
  - engine: "mysql"
    queries: "query.sql"
    schema: "schema.sql"
    gen:
      go:
        package: "user"
        out: "user"

在db目录下执行如下命令,生成相关代码:

bash 复制代码
sqlc generate

如果一切正常,将会在db/user目录下生成相关数据库操作代码。

实现服务端代码

我们修改服务端main.go,部分新增代码如下:
服务端-新增接口实现

go 复制代码
// CreateUser 创建用户
func (s *Server) CreateUser(ctx context.Context, req *pb.CreateUserRequest) (resp *pb.CreateUserResponse, err error) {
	resp = &pb.CreateUserResponse{}

	// 获取密码hash值
	passwordHash, err := bcrypt.GenerateFromPassword([]byte(req.Password), 12)

	if err != nil {
		err = status.Error(codes.Internal, "password hash failed:"+err.Error())
		return
	}

	queries := user.New(conn.GetMySQLConn())
	result, err := queries.CreateUser(ctx, user.CreateUserParams{
		Username: req.Username,
		Mobile:   req.Mobile,
		Email:    req.Email,
		Password: passwordHash,
	})

	if err != nil {
		err = status.Error(codes.Internal, "create user failed:"+err.Error())
		return
	}

	// 获取插入的id
	id, err := result.LastInsertId()

	if err != nil {
		err = status.Error(codes.Internal, "create user failed")
		return
	}

	resp.Id = id

	return
}

// GetUser 获取用户
func (s *Server) GetUser(ctx context.Context, req *pb.GetUserRequest) (resp *pb.GetUserResponse, err error) {
	resp = &pb.GetUserResponse{}

	defer func() {
		if err1 := recover(); err1 != nil {
			s.logger.Fatal("get user failed", zap.Any("err", err1))
			err = status.Error(codes.Internal, fmt.Sprintf("panic: %v", err1))
		}

		if err != nil {
			s.logger.Error("get user failed", zap.Error(err))
		} else {
			s.logger.Info("get user success")
		}
	}()

	queries := user.New(conn.GetMySQLConn())

	result, err := queries.GetUserById(ctx, uint32(req.Id))

	if err != nil {
		if !errors.Is(err, sql.ErrNoRows) {
			err = status.Error(codes.Internal, "get user failed")
			return
		}

		err = status.Error(codes.NotFound, "user not found")
		return
	}

	resp.Id = int64(result.ID)
	resp.Username = result.Username
	resp.Mobile = result.Mobile
	resp.Email = result.Email

	return
}

// CheckPassword 校验密码
func (s *Server) CheckPassword(ctx context.Context, req *pb.CheckPasswordRequest) (resp *pb.CheckPasswordResponse, err error) {
	resp = &pb.CheckPasswordResponse{}

	defer func() {
		if err1 := recover(); err1 != nil {
			s.logger.Fatal("check password failed", zap.Any("err", err1))
			err = status.Error(codes.Internal, fmt.Sprintf("panic: %v", err1))
		}

		if err != nil {
			s.logger.Error("check password failed", zap.Error(err))
		} else {
			s.logger.Info("check password success")
		}
	}()

	queries := user.New(conn.GetMySQLConn())

	// 查询用户信息
	result, err := queries.GetUserByUsername(ctx, req.Username)

	if err != nil {
		if !errors.Is(err, sql.ErrNoRows) {
			err = status.Error(codes.Internal, "check password failed:"+err.Error())
			return
		}

		err = status.Error(codes.NotFound, "user not found")
		return
	}

	// 校验密码
	if err = bcrypt.CompareHashAndPassword(result.Password, []byte(req.Password)); err != nil {
		err = status.Error(codes.Unauthenticated, "password incorrect")
		return
	}

	resp.Ok = true

	return
}

完整代码,请查阅代码仓库:示例代码

接下来,我们在仓库根目录(grpcexercises)执行如下命令,来完成服务端代码的编译和运行:

bash 复制代码
make consuldemo-build && ./consuldemo/bin/server

如果事情ok,将会出现如下界面:

使用evans测试代码

为了方便,我们就不适用go语言编写客户端代码,而是直接使用evans来测试。测试过程如下:

首先,让我们来执行evans -r repl --port 5630来启动evans。

然后,按下图所示操作:

接下来,让我们来分别测试添加用户(CreateUser)和查询用户信息(GetUser)接口。

测试添加用户接口

在evans中,执行如下命令:

如图所示,执行成功,返回的用户id是3。

测试查询用户接口

接下来,让我们测试一下查询用户接口,查询上一步添加成功的用户的信息:

可以看到,查询的结果与我们预期的一致。

结束语

今天我们在grpc服务中添加了数据库操作,并实现了一些简单操作。在现实中,如果业务变复杂以后,一般会有专门的服务来处理用户信息,这个时候,用grpc来实现相关服务,是个不错的选择。

后续,我们将继续完善grpc服务,添加更多功能。

本章示例代码地址:示例代码

本文由mdnice多平台发布

相关推荐
aircrushin9 分钟前
给宝宝办了个宴,朋友用trae做的工具帮了大忙
前端·后端
码上小翔哥15 分钟前
Jackson 配置深度解析
java·后端
程序员Sunday18 分钟前
爆肝万字!这应该是全网最全的 Codex 实战教程了
前端·后端·ai编程
aircrushin19 分钟前
朋友用trae搭建的工具,解决了旅行拍照共享的大事儿
前端·后端
星栈20 分钟前
把业务逻辑写成纯函数之后,我再也不想写 Service 层了
后端·开源
未秃头的程序猿20 分钟前
如何用 AI 写出符合规范的 Java 代码?我总结了 7 条有效建议
java·后端·ai编程
阿聪谈架构21 分钟前
第10章:Agent 记忆系统 —— 让 AI 真正"记住"你
人工智能·后端
木雷坞23 分钟前
我把 AI Coding Agent 的 MCP 工具链放进容器里跑了一遍
后端
BING_Algorithm30 分钟前
开发常用Linux命令
linux·后端
Java编程爱好者36 分钟前
ThreadLocal 用了 WeakReference,为什么还会内存泄漏
后端