spring - 如何在后期初始化中保留不可为空的属性

标签 spring spring-boot kotlin dto data-class

以下问题:在具有 Spring-Boot 的客户端/服务器环境中和 Kotlin客户端想要创建类型 A 的对象,因此通过 RESTful 端点将数据发布到服务器。

实体 A 实现为 data class在 Kotlin 中是这样的:

data class A(val mandatoryProperty: String)

从业务角度来看,该属性(也是主键)绝不能为空。但是,客户端不知道它,因为它由服务器上的 Spring @Service Bean 生成非常昂贵。

现在,在端点 Spring 尝试将客户端的有效负载反序列化为类型 A 的对象,但是,mandatoryProperty在那个时间点是未知的,这将导致映射异常。

绕过这个问题的几种方法,没有一种让我真正感到惊讶。
  • 不要期望端点处有类型 A 的对象,而是获取一组描述 A 的参数,这些参数会一直传递,直到实体实际创建并出现 requiredProperty 为止。实际上相当麻烦,因为有比单个属性更多的属性。
  • 与 1 非常相似,但创建了一个 DTO。然而,这是我的最爱之一,因为 data classes不能扩展这意味着将类型 A 的属性复制到 DTO 中(强制属性除外)并复制它们。此外,当 A 增长时,DTO 也必须增长。
  • 使 requiredProperty 可以为空并使用 !!整个代码中的运算符。可能是最糟糕的解决方案,因为它破坏了可空和不可空变量的意义。
  • 客户端将为mandatoryProperty 设置一个虚拟值,该值在生成属性后立即被替换。但是,A 由端点验证,因此虚拟值必须服从其 @Pattern约束。所以每个虚拟值都是一个有效的主键,这给我一种不好的感觉。

  • 我可能监督过的其他更可行的方法是什么?

    最佳答案

    我不认为对此有一个通用的答案......所以我只会给你我关于你的变体的 2 美分......

    您的第一个变体具有其他人真正拥有的好处,即您不会将给定的对象用于其他任何目的(即仅用于端点或后端目的),但这可能会导致繁琐的开发。

    第二个变体很好,但可能会导致其他一些开发错误,例如当你认为你使用了实际的 A但你宁愿在 DTO 上操作。

    变体 3 和 4 在这方面与 2 类似...您可以将其用作 A即使它仅具有 DTO 的所有属性。

    所以......如果你想走安全路线,即没有人应该将这个对象用于其他任何事情,那么它的特定目的你应该使用第一个变体。 4 听起来更像是一个黑客。 2 和 3 可能没问题。 3 因为你实际上没有 mandatoryProperty当您将其用作 DTO 时...

    尽管如此,因为你有你最喜欢的(2),我也有一个,我将专注于 2 和 3,从 2 开始,使用带有 sealed class 的子类方法。作为父类(super class)型:

    sealed class AbstractA {
      // just some properties for demo purposes
      lateinit var sharedResettable: String 
      abstract val sharedReadonly: String
    }
    
    data class A(
      val mandatoryProperty: Long = 0,
      override val sharedReadonly: String
      // we deliberately do not override the sharedResettable here... also for demo purposes only
    ) : AbstractA()
    
    data class ADTO(
      // this has no mandatoryProperty
      override val sharedReadonly: String
    ) : AbstractA()
    

    一些演示代码,演示用法:
    // just some random setup:
    val a = A(123, "from backend").apply { sharedResettable = "i am from backend" }
    val dto = ADTO("from dto").apply { sharedResettable = "i am dto" }
    
    listOf(a, dto).forEach { anA ->
      // somewhere receiving an A... we do not know what it is exactly... it's just an AbstractA
      val param: AbstractA = anA
      println("Starting with: $param sharedResettable=${param.sharedResettable}")
    
      // set something on it... we do not mind yet, what it is exactly...
      param.sharedResettable = UUID.randomUUID().toString()
    
      // now we want to store it... but wait... did we have an A here? or a newly created DTO? 
      // lets check: (demo purpose again)
      when (param) {
        is ADTO -> store(param) // which now returns an A
        is A -> update(param) // maybe updated also our A so a current A is returned
      }.also { certainlyA ->
        println("After saving/updating: $certainlyA sharedResettable=${certainlyA.sharedResettable /* this was deliberately not part of the data class toString() */}")
      }
    }
    
    // assume the following signature for store & update:
    fun <T> update(param : T) : T
    fun store(a : AbstractA) : A
    

    示例输出:
    Starting with: A(mandatoryProperty=123, sharedReadonly=from backend) sharedResettable=i am from backend
    After saving/updating: A(mandatoryProperty=123, sharedReadonly=from backend) sharedResettable=ef7a3dc0-a4ac-47f0-8a73-0ca0ef5069fa
    Starting with: ADTO(sharedReadonly=from dto) sharedResettable=i am dto
    After saving/updating: A(mandatoryProperty=127, sharedReadonly=from dto) sharedResettable=57b8b3a7-fe03-4b16-9ec7-742f292b5786
    

    丑的部分我还没给你看,你自己已经提到了...你如何改造你的ADTOA反之亦然?我会把它留给你。这里有几种方法(手动,使用反射或映射实用程序等)。
    此变体将所有特定于 DTO 的属性与非特定于 DTO 的属性干净地分开。但是它也会导致冗余代码(所有 override 等)。但至少您知道您操作的是哪种对象类型,并且可以相应地设置签名。

    像 3 这样的东西可能更容易设置和维护(关于 data class 本身;-)),如果你正确设置边界,它甚至可能很清楚,当有一个 null 时。在那里和什么时候不......所以也展示了那个例子。首先从一个相当烦人的变体开始(烦人的是,当您尝试访问尚未设置的变量时,它会抛出异常),但至少您省去了 !!null - 检查这里:
    data class B(
      val sharedOnly : String,
      var sharedResettable : String
    ) {
      // why nullable? Let it hurt ;-)
      lateinit var mandatoryProperty: ID // ok... Long is not usable with lateinit... that's why there is this ID instead
    }
    data class ID(val id : Long)
    

    演示:
    val b = B("backend", "resettable")
    //  println(newB.mandatoryProperty) // uh oh... this hurts now... UninitializedPropertyAccessException on the way
    val newB = store(b)
    println(newB.mandatoryProperty) // that's now fine...
    

    但是:即使访问 mandatoryProperty会抛出 Exception它在 toString 中不可见如果您需要检查它是否已经被初始化(即通过使用 ::mandatoryProperty::isInitialized ),这看起来也不好看。

    所以我向你展示了另一种变体(同时也是我最喜欢的,但是......使用 null ):
    data class C(val mandatoryProperty: Long?,
      val sharedOnly : String,
      var sharedResettable : String) {
      // this is our DTO constructor:
      constructor(sharedOnly: String, sharedResettable: String) : this(null, sharedOnly, sharedResettable)
      fun hasID() = mandatoryProperty != null // or isDTO, etc. what you like/need
    }
    // note: you could extract the val and the method also in its own interface... then you would use an override on the mandatoryProperty above instead
    // here is what such an interface may look like:
    interface HasID {
      val mandatoryProperty: Long?
      fun hasID() = mandatoryProperty != null // or isDTO, etc. what you like/need
    }
    

    用法:
    val c = C("dto", "resettable") // C(mandatoryProperty=null, sharedOnly=dto, sharedResettable=resettable)
    when {
        c.hasID() -> update(c)
        else -> store(c)
    }.also {newC ->
        // from now on you should know that you are actually dealing with an object that has everything in place...
        println("$newC") // prints: C(mandatoryProperty=123, sharedOnly=dto, sharedResettable=resettable)
    }
    

    最后一个有好处,您可以使用 copy -方法再次,例如:
    val myNewObj = c.copy(mandatoryProperty = 123) // well, you probably don't do that yourself...
    // but the following might rather be a valid case:
    val myNewDTO = c.copy(mandatoryProperty = null)
    

    最后一个是我最喜欢的,因为它需要最少的代码并且使用 val取而代之(因此也不会发生意外覆盖,或者您改为对副本进行操作)。您也可以为 mandatoryProperty 添加一个访问器如果您不喜欢使用 ?!! ,例如
    fun getMandatoryProperty() = mandatoryProperty ?: throw Exception("You didn't set it!")
    

    最后,如果你有一些辅助方法,如 hasID ( isDTO 或其他)从上下文中也可以清楚地了解您到底在做什么。最重要的可能是建立一个每个人都理解的约定,以便他们知道何时应用什么或何时期望特定的东西。

    关于spring - 如何在后期初始化中保留不可为空的属性,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/52213701/

    相关文章:

    Kotlin:即使 map 相等, map 比较也会抛出失败

    java - Spring Batch Partitioning 在 itemReader 中注入(inject) stepExecutionContext 参数

    java - 我如何在 JMockit 中期望和验证相同的方法

    Spring AOP - 如何转换代理 bean?

    java - spring boot 应用程序作为可执行 jar 运行,但不能作为部署在 jboss EAP 6 上的 war

    java - 即使使用@Fetch(FetchMode.JOIN),JPA + Hibernate 也会出现太多查询问题

    Spring @Scheduled - 并发运行

    java - 从 context.xml 加载 Bean 属性值

    intellij-idea - 要获得AbstractKotlinInspection包括哪些kotlin库?

    kotlin - 在jetpack compose中将默认方向设置为横向?