bpmappersを使ってDjangoテンプレート上の記述を少しだけ簡単にする

DjangoでA<-B<-Cという方向で関係性を持ったモデルがあったとする。
データをいくつか作って、テンプレートで表示させる際にManyToManyフィールドがあると、途中の参照でManagerが入ってしまう。
その際にModelMapperを使って記述を少し簡単にしよう、という話。
まあ実際に使える場面は多くない。

myapp/models.py

C.b_listがManyToManyになるため、インスタンス化したc.b_listはManagerになる。

from django.db import models


class A(models.Model):
    name = models.CharField(max_length=10)

    def __unicode__(self):
        return self.name


class B(models.Model):
    name = models.CharField(max_length=10)
    a = models.ForeignKey(A, null=True, blank=True)

    def __unicode__(self):
        return self.name


class C(models.Model):
    name = models.CharField(max_length=10)
    b_list = models.ManyToManyField(B, blank=True)

    def __unicode__(self):
        return self.name

myapp/fixtures/initial_data.json

表示する際に使うデータはこんな感じ。

[
  {
    "pk": 1, 
    "model": "myapp.a", 
    "fields": {
      "name": "A1"
    }
  }, 
  {
    "pk": 2, 
    "model": "myapp.a", 
    "fields": {
      "name": "A2"
    }
  }, 
  {
    "pk": 3, 
    "model": "myapp.a", 
    "fields": {
      "name": "A3"
    }
  }, 
  {
    "pk": 1, 
    "model": "myapp.b", 
    "fields": {
      "a": 1, 
      "name": "B1"
    }
  }, 
  {
    "pk": 2, 
    "model": "myapp.b", 
    "fields": {
      "a": 2, 
      "name": "B2"
    }
  }, 
  {
    "pk": 3, 
    "model": "myapp.b", 
    "fields": {
      "a": 3, 
      "name": "B3"
    }
  }, 
  {
    "pk": 4, 
    "model": "myapp.b", 
    "fields": {
      "a": null, 
      "name": "B4"
    }
  }, 
  {
    "pk": 5, 
    "model": "myapp.b", 
    "fields": {
      "a": null, 
      "name": "B5"
    }
  }, 
  {
    "pk": 6, 
    "model": "myapp.b", 
    "fields": {
      "a": null, 
      "name": "B6"
    }
  }, 
  {
    "pk": 1, 
    "model": "myapp.c", 
    "fields": {
      "name": "C1", 
      "b_list": [
        1, 
        2
      ]
    }
  }, 
  {
    "pk": 2, 
    "model": "myapp.c", 
    "fields": {
      "name": "C2", 
      "b_list": []
    }
  }, 
  {
    "pk": 3, 
    "model": "myapp.c", 
    "fields": {
      "name": "C3", 
      "b_list": [
        3, 
        4, 
        5
      ]
    }
  }, 
  {
    "pk": 4, 
    "model": "myapp.c", 
    "fields": {
      "name": "C4", 
      "b_list": [
        6
      ]
    }
  }
]

myapp/mappers.py

ここで、bpmappers.djangomodelのModelMapperを使う。
CモデルからMapperクラスを作成。

from bpmappers.djangomodel import ModelMapper

from myapp.models import C


class MyMapper(ModelMapper):
    class Meta:
        model = C

myapp/views.py

Cモデルからいくつかのオブジェクトを取り出して、MyMapperでマッピングしてindex.htmlテンプレートに渡す。

from django.views.generic import TemplateView

from myapp.mappers import MyMapper
from myapp.models import C


class MyView(TemplateView):
    template_name = 'index.html'

    def get_context_data(self, **kwargs):
        context = super(MyView, self).get_context_data(**kwargs)
        context['data'] = [MyMapper(obj).as_dict() for obj in C.objects.all()[:5]]
        return context

ここで、Cモデルのマッピングした結果は {'b_list': [...], ...} のような辞書になり、b_listはリストになる。
また、マッピングの時点でallメソッドを呼んでいるので、ここでデータベースへのアクセスが発生する。

myapp/templates/index.html

テンプレート内では、Managerではなく、マッピング後の辞書を使ってデータを表示する。

<!DOCTYPE html>
<html>
<head>
  <meta http-equiv="Content-type" content="text/html; charset=UTF-8"/>
</head>
<body>
  <h1>bpmappers.djangomodel.ModelMapper</h1>
  <ul>
    {% for c in data %}
      <li>{{ c.name }}
        {% if c.b_list %}
          <ul>
          {% for b in c.b_list %}
            <li>{{ b.name }}</li>
            {% if b.a %}
              <ul>
                <li>{{ b.a.name }}</li>
              </ul>
            {% endif %}
          {% endfor %}
          </ul>
        {% endif %}
      </li>
    {% endfor %}
  </ul>
</body>
</html>

Mapperを使わない場合は、c.b_listのところがc.b_list.allのようになる。
テンプレート側にデータベースへの問い合わせの記述が入ってしまうと複雑になってくるので、こう書けるほうが少し簡単で嬉しい。

実行結果

$ python manage.py syncdb --noinput
$ python manage.py runserver

データの取得方法を変更する場合

c.b_list.allではなく、filterメソッドで絞込みたい場合、この例だとMapperクラスの記述だけで対応できる。
規模が大きい場合は、データの取得をそれだけでまとめてしまって、マッピングコードも分離したほうが良さそう。