代码之家  ›  专栏  ›  技术社区  ›  Yanick Rochon

如果字段是MongoDB中的字符串,如何有条件地将其转换为日期?

  •  1
  • Yanick Rochon  · 技术社区  · 7 年前

    在创建查询时,我意识到我继承的应用程序有一个集合 timestamp 包含 string ISODate 价值。

    因此,这个聚合阶段:

    {
      "$addFields": {
        "timestamp": {
          "$dateFromString": {
            "dateString": "$activity.timestamp"
          }
        },
        "minTimestamp": {
          "$dateFromString": {
            "dateString": "2016-01-01"
          }
        },
        "maxTimestamp": {
          "$dateFromString": {
            "dateString": "2017-01-01"
          }
        }
      }
    }
    

    产生错误: $dateFromString requires that 'dateString' be a string, found: date 在某些文件上。

    显然,逻辑上的答案是将所有字段值转换为 等酸盐 但是,这个系统的庞然大物似乎保存了为该字段设置的不一致的值,而且我不可能预先保证该类型。

    有没有一种方法可以有条件地将字段转换为 等酸盐 ?

    该应用程序正在MongoDB 3.6.4上运行。

    1 回复  |  直到 7 年前
        1
  •  2
  •   Neil Lunn    7 年前

    如果某些字段实际上是bson日期,那么您可能希望将它们单独保存并按原样输出。为此你可以使用 $type 伴随着 $cond 表达式:

    { "$addFields": {
      "timestamp": {
        "$cond": {
          "if": { "$eq": [{ "$type": "$activity.timestamp" }, "string" ] },
          "then": {
            "$dateFromString": {
              "dateString": "$activity.timestamp"
            }
          },
          "else": "$activity.timestamp"
        }
      }
    }}
    

    从MongoDB 3.4和更高版本可以看出,这是 $类型 加入。

    注意MongoDB 4.0及以上版本的用户 $convert 运算符实际上具有内置的错误处理分支:

    { "$addFields": {
      "timestamp": {
        "$convert": {
          "input": "$activity.timestamp",
          "to": "date",
          "onError": "Neither date or string"
        }
      }
    }}
    

    这个 onError 可以是任何表达式,并在转换无效的实例中返回。查找bson日期实际上不是一个错误,只有无效的数字或字符串值或不支持转换的其他类型才会发生错误。

    如果您确定数据始终是用于转换的bson日期或有效字符串,则 $toDate 帮助器,它基本上是 美元兑换 没有 出错 处理:

    { "$addFields": {
      "timestamp": { "$toDate": "$activity.timestamp" }
    }}
    

    因此,一些数据清理和/或查询条件通常可以与这些条件结合使用,以获得更精简的编码体验。


    关于$datetostring用法的说明

    在这个问题中 $dateToString 也用于将“静态值”从字符串转换为bson日期。这不是个好主意。

    在用自然语言代码更清晰地表达的服务器表达式中运行函数代码不是也从来不是一个好的实践。作为MongoDB一般哲学的一部分,应该并且可以用该语言表达的部分应该这样做。

    对于javascript简单 Date 对象在提交到服务器时被序列化为bson日期:

    "minTimestamp": new Date("2016-01-01")
    

    因为“字符串”的值是 外部的 然后它不需要服务器端表达式来操作它。就像发布一个查询一样,您可以强制转换这些类型 之前 您提交到服务器,而不是随后提交。

    同样的概念适用于所有语言实现,因为所有语言都有一个“日期”类型,实现的驱动程序无论如何都理解并正确地将其序列化为bson日期。


    转换

    尽管如此,将军 “最佳实践” 当然,这里是要实际转换数据。”不管“是否庞大”,在运行时依赖数据转换只会使情况变得更糟。如果您在这种运行时转换中的实际意图是在进一步的输出处理中使用bson日期,而不仅仅是一个漂亮的输出,那么情况就更糟了。

    里程数随方法的不同而变化,但基本的方法是迭代集合并就地更新值,以及 $type “查询”运算符可以帮助您在此处进行选择:

    // Presuming date strings in"yyyy-mm-dd" format
    var batch = [];
    
    db.collection.find({ "activity.timestamp": { "$type": "string" } }).forEach(d => {
      batch.push({
        "updateOne": {
          "filter": { "_id": d._id },
          "update": { "$set": { "activity.timestamp": new Date(d.activity.timestamp) } }
        }
      });
    
      if (batch.length >= 1000) {
        db.collection.bulkWrite(batch);
        batch = [];
      }
    })
    
    if (batch.length > 0) {
      db.collection.bulkWrite(batch);
      batch = [];
    }
    

    或者运行聚合 $out 如果约束允许,则指向新集合:

    { "$addFields": {
      "activity": {
        "timestamp": {
          "$cond": {
            "if": { "$eq": [{ "$type": "$activity.timestamp" }, "string" ] },
            "then": {
              "$dateFromString": {
                "dateString": "$activity.timestamp"
              }
            },
            "else": "$activity.timestamp"
          }
        }
      }
    }},
    { "$out": "newcollection" }
    

    上面显示的任何聚合方法都可以在支持它们的地方使用,但只是在示例中显示。还要注意 $addFields 不允许嵌套对象语法,因为输出被“合并”到现有文档结构中。

    即使在生产系统上,您也可以始终输出到新集合,然后以最少的停机时间删除和重命名。这里的主要约束实际上是索引重建,这比集合重命名花费的时间要长得多。