TL;DR
- エンジニアが作業せずに仕様がコロコロ変わるモデルに対してどうするかを考えた
- その結果JSON/YAMLでシリアライザーを定義するdjango+restframework用のライブラリを作った
前置き
JSON/YAMLでdjangorestframeworkのシリアライザーを定義出来るライブラリを作成している。
まだREADME等のドキュメントを書いていないのでpypiには公開していないが、近日中に公開する予定である。
その前に実装方針となぜ作ったのかをドキュメントを書くために残しておこうと思った & せっかくだから公開することにした。
概要
仕様変更のたびにフィールドの追加や削除が頻繁に行われるモデルは、きっと扱いたいデータ自体がdjangoのモデルで表現するには無理のあるデータなのではないかとずっと考えていた。
しかし既成概念からはなかなか抜け出せず、その解決案をずっと模索していた。
例えば街でアンケートのようなものをとるWebアプリを作成したとする。最初は名前と年齢の2つのフィールドのみだとする。そして、読んでもらっている人の想像通り、新たなフィールドをパラパラと顧客が申し出る。年齢、性別、職種、趣味などがそれに当たる。
エンジニアはそのたびにモデルを変更を迫られ、マイグレーション作業を行わなければならない。
この問題を避けるにはどうしたら良いのか考察した結果、
- 入力項目の変更が多いモデルの場合は、入力用フィールドとモデルフィールドを対にして考えない
- エンジニア以外でも入力項目を変更可能にする
という2つを実現することができれば、ハッピーになれるのではないだろうか。
このドキュメントではdjango及びdjango restframeworkを用いて上記の二つを実現する方法を紹介する。
変更が多いデータの入力項目とモデルのフィールドを対にして考えない方法を考察する
djangoを利用している人のほとんどの人はモデルフォームを利用しているだろう。これにより、モデルクラスを定義さえすればWeb上の入力フォーム用の
入力フォームを同寺に用意することが出来る。もちろん、モデルのフィールドを追加すれば自動的にフォームのフィールドも追従してくれる(fields = "_all_" にした場合)
djangoでRESful APIを扱うためのrestframeworkにも同様の機能であるモデルシリアライザーが用意されている。これにより、APIの入出力のデータ構造をモデルから
作成することが出来る。
しかし、"入力項目の変更が多いモデルの場合は、入力用フィールドとモデルフィールドを対にして考えない"を実現する以上、モデルからフォーム/シリアライザーを定義してはならない。
そのためには手動でフォーム/シリアライザーを定義すれば良い。そして、フォーム/シリアライザーの入力結果を、モデルの単一のカラムに対してJSONやYAML,またはpickle化した状態で保存してしまえば良い。
以下に例を示す。
>>> from rest_framework import serializers
>>> import json
>>> class EnqueteSerializer(serializers.Serializer):
... name = serializers.CharField(required=True)
... age = serializers.IntegerField(required=True)
... sex = serializers.ChoiceField((("male", "男性", ("female", "女性", ))), required=True)
>>> enquete_serializer = EnqueteSerializer(data={"name": "testuser-1", "age": 36, "sex": "male"})
>>> enquete_serializer.is_valid()
>>> json.dumps(enquete_serializer.validated_data)
'{"name": "testuser-1", "age": 36, "sex": "male"}'
この結果をDBのひとつのカラムに保存してしまえば、入力用フィールドとモデルフィールドを対にすることなく扱うことが出来る。
また、変更があってもマイグレーション作業をする必要がなくなる。
しかしPythonのコードでシリアライザーを定義するということは、そのクラスを定義したファイルをサーバーにデプロイしなければならないため、エンジニアの苦労は変わらない。
次に、どうしたらエンジニアが一切作業をしなくてもシリアライザーの定義を変更出来るかを考える。
エンジニア以外でも入力項目を変更可能にする
通常、シリアライザーの定義はPythonのコードで記述する。それ故にエンジニア以外がシリアライザーを変更することは難しい。
ではシリアライザーをPythonのコードではなく、構造化されたテキスト、即ちYAMLやJSONで記述することができたらどうだろうか。
記述方法さえ指南すればエンジニア以外でもシリアライザーを変更することが出来るのではないだろうか。
次にいかにしてシリアライザーの定義をYAMLやJSONで記述するかを考察する。
YAMLでシリアライザー定義する方法を考察する
YAMLでシリアライザーを定義するには以下の三点を抑えれば定義することが出来る。
- シリアライザー名
- フィールド名
- フィールドタイプ
それに加え、以下も追加で考える
- 各フィールドのオプション(requiredやchoicesのリスト)
上記を踏まえてEnqueteSerializerをYAMLで表現すると、以下のようになる。
main:
name: EnqueteSerializer
fields:
- name: name
field: CharField
field_kwargs:
required: true
- name: age
field: IntegerField
field_kwargs:
required: true
- name: sex
field: ChoiceField
field_args:
- - - male
- 男性
- - female
- 女性
field_kwargs:
required: true
あとは、このYAMLを解析してシリアライザーを生成するコードを書けば良い。
しかしこれだけでは複雑なシリアライザーを作ることができない。つまりシリアライザーの中にシリアライザーを内包するものが作成できない。
HTMLのフォームはテキストインプットや、ラジオ、セレクト、ボタンといった情報を扱うフォームの要素がある。
しかし、情報をグルーピングして持つようなものは存在しない。
例えばSNSのグループのように、グループの中にユーザーを追加/削除/変更するような画面を作ろうとした場合、エンジニアは面倒な作業を強いられていた。また、実装したとしても画面遷移が避けられなかった。
昨今では複雑なフォームを作らず、UI/UXはJSのフレームワーク等に任せてデータのやり取りはRESTful APIでデータをやり取をする方法が主流である。
restframeworkにも複雑な構造のデータをやり取りするためのシリアライザーを定義する方法がある。
以下にグループとユーザーの関係を表したシリアライザーの例を示す。
>>> from rest_framework import serializers
>>> class PersonSerializer(serializers.Serializer):
... name = serializers.CharField(required=True)
... age = serializers.IntegerField(required=True)
... sex = serializers.ChoiceField(
... (("male", "男性",), ("female", "女性",),),
... required=True
... )
...
>>> class GroupSerializer(serializers.Serializer):
... group_name = serializers.CharField(required=True)
... persons = PersonSerializer(many=True)
...
>>> group = GroupSerializer(data={
... "group_name": "Friend group",
... "persons": [
... {"name": "USER AAA", "age": 10, "sex": "male"},
... {"name": "USER BBB", "age": 20, "sex": "female"},
... {"name": "USER CCC", "age": 20, "sex": "female"},
... ]
... })
>>> group.is_valid()
>>> group.validated_data
OrderedDict([('group_name', 'Friend group'),
('persons',
[OrderedDict([('name', 'USER AAA'),
('age', 10),
('sex', 'male')]),
OrderedDict([('name', 'USER BBB'),
('age', 20),
('sex', 'female')]),
OrderedDict([('name', 'USER CCC'),
('age', 20),
('sex', 'female')])])])
上記の例を見ると、GroupSerializerクラスはPersonSerializerを内包している。つまり、GroupSerializerはPersonalSerializerに依存している。
この依存関係をYAMLで表現するためには、mainのシリアライザーが依存するシリアライザーのリストを扱う"depending_serializers"の項目を用意し、その中にシリアライザーを定義していく。
つまり、ここではPersonSerializerを"depending_serializers"の項目の中に定義し、"main"の項目の中にGroupSerializerを定義する。
main:
name: GroupSerializer
fields:
- name: group_name
field: CharField
field_kwargs:
required: true
help_text: Please enter group name
- name: persons
field: PersonSerializer
field_kwargs:
many: true
depending_serializers:
- name: PersonSerializer
fields:
- name: name
field: CharField
field_kwargs:
required: true
- name: age
field: IntegerField
field_kwargs:
required: true
- name: sex
field: ChoiceField
field_args:
- - - male
- 男性
- - female
- 女性
field_kwargs:
required: true
これで複雑なシリアライザーを定義する事ができた。
しかし、このYAMLの定義をファイルに保存してしまうとサーバー上にデプロイしなくてはならない問題が残るため、エンジニアが手を動かさなければならない問題が残ってしまう。
次は、如何にしてデプロイを行わずシリアライザーの定義を作成/更新するかを考察する。
デプロイを行わずシリアライザーの定義を作成/更新する
シリアライザーの定義を如何にしてエンジニアが手を動かさず更新すればよいか。その答えはWebインターフェイスだ。本題材では土台としてdjangoを利用している。djangoにはすぐに利用可能なadmin画面が存在する。これを利用してシリアライザーの定義を書き込めるものを作れば良い。
Webインターフェイスであれば定義の記述ミスや記述後のプレビューなども出来るため、ますますエンジニア以外が定義を更新することに現実味を帯びてくる。
また、CodeMirror2等を利用したTextarea用のウィジェットを組み込めば、以下の様にエディタに色を付けることも可能だ。
以下に実装画面の例を示す。
下に記述されているのが、GroupSerializerの例である。上には、記述したシリアライザーの結果である。
まとめと公開
今回のドキュメントは実装方針と、どのように定義を扱うかに焦点を当てた。最終的にはこれを利用しやすくしたものをpypiに公開する予定である。
実装例及び最終的にpypiにて公開予定のライブラリのリポジトリは以下のURLを下記に示す。
https://github.com/salexkidd/restframework-definable-serializer
また、pypiに上げ次第、使い方の例を示すサンプルアプリケーションも公開する予定である。
追記
とある事情(というかpypiのUpload+削除ミス)により、パッケージ名を変更する必要がでてしまったのでタイトル及びURLを変更しました。