【问题标题】:Scala: accessing case class fields when returning a base class after a match statementScala:在匹配语句后返回基类时访问案例类字段
【发布时间】:2021-09-14 10:23:08
【问题描述】:

我目前正在尝试为数据管道创建一个封装各种配置方面(以 json 提供)的类和子类;我还在学习scala。我正在使用 Play 框架库来解析 json 字符串输入 - https://www.playframework.com/documentation/2.8.x/ScalaJson

我有一些目前可以运行的代码,但其中有几个方面感觉不对,而且这感觉不是正确的方法。

应用程序的工作流需要获取一个 json 字符串,针对各种相似但略有不同的结构对其进行解析和验证,然后使这些值可用于下游处理的其他位(例如,如果文件类型是 csv 文件,因此设置一些配置,如果它是一个 json 文件,请改为这样做);但重要的是要注意这是一个动态过程。在我看来,这似乎是案例类的完美用例,但我感觉我误解了它们的用途。

所以我有一个密封的(以确保所有匹配项都是已知的)抽象类 FileConfig,目前有两个子类,DelimitedConfig 和 JsonConfig。 DelimitedConfig 类还使用了一个额外的案例类DelimitedFileTypeDetails,这本质上是两者之间的主要区别,但是随着我的继续,还会添加其他偏差;而且我也有所有三个类的伴随对象,以便利用播放框架格式方法:

sealed abstract class FileConfig

case class DelimitedFileTypeDetails (delimiter: String, hasColumnHeaders: Boolean, rowsToSkip: Int)

object DelimitedFileTypeDetails {
    implicit val format = Json.format[DelimitedFileTypeDetails]
}

case class DelimitedConfig (deltaLakeDatabricksMountPoint: String,
    restrictedDeltaLakeDatabricksMountPoint: String,
    notebookToRun: String,
    rowHashExclusionColumns: List[String],
    deltaDatabase: String,
    mergeKeySQLClause: String,
    mergeUpdateFilterSQLClause: String,
    fileToMerge: String,
    containsPIIData: Boolean,
    piiEntityNames: List[String],
    transformsClass: String,
    transformsMethod: String,
    extractStartTime: String,
    fileType: String,
    fileTypeDetails: DelimitedFileTypeDetails
    ) extends FileConfig {
      require(fileType == "Delimited", "fileType must be 'Delimited' for class DelimitedConfig")
    }

object DelimitedConfig {
  implicit val format = Json.format[DelimitedConfig]
}

case class JsonConfig (deltaLakeDatabricksMountPoint: String,
    restrictedDeltaLakeDatabricksMountPoint: String,
    notebookToRun: String,
    rowHashExclusionColumns: List[String],
    deltaDatabase: String,
    mergeKeySQLClause: String,
    mergeUpdateFilterSQLClause: String,
    fileToMerge: String,
    containsPIIData: Boolean,
    piiEntityNames: List[String],
    transformsClass: String,
    transformsMethod: String,
    extractStartTime: String,
    fileType: String) extends FileConfig {
      require(fileType == "Json", "fileType must be 'Json' for class JsonConfig")
    }

object JsonConfig {
  implicit val format = Json.format[JsonConfig]
}

object FileConfig {
  implicit val format = Json.format[FileConfig]
}

在这些之后,我还有一个函数可以使用播放框架的 validate 方法对任何提供的 json 字符串执行匹配;这将是第一个问题 - 这感觉真的很笨重,有没有办法修改对象的嵌套评估?本质上,它检查它是否满足第一个子类,如果不满足(结果为 JsError),则检查下一个,依此类推:

object FileConfigParser {

  /**
    * Parse the provided json to ensure it matches the specified format.
    *
    * @param jsonString - a string representation of the json
    * @return - FileConfig of the parsed json
    */
  def parseFileConfig(jsonString: String): FileConfig = {

    val json = Json.parse(jsonString)

    json.validate[DelimitedConfig] match {
      case s: JsSuccess[DelimitedConfig] => s.get.asInstanceOf[DelimitedConfig]
      case e: JsError => {
        json.validate[JsonConfig] match {
          case s: JsSuccess[JsonConfig] => s.get.asInstanceOf[JsonConfig]
          case e: JsError => {
            throw new Error("Invalid json provided")
          }
        }
      }
    }
  }
}

这一切正常,当我运行该函数时,验证成功;但是,由于 parseFileConfig 函数的结果,我无法访问类字段,直到我在主对象中运行进一步的匹配:-

val jsonString = """{
      "sourceDatabricksMountPoint": "/mnt/rawlayer/myData/",
      "deltaLakeDatabricksMountPoint": "/mnt/delta/",
      "restrictedDeltaLakeDatabricksMountPoint": "/mnt/sensitive_delta/",
      "notebookToRun":"/Ingestion/LoadDeltaLake",
      "rowHashExclusionColumns": ["RowLastUpdatedTime"],
      "deltaDatabase": "myDb",
      "restrictedDeltaDatabase": "NICE_WFM_RESTRICTED",
      "deltaTableName": "myTable",
      "mergeKeySQLClause": "s.date=t.date AND s.ID=t.ID",
      "mergeUpdateFilterSQLClause":"s.RowHash <> t.RowHash",
      "fileToMerge": "myFile.txt",
      "fileType":"Delimited",
      "fileTypeDetails": {"delimiter": "|",
                          "hasColumnHeaders": true,
                          "rowsToSkip": 1
                              },
      "containsPIIData": true,
      "piiEntityNames": ["name", "surname"],
      "transformsMethod": "convertDateTimeColumns",
      "transformsClass": "com.example.transforms.CustomTransform",
      "extractStartTime":"2020-12-10T00:00:00.000000"
      }"""

    val fileConfig: FileConfig = parseFileConfig(jsonString)

    println(fileConfig.getClass())

    // if you uncomment this it doesn't work
    // println(fileConfig.fileType)

    // but this does work
    val fileType = fileConfig match {
      case d: DelimitedConfig => d.fileType
      case j: JsonConfig => j.fileType
    }

    println(fileType)

必须对主代码中的每个字段运行匹配感觉有点乏味,有没有办法在类中执行此操作,或者让这些字段可以从类中访问?或者....我只是做错了,还有更好的方法吗?

提前致谢,

马特

【问题讨论】:

  • 只要将公共字段声明为FileConfig的抽象成员即可。

标签: json scala playframework subclass case-class


【解决方案1】:

如果每个FileConfig 都有一个给定类型的成员(例如fileType),您可以将该成员放入FileConfig

sealed abstract class FileConfig {
  def fileType: String
}

然后在扩展FileConfig 的各种案例类中,您将拥有override val fileType: String

也就是说,我会重新考虑让fileType 成为case classes 中的一个字段。如果每个DelimitedConfig 将有fileType"Delimited",那么将其设置为def 然后定义一个Format[FileConfig] 在反序列化时查看fileType 字段并将其注入到序列化时的 JSON。我使用 Play JSON 已经有一段时间了,所以我不记得那里的技术了。

【讨论】:

  • 谢谢,我知道这很简单;我尝试将它们添加为抽象类的参数,但没有在抽象中定义它们。不是每个文件都会被“分隔”,还会有更多的文件类型出现; json、固定宽度等
  • 每个DelimitedConfig 都将是Delimited,对吧?
  • 啊,是的,对不起,会的。
猜你喜欢
  • 1970-01-01
  • 1970-01-01
  • 2019-03-09
  • 2023-03-11
  • 2017-02-25
  • 1970-01-01
  • 1970-01-01
  • 2020-10-04
  • 1970-01-01
相关资源
最近更新 更多