django-datatables-viewによるServer-side processingで、モデルの外部キーの項目を表示する

バックエンドがDjangoの環境にて、jQuery DataTable を使う機会がありました。
DataTables | Table plug-in for jQuery

データ量がそれなりにあったので、DataTableのServer Side Processingを使いました。
DataTables example - Server-side processing

DjangoでServer Side Processingする場合は、ライブラリ django-datatables-view を使えば良さそうでした*1

 
ただ、 django-datatables-view にて、モデルの外部キーの項目を表示する時に少々詰まったので、メモを残します。

 

目次

 

環境

なお、サンプルの見栄えを良くするため、Bootstrap 4.5.2 も使っています。

 

やりたいこと

以下の通り、3つのモデルがあるとします。

class Family(models.Model):
    """ 科 """
    name = models.CharField('名前', max_length=50)


class Species(models.Model):
    """ 種 """
    family = models.ForeignKey('fk_app.Family', on_delete=models.CASCADE)
    name = models.CharField('名前', max_length=50)


class Cultivars(models.Model):
    """ 品種 """
    species = models.ForeignKey('fk_app.Species', on_delete=models.CASCADE)
    name = models.CharField('名前', max_length=50)

 
これらのモデルのうち、 Cultivarsdjango-datatables-viewmodel に指定し、こんな感じで表示したいとします。

注意点としては

  • は、品種から見て外部キーの外部キー
  • は、品種から見て外部キー

です。

f:id:thinkAmi:20201008193925p:plain

 

対応

通常の項目である idname

class CultivarsDataTableView(BaseDatatableView):
    model = Cultivars

    columns = [
        'id',
        'name',
    ]

と、 column に列名をそのまま指定できます。

一方、外部キーの項目については、そのまま species を指定しても、うまく表示できません。

f:id:thinkAmi:20201008204259p:plain:w400

 

そのため、Django側・JavaScript側の両方で対応が必要になります。

 

Django
render_column() のオーバーライド

表示ができていないので、render_column()をオーバーライドし、外部キー列が指定された場合は、その name を表示するようにします。

class CultivarsDataTableView(BaseDatatableView):
    model = Cultivars

    columns = [
        'id',
        'family',
        'species',
        'name',
    ]

    def render_column(self, row, col):
        # 列 familyが来たら、外部キーの外部キーのnameを返す 
        if col == 'family':
            return row.species.family.name

        if col == 'species':
            return row.species.name

 
CultivarsDataTableViewの columns に合わせ、JavaScriptcolumnDefs を定義します。

columnDefs: [
  {targets: 0, data: 'id'},
  {targets: 1, data: 'family'},
  {targets: 2, data: 'species'},
  {targets: 3, data: 'name'},
]

 
また、DataTableを表示するHTMLテンプレートのtableタグは

    <table id="demo" class="table table-striped table-bordered dataTable" style="width:100%">
      <thead>
      <tr>
        <th>ID</th>
        <th></th>
        <th></th>
        <th>品種</th>
      </tr>
      </thead>
    </table>

とします。

 
この場合、画面はうまく表示されます。

しかし、外部キーの外部キーの列 (ここでは ) でソートしようとすると、エラーとなります。

f:id:thinkAmi:20201008213420p:plain:w400

 
Djangoの実行ログにも以下が記録されています。

raise FieldError("Cannot resolve keyword '%s' into field. "
django.core.exceptions.FieldError: Cannot resolve keyword 'family' into field. Choices are: id, name, species, species_id

 

columnsの定義変更

エラーで落ちている ordering メソッドのソースコードを見ると、columnsなどで定義した列名をDjangoのQuerySetのorder_byメソッドに渡しているようです。

そのため、order_byで解釈できる列として定義してあげれば良さそうでした。

そこで、Django側を

class CultivarsDataTableView(BaseDatatableView):
    model = Cultivars

    columns = [
        'id',
        # order_byで解釈できるように定義
        'species__family__name',
        'species__name',
        'name',
    ]

    def render_column(self, row, col):
        # 列名が変わったので、render_columnメソッドのif文も変更
        if col == 'species__family__name':
            return row.species.family.name

        if col == 'species__name':
            return row.species.name

        return super().render_column(row, col)

とします。

また、columnsの定義が変わったので、JavaScript側の columnDefs

columnDefs: [
  {targets: 0, data: 'id'},
  {targets: 1, data: 'species__family__name'},
  {targets: 2, data: 'species__name'},
  {targets: 3, data: 'name'},
]

のように変更します。

 
その結果、 列をクリックしてもエラーが起きなくなりました。

 

まとめ
  • Django
    • 外部キーの属性を表示したい場合は、columns__ で結合して定義する
    • 上記の名前で描画できるよう、 render_column() メソッドをオーバーライドし、外部キーの該当の値を指定する
  • JavaScript
    • columnDefsの dataキーに、 Djangocolumns で定義した名前を指定する

 

ソースコード

GitHubに上げました。 fk_app が今回のアプリです。
https://github.com/thinkAmi-sandbox/django-datatables-view-sample

*1:ライブラリを使わずに実装したことがある同僚の @qtatsu いわく、これはとても便利とのことです