Humanity

Edit the world by your favorite way

DB 設計と URL 設計、SPA、バックエンドについてダラダラと書いた

昨日 Web アプリの DB 設計をちゃんとやろう、と思ってふとノートに色々と書いていた。 その時に何となく感じる事があったのでメモする。

色々当たり前の事も書いてあると思うけど、わざわざブログに書いてるのは、自分の中で一本頭の中の線が繋がった気がしたから、書き留めておきたいと思った。 ただ、以下の文章は自分でも何度も同じような事を冗長に書いてるなーと思う。 それは恥ずかしながら自分がこれまで DB 定義を見る事はあっても、自分でまともに DB 設計をしたのってこれがほぼ初めてのような気がするので、 初めての分野なら、冗長で拙くて読みづらいのはしょうがないと思って、自分の考えの整理のために書き殴る事にした。


SPA や API-first で設計を行っていく事が多くなるにつれ、これらのアプリはより DB 設計が重要となってくると思う。 画面、API、DB のどこを起点として設計を始めるか、はまだ自分の中で指針が無いけど、少なくとも SPA においては DB のエンティティが API のリソースと密結合になってくる(というより似通ってくる)。

エンティティとリソースはもちろん別物である。 リソースとは、言ってしまえばエンティティのスナップショットであり、ある側面からデータを具象化した結果。 その側面は「○○の履歴」といったように単なる子要素の情報であるとか、「年度ごとのデータと月度ごとのデータ」のように同じデータを違う側面で見た情報だったりする。 テーブルの関係はグラフ構造だけども、それを三次元空間のある位置から立体的に見た時に見える視点がリソース。


リソースやエンティティは当然、参照・追加・更新・削除ができる。 あるリソースをツリー構造にマッピングし、GET・POST・PUT・DELETE 等の動詞を用いて、リソースを構成するエンティティの各プロパティ(DB のカラム)を更新する入り口を与える。 これが URL 設計。

当然、各テーブルの整合性は保たなければならない。 それが API のエンドポイントにリクエストが投げられた時に担保しなければならない事。 ちなみにここで「リソース」ではなく「エンドポイント」と呼んでいる理由は「エンドポイント=動詞(HTTPメソッド) + リソース(URL)」と考えているから。

ちなみに SPA、というより API にする事で何が嬉しいかというと、「画面側の都合」感のある処理が入り混じりにくい事。 「リソース定義したからあとはそっちで勝手にアクセスしてね」ができる。 要するにデータストアとそれ以外の処理が必然的に別れる。

そして自分が SPA が好きなのは、「静的ファイル配信したから、あとはブラウザに解釈・実行してもらって、アプリの仕事はそっちでやってね」ができるから。 従来はリクエストとページリロードが同一の物として扱わなければならなかったためにバックエンドに様々な処理が入り乱れていた。 PRG パターンとか汚い処理というかハックの最たるものだと自分は思ってるのだけど、SPA ではバックエンドに必要な時以外はリクエストを投げないという、まぁ言ってみれば当たり前の事が可能になるので、バックエンド側はバックエンドの仕事に注力できる。 つまりバックエンドのコードが綺麗になる。っていうのは本題ではないけど、自分が SPA が好きな理由の一つ。

そうやって段々とバックエンドがバックエンド本来の処理に注力していった結果、結構な処理がフロントエンドに持っていける事が分かる。 大体の場合どちらに処理を実装するか分ける指針となるのは、2つを分けているネットワークのボトルネックだと思う。 極端に言えば、ループの中で何度もリクエスト飛ばしてリソースに参照や更新する必要があるとかいった場合、それはバックエンドでやった方がいい。 例えばチェックボックスで選択した物の一括更新処理とか。


バックエンド本来の処理に注力した結果、リソース(正確に言うと API の IF)とエンティティがかなり似通ったものになってくると思う。少なくとも GET の場合は。 ただそうなってくると、バックエンド側を必要以上に分ける必要あるのか疑問になってくる。

フロントエンドは必要かもしれない。 SPA にするならルーターの仕組みは必須だし、ルーターMVC の C と言えるかもしれない。 ただ MV* とか色んな名前で呼ばれてる通り、MVC に当てはめようとするのは危険。 バックエンドしか無かった時代のようにレスポンス返したら終わりじゃなくて、もっと細かい単位(コンポーネント)で V が切り替わったり、データを参照したり更新したりする。 そもそも XMLHttpRequest が非同期だし。

これまで自分が業務で関わってきたバックエンド側のアーキテクチャは、大体 Controller(プレゼンテーション層)、Service(ビジネスロジック層)、Dao(データアクセス層)の三層アーキテクチャで作られているのが一般的だった。 ただそこでよく見かけたのが、DTO(データトランスファー オブジェクト)と呼ばれるクラスで各層の橋渡しをしていること。 Restful API だと、これまで言った通りほぼ項目が同じになりがちなため、オブジェクトのコピーのコストが発生する。 しかも、それぞれのクラスのメンテナンスコストもかかる。 いちいちゲッターセッター呼んで詰め替えとかやってらんないから、BeanUtils#copyProperties() とかのリフレクションで自動的にコピーする系のメソッドを使ってたりするけど、片方のクラスに項目を1個定義するの忘れたから RuntimeException が出るとかいう…

三層アーキテクチャ自体は別にいいと思う。 ただ Restful API だとプレゼンテーション層の仕事は減るからほぼサービス呼び出すだけになる。 けどレスポンスヘッダーを変えたいとか、POST の場合は 201 Created を返したいとか、DELETE の場合は 204 No Content だとか、そういうのはプレゼンテーション層の役割な気がする。 まぁ定義次第というか各クラスでの責務がちゃんと決められてさえいれば良い。

バックエンドにそこまでの構造化が必要か?って考えてるのは決してバックエンドの仕事を軽視してる訳じゃなく(今までの仕事はずっとバックエンドだったので)、むしろバックエンド側は共通化できる部分が結構あるんじゃないかと思うから。 いきなり共通化と言われても何のことか分からないと思うので、バックエンドでの共通化できそうな処理を列挙してみます。以下はあくまで例です。

  • リクエストを受けたらコントローラーでログ出力
  • 認証・認可
  • gzip でレスポンスを圧縮
  • 例外が起きたらエラー用のレスポンスを返す

こういった処理は AOP なりマイクロサービスの API Gateway Pattrn なりで結構横断的に共通化できそう。 これらを除くと、もしかしたら DB に永続化する位しか処理がほとんどなくなってしまうのではないか。

あと、以前とあるプロジェクトで、Dao クラスで例外を投げて、サービスクラスでキャッチしてそれをラップした例外を再 throw して、コントローラーでキャッチしてそれをラップした例外を再 throw して …としてる Java コードを見て、なんだかなーと思った。 (というかまぁ、結構現場で見かけるコードなんだけど…)

例外処理って本質的ではないけど、コードが膨れ上がる原因の一つだったりする。 もちろん無くてはならない処理だけど。 でも実際のビジネスロジック的には大した事してないのにここまでたらい回しにされるのはなーという。 そんな訳で定形的にコントローラーからサービス辿って Dao 辿ってを繰り返すんだけど、プレゼンテーション層の仕事が少なくなったのだから、ビジネスロジック層とデータアクセス層だけに分ける、つまりコントローラーから Dao 直接呼んじゃっていいんじゃね?と思ったり。

コントローラーで例外処理をキャッチしたり、レスポンス用のオブジェクトを返すといった処理は、少なくとも Java EEJAX-RS や Spring ではコントローラーで書く必要はないはず。 JAX-RS の例だけど、前者は ExceptionMapper、後者はコントローラーで返したオブジェクトがそのまま JSONXML に自動でシリアライズされる。 ちなみに JAX-RS ではコントローラーではなく HogeResource みたいなクラス名を付けるのが慣習らしい。