【问题标题】:How to ignore nulls while unmarshalling a MongoDB document?如何在解组 MongoDB 文档时忽略空值?
【发布时间】:2020-01-17 04:42:17
【问题描述】:

我想知道是否有任何方法可以让我在将 MongoDB 文档解组为 Go 结构时忽略空类型。

现在我有一些自动生成的 Go 结构,如下所示:

type User struct {
  Name  string `bson:"name"`
  Email string `bson:"email"`
}

更改此结构中声明的类型不是一种选择,这就是问题所在;在我没有完全控制权的 MongoDB 数据库中,一些文档已经插入了空值,最初我并不期望空值。像这样的:

{
  "name": "John Doe",
  "email": null
}

由于在我的结构中声明的字符串类型不是指针,它们无法接收nil 值,所以每当我尝试在我的结构中解组此文档时,它都会返回错误。

防止将此类文档插入数据库将是理想的解决方案,但对于我的用例,忽略空值也是可以接受的。因此,在解组文档后,我的 User 实例将如下所示

User {
  Name:  "John Doe",
  Email: "",
}

我试图找到一些注释标志,或者可以传递给方法Find/FindOne 的选项,或者甚至可能是一个查询参数,以防止从数据库返回任何包含空值的字段.到现在都没有成功。

mongo-go-driver 中是否有针对此问题的内置解决方案?

【问题讨论】:

    标签: mongodb go mongodb-query bson mongo-go


    【解决方案1】:

    问题是目前的bson编解码器不支持将string编码/解码成/从null

    解决这个问题的一种方法是为string 类型创建一个自定义解码器,我们在其中处理null 值:我们只使用空字符串(更重要的是不报告错误)。

    自定义解码器由bsoncodec.ValueDecoder 类型描述。他们可以在bsoncodec.Registry 注册,例如使用bsoncodec.RegistryBuilder

    注册可以在多个级别设置/应用,甚至可以在整个mongo.Clientmongo.Databasemongo.Collection 上设置/应用,在获取它们时,作为其选项的一部分,例如options.ClientOptions.SetRegistry().

    首先让我们看看我们如何为string 做到这一点,接下来我们将看看如何改进/将解决方案推广到任何类型。

    1。处理null 字符串

    首先,让我们创建一个自定义字符串解码器,它可以将null 转换为(n 个空)字符串:

    import (
        "go.mongodb.org/mongo-driver/bson/bsoncodec"
        "go.mongodb.org/mongo-driver/bson/bsonrw"
        "go.mongodb.org/mongo-driver/bson/bsontype"
    )
    
    type nullawareStrDecoder struct{}
    
    func (nullawareStrDecoder) DecodeValue(dctx bsoncodec.DecodeContext, vr bsonrw.ValueReader, val reflect.Value) error {
        if !val.CanSet() || val.Kind() != reflect.String {
            return errors.New("bad type or not settable")
        }
        var str string
        var err error
        switch vr.Type() {
        case bsontype.String:
            if str, err = vr.ReadString(); err != nil {
                return err
            }
        case bsontype.Null: // THIS IS THE MISSING PIECE TO HANDLE NULL!
            if err = vr.ReadNull(); err != nil {
                return err
            }
        default:
            return fmt.Errorf("cannot decode %v into a string type", vr.Type())
        }
    
        val.SetString(str)
        return nil
    }
    

    好的,现在让我们看看如何将这个自定义字符串解码器用于mongo.Client

    clientOpts := options.Client().
        ApplyURI("mongodb://localhost:27017/").
        SetRegistry(
            bson.NewRegistryBuilder().
                RegisterDecoder(reflect.TypeOf(""), nullawareStrDecoder{}).
                Build(),
        )
    client, err := mongo.Connect(ctx, clientOpts)
    

    从现在开始,使用这个client,每当你将结果解码为string值时,这个注册的nullawareStrDecoder解码器将被调用来处理转换,它接受bson null值并设置Go空字符串"".

    但我们可以做得更好...继续阅读...

    2。处理任何类型的null 值:“类型中性”空值感知解码器

    一种方法是创建一个单独的自定义解码器,并为我们希望处理的每种类型注册它。这似乎是很多工作。

    我们可以(并且应该)做的是创建一个单独的“类型中性”自定义解码器,它只处理 nulls,如果 BSON 值不是 null,则应该调用默认解码器来处理非null 值。

    这非常简单:

    type nullawareDecoder struct {
        defDecoder bsoncodec.ValueDecoder
        zeroValue  reflect.Value
    }
    
    func (d *nullawareDecoder) DecodeValue(dctx bsoncodec.DecodeContext, vr bsonrw.ValueReader, val reflect.Value) error {
        if vr.Type() != bsontype.Null {
            return d.defDecoder.DecodeValue(dctx, vr, val)
        }
    
        if !val.CanSet() {
            return errors.New("value not settable")
        }
        if err := vr.ReadNull(); err != nil {
            return err
        }
        // Set the zero value of val's type:
        val.Set(d.zeroValue)
        return nil
    }
    

    我们只需要弄清楚nullawareDecoder.defDecoder 的用途。为此,我们可以使用默认注册表:bson.DefaultRegistry,我们可以查找各个类型的默认解码器。很酷。

    所以我们现在要做的是为我们想要处理nulls 的所有类型注册一个nullawareDecoder 的值。这并不难。我们只列出我们想要的类型(或这些类型的值),我们可以通过一个简单的循环来处理所有问题:

    customValues := []interface{}{
        "",       // string
        int(0),   // int
        int32(0), // int32
    }
    
    rb := bson.NewRegistryBuilder()
    for _, v := range customValues {
        t := reflect.TypeOf(v)
        defDecoder, err := bson.DefaultRegistry.LookupDecoder(t)
        if err != nil {
            panic(err)
        }
        rb.RegisterDecoder(t, &nullawareDecoder{defDecoder, reflect.Zero(t)})
    }
    
    clientOpts := options.Client().
        ApplyURI("mongodb://localhost:27017/").
        SetRegistry(rb.Build())
    client, err := mongo.Connect(ctx, clientOpts)
    

    在上面的示例中,我为 stringintint32 注册了可识别空值的解码器,但您可以根据自己的喜好扩展此列表,只需将所需类型的值添加到上面的 customValues 切片中.

    【讨论】:

    • 感谢您的回答。你能给我另一个指导吗?如果我希望其他类型(例如 int 和 float)具有相同的行为,我应该如何进行?
    • @Andrew 请查看已编辑的答案。我展示了如何创建一个“类型中立”的 null 感知解码器,并提供了一个如何为多种类型注册它的示例。
    • 太棒了-我希望几个月前就看到了-谢谢@icza
    • 绝对漂亮的答案。实际上也导致我使用 go.mongodb.org/mongo-driver/* 而不是 github.com/mongodb/mongo-go-driver/*
    【解决方案2】:

    您可以通过运营商$existsQuery for Null or Missing Fields了解详细说明。

    在mongo-go-driver中,你可以试试下面的查询:

    email => nil 查询匹配包含 email 字段(其值为 nil不包含电子邮件字段

    cursor, err := coll.Find(
       context.Background(),
       bson.D{
          {"email", nil},
    })
    

    您只需在上述查询中添加$ne 运算符即可获取电子邮件中没有字段电子邮件或没有值nil 的记录。更多运营商详情$ne

    【讨论】:

    • 这不是我真正想要做的。我不是试图通过 null 值的存在与否来查询元素,而是试图将可能包含空值的文档解析为一个不接受 nil 值的结构。
    【解决方案3】:

    如果您提前知道 mongoDB 记录中哪些字段可能为空,则可以在结构中使用指针:

    type User struct {
      Name  string `bson:"name"` // Will still fail to decode if null in Mongo
      Email *string `bson:"email"` // Will be nil in go if null in Mongo
    }
    

    请记住,现在您需要针对从 mongo 解码后使用此值的任何内容进行更多防御性编码,例如:

    var reliableVal string
    if User.Email != nil {
        reliableVal = *user.Email
    } else {
        reliableVal = ""
    }
    

    【讨论】:

      猜你喜欢
      • 1970-01-01
      • 1970-01-01
      • 2010-09-21
      • 2021-05-28
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 2020-11-07
      相关资源
      最近更新 更多