REST API での部分更新

REST APIを作成していて、ずっとモヤっとしたままだったのが、PATCHで部分更新したいときの設計です。

今回、いろいろ試してみて、playframeworkの実装で少しだけスッキリしたのでメモしてみます。まだすっきりしない部分もあるので、こういう方法が一般的、というようなアドバイスがあれば是非知りたいです。

編集をPATCHにするかPUTにするかという投稿は比較的沢山ありますが、更新を部分更新にするか全体更新にするかの設計思想みたいなものはなかなか見つからないように思います。

参考になったのは、このサイト
https://taichiw.hatenablog.com/entry/2014/07/05/133720
2014年の記事ですが、同じようなところでモヤっとしていたので参考になります。

例えば、profile というテーブルがあり、name(名前),address(住所), tel(電話番号)という情報を持っていたときに、addressとtel はnullable (= 必須ではない)場合を考えます。ちなみに新規登録の場合は、必須でない情報はnull でそうしんしても、項目そのものをformに含まなくても、どちらでもvalidationには問題なく、下記のような感じで実装できます。

 case class CreateData(name:String,address:Option[String],tel:Option[String])
 implicit val createDataReads: Reads[CreateData] = (
     (JsPath \ "name").read[String] and
     (JsPath \ "address").readNullable[String] and
     (JsPath \ "tel").readNullable[String] 
   )(CreateData.apply _)

これが、更新になった場合が少しやっかいになります。

まず、部分更新にするか全体更新にするかですが、部分更新の場合は、更新したい情報だけを送信します。
例えば、住所に更新があり、名前と電話番号はそのままであれば、address=xxxx の情報だけをクライアントはサーバーに送信します。

逆に、全体更新の設計の場合は、変更があるなしに関わらず、すべての情報、(name,address,tel)を送る設計になります。全体更新の場合は、これ以上あまり考えることはないので、比較的簡単なのですが、APIを使うクライアント側は、全ての情報を送信しないとならなくなるので、かなり手間がかかります。
今回の例のようにパラメータが3つなら問題ないですが、10個以上あったり、classが入れ子になったりしている場合は、全てのデータを正確に送信するクライアントサイドのプログラムを書くのは、難しくはないですが、面倒です。

また、誤って不足のデータを送信してしまった場合、意図しないデータの喪失があり得るので、かなり注意が必要となります。

上記のような理由から、なるべく部分更新でAPIを設計しておきたいのですが、その場合は違う問題が出てきます。
必須でない情報がある場合、「情報を更新しない」場合と「情報を削除したい場合」を分ける必要が出てきます。

今回の例で言うと、登録時に設定した電話番号を削除したい場合、という想定の場合です。address=undefind の場合は登録を済みの電話番号を削除、formにaddressを含まない場合は、更新をしないというように分けたくなります。ただ、上記のplayframework の readNullable での取得は、どちらの場合でもNoneが返ることとなり、区別がつかず、どうしたものかなと思っていました。

また、細かいvalidationはcreateとupdateで同じ機構で扱いたいという別の問題もあります。

今回なんとなく少しすっきりとした実装は、以下のように元々ある(例えば)Profileというcase classを利用してvalidationしていくことにしました。

Profile(
    id = item.id
    name = (json \ "name") match{
        case JsDefined(name) => name match{
            case JsString(s) => {s}
                case _ => book.name
            }
            case _ => {book.name}
        },

    address =  (json \ "address") match{
           case JsDefined(address) => address match{
                 case JsString(s) => Option(s)
                 case _ => None
               }
               case _ => book.address
             },        
    tel =  (json \ "tel") match{
               case JsDefined(tel) => tel match{
                 case JsString(s) => Option(s)
                 case _ => None
               }
               case _ => item.tel
             }
)

updateなので、どちらにしても、最初に該当のレコードがあるかどうかを、validateすることになる思いますので、そこで使ったレコードを利用し、必須でない項目については、JsDefined でみつからなければ、DBから使ってきたレコードの値をそのまま利用して、新規のupdate用のProfileレコードを作ることにしました。

validationはこの値を利用して、createで使うものと同じ機構に流せるので、createのときとupdate のときで、同じvalidationの機構を利用することが可能になり、validationの抜けや相違などがなくなりそうです。

ただ、この場合もDBへの更新は、全更新と同じにことになるので、リソースの無駄遣いかなと思うところもありますが、部分更新で必須項目と必須でない項目を、わりとすっきり分けられたかなと思います。

他のframeworkだとこの辺りは、framework側でうまいことやってくれることも多いと思いますが、この部分はおかしいのではないかとか、もっといい方法があるというアドバイスがあれば、ぜひいただきたいです。

この記事が気に入ったらサポートをしてみませんか?