在Go中绕过SQL空值问题

13

我想使用Go为现有数据库制作一个API,该数据库广泛使用null值。Go将不会扫描null并将其转换为空字符串(或等价物),因此我需要实现一个解决方法。

我发现的解决方法让我感到不满意。实际上,我因为这个问题而寻找了一种动态语言,但Go具有某些吸引力,如果可能的话,我想坚持使用它。以下是未能令人满意的解决方法:

  1. 在数据库中不使用null值。这不可行,因为数据库是预先存在的,我无法干涉其结构。数据库比我的应用程序更重要,而不是相反。
  2. 在sql查询中,在数据到达我的应用程序之前,使用COALESCE、ISNULL等将null值转换为空字符串(或等效值)。这不可行,因为有很多字段和表。除了一些明显的字段(主键、姓氏)之外,我不确定哪些字段可以被依赖于不给我null值,所以我会在所有地方防御性地混淆我的sql查询。
  3. 使用sql.NullString、sql.NullInt64、sql.NullFloat64等将null值转换为空字符串(或等效值)作为中间步骤,然后将其安置到目标类型中。这与上述问题相同,只是我混淆了我的Go代码而不是我的sql查询。
  4. 使用*指针和[]byte的组合,将每个项目扫描到内存位置中而不将其提交给特定类型(除了[]byte),然后以某种方式处理原始数据。但是,为了对数据进行有意义的处理,您必须将其转换为更有用的内容,然后你又回到了sql.Nullstring或if x==nil{handle it},这在任何需要处理的字段的情况下都会逐个发生。因此,我们再次看到杂乱无章、混乱、容易出错的代码,并且我总是在重复自己,而不是在编码时DRY。
  • 寻求 Go ORM 库的帮助。我尝试了,但出乎意料地,它们都没有解决这个问题。
  • 编写自己的辅助包来将所有空字符串转换为“”,所有空整数转换为0,所有空浮点数转换为0.00,所有空布尔值转换为false等,然后将其作为扫描 SQL 驱动程序的一部分,得到常规的字符串、整数、浮点数和布尔值。

    不幸的是,如果 6 是解决方案,那我没有专业知识。我怀疑解决方案可能涉及像“如果要扫描的项的预期类型是字符串,则将其作为 sql.NullString 并从中提取空字符串。但如果要扫描的项是 int,则将其作为 NullInt64,并从中获得零。但如果...(等等)”。

  • 还有什么我漏掉了吗?谢谢。


    3
    这个问题的答案是 http://golang.org/pkg/database/sql/#NullString。 - elithrar
    2
    你的一些项目假定 SQL NULL 应该被视为 "" 或 0。这几乎从来不是这种情况,如果是这样的话,数据库就不会使用 NULL(通常表示“未设置”,“不可用”,“不适用”等)。对于没有 NULL 基本类型概念的编程语言(例如指针可以为 nil 但 int 不行),您必须使用每个可空列两个变量(例如 value intvalid bool,如 sql.NullString)或每行/记录一个标志字段来记录有效状态。这根本不是可以通过魔术代码轻松解决的问题。 - Dave C
    1
    确实。在我们的数据库中,一些人有中间名字。中间名字真的不是“”,所以会显示NULL。这样可以避免我错误地认为人们的中间名字实际上是“”,并在地址“”或电话号码0上无意中尝试给他们发电子邮件或打电话。但说真的,我认为数据库制造商之所以使用NULL只是因为它存在,并且比空字符串略微更正确。 - Josh B
    @elithrar,sql.Nullstring不是解决方案,请参见第3点。数据库广泛使用null值。null值的意思只是“不存在”。我不会为每个可能为空的数据库字段跳舞。不使用Go将是更好的答案。 - Josh B
    我评论了 #3 - 如果您要使用 Go,它是最合适的选项。考虑到您在“go”标签下发布了这篇文章,这个假设是合理的。您可以轻松编写一个 isNil 助手来简化此过程。您也可以使用具有类型的 null 零值的其他语言。 - elithrar
    显示剩余2条评论
    1个回答

    7
    使用指针作为sql扫描目标变量可以使数据被扫描、处理(在检查是否!= nil的情况下)并转换为json,从API发送出去,而不必在各处放置数百个sql.Nullstring、sql.Nullfloat64等。空值通过序列化后的json奇迹般地被保留并发送出去。(请参见底部的Fathername)。在另一端,客户端可以使用javascript处理空值,因为它更适合处理它们。
    func queryToJson(db *sql.DB) []byte {
        rows, err := db.Query(
          "select mothername, fathername, surname from fams" +
          "where surname = ?", "Nullfather"
        )
        defer rows.Close()
    
        type record struct {
            Mname, Fname, Surname *string  // the key: use pointers
        }
        records := []record{}
    
        for rows.Next() {
            var r record
            err := rows.Scan(r.Mname, r.Fname, r.Surname) // no need for "&"
            if err != nil {
                log.Fatal(err)
            }
            fmt.Println(r)
            records = append(records, r)
        }
        j, err := json.Marshal(records)
        if err != nil {
            log.Fatal(err)
        }
        return j
    }
    j := queryToJson(db)
    fmt.Println(string(j)) // [{"Mothername":"Mary", "Fathername":null, "Surname":"Nullfather"}]
    

    如果你需要JSON,这样做是可以的。但是编组速度较慢,如果你不需要JSON,这将浪费大量计算资源。 - MattyB
    1
    @MattyB 如果你不想进行编组,你可以在编组行之前停止函数并返回“records”变量,但是在使用数据之前,你需要以其他方式处理那些可能为空的指针。对我来说,编组似乎足够快;它通过网络发送并由Web浏览器中的JavaScript消耗。 - Josh B

    网页内容由stack overflow 提供, 点击上面的
    可以查看英文原文,
    原文链接