从 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_allgogoproto.marshaler_allgogoproto.unmarshaler_all。您可以在extensions page 找到它们的含义。 Hi 本身包含两个字段:

    id 指定了其他选项:gogoproto.customtypegogoproto.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 的格式:) 我希望这会有所帮助。

【讨论】:

为什么您需要确保有自定义的 BS​​ON 编组和取消编组方法?确保有 bson 标签还不够?为什么?【参考方案3】:

遇到过同样的问题,有几种解决方法。它们分为两种通用方法:

    使用相同的数据类型 使用两种不同的结构类型并在它们之间进行映射

如果要使用相同的数据类型,则必须修改代码生成

您可以使用 gogoprotobuf 之类的东西,它具有添加标签的扩展名。这应该在你的结构中给你bson标签。

您还可以使用正则表达式或涉及 go 抽象语法树的更复杂的方法对生成的文件进行后处理。

如果您选择在它们之间进行映射:

    使用reflection。您可以编写一个包含两个结构的包,并尝试从一个结构中获取值并将其应用于另一个结构。您将不得不处理极端情况(轻微的命名差异、哪些类型是等效的等),但如果出现极端情况,您将能够更好地控制它们。

    使用 JSON 作为中介。只要生成的 json 标签匹配,这将是一个快速的编码练习,并且如果这不是在代码中的紧密循环中,序列化和反序列化的性能损失可能是可以接受的。

    手写或代码生成映射功能。根据你有多少结构,你可以写出一堆在两者之间转换的函数。

在我的工作场所,我们最终做了所有这些工作:派生 protoc 生成器来做一些自定义标签,一个基于反射的结构覆盖包,用于在任意结构之间进行映射,以及一些对性能更敏感的手写结构或自动化程度较低的映射。

【讨论】:

以上是关于从 bson 结构转换 protoc 生成的结构的最佳方法是啥?的主要内容,如果未能解决你的问题,请参考以下文章

如何将.proto通过命令转换成.java文件

使用 bson 的 golang 结构默认值

mongo学习笔记---1

如何在结构中定义多个名称标签

MGO 驱动程序中的更新查询,适用于 bson.M,但不适用于自定义结构

mongoDB学习