らっちゃいブログ

日々の学びと気づきを発信するブログ

DjangoでDBのフィールドに選択肢を設定する

スポンサーリンク

概要

やりたいこととしては、特定のフィールドに指定できる値をEnum化するようなイメージです。

選択肢リストを設定する

フィールドに choices を指定することで選択肢を設定することができます。

choices の中身ですが『2要素のタプルからなる iterable(リストまたはタプル)』と定義されています。

つまりはこんな感じ。

GENDER_CHOICES = (
    (1, 'Male'),
    (2, 'Female'),
)
class Person(models.Model):
    gender = models.IntegerField(choices=GENDER_CHOICES)

見てわかる通り、一つ目の要素がDBに格納する値で二つ目が表示名です。

選択肢を利用するメリット/デメリット

メリットとしては、そのフィールドに格納され得るデータがプログラム上で定義できるため、追加変更が楽です。

また、以下のようにして設定値に対応する表示名を得ることができます。個人的にはこれがやりたくて使ってるところもあります。

person = Person(gender=1)
person.get_gender_display()
'Male'

デメリットですが、定義した選択肢の値が格納されることを強制できません。つまり設定した選択肢以外の値も保存され得ます。ですので、自分で入力チェックなりビジネスロジックを書いて意図しない値が入らないことを保証する必要があります。

MySQLENUM型でいいじゃん

という方もいそうなので、この話題にも言及しておきましょう。

ENUMを実際に使うとこんな感じでしょうか。

CREATE TABLE person (
  gender ENUM('Male', 'Female')
);

MySQLENUM型を使っても良いのですが、選択肢を増減するためにテーブル定義を変更する必要が出てくるため、運用を考えると使いたくないのです。また、DBに格納される値と同じEnum定数をプログラム上にも定義するケースが多く、定数の二重管理になりやすい点も無視できませんね。

ちなみにENUM型はSQLアンチパターン本でも扱われていますので、世間的にもよろしくないものと認知されていると思います。

SQLアンチパターン

SQLアンチパターン

SQLアンチパターンによると、ENUM型を使ってよいとされているのは、ON/OFFのように変更されることのない相互排他的な値を扱う場合だけとのことです。

今回例として挙げた性別はこれに該当するでしょうか?

いいえ、該当しません。依頼人の要望で『内緒』を保存したいということもあり得ます。もはや、ENUMは封印してしまうべきでしょう。

定数を管理するテーブルを作ればいいじゃん

ENUM を使わない場合は、以下のように参照テーブルを作る方法が一般的ですね。Django 以外を使うときは私もそうします。

CREATE TABLE gender (
  id VARCHAR(20),
  name VARCHAR(20)
);
INSERT INTO gender (key, name) VALUES (1, '男性'), (2, '女性');
 
CREATE TABLE person (
  gender_id INT(11),
  FOREIGN KEY (gender_id) REFERENCES gender(id) ON UPDATE CASCADE
);

確かにこれならやりたいことは実現できます。しかし、いつも思うのですが、表示名を取得するの面倒じゃないですか?わざわざ gender テーブルをジョインして name フィールドを引っ張ってくる必要がありますよね。それを避けるためには、結局プログラム上にIDと表示名を管理する Enum を定義することになり、定数の二重管理に逆戻りです。

結局、どうやってもプログラム上に Enum を定義することになるのですから、それだけで全部管理できるようにするのがベストだとは思いませんか?

本記事にて『入力チェックなりビジネスロジックを書いて意図しない値が入らないことを保証する必要がある』というデメリットをお伝えしましたが、そんなに難しいことではありませんよね。選択肢にない値を保存しようとしたらエラーにするだけで良いのですから、一か所に簡単なロジックを書くだけで済みます。

結論

ENUM型を使うのはやめてフィールドに選択肢を設定しよう!