Как денормализовать иерархические данные модели Django для вывода в CSV или Excel с помощью Djange REST Framework?

Предположим, мы создаем приложение адресной книги с помощью Django REST Framework и хотим вывести конечную точку, которая экспортирует всех людей. У каждого человека может быть один или несколько телефонных номеров.

Примерные данные могут выглядеть следующим образом:

[
  {
    'name': 'Jon Doe',
    'phone': 
    [
      {
        'type': 'home',
        'number': '+1 234 5678'
      }
    ]
  },
  {
    'name': 'Jane Doe',
    'phone':
    [
      {
        'type': 'home',
        'number': '+2 345 6789'
      },
      {
        'type': 'work',
        'number': '+3 456 7890'
      }
    ]
  }
]   

Поскольку мы хотим экспортировать CSV или таблицы Excel, мы хотим денормализовать данные так, чтобы каждый телефонный номер получил свою собственную строку.

Результат может выглядеть следующим образом:

name,phone.type,phone.number
Jon Doe,home,+1 234 5678
Jane Doe,home,+2 345 6789
Jane Doe,work,+3 456 7890

Вопрос в том, где именно я должен провести денормализацию. Я вижу два варианта:

  1. Напишите пользовательский Serializer, который выполняет денормализацию. Положительным моментом будет то, что это приведет к единому изменению, которое будет работать для каждого Renderer, так что я смогу экспортировать CSV и Excel с помощью, например, djangorestframework-csv и drf-renderer-xlsx. С другой стороны, это будет мешать рендерингу, который не выигрывает от денормализации, например JSON или XML.
  2. Выделите каждый Renderer, нуждающийся в денормализации, и переопределите метод process_data(), чтобы сначала провести денормализацию, а затем вызвать реализацию суперкласса.
  3. Напишите пользовательский View, который выполняет денормализацию на основе согласованного рендерера, как описано в https://www.django-rest-framework.org/api-guide/renderers/#varying-behavior-by-media-type.

Это похоже на проблему, которая может возникнуть у многих людей, поскольку экспорт табличных данных является очень распространенной функцией. Не подскажете, с чего мне начать или что будет лучшей альтернативой?

Предполагая, что у вас есть модель Contact (замените это на любую другую модель, которая у вас есть), с помощью Pandas вы можете вернуть CSV-файл из Django ORM QuerySet.

import pandas as pd

from .models import Contact

def export_contacts(self, *args, **kwargs):
    queryset = Contact.objects.all()
    df = pd.DataFrame(list(queryset))
    
    return df.to_csv()

Вы можете добавить это в отдельный модуль, который можно вызывать прямо из представления, нового представления для этого или любого другого места, где вам это нужно.

Предположим, что у вас есть такие модели, как:

class Person(models.Model):
    name = models.CharField(max_length=100)
    def __str__(self):
        return self.name

class Phone(models.Model):
    person = models.ForeignKey(Person, on_delete=models.CASCADE)
    type = models.CharField(max_length=100)
    number = models.CharField(max_length=100)
    def __str__(self):
        return self.number

Затем создайте сериализатор и набор представлений для Phone (а не Person) следующим образом:

class PhoneSerializer(serializers.HyperlinkedModelSerializer):
    person = serializers.StringRelatedField()
    class Meta:
        model = Phone
        fields = ['person', 'type', 'number']

class PhoneViewset(viewsets.ModelViewSet):
    queryset = Phone.objects.all()
    serializer_class = PhoneSerializer

router = routers.DefaultRouter()
router.register(r'phone', PhoneViewset)

Тогда DRF создаст нечто подобное с помощью своего стандартного рендерера json:

[
    {"person":"Mufune Toshirō","type":"home","number":"000-123-4567"},
    {"person":"Mufune Toshirō","type":"work","number":"000-345-6789"},
    {"person":"Tōno Eijirō","type":"home","number":"000-234-4567"},
    {"person":"Nakadai Tatsuya","type":"home","number":"000-234-6789"},
    {"person":"Tsukasa Yōko","type":"cell","number":"000-987-6543"}
]

Для получения CSV-вывода можно установить djangorestframework-csv. Это превратит вышеописанное в следующее:

number,person,type
000-123-4567,Mufune Toshirō,home
000-345-6789,Mufune Toshirō,work
000-234-4567,Tōno Eijirō,home
000-234-6789,Nakadai Tatsuya,home
000-987-6543,Tsukasa Yōko,cell

В реальной базе данных с большим количеством таблиц и сложными отношениями я бы создал представление базы данных из запроса, содержащего все поля, денормализованные так, как я хочу. Затем я бы создал неуправляемую модель (что-то вроде этого), чтобы ее можно было экспортировать из DRF.

В приведенном выше примере вид может быть таким:

CREATE VIEW phonelist AS
    SELECT a.name, b.type, b.number  
    FROM Person a left join Phone b on (a.id = b.person);
Вернуться на верх