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多平台发布

相关推荐
烛阴11 分钟前
bignumber.js深度解析:驾驭任意精度计算的终极武器
前端·javascript·后端
服务端技术栈30 分钟前
电商营销系统中的幂等性设计:从抽奖积分发放谈起
后端
你的人类朋友1 小时前
✍️Node.js CMS框架概述:Directus与Strapi详解
javascript·后端·node.js
面朝大海,春不暖,花不开1 小时前
自定义Spring Boot Starter的全面指南
java·spring boot·后端
钡铼技术ARM工业边缘计算机2 小时前
【成本降40%·性能翻倍】RK3588边缘控制器在安防联动系统的升级路径
后端
CryptoPP2 小时前
使用WebSocket实时获取印度股票数据源(无调用次数限制)实战
后端·python·websocket·网络协议·区块链
白宇横流学长2 小时前
基于SpringBoot实现的大创管理系统设计与实现【源码+文档】
java·spring boot·后端
草捏子3 小时前
状态机设计:比if-else优雅100倍的设计
后端
考虑考虑5 小时前
Springboot3.5.x结构化日志新属性
spring boot·后端·spring
涡能增压发动积5 小时前
一起来学 Langgraph [第三节]
后端