gRPC and MySql Part 2

February 8, 2019

Firstly, make sure you completed the MySql setup that was presented in the last entry. Then we have a little context of the simple user table we will interact with. Or if you rather just run the database locally instead of docker that is fine also; up to you.

Ok some of this will sound familiar to the HelloWorld blog entry as far as the process of creating proto file, etc. So please bear with me.

So let’s create our protobuf file for our User table. The code is all in GitHub. I created a user directory and another directory named proto

> mkdir -p user/proto

Define user.proto

Now create a file named user.proto and add the following content. This is a simple insert, read, and delete example. You will see we have requests and responses for all 3 actions. These requests and responses have the makeup of what is required in the request and what is returned in the response. You will also see in the bottom of user.proto the rpc calls we define.

syntax= "proto3";                                                                                                                                                                                                                            
                                
message User {
  int64 id = 1;
  string firstName = 2;
  string lastName = 3;
  string address = 4;                                  
}                                          

message CreateUserRequest {
  User user = 1;
}

message CreateUserResponse {
  int64 id = 1;
}

message ReadUserRequest {
  int64 id = 1;
}                                   

message ReadUserResponse {
  User user = 1;
}                                   

message DeleteUserRequest {
  int64 id = 1;
}                                    
                                    
message DeleteUserResponse {
  int64 rowsDeleted = 1;
}
                                    
service UserService {
  rpc Create(CreateUserRequest) returns (CreateUserResponse);

  rpc Read(ReadUserRequest) returns (ReadUserResponse);
                                    
  rpc Delete(DeleteUserRequest) returns (DeleteUserResponse);
}                                   

Generate Code from user.proto

I assume you are in the user directory we created. If so run the following command

> protoc -I proto/ user.proto --go_out=plugins=grpc:proto 

You should now see a file named user.pb.go in the proto directory now. If you look at that file you see you will have to implement the UserServiceServer and that the UserServiceClient is already implemented.

UserServiceServer Implementation

I created another directory under user named user_server and created a file named main.go

package main                                                                                                                                                                                                                                 
import (    
  "context"                                        
  "database/sql"    
  "fmt"    
  "log"    
  "net"    
    
  pb "github.com/calam1/user/proto"    
  _ "github.com/go-sql-driver/mysql"    
  "google.golang.org/grpc"    
  "google.golang.org/grpc/codes"    
  "google.golang.org/grpc/reflection"    
  "google.golang.org/grpc/status"    
)    

// implementation of UserServiceServer defined in generated go file
type userServer struct {    
  db *sql.DB    
}    
    
// NewUserServiceServer returns implemented server    
func NewUserServiceServer(db *sql.DB) pb.UserServiceServer {    
  return &userServer{db: db}    
}    

// get database connection
func (u *userServer) connect(ctx context.Context) (*sql.Conn, error) {    
  c, err := u.db.Conn(ctx)    
  if err != nil {    
    return nil, status.Error(codes.Unknown, "failed to connect to db"+err.Error())    
  }    
  return c, nil    
}    
    
// Create create a user    
func (u *userServer) Create(ctx context.Context, req *pb.CreateUserRequest) (*pb.CreateUserResponse, error) {    
  c, err := u.connect(ctx)    
  if err != nil {    
    return nil, err    
  }    
                                     
  defer c.Close()    
    
  result, err := c.ExecContext(ctx, "INSERT INTO User(`FirstName`, `LastName`, `Address`) VALUES (?, ?, ?)", req.User.FirstName, req.User.LastName, req.User.Address)    
                                     
  if err != nil {                                        
    return nil, status.Error(codes.Unknown, "failed to insert into User table"+err.Error())    
  }    
                                     
  id, err := result.LastInsertId()    
  if err != nil {    
    return nil, status.Error(codes.Unknown, "failed to retrieve last inserted record for User"+err.Error())    
  }    

  return &pb.CreateUserResponse{
    Id: id,
  }, nil
}

// Read get a user
func (u *userServer) Read(ctx context.Context, req *pb.ReadUserRequest) (*pb.ReadUserResponse, error) {
  c, err := u.connect(ctx)
  if err != nil {
    return nil, err
  }

  defer c.Close()

  result, err := c.QueryContext(ctx, "SELECT `ID`, `FIRSTNAME`, `LASTNAME`, `ADDRESS` FROM User WHERE `ID` = ?", req.Id)

  if err != nil {
    return nil, status.Error(codes.Unknown, "failed to select id from User table"+err.Error())
  }

  // don't forget to close the result otherwise you will hang and timeout
  defer result.Close()

  var usr pb.User

  if result.Next() {
    if err := result.Scan(&usr.Id, &usr.FirstName, &usr.LastName, &usr.Address); err != nil {
      return nil, status.Error(codes.Unknown, "failed to retrieve values from User"+err.Error())
    }
  } else {
    return nil, status.Error(codes.NotFound, fmt.Sprintf("User with id %d is not found", req.Id))
  }

  return &pb.ReadUserResponse{
    User: &usr,
  }, nil
}

func (u *userServer) Delete(ctx context.Context, req *pb.DeleteUserRequest) (*pb.DeleteUserResponse, error) {
  c, err := u.connect(ctx)
  if err != nil {
    return nil, err
  }
  defer c.Close()

  result, err := c.ExecContext(ctx, "DELETE FROM User WHERE ID = ?", req.Id)
  if err != nil {
    return nil, status.Error(codes.Unknown, "failed to delete User"+err.Error()) 
  }

  rows, err := result.RowsAffected()
  if err != nil {
    return nil, status.Error(codes.Unknown, "failed to retrieve rows affected"+err.Error())
  }


  if rows == 0 {
    return nil, status.Error(codes.NotFound, fmt.Sprintf("User with id %d is not found", req.Id))
  }

  return &pb.DeleteUserResponse{
    RowsDeleted: rows,
  }, nil
}

func main() {

  // this is hard coded for simplicity sake, you can use golang flags to get the attributes
  dsUser := "chris:password@tcp(172.17.0.2:3306)/customers?User"

  // connect to the db
  db, err := sql.Open("mysql", dsUser)
  if err != nil {
    log.Fatalf("error opening database %v", err)
  }

  defer db.Close()

  // grpc server listen port
  listener, err := net.Listen("tcp", ":50051")
  if err != nil {
    log.Fatalf("listener failed:%v", err)
  }

  // create a new server based off of the proto generated code
  userAPI := NewUserServiceServer(db)

  // create a grpc server
  grpcServer := grpc.NewServer()
  // register grpc server and user service server
  pb.RegisterUserServiceServer(grpcServer, userAPI)
  reflection.Register(grpcServer)
  if err := grpcServer.Serve(listener); err != nil {
    log.Fatalf("failed to serve %v", err)
  }
}                       

Note In the above code, the server has the database string hard coded. You can use flags to read the arguments in and parse them to build the database string

  // dbuser := "<user>:<password>@tcp(<mysql-dns-url>:<db-port>)/<database>?<table>"
  dsUser := "chris:password@tcp(172.17.0.2:3306)/customers?User"

If you are running docker to host the database then run the following command to get thei ip address for the database url

> docker inspect mysql | grep -i ipaddress

Build the Client

Under the user directory create a directory name user_client and create a file name main.go and add the following content

This client will do a insert, read, and delete as a simple example.

package main                                                                                                                                                                                                                                 
  
import (
  "context"
  "log"                                                       
  "time"

  pb "github.com/calam1/user/proto"
  "google.golang.org/grpc"   
)
                                 
const (
  address = "localhost:50051"
)
  
func main() {
  // insecure grpc connection   
  conn, err := grpc.Dial(address, grpc.WithInsecure())
  if err != nil {   
    log.Fatalf("connection error %v", err)
  }

  defer conn.Close()
  
  client := pb.NewUserServiceClient(conn)
 
  // timeout
  ctx, cxl := context.WithTimeout(context.Background(), 10*time.Second)
  defer cxl()
      
  // create user                                         
  reqCreate := pb.CreateUserRequest{   
    User: &pb.User{
      FirstName: "Chris",             
      LastName:  "Lam",            
      Address:   "123 Main Street USA",
    },
  }
   
  respCreate, err := client.Create(ctx, &reqCreate)
  if err != nil {                 
    log.Fatalf("Create User failed %v", err)
  }                               
  log.Printf("Created User: %v", respCreate)
  
  id := respCreate.Id
   
  // read user                     
  reqRead := pb.ReadUserRequest{
    Id: id,
  }

  respRead, err := client.Read(ctx, &reqRead)
  if err != nil {
    log.Fatalf("read failed %v", err)
  }

  log.Printf("read response %v", respRead)

  // delete user
  reqDelete := pb.DeleteUserRequest{
    Id: id,
  }

  respDelete, err := client.Delete(ctx, &reqDelete)
  if err != nil {
    log.Fatalf("delete failed for %v on erro r%v", id, err)
  }

  log.Printf("delete response %v", respDelete)
}                                                            

Build and Run

Note: yes, you should not use underscores in golang, camelcase is the preferred style, but it is just easier to read for me

Under the user_server directory run

> build go .

you will see a generated file named user_server.

Start the server

> ./user_server

Under the user_client directory run

> build go .

you will see a generated file named user_client.

Start the client

> ./user_client

You will see something like this

2019/02/08 14:52:43 Created User: id:7 
2019/02/08 14:52:43 read response user:<id:7 firstName:"Chris" lastName:"Lam" address:"123 Main Street USA" > 
2019/02/08 14:52:43 delete response rowsDeleted:1