разработчик с маленьким Django


— Стыдно признаться, но в нашей компании мы до сих пор используем Django…

Так начинали со мной разговор представители навороченных стендов российских конференций Pycon Russia 2021 и Moscow Python Conf++ 2021, где я выступал с докладами про Django.

Эдакий "coming out" без объяснений, почему это стыдно, и зачем в этом надо признаваться. Если уж «Все леди делают это» так давайте говорить об этом, как о чем-то нормальном! Я, например, рассказываю, как делать это в удовольствие и с естественными извращениями. Я про работу с Django, конечно, а вы, о чем подумали?

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

Что начинает делать разработчик Django-проекта, когда кажется, что необходимых инструментов нет? Качать тоннами ненужные батарейки с https://djangopackages.org/ или начинает рассказывать о том, как Django может привести к ментальным заболеваниям.

Во время вышеупомянутых конференций, на докладе про выбор подходящего python-фреймворка Django Admin упрекнули в отсутствии Nested-Inlines (inline в inline). Мол, больше десятилетия лежит Issue на djangoproject и разработчики не могут это поправить.

А что если Nested-Inlines в Django Admin был возможен с начальных версий? Подтвердить или опровергнуть мои слова смогут дочитавшие эту статью до конца. Остальным остается смириться: понимание работы Django с полным отсутствием нормальной документации требует особой любви к извращениям.

И, вот теперь, слайды:

Предположим, у Вас проект e-commerсe. В проекте есть модель Shop, каждый объект этой модели содержит объекты модели Product, которые, в свою очередь, могут иметь связь с несколькими объектами модели Images.

from django.db import models
from django.utils.translation import gettext_lazy as _

class Shop(models.Model):
    class Meta:
        verbose_name = _('Это модель магазина')

    title = models.CharField(verbose_name=_('это название магазина'), max_length=255)

class Product(models.Model):
    class Meta:
        verbose_name = _('Это модель продукта в магазине')

    title = models.CharField(verbose_name=_('это название продукта'), max_length=255)
    shop = models.ForeignKey(Shop, verbose_name=_('это ссылка на магазин'), on_delete=models.CASCADE)

class Image(models.Model):
    class Meta:
        verbose_name = _('Это картинка продукта')

    src = models.ImageField(verbose_name=_('это файл картинки'))
    product = models.ForeignKey(Product, verbose_name=_('это ссылка на продукт'), on_delete=models.CASCADE)

Давайте создадим администраторы моделей, для управления данными.

from django.contrib.admin.options import ModelAdmin

class ImageModelAdmin(ModelAdmin):
    fields = 'title',

class ProductModelAdmin(ModelAdmin):
    fields = 'title',

class ShopModelAdmin(ModelAdmin):
    fields = 'title',

Знающий Django разработчик на этом месте, скорее всего, воскликнет:

— Администраторы моделей не зарегистрированы!

И, скорее всего, это потому, что он просто пропустил информацию про авторегистрацию ModelAdmin, которая, как суслик, вроде его нет, а он есть.

Сразу сделаем через Model-Inline правку продуктов непосредственно на странице правки магазина, и добавление картинок на странице правки продукта.

from django.contrib.admin.options import ModelAdmin, TabularInline, StackedInline
from django.utils.translation import gettext_lazy as _
from .models import Image, Product

class ImageAdminInline(TabularInline):
    extra = 1
    model = Image

class ProductModelAdmin(ModelAdmin):
    inlines = ImageAdminInline,
    fields = 'title',

class ProductInline(StackedInline):
    extra = 1
    model = Product

class ShopModelAdmin(ModelAdmin):
    inlines = ProductInline,
    fields = 'title',

Если Вы вовремя остановились в своем проекте на этом этапе, то Вы точно имеете минимум одну проблему: ошибка сохранения объекта из формы правки, если объект одновременно правят несколько пользователей.

В примере выше — ошибка будет появляться при работе нескольких пользователей в ShopModelAdmin с одним и тем же объектом модели Shop.

Допустим, Вы решили эту проблему батарейкой для версионирования, поскольку о версионировании из Django-коробки вам никто не рассказал. Заодно Вы прикрутили сторонний модуль раздачи прав для доступа к отдельным объектам моделей, поскольку, вы и предположить не могли, что это тоже возможно в Django.

Все? Работа завершена? А не, показалось…
Завершенный Django-проект? Да не, бред какой-то

Задача размещения Любого Inline в любом месте формы правки объекта.


Ваш любимый менеджер приходит и спрашивает:
— А можно на форме правки продукта сначала показать Inline картинок, а только потом название продукта?
— Да, можно.

По — умолчанию в Django в форме правки объекта модели в Панели администраторов сначала стоят все поля редактируемого объекта, а потом идет блок инлайнов. Задачу вставки одного inline «куда хочется» на форму решается разными путями.
Первое сносное решение я встретил в 2015 году. Там было про переопределение шаблона формы правки 'admin/change_form.html' примерно так.

Почти «съедобное» решение я встретил позже в 2018 году у какого-то чеха, он делал вставку inline через дополнительные поля AdminForm с переопределением шаблона. Это выглядело проще и универсальнее, но еще не тянуло на «простое» решение на Django. Увы, без proof link.
А можно ли разместить любой Inline в любом месте формы администратора модели только силами Django с минимальным количеством кода? Возможно! Но это не точно…

В Django можно все. Но это не точно.

Вероятно, Вам знакома парадигма добавленного поля в ModelAdminForm. Вы объявляете несуществующее в модели поле в полях AdminModelForm, отмечаете его только для чтения. В момент рендера формы результат вызова одноименного метода ModelAdmin будет отображен на форме. Важно, что последовательность рендера ModelAdmin такова, что сначала создаются inline, потом рендерится форма, и, потом, рендерится блок inline. Этим мы и воспользуемся:

from django.contrib.admin.options import ModelAdmin, TabularInline
from django.utils.translation import gettext_lazy as _
from django.template.loader import get_template
from .models import Image

class ImageAdminInline(TabularInline):
    extra = 1
    model = Image

class ProductModelAdmin(ModelAdmin):
    inlines = ImageAdminInline,
    fields = 'image_inline', 'title',
    readonly_fields= 'image_inline',

    def image_inline(self, *args, **kwargs):
        context = getattr(self.response, 'context_data', None) or {} # somtimes context.copy() is better
        inline = context['inline_admin_formset'] = context['inline_admin_formsets'].pop(0)
        return get_template(inline.opts.template).render(context, self.request)

    def render_change_form(self, request, *args, **kwargs):
        self.request = request
        self.response = super().render_change_form(request, *args, **kwargs)
        return self.response

В момент рендера change_form будет вызван метод image_inline, который вынет из списка еще не обработанных inline_admin_formsets один inline_formset, и отрендерит его там, где хочется. Ниже change_form отрендерятся оставшиеся inline_admin_formsets, если они есть у ModelAdmin.

all inlines after admin change form vs inline into admin change form

Поскольку, мы исправили только рендер, то в остальных случаях все продолжит работать так, как и работало. Потому больше ничего не трогаем.

Знающий Django разработчик на этом месте воскликнет:

— Нельзя использовать self в методах ModelAdmin как контейнер для хранения данных!
Замечание принято. Действительно, в Django без доработок этот код приведет к появлению ошибок.
Кто не в курсе – для панелей администраторов Django хранит в реестре проинициализированные объекты классов ModelAdmin, работа всех пользователей обслуживается только этими объектами. Использовать эти объекты как контейнеры для пользовательских данных не получится. Тут Django полностью противоречит своей парадигме GCBV. Как это достаточно просто исправить, объяснено мною тут.
Все. Работа завершена. А, не, показалось…

Задача размещения Nested-Inline в формe правки объекта.


Все тот же ваш любимый менеджер приходит и спрашивает:
-А можно на форме добавления продуктов в магазин сразу показать Inline для картинок?
— Да. Можно.

Вы, конечно же, любите своих менеджеров и бежите закачивать Django-Nested-Inline. По пути Вы отмечаетесь на djangoproject.com в issue на добавление nested-inline. Вас вовсе не останавливает мысль о том, что проблему синхронной правки товаров в магазине никто не отменял, и теперь она начнет проявляться в геометрической прогрессии:

$\sum_{0}^{Nuser}err = N_{inlines}*N_{nested-inlines}*N_{managers}$

Эта угроза всегда раньше обходила вас стороной, только потому, что у вас не было Nested-Inline. Исправим это упущение.
Идея Nested-Inline базируется на предыдущей идее о размещении Inline в любом месте формы. Просто продолжим эту мысль дальше: Добавленное поле вставляем внутрь Admin inline, а в такое поле, как мы уже делали, вставляем еще inline, и получим автоматически какую-то херню Inline-в-Inline.

Давайте же сделаем это:

Создадим добавленное поле в инлайне продукта.

class ProductInline(StackedInline):
    model = Product
    fields = 'title', 'image_inline'
    readonly_fields = 'image_inline',
    extra = 1

    def image_inline(self, obj=None, *args, **kwargs):
        context = getattr(self.modeladmin.response, 'context_data', None) or {}
        admin_view = ProductModelAdmin(self.model, self.modeladmin.admin_site).add_view(self.modeladmin.request)
        inline = admin_view.context_data['inline_admin_formsets'][0]
        return get_template(inline.opts.template).render(context | {'inline_admin_formset': inline}, self.modeladmin.request)

В добавленное поле объекта ProductInline я вставил ImageInline, взятый из ProductModelAdmin.
Разумеется, это не конечный вариант, например, для существующего объекта надо вызывать change_view, и передавать ему object_id. Предлагаю вам подумать об этом на досуге.
Далее вызовем ShopModelAdmin с исправленным ProductInline. Заодно добавим необходимые данные для рендера nested inline (и не только) в добавленном поле image_inline.

class ShopModelAdmin(ModelAdmin):
    inlines = ProductInline,
    fields = 'title',

     def render_change_form(self, request, *args, **kwargs):
        self.request = request
        response = self.response = super().render_change_form(request, *args, **kwargs)
        return response

    def get_inline_instances(self, *args, **kwargs):
        yield from ((inline, vars(inline).update(modeladmin=self))[0] for inline in super().get_inline_instances(*args, **kwargs))

Запускаем. Как я и сообщал, получилась хрень.

кривой nested-inline в Django

Django-Core разработчики может и хороши в Python, но вот c Javascript функциями на старом jquery в панелях администраторов творится какая-то лютая дичь. Глубоко по-читаемый мной kesn отмечал, что:
нужно всего-то пожениться на джанговском javascript и всё там переписать.
Не, не надо. Это будет мезальянс. Просто поверьте мне на слово, что в файле inlines.js надо поменять всего три строки:

//  django\contrib\admin\static\admin\js\inlines.js
// row 335:
$(selector).stackedFormset(selector, inlineOptions.options);
// change to 
$(this).find("[id^=" + inlineOptions.name.substring(1) + "-]").stackedFormset(selector, inlineOptions.options);

// rows 338-339:
selector = inlineOptions.name + "-group .tabular.inline-related tbody:first > tr.form-row";
$(selector).tabularFormset(selector, inlineOptions.options);
// change to 
selector = inlineOptions.name + "-group .tabular.inline-related tbody:first > tr[id^=" + inlineOptions.name.substring(1) + "-].form-row ";
$(this).children().tabularFormset(selector, inlineOptions.options);

Увы, ребята в djangoproject ошибку inline.js не признают. Если Вы все сделали правильно, получите вот такую красоту:

рабочая версия Nested-Inline

Можете усложнить задачу и сделать больше уровней вложений. Рекомендую для глубоких вложенностей использовать TabularInline, а то StackedInline после второго вложения грустно выглядит.

Все. Работа завершена. А, не, показалось…

Задача сохранения данных


С одной стороны, я показал, как получить работающий масштабируемый рендер Inline-в-Inline.
С другой…. А вы не думали, как это все будет сохраняться?

Созданная нами только что проблема сохранения состоит из нескольких частей.

  • Для работы всей этой пидерсии на нужны правильные префиксы форм для вложенных инлайнов. Это просто решаемая задача.
  • ModelAdmin валидирует данные для главного объекта и для объектов первой вложенности. Валидацию для вложенных инлайнов надо делать отдельно. Это тоже решаемая задача, но чуть сложнее.
  • Обычное сохранение в панели администраторов подразумевает сохранение главного объекта и объектов первой вложенности. Вызов сохранения информации вложенных инлайнов надо делать отдельно. Это продолжение предыдущей задачи.
  • А вот как быть с тем, что множественная правка несколькими пользователями одновременно множества разнородных объектов приводит к множественным конфликтам состояний объектов? Это оставлю вам на размышление. Вы же Nested-inline хотели? Теперь мучайтесь.

Передача префикса


Выполняется через переопределение функции __init__ формы инлайна, где мы будем передавать prefix формы как префикс для вложенного формсета.

class MyForm(StackedInline.form):
    def __init__(self, *args, **kwargs):
        super(MyForm, self).__init__(*args, **kwargs)
        self.instance.form = self

class ProductInline(StackedInline):
    ......
    form = MyForm

    def image_inline(self, obj=None, *args, **kwargs):
        ......
        inline.formset.prefix = f'{inline.formset.prefix}-{obj.form.prefix}'
        return get_template(inline.opts.template).render(self.context, self.context['request'])

Чуть позже палки в колеса с префиксами опять начнет вставлять родной javascript. Пока, вам придется поверить на слово, что для первой работоспособной версии и так сойдет.

Валидация конечных данных


С валидацией придется повозиться. Вешается, она на ту же форму MyForm. Но есть большое НО! Если вызвать, метод add_view c request.POST — то данные этого вдоженного инлайна сохранятся, даже если вы этого не хотите. В Django modelAdmin вообще невозможно проверить, что данные валидны. Я сообщил об этом в очередном djangoproject issue, и был в очередной раз послан не понят. И, кстати, на этом моменте я понял, что формирование вложенного inline проще делать в MyForm:

class MyForm(StackedInline.form):
    ....
    def is_valid(self):
        return super().is_valid() and self.nested.formset.is_valid()

    @cached_property
    def nested(self):
        modeladmin = ProductModelAdmin(self._meta.model, self.modeladmin.admin_site)
        formsets, instances = modeladmin._create_formsets(self.modeladmin.request, self.instance, change=self.instance.pk)
        inline = modeladmin.get_inline_formsets(self.modeladmin.request, formsets[:1], instances[:1], self.instance)[0]
        inline.formset.prefix = f'{self.prefix}_{formsets[0].prefix}'.replace('-', '_')
        return inline

class ProductInline(StackedInline):
    ....
    def image_inline(self, obj=None, *args, **kwargs):
        context = getattr(self.modeladmin.response, 'context_data', None) or {}
        return get_template(obj.form.nested.opts.template).render(context | {'inline_admin_formset': obj.form.nested}, self.modeladmin.request)

    def get_formset(self, *args, **kwargs):
        formset = super().get_formset(*args, **kwargs)
        formset.form.modeladmin = self.modeladmin
        return formset


Сохранение вложенностей


image

Автосохранение вложенных инлайнов сделаем через переопределение метода save формы MyForm

class MyForm(StackedInline.form):
    def save(self, *args, **kwargs):
        return super().save(*args, **kwargs) or self.nested.formset.save(*args, **kwargs)

Вроде сохранилось:

image

Краткая сводка моих действий:


  1. Создал приложение с тремя связанными классами и администраторы этих моделей с Inline.
  2. Встроил добавленное поле в ChangeForm для вставки туда Inline.
  3. Встроил добавленное поле в Inline.
  4. На примере вставки Inline в добавленное поле ChangeForm, я вставил в добавленное поле Inline еще один inline, полученный из администратора другой модели.
  5. Решил вопрос генерации префиксов форм из inline, вложенных в inline
  6. Решил вопрос валидации данных форм из inline, вложенных в inline
  7. Сохранил данные форм из inline, вложенных в inline
  8. Как смог, поправил inlines.js

Что не решено:

  1. inlines.js не верно создает кнопки удаления для tabularinline форм, если extra != 0
  2. inlines.js криво работает с префиксами вложенных инлайнов, у новосозданных родительских инлайнов.
  3. inlines.js не учитывает extra для вложенных inline у новосозданных родительских инлайнов.
  4. Не обработан случай, если метод nested формы MyForm вернет что-то другое.

Упомянутые в статье возможности Django


  • Авторегистрация ModelAdmin
  • Версионирование состояний объектов Django-моделей
  • Управление доступом к объектам Django-моделей

Итоговое решение заняло около 20 строк кода на Django 4.0, масштабируется и не использует сторонние библиотеки. Смотрите в репозитории, пробуйте. Буду рад, если подскажете, что я упустил.

Выводы


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

P.S. Большой дисклеймер о том, что все персонажи из статьи являются вымышленными, и любое совпадение с реально живущими или жившими людьми не случайно.

P.P.S. Предложенная идея работает в моих проектах уже долгое время, я благодарю моего коллегу Павла П., который является тестером всех моих сумасшедших идей и участвовал в доработке этой.


«Django — это наше все» лозунг команды разработчиков, где я уже 7 лет занимаюсь обслуживанием и обновлением Django проектов. Если в Вашем Django-проекте есть нестандартные или сложные задачи — давайте попробуем решить их вместе.

Комментарии (22)


  1. baldr
    02.03.2022 11:04
    +2

    — Стыдно признаться, но в нашей компании мы до сих пор используем Django…

    Не Django, а "Django admin". Сам джанго сделан достаточно гибко и прозрачно, чтобы можно было на нем сделать что угодно.

    А вот админка - это уже попытка сделать красивый универсальный CRUD, но там разработчики перехитрили сами себя. За последние 10 лет я не видел ни одного проекта, в котором используется django-admin, который бы не был испещрен хитрыми хаками и попытками дописать какую-то хотя бы маленькую фичу. Типа добавления кнопочки, но не куда угодно, а в правый верхний угол. И красного цвета.

    Это ладно если просто кнопочка, но ведь люди целый продукты только на админке делают. Вот там - реально кровь из глаз.. Понятно что начали с простой задачи, которая укладывалась в возможности админки. Потом добавили кнопочку. Потом формочку, потом немного жабаскрипта, а потом понеслась....

    Это лично мое мнение - django-admin проще заменить самописной админкой, но удобной для конкретного бизнеса. Всегда. Если у человека достаточно опыта чтобы знать о всех (скрытых!) фичах админки - у него уже достаточно опыта, чтобы самому за несколько дней написать нужные вьюшки и темплейты с бутстрапом. И потом всем же легче поддерживать.


    1. kalombo
      02.03.2022 12:56
      +1

      А вот админка - это уже попытка сделать красивый универсальный CRUD, но там разработчики перехитрили сами себя.

      Мне кажется, перехитрили себя те, кто думают, что админка Django - это универсальный CRUD. Т.к. ничего в мире универсального быть не может, везде есть плюсы и минусы. Точно также как не может быть универсального совета использовать дефолтную админку или самому писать. Если вам хватает дефолтной админки, то зачем делать свою? Например, вы добавили некоторые модели с несложными конфигами, чтобы чуть упростить себе жизнь и не ходить в бд, а смотреть в веб-интерфейсе. Если же вы начинаете решать проблемы, как автор статьи, разбираться в шаблонах, заменять скрипты, делать какие-то сложные сохранения, то это повод задуматься, а не быстрее было бы написать что-то своё?


      1. hardtop
        03.03.2022 10:37

        Проблема в том, что не всегда понятно, когда остановиться. Ведь для 80% моделей достаточно штатной админки. А вот остальные 20% заставляют выдумывать костыли.

        Написать свою можно. Я раз 5 так делал. Сложно развивать.


        1. kalombo
          03.03.2022 10:54
          +1

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


  1. kostnikolas
    02.03.2022 11:55
    +4

    Я что-то пропустил? Почему начали стыдиться Django?


  1. anonymous
    00.00.0000 00:00


    1. danilovmy Автор
      02.03.2022 12:29

      не думаю, что этот комментарий относится к моей статье.


  1. gvtret
    02.03.2022 13:17

    И вот это вы выставляете на показ?! Сочувствие, только сочувствие...


    1. danilovmy Автор
      02.03.2022 13:22
      +2

      Спасибо за сочувствие. Согласен, что хороший дизайнер нам не помешает. Хорошо, что на знания фреймворка Django это не влияет.


      1. gvtret
        02.03.2022 14:51
        +1

        Желаю хорошего фронтендера Вам в команду))


      1. gvtret
        02.03.2022 14:58

        Просто, как это.... "Встечают по одежке". Вроде так.


  1. kesn
    02.03.2022 15:39
    +1

    Ох... Как будто порнуху для джангистов посмотрел :D

    Респект Максу, сам бы я плюнул на эту затею где-то на середине


    1. gvtret
      02.03.2022 16:06

      Порнуха - это полный разбор

      exclude()

      values()

      values_list()

      select_related()

      order_by()

      exists()

      count()

      first() and last()

      in_bulk()

      explain()

      latest()

      earliest()

      Вот это хардкор с извращениями)))


      1. danilovmy Автор
        02.03.2022 16:54
        +1

        Псс чел, есть у меня для тебя порево! Решение "MySQL delete duplicate records but keep the latest" через один запрос без .extra() .group_by() и подзапросов но с созданием объектов Join(), WhereNode(), и ExtraWhere(), не обещаю полный разбор, но пофапать есть на что.

        Кстати, .get() я уже расписывал, и что ошибка там была до Dj3.01


        1. gvtret
          02.03.2022 16:56
          +1

          Ах ты ж, шельма!!! Почти кончил)))


  1. trueMoRoZ
    02.03.2022 16:39
    +1

    А не является ли извращением в эпоху реактивного фронта рендерить шаблоны на бэке? Интересно мнение общественности.


    1. gvtret
      02.03.2022 16:52

      А про рендеринг инкто и не говорит. Вопрос в подготовке данных))


    1. baldr
      02.03.2022 16:55
      +4

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

      Аргументом приводить скорость - тоже пфф - при открытии половины сайтов на телефоне он начинает греться и так медленно считать байты в жаваскрипте - что проще было бы отправить лишний запрос на сервер.


    1. danilovmy Автор
      02.03.2022 17:02
      +2

      Ты не поверишь.... React Server-Side Rendering (SSR), Server-Side Rendering (SSR) | Vue.js, Server-side rendering (SSR) with Angular

      Кстати мы используем SSR для Vue.js, и у нас, неожиданно, эту задачу выполняет тоже Django :).


    1. hardtop
      03.03.2022 11:40
      +2

      Вполне актуально. Быстро грузится, не нагружает браузер и писать просто. Нужен на странице условный калькулятор - берём vue и делаем <div id="app">

      Не понимаю всеобщего стремления делать абсолютно всё на условном react. Пугает JSX - писать html в коде - я на такое в эпоху раннего php насмотрелся.


  1. hardtop
    03.03.2022 10:21
    +2

    Очень крутая статья, пропитанная опытом (и местами, сарказмом). С nested-forms как-то попал в глупую ситуацию. Сайт собирался из блоков (заголовки, тексты, тексты+картинки, галереи, faq и пр.). И для большой страницы в админке получалось 100+ input, textarea, fileupload. Мало того, что это ужасно смотрится и в каше инпутов сложно разобраться, так ещё это начинает сильно тормозить.

    Частично решил проблему полем с кнопкой, которая открывает popup. Кликнул, показалась inline-form, загрузил 20 картинок к товару и всё. Но, это можно сделать только когда есть id самой карточки товара (для привязки). Приходится заводить карточку без картинок, сохранять, и только потом вызывать popup с id, чтобы загрузить картинки.


    1. danilovmy Автор
      03.03.2022 12:18
      +1

      Один вариант: Складывать все в одну форму, даже формсеты, и потом отправлять все. на обработке post - сначала обработать данные по обьекту, получишь id, потом обработать данные по формсету. Так же делает админка.

      Второй вариант: особенно если отправляешь запросы на обновление через ajax, например. то формсеты складывать в сессию, и после сейва родительского объекта проверить, что в сессии есть доп обьекты на сейв.