Наследство с особенностями - 2

Я всё продолжаю по рабочим и не только нуждам ковырять наследование моделей, поэтому как и обещал - то ли ещё есть!

  • Очередной сюрприз ждал меня, когда я обновляя свой блог-движок под текущий транк. Я обнаружил сломанным блок "Архив" из-за того, что метод dates() перестал возвращать уникальные значения на месяца, а выдавал мне всё подряд. Я сильно удивился. Но пофиксил всё при помощи, в последнее время моего любимого, set. Да, костыль, но ковырять дальше пока желания нет.

  • Далее уже по рабочим нуждам обновления тест наборы для приложения, проявились некоторые особенности работы fixtures. Для примера, допустим у нас есть такие связанные наследованием модели:

    class Base( models.Model ):
       field1 = models.IntegerField( default = 10 )
       # + неявное поле id
    
    class Derived( Base ):
       field2 = models.IntegerField( default = 1 )
       # + неявное поле base_ptr
    
    1. Сериализатор дампит модель наследника целиком, т.е. со всеми полями в том числе и родителей.

    2. При загрузке модели родителей не могут подцепить свой первичный ключ, а просто делают новый INSERT, получая тем самым новый id. Потом этот id идет вверх по иерархии и затирает имеющиеся уже(загруженные из фиксчюры) значения. Похоже, что из-за этого ломается и CRUD.

    Так же это эффект проявляется когда вы просто хотите присоединить к уже имеющемуся базовому объекту какое-то дополнение в виде объекта отнаследованной модели. Сделать какими-то очевидными путями это не возможно. Первый вариант, который приходит на ум не срабатывает:

    >> Base.objects.count()
    1
    >> Derived.objects.count()
    0
    >> base = Base.objects.get()
    >> d = Derived.objects.create( base_ptr = base )
    >> Derived.objects.count()
    1
    >> Base.objects.count()
    2
    

    Вот так вот, мы получаем лишний объект предок. Не хорошо. Суть проблемы в том, что метод модели save_base, в который выродился метод save после qsfr, не может найти первичный ключ для предка и решает его "любезно" создать. Чего нам не надо.

    Думаем дальше. Ну хорошо дадим ему явно первичный ключ предка(ха! а что будет, если наследование множественное, а ключи у предков названы одинаково, но значения у них разные? Гоним эти мысли прочь, а то лишимся сна:)). Вариант:

    >> Base.objects.count()
    0
    >> Derived.objects.count()
    0
    >> base = Base.objects.create( field1 = 777 )
    >> d = Derived.objects.create( base_ptr = base, id = base.id )
    >> Derived.objects.count()
    1
    >> Base.objects.count()
    1
    >> Base.objects.get().field1
    10
    

    Поняли? Да, не найдя у объекта d атрибуты field1, его джанга взяла по умолчанию, затерев тем самым уже имеющееся значение 777.

    Собственно следующий трюк логическое продолжение предыдущего, но лишающий джангу радости подставлять дефолтные значения (если их нет, кстати, то выругается СУБД, если поле не null = True):

    base = Base.objects.create( field1 = 777 )
    d = Derived()
    d.__dict__.update( base.__dict__ )
    d.save()
    

    Вот этот вариант пока работает. Будем посмотреть.

  • При идеи полной прозрачности для клиентского кода, что объект модели наследника является объектом модели предка, то с удаление объекта наследника не всё очевидно. Если вызвать delete у объекта наследника, то удалиться только он, а предок останется. Пример:

    >> Derived.objects.count()
    1
    >> Base.objects.count()
    1
    >> d = Derived.objects.get()
    >> d.delete()
    >> Derived.objects.count()
    0
    >> Base.objects.count()
    1
    

    Во как! Если вам такое поведение не нужно, лечение достаточно оригинальное - переопределить метод delete у модели наследника в которой удалить своего предка. Иллюстрация:

    def delete( self ):
       self.base_ptr.delete()
    

    Из-за чего так. При удалении предка происходит стандартная попытка ORM джанги сохранить ссылочную целостность данный, что заставляет его искать присоединенные к этому объекты и их тоже тащить за собой в мир иной. А поскольку наследник и предок соединены неявный OneToOneField, удаляя предка мы автоматически удалим его наследника.

  • Так же можно отметить интересный факт найденный Владимиром Володиным.

  • Присоединяйтесь!

Вот такая очередная сводка с фронтов освоения текущего транка. Чувствую, что это не конец и немного начиню понимать тех кто говорит, что джанга пока не стабильна. Да временами бывает.

Комментарии 4

  1. drey написал:

    Не кажется ли, что в виду такой витееватости, появляется большое поле для ошибок? У тебя опыта работы с джангой и разработки веб-приложений намного больше моего, насколько велика необходимость использования наследования на практике? Стоит ли овчинка выделки?

    Оставлен 03 Июнь 2008 в 05:15
  2. Александр Кошелев написал:

    Я все таки думаю, что это не баг.

    Да, полностью согласен, это фича:) Просто наличие ссылки на объект предок сбивает с толку и кажется, что можно так сделать. Оказывается, не всё так просто.

    Оставлен 03 Июнь 2008 в 15:45
  3. Александр Кошелев написал:

    Не кажется ли, что в виду такой витееватости, появляется большое поле для ошибок? У тебя опыта работы с джангой и разработки веб-приложений намного больше моего, насколько велика необходимость использования наследования на практике? Стоит ли овчинка выделки?

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

    Оставлен 03 Июнь 2008 в 15:48
  4. drozzy написал:

    Privit, sorry ale po kirilics ne mozu drukuvati!

    Interesting topic. My question is why don't you use abstract class? Don't be too harsh on, I'm still new to Django. Here is what I do:

    
           class Page(models.Model):
      title = models.CharField(max_length=100)
      body = models.TextField(blank=True)
      priority = models.IntegerField(default=0,
    
      def __unicode__(self):
          return self.title
    
      class Meta:
            abstract = True
    
    class MainPage(Page):
      class Admin:
        list_display = ('title', 'priority')
      class Meta:
           ordering = ['priority']
          
    

    The problem I am having is having to set the admin/meta options repeatedly.

    Оставлен 05 Июнь 2008 в 07:56

Пингбеки 1

  1. От Небольшой Джанго обход | Oduvan's Web Blog 18 Апрель 2010 в 11:40

    И три небольших статьи Александра о наследовании.