私の知識によると:
PUT
-表現全体でオブジェクトを更新(置換)PATCH
-指定されたフィールドのみでオブジェクトを更新(更新)Springを使用して、非常に単純なHTTPサーバーを実装しています。ユーザーが自分のデータを更新する場合、エンドポイントに対してHTTP PATCH
を作成する必要があります(たとえば、api/user
)。彼のリクエスト本文は、次のような@RequestBody
を介してDTOにマッピングされます。
class PatchUserRequest {
@Email
@Length(min = 5, max = 50)
var email: String? = null
@Length(max = 100)
var name: String? = null
...
}
次に、このクラスのオブジェクトを使用して、ユーザーオブジェクトを更新(パッチ)します。
fun patchWithRequest(userRequest: PatchUserRequest) {
if (!userRequest.email.isNullOrEmpty()) {
email = userRequest.email!!
}
if (!userRequest.name.isNullOrEmpty()) {
name = userRequest.name
}
...
}
私の疑問は、クライアント(Webアプリなど)がプロパティをクリアしたい場合はどうでしょうか?このような変更は無視します。
ユーザーがプロパティをクリアしたい場合(意図的にnullを送信した場合)、または単に変更したくない場合、どのように知ることができますか?どちらの場合もオブジェクトではnullになります。
ここには2つのオプションがあります。
@Valid
を使用しています。RESTおよびすべての優れた慣行と調和して、そのようなケースをどのように適切に処理する必要がありますか?
編集:
そのような例ではPATCH
を使用すべきではないと言うことができ、PUT
を使用してユーザーを更新する必要があります。しかし、モデルの変更(新しいプロパティの追加など)はどうでしょうか?ユーザーが変更されるたびに、API(またはユーザーエンドポイントのみ)をバージョン管理する必要があります。例えば。古いリクエストボディでPUT
を受け入れるapi/v1/user
エンドポイントと、新しいリクエストボディでPUT
を受け入れるapi/v2/user
エンドポイントがあります。私はそれが解決策ではなく、PATCH
が理由で存在すると思います。
patchyは、SpringでPATCH
を適切に処理するために必要な主要な定型コードを処理する小さなライブラリです。
_class Request : PatchyRequest {
@get:NotBlank
val name:String? by { _changes }
override var _changes = mapOf<String,Any?>()
}
@RestController
class PatchingCtrl {
@RequestMapping("/", method = arrayOf(RequestMethod.PATCH))
fun update(@Valid request: Request){
request.applyChangesTo(entity)
}
}
_
PATCH
要求はリソースに適用される変更を表すため、明示的にモデル化する必要があります。
1つの方法は、クライアントから送信されたすべてのkey
がリソースの対応する属性への変更を表す単純な古い_Map<String,Any?>
_を使用することです。
_@RequestMapping("/entity/{id}", method = arrayOf(RequestMethod.PATCH))
fun update(@RequestBody changes:Map<String,Any?>, @PathVariable id:Long) {
val entity = db.find<Entity>(id)
changes.forEach { entry ->
when(entry.key){
"firstName" -> entity.firstName = entry.value?.toString()
"lastName" -> entity.lastName = entry.value?.toString()
}
}
db.save(entity)
}
_
ただし、上記の手順は非常に簡単です。
上記は、ドメインレイヤーオブジェクトに検証アノテーションを導入することで軽減できます。これは単純なシナリオでは非常に便利ですが、ドメインオブジェクトの状態または変更を実行するプリンシパルの役割に応じて 条件付き検証 を導入するとすぐに実用的ではなくなる傾向があります。さらに重要なのは、製品がしばらく使用され、新しい検証ルールが導入された後、ユーザー編集以外のコンテキストでエンティティを更新できるようにすることは非常に一般的です。 ドメインレイヤーに不変式を強制する の方が実用的と思われますが、 エッジで検証を維持する です。
これは実際に取り組むのが非常に簡単で、80%のケースで次のように機能します。
_fun Map<String,Any?>.applyTo(entity:Any) {
val entityEditor = BeanWrapperImpl(entity)
forEach { entry ->
if(entityEditor.isWritableProperty(entry.key)){
entityEditor.setPropertyValue(entry.key, entityEditor.convertForProperty(entry.value, entry.key))
}
}
}
_
Kotlinの委任プロパティ のおかげで、_Map<String,Any?>
_の周りにラッパーを構築するのは非常に簡単です:
_class NameChangeRequest(val changes: Map<String, Any?> = mapOf()) {
@get:NotBlank
val firstName: String? by changes
@get:NotBlank
val lastName: String? by changes
}
_
そして、 Validator
インターフェイスを使用して、リクエストに存在しない属性に関連するエラーを次のように除外できます。
_fun filterOutFieldErrorsNotPresentInTheRequest(target:Any, attributesFromRequest: Map<String, Any?>?, source: Errors): BeanPropertyBindingResult {
val attributes = attributesFromRequest ?: emptyMap()
return BeanPropertyBindingResult(target, source.objectName).apply {
source.allErrors.forEach { e ->
if (e is FieldError) {
if (attributes.containsKey(e.field)) {
addError(e)
}
} else {
addError(e)
}
}
}
}
_
明らかに HandlerMethodArgumentResolver
で開発を合理化できますが、これは以下で行いました。
上記で説明した内容を、使いやすいライブラリにラップすることは理にかなっていると思いました-見よ patchy 。 patchyを使用すると、宣言型検証に加えて、厳密に型指定された要求入力モデルを使用できます。あなたがしなければならないことは、設定@Import(PatchyConfiguration::class)
をインポートし、モデルにPatchyRequest
インターフェースを実装することです。
私は同じ問題を抱えていたので、ここに私の経験/解決策があります。
パッチをあるべき姿で実装することをお勧めします。
そうしないと、すぐに理解しにくいAPIが手に入ります。
だから私はあなたの最初のオプションをドロップします
クライアントがプロパティを削除する場合、空の文字列を送信する必要があることに同意します(ただし、日付やその他の非文字列型についてはどうですか?)
私の意見では、2番目のオプションは実際には良いオプションです。そして、それも私たちがやったことです。
このオプションで検証プロパティを機能させることができるかどうかはわかりませんが、この検証がドメインレイヤー上にないようにする必要がありますか?これにより、ドメインから例外がスローされる可能性があります。例外は残りのレイヤーによって処理され、不正なリクエストに変換されます。
これは、1つのアプリケーションで行った方法です。
class PatchUserRequest {
private boolean containsName = false;
private String name;
private boolean containsEmail = false;
private String email;
@Length(max = 100) // haven't tested this, but annotation is allowed on method, thus should work
void setName(String name) {
this.containsName = true;
this.name = name;
}
boolean containsName() {
return containsName;
}
String getName() {
return name;
}
}
...
JsonデシリアライザーはPatchUserRequestをインスタンス化しますが、存在するフィールドに対してのみセッターメソッドを呼び出します。そのため、欠落フィールドの包含ブール値はfalseのままになります。
別のアプリでは、同じ原理を使用しましたが、少し異なります。 (私はこれを好む)
class PatchUserRequest {
private static final String NAME_KEY = "name";
private Map<String, ?> fields = new HashMap<>();;
@Length(max = 100) // haven't tested this, but annotation is allowed on method, thus should work
void setName(String name) {
fields.put(NAME_KEY, name);
}
boolean containsName() {
return fields.containsKey(NAME_KEY);
}
String getName() {
return (String) fields.get(NAME_KEY);
}
}
...
PatchUserRequestでMapを拡張することでも同じことができます。
別のオプションとして、独自のJSONデシリアライザーを作成することもできますが、私はそれを試していません。
そのような例ではPATCHを使用すべきではなく、PUTを使用してユーザーを更新する必要があると言えます。
私はこれに同意しません。あなたが言ったのと同じようにPATCH&PUTも使用します:
お気づきのように、主な問題は、明示的なnullと暗黙的なnullを区別するための複数のnullのような値がないことです。この質問にタグを付けたので、私は Delegated Properties および Property References を使用するソリューションを考え出した。 1つの重要な制約は、Spring Bootで使用されるJacksonと透過的に機能することです。
アイデアは、委任されたプロパティを使用して、どのフィールドが明示的にnullに設定されているかという情報を自動的に保存することです。
最初にデリゲートを定義します。
_class ExpNull<R, T>(private val explicitNulls: MutableSet<KProperty<*>>) {
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
}
}
_
これはプロパティのプロキシのように機能しますが、指定されたMutableSet
にnullプロパティを格納します。
DTO
で:
_class User {
val explicitNulls = mutableSetOf<KProperty<*>>()
var name: String? by ExpNull(explicitNulls)
}
_
使用法は次のようなものです。
_@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))
}
_
これは、ジャクソンが2番目の場合にuser.setName(null)
を明示的に呼び出し、最初の場合に呼び出しを省略するためです。
もちろん、もう少し凝ったものを取得し、DTOが実装する必要のあるインターフェイスにメソッドを追加することもできます。
_interface ExpNullable {
val explicitNulls: Set<KProperty<*>>
fun isExplicitNull(property: KProperty<*>) = property in explicitNulls
}
_
user.isExplicitNull(User::name)
を使用すると、チェックが少しうまくなります。
一部のアプリケーションで行うことは、値が設定されているかどうかを区別できるOptionalInput
クラスを作成することです。
class OptionalInput<T> {
private boolean _isSet = false
@Valid
private T value
void set(T value) {
this._isSet = true
this.value = value
}
T get() {
return this.value
}
boolean isSet() {
return this._isSet
}
}
次に、リクエストクラスで:
class PatchUserRequest {
@OptionalInputLength(max = 100L)
final OptionalInput<String> name = new OptionalInput<>()
void setName(String name) {
this.name.set(name)
}
}
プロパティは、@OptionalInputLength
。
使用法は次のとおりです。
void update(@Valid @RequestBody PatchUserRequest request) {
if (request.name.isSet()) {
// Do the stuff
}
}
注:コードはgroovy
で記述されていますが、アイデアは得られます。すでにいくつかのAPIでこのアプローチを使用しており、非常にうまく機能しているようです。