Как безопасно наследовать от django models.Model класс? - PullRequest
1 голос
/ 10 января 2020

У меня следующая структура БД:

class Word(models.Model):
    original = models.CharField(max_length=40)
    translation = models.CharField(max_length=40)

class Verb(Word):
    group = models.IntegerField(default=1)

На мой взгляд, мне нужно сначала создать объект Word, а после определения его группы (в зависимости от Word.original) создать Verb объект, и сохраните его.

Каков наилучший способ наследования от класса Word и сохранения объекта как Verb?

Есть несколько решений, которые я пробовал:

1) Модификация метода __init__ в Verb:

class Verb(Word):
    group = models.IntegerField(default=1)

    def __init__(self, base_word):
        self.original = base_word.original
        self.translation = base_word.translation

Это вызывает много ошибок, поскольку я переопределяю встроенный в django метод __init__.

2) Использование super().__init__():

class Verb(Word):
    group = models.IntegerField(default=1)

    def __init__(self, base_word):
        super().__init__()
        self.original = base_word.original
        self.translation = base_word.translation

Очевидно, это работает довольно хорошо:

base_word = Word()
new_verb = Verb(base_word)
new_verb.save()

Но есть две проблемы:

  1. Вызывает ошибку при попытке увидеть объекты на django странице администратора:
__init__() takes 2 positional arguments but 9 were given
Это все еще слишком много кода, это не правильно. Мне все еще нужно написать это:
self.original = base_word.original
self.translation = base_word.translation

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

1 Ответ

1 голос
/ 10 января 2020

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

  • Не изменяйте подпись из __init__ - это означает, что вы не должны изменять аргументы, которые принимает метод.
  • Выполните пользовательский __init__ logi c после , вызвав метод super().__init__(*args, **kwargs).

В этом конкретном случае вы можете использовать django функции наследования прокси-модели .

VERB = "V"
NOUN = "N"
# ...
WORD_TYPE_CHOICES = (
    (VERB, "Verb"),
    (NOUN, "Noun"),
    # ...
)

class Word(models.Model):
    original = models.CharField(max_length=40)
    translation = models.CharField(max_length=40)

    WORD_TYPE = ""  # This is overridden in subclasses

    word_type = models.CharField(
        max_length=1,
        blank=True,
        editable=False,  # So that the word type isn't editable through the admin.
        choices=WORD_TYPE_CHOICES,
        default=WORD_TYPE,  # Defaults to an empty string
    )

    def __init__(self, *args, **kwargs):
        # NOTE: I'm not 100% positive that this is required, but since we're not
        # altering the signature of the __init__ method, performing the
        # assignment of the word_type field is safe.
        super().__init__(*args, **kwargs)
        self.word_type = self.WORD_TYPE

    def __str__(self):
        return self.original

    def save(self, *args, **kwargs):
        # In the save method, we can force the subclasses to self-assign
        # their word types.
        if not self.word_type:
            self.word_type = self.WORD_TYPE
        super().save(*args, **kwargs)

class WordTypeManager(models.Manager):
    """ This manager class filters the model's queryset so that only the
    specific word_type is returned.
    """
    def __init__(self, word_type, *args, **kwargs):
        """ The manager is initialized with the `word_type` for the proxy model. """
        self._word_type = word_type
        super().__init__(*args, **kwargs)

    def get_queryset(self):
        return super().get_queryset().filter(word_type=self._word_type)

class Verb(Word):
    # Here we can force the word_type for this proxy model, and set the default
    # manager to filter for verbs only.
    WORD_TYPE = VERB
    objects = WordTypeManager(WORD_TYPE)

    class Meta:
        proxy = True

class Noun(Word):
    WORD_TYPE = NOUN
    objects = WordTypeManager(WORD_TYPE)

    class Meta:
        proxy = True

Теперь мы можем рассматривать различные типы слов как если они были отдельными моделями, или обращались ко всем вместе через модель Word.

>>> noun = Noun.objects.create(original="name", translation="nombre")
>>> verb = Verb(original="write", translation="escribir")
>>> verb.save()

# Select all Words regardless of their word_type
>>> Word.objects.values_list("word_type", "original")
<QuerySet [('N', 'name'), ('V', 'write')]>

# Select the word_type based on the model class used
>>> Noun.objects.all()
<QuerySet [<Noun: name>]>
>>> Verb.objects.all()
<QuerySet [<Verb: write>]>

Это также работает с admin.ModelAdmin классами.

@admin.register(Word)
class WordAdmin(admin.ModelAdmin):
    """ This will show all words, regardless of their `word_type`. """
    list_display = ["word_type", "original", "translation"]

@admin.register(Noun)
class NounAdmin(WordAdmin):
    """ This will only show `Noun` instances, and inherit any other config from
    WordAdmin.
    """
...