从 bson 结构转换 protoc 生成的结构的最佳方法是啥?
Posted
技术标签:
【中文标题】从 bson 结构转换 protoc 生成的结构的最佳方法是啥?【英文标题】:What would be the best approach to converting protoc generated structs from bson structs?从 bson 结构转换 protoc 生成的结构的最佳方法是什么? 【发布时间】:2017-12-22 14:46:55 【问题描述】:我正在用 Golang 编写一个 RESTful API,它也有一个 gRPC api。 API 连接到 MongoDB 数据库,并使用结构来映射实体。我还有一个 .proto 定义,它与我用于 MongoDB 的结构相匹配。
我只是想知道是否有一种方法可以共享或重新使用 .proto 定义的代码来进行 MongoDB 调用。我注意到 strucs protoc 生成的每个字段都有 json 标签,但显然没有 bson
标签等。
我有类似的东西......
// Menu -
type Menu struct
ID bson.ObjectId `json:"id" bson"_id"`
Name string `json:"name" bson:"name"`
Description string `json:"description" bson:"description"`
Mixers []mixers.Mixer `json:"mixers" bson:"mixers"`
Sections []sections.Section `json:"sections" bson:"sections"`
但是我也有 protoc 生成的代码...
type Menu struct
Id string `protobuf:"bytes,1,opt,name=id" json:"id,omitempty"`
Name string `protobuf:"bytes,2,opt,name=name" json:"name,omitempty"`
Description string `protobuf:"bytes,3,opt,name=description" json:"description,omitempty"`
Mixers []*Mixer `protobuf:"bytes,4,rep,name=mixers" json:"mixers,omitempty"`
Sections []*Section `protobuf:"bytes,5,rep,name=sections" json:"sections,omitempty"`
目前我必须根据我正在做的事情在两个结构之间进行转换。这很乏味,我可能会受到相当大的性能影响。那么有没有更好的方法在两者之间进行转换,或者将其中一个重新用于两项任务?
【问题讨论】:
可以手动添加bson
标签。您是否尝试过将其作为测试?如果它有效,您可能可以编写一个脚本来处理它。
使用bson.ObjectId
,您可以将两者都放入结构(或嵌入)中,然后确保当您从任一来源检索一个时,填充空的。我想这仍然会暴露一些繁琐的工作,但不如转换整个结构那么多。
麻烦的是,我正计划在构建或其他东西时自动生成代码,所以它会覆盖它。我想我不能这样做并手动更新它,但感觉应该有一个标准的方法来做到这一点。肯定有很多人在 Golang 中向 gRPC 发送 mongodb 查询?嵌入ID实际上可以工作!正如你提到的,仍然很棘手
你可以看看gogoprotobuf's扩展moretags
。我将它用于这个用例,它工作正常。
@MarkusWMahlberg 你是如何处理 ID 参数命名不匹配(Id 字符串和 ID bson.ObjectId)的?
【参考方案1】:
我正在测试过程中,可能很快就会提供代码,(如果你没有看到它并且想要它,请联系我)但https://godoc.org/go.mongodb.org/mongo-driver/bson/bsoncodec 看起来像票。 protoc 将制作您的结构,您不必为自定义它们而烦恼。然后你可以自定义 mongo-driver 为你做某些类型的映射,看起来他们的库很不错。
这很棒,因为如果我使用 protogen 结构,那么我希望将它用于我的应用程序核心/域层。我不想担心那里的mongoDB兼容性。
所以现在,在我看来,@Liyan Chang 的回答说
如果您想使用相同的数据类型,则必须修改代码生成 不一定是这样。因为您可以选择使用 1 种数据类型。
您可以使用一种生成的类型,并在使用此编解码器系统获取数据并将数据设置到 DB 方面似乎满足您的任何需求。
见https://***.com/a/59561699/8546258 - bson 结构标签不是全部。看起来编解码器完全可以帮助解决这个问题。
请参阅 https://***.com/a/58985629/8546258 了解有关编解码器的一般性文章。
请记住,这些编解码器是在 mongodb go 驱动程序的 1.3 中发布的。我发现这个指引我去那里:https://developer.mongodb.com/community/forums/t/mgo-setbson-to-mongo-golang-driver/2340/2?u=yehuda_makarov
【讨论】:
嘿,你能让它工作吗?如果可以,您能否提供一些示例代码? @Chandraaditya 嘿,不。我对这个想法有点厌倦了,然后研究了 postgres/mapping。然后发现有些人更喜欢使用映射器从 protobuf 移动到数据层。并且再也没有回到这一点。【参考方案2】:我玩过它并有一个工作示例:
github.com/gogo/protobuf v1.3.1
go.mongodb.org/mongo-driver v1.4.0
google.golang.org/grpc v1.31.0
首先我想分享我的proto/contract/example.proto
文件:
syntax = "proto2";
package protobson;
import "gogoproto/gogo.proto";
option (gogoproto.sizer_all) = true;
option (gogoproto.marshaler_all) = true;
option (gogoproto.unmarshaler_all) = true;
option go_package = "gitlab.com/8bitlife/proto/go/protobson";
service Service
rpc SayHi(Hi) returns (Hi)
message Hi
required bytes id = 1 [(gogoproto.customtype) = "gitlab.com/8bitlife/protobson/custom.BSONObjectID", (gogoproto.nullable) = false, (gogoproto.moretags) = "bson:\"_id\""] ;
required int64 limit = 2 [(gogoproto.nullable) = false, (gogoproto.moretags) = "bson:\"limit\""] ;
它包含一个简单的 gRPC 服务Service
,它具有SayHi
方法和请求类型Hi
。它包括一组选项:gogoproto.sizer_all
、gogoproto.marshaler_all
、gogoproto.unmarshaler_all
。您可以在extensions page 找到它们的含义。 Hi
本身包含两个字段:
id
指定了其他选项:gogoproto.customtype
和 gogoproto.moretags
limit
仅带有 gogoproto.moretags
选项
gogoproto.customtype
中用于id
字段的BSONObjectID
是我定义为custom/objectid.go
的自定义类型:
package custom
import (
"go.mongodb.org/mongo-driver/bson/bsontype"
"go.mongodb.org/mongo-driver/bson/primitive"
)
type BSONObjectID primitive.ObjectID
func (u BSONObjectID) Marshal() ([]byte, error)
return u[:], nil
func (u BSONObjectID) MarshalTo(data []byte) (int, error)
return copy(data, (u)[:]), nil
func (u *BSONObjectID) Unmarshal(d []byte) error
copy((*u)[:], d)
return nil
func (u *BSONObjectID) Size() int
return len(*u)
func (u *BSONObjectID) UnmarshalBSONValue(t bsontype.Type, d []byte) error
copy(u[:], d)
return nil
func (u BSONObjectID) MarshalBSONValue() (bsontype.Type, []byte, error)
return bsontype.ObjectID, u[:], nil
它是必需的,因为我们需要为这两者定义一个自定义的编组和取消编组方法:协议缓冲区和 mongodb 驱动程序。这允许我们使用这种类型作为 mongodb 中的对象标识符。为了向 mongodb 驱动程序“解释”它,我在 proto 文件中使用 (gogoproto.moretags) = "bson:\"_id\""
选项将其标记为 bson
标记。
从我使用的 proto 文件生成源代码:
protoc \
--plugin=/Users/pstrokov/go/bin/protoc-gen-gogo \
--plugin=/Users/pstrokov/go/bin/protoc-gen-go \
-I=/Users/pstrokov/Workspace/protobson/proto/contract \
-I=/Users/pstrokov/go/pkg/mod/github.com/gogo/protobuf@v1.3.1 \
--gogo_out=plugins=grpc:. \
example.proto
我已经在运行 MongoDB 实例的 MacOS 上对其进行了测试:docker run --name mongo -d -p 27017:27017 mongo
:
package main
import (
"context"
"log"
"net"
"time"
"gitlab.com/8bitlife/protobson/gitlab.com/8bitlife/proto/go/protobson"
"go.mongodb.org/mongo-driver/bson"
"go.mongodb.org/mongo-driver/mongo"
"go.mongodb.org/mongo-driver/mongo/options"
"google.golang.org/grpc"
)
type hiServer struct
mgoClient *mongo.Client
func (s *hiServer) SayHi(ctx context.Context, hi *protobson.Hi) (*protobson.Hi, error)
collection := s.mgoClient.Database("local").Collection("bonjourno")
res, err := collection.InsertOne(ctx, bson.M"limit": hi.Limit)
if err != nil panic(err)
log.Println("generated _id", res.InsertedID)
out := &protobson.Hi
if err := collection.FindOne(ctx, bson.M"_id": res.InsertedID).Decode(out); err != nil return nil, err
log.Println("found", out.String())
return out, nil
func main()
ctx, cancel := context.WithCancel(context.Background())
defer cancel()
lis, err := net.Listen("tcp", "localhost:0")
if err != nil log.Fatalf("failed to listen: %v", err)
clientOptions := options.Client().ApplyURI("mongodb://localhost:27017")
clientOptions.SetServerSelectionTimeout(time.Second)
client, err := mongo.Connect(ctx, clientOptions)
if err != nil log.Fatal(err)
if err := client.Ping(ctx, nil); err != nil log.Fatal(err)
grpcServer := grpc.NewServer()
protobson.RegisterServiceServer(grpcServer, &hiServermgoClient: client)
go grpcServer.Serve(lis); defer grpcServer.Stop()
conn, err := grpc.Dial(lis.Addr().String(), grpc.WithInsecure())
if err != nil log.Fatal(err) ; defer conn.Close()
hiClient := protobson.NewServiceClient(conn)
response, err := hiClient.SayHi(ctx, &protobson.HiLimit: 99)
if err != nil log.Fatal(err)
if response.Limit != 99 log.Fatal("unexpected limit", response.Limit)
if response.Id.Size() == 0 log.Fatal("expected a valid ID of the new entity")
log.Println(response.String())
抱歉最后一个代码 sn-p 的格式:) 我希望这会有所帮助。
【讨论】:
为什么您需要确保有自定义的 BSON 编组和取消编组方法?确保有 bson 标签还不够?为什么?【参考方案3】:遇到过同样的问题,有几种解决方法。它们分为两种通用方法:
-
使用相同的数据类型
使用两种不同的结构类型并在它们之间进行映射
如果要使用相同的数据类型,则必须修改代码生成
您可以使用 gogoprotobuf 之类的东西,它具有添加标签的扩展名。这应该在你的结构中给你bson
标签。
您还可以使用正则表达式或涉及 go 抽象语法树的更复杂的方法对生成的文件进行后处理。
如果您选择在它们之间进行映射:
使用reflection
。您可以编写一个包含两个结构的包,并尝试从一个结构中获取值并将其应用于另一个结构。您将不得不处理极端情况(轻微的命名差异、哪些类型是等效的等),但如果出现极端情况,您将能够更好地控制它们。
使用 JSON 作为中介。只要生成的 json 标签匹配,这将是一个快速的编码练习,并且如果这不是在代码中的紧密循环中,序列化和反序列化的性能损失可能是可以接受的。
手写或代码生成映射功能。根据你有多少结构,你可以写出一堆在两者之间转换的函数。
在我的工作场所,我们最终做了所有这些工作:派生 protoc 生成器来做一些自定义标签,一个基于反射的结构覆盖包,用于在任意结构之间进行映射,以及一些对性能更敏感的手写结构或自动化程度较低的映射。
【讨论】:
以上是关于从 bson 结构转换 protoc 生成的结构的最佳方法是啥?的主要内容,如果未能解决你的问题,请参考以下文章