🗂

django-rest-frameworkのViewSet(APIView)で外部キー制約を設定していないカラムを結合する

2022/03/25に公開

はじめに

django-rest-frameworkのViewSet(get_querysetのオーバーライド)で外部キー制約を設定していないカラムを結合しようとした時に思ったより苦戦したのでその時のメモ。

サンプルコード

サンプルの方にはViewSet(APIView)を使ったサンプルも書いてあります(myapp/views.pyの下の方)
https://github.com/nelsia/django-join-sample

書いていること

  • djangoのJoin, ForeignObjectを使った結合
  • djangoのraw queryを使った結合(こちらの方法はViewSetでうまく使えなかった)

前提

  • django-rest-frameworkを使用しています
  • 以下の単純なテーブル構造を前提として書いています

テーブル構造

Recordテーブルのphone_numberにCustomerテーブルのcustomer_nameを結合

myapp/models.py
from django.db import models


class Record(models.Model):
  phone_number = models.CharField(max_length=15)


class Customer(models.Model):
  phone_number = models.CharField(max_length=15)
  customer_name = models.CharField(max_length=100)

実装方法

Join, ForeignObjectを使用した方法

myapp/views.py
from django.db.models.sql.datastructures import Join
from django.db.models.fields.related import ForeignObject
from django.db.models.options import Options

from rest_framework.decorators import api_view
from rest_framework.request import Request
from rest_framework.response import Response

from myapp.models import Record, Customer


@api_view(["GET"])
def get_join(request: Request) -> Response:
  # ForeignObjectのtoに結合したいテーブルを指定
  fo = ForeignObject(
    to=Customer,
    on_delete=lambda: x,
    from_fields=[None],
    to_fields=[None],
    rel=None,
    related_name=None
  )

  # 結合処理
  fo.opts = Options(Record._meta)
  fo.opts.model = Record
  fo.get_joining_columns = lambda: (("phone_number", "phone_number"), )

  jo = Join(
    table_name=Customer._meta.db_table,
    parent_alias=Record._meta.db_table,
    table_alias="T1",
    join_type="LEFT JOIN",
    join_field=fo,
    nullable=False # 結合したカラムがNullの時にレコードを削除するか
  )

  # phone_numberが0から始まるレコードを抽出(objects.allだと何故か動かない)
  q = Record.objects.filter(phone_number__startswith="0")

  q.query.join(jo)

  # extraで結合したテーブルのカラムを追加
  q = q.extra(
    select={
      "customer_name": "myapp_customer.customer_name",
    }
  ).values(
    "id", "phone_number", "customer_name" 
  )
  
  # レスポンス用のリストを作成
  res = [
    {
      "id": r["id"],
      "phone_number": r["phone_number"],
      "customer_name": r["customer_name"],
    }
    for r in q
  ]

  return Response(res)

raw queryを使用した方法

よく使う方法の1つですが、django-rest-frameworkのAPIViewで使う時にうまくいかなかった。

myapp/views.py
from rest_framework.decorators import api_view
from rest_framework.request import Request
from rest_framework.response import Response

from myapp.models import Record, Customer


@api_view(["GET"])
def get_raw_query(request: Request) -> Response:
  rec = Record.objects.raw("""
    SELECT
      r.id AS id,
      r.phone_number AS phone_number,
      c.customer_name AS customer_name
    FROM
      myapp_record AS r
    LEFT OUTER JOIN
      myapp_customer AS C
    ON r.phone_number = c.phone_number
    WHERE
      r.phone_number LIKE "0%"
  """)
  
  # レスポンス用のリストを作成
  res = [
    {
      "id": r.id,
      "phone_number": r.phone_number,
      "customer_name": r.customer_name
    }
    for r in rec
  ]

  return Response(res)

参考

https://stackoverflow.com/questions/21271835/left-join-django-orm

https://docs.djangoproject.com/en/4.0/ref/models/querysets/#extra

Discussion