How to do PATCH properly in strongly typed languages based on Spring - example

后端 未结 4 1329
甜味超标
甜味超标 2020-12-23 16:10

According to my knowledge:

  • PUT - update object with its whole representation (replace)
  • PATCH - update object with given fiel
4条回答
  •  忘掉有多难
    2020-12-23 16:55

    As you noted the main problem is that we don't have multiple null-like values to distinguish between explicit and implicit nulls. Since you tagged this question Kotlin I tried to come up with a solution which uses Delegated Properties and Property References. One important constraint is that it works transparently with Jackson which is used by Spring Boot.

    The idea is to automatically store the information which fields have been explicitly set to null by using delegated properties.

    First define the delegate:

    class ExpNull(private val explicitNulls: MutableSet>) {
        private var v: T? = null
        operator fun getValue(thisRef: R, property: KProperty<*>) = v
        operator fun setValue(thisRef: R, property: KProperty<*>, value: T) {
            if (value == null) explicitNulls += property
            else explicitNulls -= property
            v = value
        }
    }
    

    This acts like a proxy for the property but stores the null properties in the given MutableSet.

    Now in your DTO:

    class User {
        val explicitNulls = mutableSetOf>() 
        var name: String? by ExpNull(explicitNulls)
    }
    

    Usage is something like this:

    @Test fun `test with missing field`() {
        val json = "{}"
    
        val user = ObjectMapper().readValue(json, User::class.java)
        assertTrue(user.name == null)
        assertTrue(user.explicitNulls.isEmpty())
    }
    
    @Test fun `test with explicit null`() {
        val json = "{\"name\": null}"
    
        val user = ObjectMapper().readValue(json, User::class.java)
        assertTrue(user.name == null)
        assertEquals(user.explicitNulls, setOf(User::name))
    }
    

    This works because Jackson explicitly calls user.setName(null) in the second case and omits the call in the first case.

    You can of course get a bit more fancy and add some methods to an interface which your DTO should implement.

    interface ExpNullable {
        val explicitNulls: Set>
    
        fun isExplicitNull(property: KProperty<*>) = property in explicitNulls
    }
    

    Which makes the checks a bit nicer with user.isExplicitNull(User::name).

提交回复
热议问题