こんにちは、プロダクトエンジニアリングのちょうです。最近天気は暑かったり、寒かったりするので、体調管理に十分気をつけてください。さて、サービスが増えるにつれ、元々大きなサービスをマイクロサービス化するのは珍しくありません。そんな中で、一つ大きなサービスのデータアクセスをデータベースからリモートAPIに変える際に、かならずいろんな問題が出てきます。今回は私が実際やっていたRuby on Railsのデータアクセス移行で学んだことをシェアしたいと思います。
移行する対象は3つのモデル、A、BとC。これらすべてのモデルをAPIの形に書き換えます。 Railsでしたら、モデルで直接データアクセスできます。例えば
A.find(1) B.where(a: 1).select('foo')
APIに変えたいなら、そのままなんらかマジック的なgemを使って、APIコールに変えてもいいのかと思うなら、おそらく方向性が違います。私達がやりたいことはこのデータアクセスをAPIコールに変えたいです。そして現在の利用シーンを洗い出してそれにお応じてAPIを作ります。利用シーンの洗い出しはどうしても避けられないです。必須な作業の上にまたそのgemの複雑なマッピングを対応すると仕事量が増えます。既存のコードを変えられない場合以外、やはり正攻法で行きましょう。
Railsのデータアクセスを正攻法で行くには、Railsのモデルはなんなのかを理解する必要があります。あまり意識されていないかもしれませんが、Railsのモデルはすくなくともこのような役割があります。
- データモデル
- データアクセス
- フォーム
ソフトウェアデザインにおいて、一つのクラスに複数の役割をもつのは基本的にはアンチパターンで、SRP(Single Responsibility Principle)違反です。SRP違反はさておき、書き換えにあたって、これらの役割を分けるのは正攻法だと私が考えています。なぜなら、
- データモデルはAPIクライアントが提供する
- データアクセスはどっちにしても書き換えする必要がある。ただAPI直コールではなく、データアクセスのクラスの作る
- フォーム、既存サービス側でバリデーションをかけてもいいし、API側でチェックして、そのリスポンスでエラーメッセージを表示してもいい
つまり、分けて考えるとスムーズにいけますね。 Userというモデルの例をしましょう。
class User < ActiveRecord::Base # id # email # password has_secure_password validates :email, presence: true validates :password, presence: true end User.find(1) User.create!(params.require(:user).permit(:email, :password))
まずAPIクライアントのモデルはこちらです。もしクライアントがないならモデルを自分で作りましょう。
class ApiUser attr_access :id attr_access :email attr_access :password end
passwordに関して、作成や更新するとき書き込みのみなので、APIで返すことはないです。ゆえ、データベースのカラムpassword_digestはモデルにありません。
データアクセスのクラスはこちらです。
class ApiUserRepository def find_user(id) end def save_user(user) end end
コードはシンプル、実際ほとんどのコードは一行でしょう。ただデータアクセスのクラスを省いて、既存のコードをAPIコールに書き換えるのはやめておいたほうがいいです。もしキャッシュを追加したいときは、大変なことになります(残念ながら、リモートAPIに変えたあとキャッシュを追加するのはよくあることです)。
最後にフォームクラスです。
class ApiUserForm include ActiveModel::Validations validates :email, presence: true validates :password, presence: true validates :password_confirmation, confirmation: true attr_access :id attr_access :email attr_access :password attr_access :password_confirmation end
フォームはデータモデルと似ているが、データモデルは基本的にAPIクライアントにあり、データモデルは画面のフォームと一致しない可能性がある(例えばここのpassword_confirmationはデータモデルにない)から、フォームクラスを作るのです。
フォームがあると、画面とモデルの違いを簡単に対応できる以外、もうひとつメリットがあります。Strong Parametersはいらなくなります。そもそもStrong Parametersがフォームクラスがなくモデルを代用しようとするとき出たセキュリティーの問題を対応するものだと私が理解しています。フォームクラスがあれば、入力パラメータからフォームのインスタンスを作って、そのまま使います。つまり
class ApiUserForm def self.create_from_params(params) form = ApiUserForm.new form.id = params[:id] # ... form end end
ということです。ここで属性が多くなると、なんらかの方法で、一括属性をセットしますが、それでもはStrong Parametersはいらないです。
もうひとつでいうと、データアクセスのクラスは基本データモデルを中心にやっているので、フォームインスタンスからデータモデルインスタンスに変換する必要があります。もちろん逆のパターンもあります。つまり
class ApiUserForm def to_api_user user = ApiUser.new # ... user end def self.from_api_user(user) form = ApiUserForm.new # ... form end end
ということです。いろいろデータコピーがあるようですが、コピー自体はフォームとデータモデルが別々にあるとう設計の結果です。もしコピーしないと、フォームの属性(Hashに変換するも同じ)はそのままAPIクライアントに渡して、APIクライアントのロジックを知らないといけないということになってしまいます。データモデルを経由するなら、データアクセスのクラスでなんらかの処理を加える(配列をCSVに変換するなど)ことも可能になります。メンテナンス性について、コピーのコードはすべてフォームクラスにまとめているので、管理しやすいです。
モデルではなく、フォームに書き換えると、モデルと同じ形でかきたい気持ちがあると思います。例えば xxx_form.save
ただ、せっかくデータアクセスクラスを作ったのに、また戻るのは難しいです。ゆえ、時間かかるかもしれませんが、 xxx_repository.save(xxx_form.to_data_model)
になれましょう。
xxx_form.save
に対してもしかすると、RailsのService Object?にしてもいいですかについて、フォームはドメインモデルでもないし、役割は入力のチェックのみなので、データアクセスの仕事はデータアクセスクラスに任せましょう。
モデルにあるcallbackのロジックはどうするかに関して、状況によって
- フォーム
- データアクセス
- API側
で実装します。特にAPI側というのは、データベースからAPIへ移行するにあたって、ビジネスロジックを移行する意味もあります。何もかも既存側でなんとかするより、API側も考えてみましょう。
いかがでしょうか。データベースからAPIへ移行するのは実際地味な作業ですが、明確かつシンプルなルールがあればすすめしやすいです。とくにRailsにおいて役割をちゃんと分けて実装すると、メンテナンスしやすいコードになると思います。
最後までご覧いただきありがとうございました。
ユニファでは一緒に働く仲間を募集しています!よかったらこちらのページもご覧ください。 unifa-e.com