概要
Modelの一覧を表示させたい時にFKと繋がってる一部Fieldも合わせて表示させたい場面では
to_representation
を使うと楽に実装できるので解説していきます
to_representaionとは?
BaseSerilaizerクラスのメソッドです
処理の流れをざっくりと解説すると以下のようになります
- retというOrderedDict型の変数を定義
- fields内にSerializer自身のFieldを代入
- For文で一つずつinstance(Serilaizerで定義したModelのinstance)からfieldの値をattribute(属性)に代入する(例:fieldがnameならModelのinstanceのnameをattributeに代入)
- If文でattributeがPKのみのObjectだったらPKをそのまま代入し、違ったらattributeをcheck_for_noneに代入
- If文でcheck_for_noneがNone(attributeがnone、つまりinstanceに該当するfieldがなかったら)の時ret[field.field_name]にNoneが入る。Noneではないのであればattributeの値が入る
class Serializer(BaseSerializer, metaclass=SerializerMetaclass):
def to_representation(self, instance):
"""
Object instance -> Dict of primitive datatypes.
"""
ret = OrderedDict()
fields = self._readable_fields
for field in fields:
try:
attribute = field.get_attribute(instance)
except SkipField:
continue
# We skip `to_representation` for `None` values so that fields do
# not have to explicitly deal with that case.
#
# For related fields with `use_pk_only_optimization` we need to
# resolve the pk value.
check_for_none = attribute.pk if isinstance(attribute, PKOnlyObject) else attribute
if check_for_none is None:
ret[field.field_name] = None
else:
ret[field.field_name] = field.to_representation(attribute)
return ret
実際に使ってみよう
ModelとSerilaizerを用意して実際の挙動を確かめてみましょう
Model
今回使うModelでは
CustomerとWorkplaceが1対1、CustomerとOrderが1対多の関係になります
# お客様
class Customer(models.Model):
id = models.UUIDField(primary_key=True, default=uuid.uuid4, editable=False)
kana = models.CharField(max_length=50)
name = models.CharField(max_length=50)
age = models.IntegerField()
post_no = models.CharField(
max_length=7, validators=[RegexValidator(r"^[0-9]{7}$", "7桁の数字を入力してください。")]
)
created_at = models.DateTimeField(auto_now_add=True)
class Meta:
db_table = "Customer"
# 勤務先
class Workplace(models.Model):
id = models.UUIDField(primary_key=True, default=uuid.uuid4, editable=False)
customer = models.OneToOneField(Customer, on_delete=models.CASCADE,related_name="workplace")
kana = models.CharField(max_length=255)
name = models.CharField(max_length=255)
phone_no = models.CharField(
max_length=11,
validators=[RegexValidator(r"^[0-9]{10,11}$","10か11桁の数字を入力してください。")],
)
created_at = models.DateTimeField(auto_now_add=True)
class Meta:
db_table = "Workplace"
# 注文
class Order(models.Model):
customer = models.ForeignKey(Customer, on_delete=models.CASCADE,related_name="order")
order_no = models.CharField(
max_length=8,
validators=[RegexValidator(r"^[0-9]{8}$","8桁の数字を入力してください。")],
unique=True
)
count = models.SmallIntegerField()
created_at = models.DateTimeField(auto_now_add=True)
class Meta:
db_table = "Order"
related_name
WorkplaceとOrderのModelはcustomerのFKからCustomerのModelを参照できますが、逆にCustomerのModelからWorkplaceとOrderのModelを参照(逆参照)できません
このようにrelated_nameを付けることでCustomerのModelからFKを持つ関連性のあるModelをto_representationを使う際に逆参照できるようになります
ただし、related_nameの名前が被るとどのModelを逆参照できるかDjango側がわからなくなるので注意が必要です
customer = models.OneToOneField(Customer, on_delete=models.CASCADE,related_name="workplace")
customer = models.ForeignKey(Customer, on_delete=models.CASCADE,related_name="order")
Serilaizer
CustomerのModel内にある
- お客様ID
- お客様名
に加え、勤務先と商品番号はそれぞれWorkplace、ItemのModel内にある
- 勤務先名
- 注文番号
をto_representationをオーバーライドすることで表示させます
class CustomerSerializer(serializers.ModelSerializer):
class Meta:
model = Customer
fields = ["id","name"]
def to_representation(self, instance):
rep = super(CustomerSerializer, self).to_representation(instance)
# 勤務先名
workplace = instance.workplace.name
# 商品番号
order = instance.order.latest("created_at")
rep["workplace"] = workplace
rep["order_no"] = order.order_no
return rep
instanceって何?
Serializerを使う際に参照するModelのinstanceにあたります
今回の例ではCustomerのModelを参照しているのでCustomerのinstanceです
print(type(instance)) # <class '<アプリケーション名>.models.Customer'>
print(instance) # <Customer: 田中一郎>
super()って何?
super()とはPythonでbuilt-in関数で
rep = super(CustomerSerializer, self).to_representation(instance)
のようにretに代入することでto_representationの定義でreturnされる戻り値のOrderDict型のretが代入されます
こうすることで
rep["workplace"] = workplace
のように任意のkeyとvalueを定義できます
OneToOneのModelを逆参照する時
CustomerとWorkplaceは1対1の関係です。instance.workplaceの後にworkplaceのfieldを後ろに付けることで
# workplace変数にWorkplaceのnameのfieldの値が入る
workplace = instance.workplace.name
rep["workplace"] = workplace
のようにkeyとvalueが定義されます
('workplace', '勤務先01')
OneToManyのModelを逆参照する時
CustomerとOrderは1対1の関係です。
前述のinstance.workplaceはWorkplace型のinstanceです
print(type(instance.workplace)) # <class '<アプリケーション名>.models.Workplace'>
print(type(instance.workplace.name)) # <class 'str'>
それに対してinstance.orderはRelatedManager型です
print(type(instance.order)) # <class 'django.db.models.fields.related_descriptors.create_reverse_many_to_one_manager.<locals>.RelatedManager'>
Customerに対応するOrderが複数あり、このままではWorkplaceのようにkeyとvalueの関係にならないため、一つのインスタンスにする必要があります
そこで今回は
order = instance.order.latest("created_at")
rep["order_no"] = order.order_no
のようにcreated_atの日付が最も新しいinstanceだけを抽出します
print(type(instance.order.latest("created_at"))) # <class '<アプリケーション名>.models.Order'>
print(type(instance.order.latest("created_at").order_no)) # <class 'str'>
orderも同様にkeyとvalueが定義されます
('order_no', '00011122')
retに何が入ってるの?
fieldsで定義したid,nameのkeyとvalueに加えて自身で定義したretのkeyとvalue(今回だとworkplaceとorder_no)が入っています
print(rep)
# OrderedDict([('id', '00000000-0000-0000-0...0000000001'), ('name', '田中一郎'), ('workplace', '勤務先01'), ('order_no', '00011122')])
Swaggerで見てみよう
Swaggerを使ってみたい方はこちらの記事を参考にしてみてください
トラブルシューティング
raise self.RelatedObjectDoesNotExist
逆参照先のテーブル内にデータがない場合に起こるエラーなのでSQLのInsert文かFixtureを使いましょう
Fixtureの使い方については以下の記事に記載してますので興味があれば読んでみてください
まとめ
なんとなく使っていてよくわからなかったto_representationですがこうやって定義ファイルをデバッグしながらやるとかなり理解が深まりました
記事の紹介
記事を書くにあたってコンテナ環境でVSCodeのリモートデバッグを使いながら検証しています
読むより実際に自分でデバッグしてみた方が理解が深まるのは間違いないのでよかったら読んでみてください
参考