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

И так, некоторые время назад состоялось моё погружение в новый для джанго мир наследования моделей. Как и в моих домашних проектах и так и в рабочих. Исследованию подверглось мульти-табличное наследование. Среди достаточного количества удобных и полезных свойств наследования преподнесло несколько неожиданностей и даже ошибок, которые были обнаружены в механизме самой джанги.

Начнем с особенностей:

  • Кастомные менеджеры наследуются. Что вполне логично, если учесть, что менеджер это всего-лишь обычное поле класса, которое при наследовании переходит к наследнику. Но, что не очень очевидно на первый взгляд, так то, что он остается "прикрепленным" к модели предка и его использование с моделью наследника будет давать не совсем ожидаемы результат. Вот пример:

    class Base( models.Model ):
       field1 = models.IntegerField()
    
       objects = models.Manager()
       my_custom_manager = MyManager()
    
    class Derived( Base ):
       field2 = models.IntegerField()
    
    #...
    
    Derived.my_custom_manager.filter( field2 = 777 )# Вот это не сработает
    # и ругнется на отсутсвие данного поля у Base модели, что есть правда
    

    Отсюда вывод - переопределяйте свои менеджеры в моделях наследниках. Вот соответствующее обсуждение в джанго-девелоперс

  • Теперь немного копнем внутрь. Как вам известно список fields у мета объекта модели содержит в себе поля данной модели. Так вот, у класса наследника DerivedModel._meta.fields он содержит все поля, в том числе и поля всех родителей. Вот с этой конкретными моментом связана найденная ошибка - видимо Малкольм сам позабыл, что он сделал, но об этом попозже чуть-чуть. Если вам нужны поля только отнаследованной модели, то есть новый список local_fields, где присутствуют только поля данной конкретной модели. Так же введен новый список parents, где перечислены все модели предки данной.

  • В стандартном CRUD'е, на странице редактирования модели наследника можно изменить и поля родителя, но при этом при сохранения джанге хочется предка создать заново, а не изменить имеющегося. Побыстрее бы уже newforms-admin влился...

И последняя особенность, которую словил я уже не из-за непосредственно наследования, но из-за qsrf, работая надо обновлением блога:

  • Эксепшен о максимальной глубины рекурсии при простом запросе к базе. Минимальный пример сделать пока не получилось, но я попробую эту проблему поизучать. Пока обошелся workaround'ом в коде.

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

class Base( models.Model ):
    field1 = models.IntegerField()

class Derived( Base ):
    field2 = models.IntegerField()

class Holder( models.Model ):
    ref = models.ForeignKey( Derived )# обратите внимание - на наследника ссылка!

#...

query_set = Holder.objects.all().select_related( "ref" )
list( query_set )# Тадам!

Вот именно в последней строчке нам база данных скажет, что у таблицы myapp_derived нет поля field1. И эта чистая правда. Это поле есть у myapp_base, но её ORM забыл приджоинить и поменять алиас таблицы.

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

Вот таким интересным стало моё погружение в qsrf. То ли ещё будет!:)

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

  1. Nick написал:

    Поправьте "Малкольм сам позабыл, что он сделала" = ... сделал, "Как вам известно список fields у мета объекта модели содержит в себя поля данной модели" = .. содержит в себе..

    Оставлен 19 Май 2008 в 17:06
  2. Александр Кошелев написал:

    Спасибо.

    Оставлен 19 Май 2008 в 17:39
  3. nekufa написал:

    Спасибо, интересная статья :)

    p.s. Поправьте только "пока Малкольм ненадолго(видимо) отошел отдел" :) "от дел", я так понимаю..

    Оставлен 19 Май 2008 в 20:05
  4. zgoda написал:

    Hard to read (Russian is hardly considered lingua franca of IT), but anyway nice rant.

    Оставлен 19 Май 2008 в 22:59
  5. sector119 написал:

    В стандартном CRUD'е, на странице редактирования модели наследника можно изменить и поля родителя, но при этом при сохранения джанге хочется предка создать заново, а не изменить имеющегося.

    Почему так происходит? У меня похожее поведение в связке newforms ModelForm + FormPreview + model inharitance. где-то теряется instance формы... связки newforms ModelForm + model inharitance и newforms ModelForm + model inharitance работает как требуется. код один и тотже для всех моделей, как-бы свои generic views.

    Оставлен 21 Май 2008 в 11:37
  6. Nick написал:

    Александр, заранее прошу прощения за вопрос не в тему статьи.

    Где вы взяли код такого вменяемого паджинатора на этом блоге? :)

    Оставлен 21 Май 2008 в 13:35
  7. Александр Кошелев написал:

    Почему так происходит?

    Самому интересно. Надо ковырять.

    Оставлен 22 Май 2008 в 01:04
  8. Александр Кошелев написал:

    Александр, заранее прошу прощения за вопрос не в тему статьи. Где вы взяли код такого вменяемого паджинатора на этом блоге? :)

    Хм... Да как-то он сам такой получился. Но он не очень вменяемый на самом деле. Надо прикрутить "..." при большом количестве страниц. А то уже скоро уже не будут помещаться:)

    Оставлен 22 Май 2008 в 01:07
  9. Владимир Володин написал:

    Александр, а вот, что нашел я:

    
           class Test(models.Model):
        name = models.CharField(max_length = 20)`
        def __unicode__(self):`
            return u'%s' % (self.name)`
    
    class TestInh(Test):
        friends = models.ManyToManyField("self", symmetrical = False, related_name = 'f_set')
        def __unicode__(self):
            return u'%s' % (self.name)
          
    

    Потом создаем троечку TestInh, добавляем первому в друзья двоих других. И пытаемся получить их в списке: t1.friends.all()

    Получаем вот что:

    
           [<TestInh: test1>], [<TestInh: test1>]
          
    

    Таблица myapp_test (из которой и берется инфа о "друзьях") джойнится по айдишнику юзера, друзей которого мы и пытаемся достать.

    Оставлен 23 Май 2008 в 12:17

Пингбеки 1

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

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