ラベル Django の投稿を表示しています。 すべての投稿を表示
ラベル Django の投稿を表示しています。 すべての投稿を表示

2009年12月26日土曜日

Django の ModelForm / ModelFormSet まとめ

ModelForm の扱いが分かってきたのでまとめ。

なぜ ModelForm を使うのか (普通の Form との違い)

普通のフォームはデータベースの構成とは独立しているので自由に入力フォームを作成することができる反面、データベースの構造を反映したフォームを作成する際には、モデルの記述とほぼ同じ内容をもう一度書き下す必要が出てくる。これは冗長であるし、またモデルの変更をフォームに反映しなければならない点で保守性が低い。
きれいに構成された Web アプリケーションであればデータの入出力はデータベース上での生成、変更、削除にほぼ対応しているわけで、そのようなシーンにおいて、モデルを反映した入力を実現するためにモデルから半自動的にフォームを生成するものが ModelForm である。

ModelForm の定義

モデルフォーム内のサブクラス "Meta" の属性として各種の定義を書く。
  • model 属性: フォームのもととなるモデル
  • fields 属性: モデルの中でこのフォームに含めたいフィールド
各々のフィールドがどのようなフォームで表示されるか (テキストなのかチェックボックスなのかプルダウンメニューなのか等々)は、モデルで指定したフィールドのタイプに基づいて適当なものが選択される。
とはいえ実際の運用においては表示の仕方を変えたいことも多々ある(最もよくあるのは hidden にしたい場合など)ので、これは変更することが可能。
これは Meta 内ではなく、モデルフォーム自身の属性として記述。

ModelFormSet

一度に複数のレコードを編集する場合、同じモデルフォームをそのまま複数並べるわけにはいかない。(同じ name を持つ input 要素が複数作成されて、submit されてもどれとどれを組み合わせるのか分からなくなるから。)
FormSet はこの問題を解決するためのもの。ひとまとめに扱う input 要素ごとに id (フォームセットの内部的なもの。HTML の id 属性ではない) を作成してフォームの name 属性を適宜変更、送信されたデータをきちんとグループ分けして一つ一つのフォームを切り分けてくれる。
ModelFormSet は、上述の Form と ModelForm の関係ように、Model を反映した FormSet。

ModelFormSet の定義

modelformset_factory に、ベースにしたいモデルを渡す。
フィールドの表示の種類を変更するためには、意図する表示を定義した ModelForm を予め定義しておき、引数に "form=モデルフォーム名" を追加してやればよい。
ここで、上述の引数として渡したモデルフォームにおいて、fields 属性が定義されていても、新たに作成される ModelFormSet はそれらを無視して全てのフィールドに関してフォームを作成する点に注意。
これを抑制するには、modelformset_factory の 引数に "fields=フィールド名リスト" を与える。

データ受信後の処理

POST でデータを受け取ってからは、送信されたデータをデータベースに書き込むことになる。
ここで、既存のデータを変更する場合には注意すべき点がある。
基本的に、ModelForm のデータを保存する場合はモデルフォームオブジェクトの save メソッドを利用する。
これは新規作成(SQL でいうと INSERT)でも上書き(SQL でいうと UPDATE)でも同じで、Django はそれらを自動的に使い分ける。
使い分けの基準は、"保存しようとしているデータの主キー値と同じ主キー値を持つレコードがあれば UPDATE、それ以外は INSERT"となっている。
よって、既存のデータを UPDATE するためには、必ず主キーをフォームの中に含めなければならない。
また、一部のフィールドのみを用いたモデルフォームの場合、除外したフィールドが必須であった場合、その値を与えなければ is_valid が False となり保存できない。
このような場合は、受信後にフォームインスタンスを作成する際に、POST データとともにもとのデータベースレコードを渡してやることで、フォームに含まれているフィールドはその値を、そうでないフィールドはデータベースの値を用いて、不足するデータを補ったフォームオブジェクトを作成することができる。
渡し方は、モデルフォームの場合は引数 instance、モデルフォームセットの場合は引数 queryset。

2009年12月22日火曜日

Django の分からないところ

調べてみたけど分からなかったことリスト。(随時更新)
知っている方、教えていただけると本当に助かります。
あるモデルの一部のフィールドのみ編集可能にしてそれ以外は表示だけさせたい

複数のフィールドを持つモデルの値のうち一部を書き換える場合。例えばこんなかんじで。オブジェクトとそれをインスタンスとして作ったフォームオブジェクトをコンテキストに突っ込めばテンプレートから参照はできるけれど、なんだか冗長だし、複数のインスタンスを一度に編集できるようにしたい場合などはテンプレートにリストが2つ渡って対応付けしたりとややこしくなる。
フォームオブジェクトから value を抜き出して表示することがテンプレートからできれば万事解決なんですが、そういう方法ってないのだろうか。

複合キーを扱いたい

とりあえず現時点では扱えないらしい?
複合キーにすべきフィールド以外に主キーをたてておく。
データモデリング的によろしくないが実用上は複合キーで検索してリレーションには主キーを使えばまあ問題ないか。
(追記) モデルオブジェクトの Meta 内で unique_together(フィールド名リスト) で指定可能だった。

ModelForm での is_validがよくわからない

何をチェックしているのかよくわからない。モデルで指定した「省略可能」 (blank=True) とか 「ヌル値許容」 (null=True) とかの条件にフォーム内データが合致しているか否かをチェックしているのだろうとは思うけれど、暗黙の主キーである id フィールドがフォームに入ってると問答無用で False を返される。なぜ?
追記: 主キーの値の重複が不正と見なされている。下に書いたように主キーを指定した場合は update になるので問題ないはずだが、なぜか invalid となっている。なぜ?
追記: ここに答えがあった。つまり
  1. 主キーは POST パラメータではなく URL で渡す
  2. そこから取得した主キーをもとにして既存レコードのインスタンスを取得する
  3. そのインスタンスと POST パラメータを一緒に渡してインスタンスを作り直す
が正解の模様。

Django は insert と update を勝手に呼び分ける

save が呼ばれたデータの Primary Key が既にデータベースに存在すれば update、なければ insert を実行する。Primary Key が Python 的に False である場合は update する。例えば Primary Key の値が入ってないデータは insert になるので、Primary Key 以外に unique 制約を持つデータなどは、既存データのつもりでうっかり Primary Key のフィールドを入れずに save したりすると insert されて unique 制約に引っかかる、とかありそう。

メモリリーク?

大量データ(300万レコードほど)をデータベースに突っ込むために Django のモデルをインポートして作成したスクリプトをコマンドラインで走らせているとどんどんメモリを食う。やっていることはレコードごとに関数を読んで、その中で新たにモデルのインスタンスをローカル変数に代入して save するというだけ。関数から抜けた後にどこかの時点で GC にかかることを期待していたのだけれど、どうもそうなってはくれない模様。