Dalam Django - Warisan Model - Apakah ini mengijinkan anda untuk menimpa atribut model induk?

99

Saya ingin melakukan ini:

class Place(models.Model):
   name = models.CharField(max_length=20)
   rating = models.DecimalField()

class LongNamedRestaurant(Place):  # Subclassing `Place`.
   name = models.CharField(max_length=255)  # Notice, I'm overriding `Place.name` to give it a longer length.
   food_type = models.CharField(max_length=25)

Ini adalah versi yang ingin saya gunakan (meskipun saya terbuka untuk saran apa pun): http://docs.djangoproject.com/en/dev/topics/db/models/#id7

Apakah ini didukung di Django? Jika tidak, apakah ada cara untuk mencapai hasil yang serupa?

Johnny 5
sumber
bisakah Anda menerima jawaban di bawah ini, dari django 1,10 mungkin :)
holms
@holms hanya jika kelas dasarnya abstrak!
Micah Walter

Jawaban:

64

Jawaban yang diperbarui: seperti yang dicatat orang dalam komentar, jawaban asli tidak menjawab pertanyaan dengan benar. Memang, hanya LongNamedRestaurantmodel yang dibuat dalam database, Placetidak.

Solusinya adalah membuat model abstrak yang mewakili "Tempat", misalnya. AbstractPlace, dan mewarisi darinya:

class AbstractPlace(models.Model):
    name = models.CharField(max_length=20)
    rating = models.DecimalField()

    class Meta:
        abstract = True

class Place(AbstractPlace):
    pass

class LongNamedRestaurant(AbstractPlace):
    name = models.CharField(max_length=255)
    food_type = models.CharField(max_length=25)

Silakan baca juga jawaban @Mark , dia memberikan penjelasan yang bagus mengapa Anda tidak dapat mengubah atribut yang diwarisi dari kelas non-abstrak.

(Catatan ini hanya mungkin karena Django 1.10: sebelum Django 1.10, memodifikasi atribut yang diwarisi dari kelas abstrak tidak mungkin.)

Jawaban asli

Sejak Django 1,10 itu mungkin ! Anda hanya perlu melakukan apa yang Anda minta:

class Place(models.Model):
    name = models.CharField(max_length=20)
    rating = models.DecimalField()

    class Meta:
        abstract = True

class LongNamedRestaurant(Place):  # Subclassing `Place`.
    name = models.CharField(max_length=255)  # Notice, I'm overriding `Place.name` to give it a longer length.
    food_type = models.CharField(max_length=25)
qmarlats.dll
sumber
8
Tempat harus abstrak, bukan?
DylanYoung
4
Saya tidak berpikir saya menjawab pertanyaan yang berbeda karena saya hanya mengatakan bahwa kode yang dipasang di pertanyaan sekarang bekerja sejak Django 1.10. Perhatikan bahwa menurut tautan yang dia posting tentang apa yang ingin dia gunakan, dia lupa membuat abstrak kelas Place.
qmarlats
2
Tidak yakin mengapa ini adalah jawaban yang diterima ... OP menggunakan warisan multi-tabel. Jawaban ini hanya berlaku untuk kelas dasar abstrak.
MrName
1
kelas abstrak tersedia jauh sebelum Django 1,10
rbennell
1
@NoamG Jawaban asli saya Placeadalah abstrak, sehingga tidak dibuat di database. Tetapi OP menginginkan keduanya Placedan LongNamedRestaurantdibuat dalam database. Oleh karena itu saya memperbarui jawaban saya untuk menambahkan AbstractPlacemodel, yang merupakan model "dasar" (yaitu abstrak) baik Placedan LongNamedRestaurantditurunkan dari. Sekarang keduanya Placedan LongNamedRestaurantdibuat dalam database, seperti yang diminta OP.
qmarlats
61

Tidak, ini bukan :

Nama bidang "bersembunyi" tidak diizinkan

Dalam pewarisan kelas Python normal, kelas anak diperbolehkan untuk mengganti atribut apa pun dari kelas induk. Di Django, ini tidak diizinkan untuk atribut yang merupakan Fieldcontoh (setidaknya, tidak saat ini). Jika kelas dasar memiliki bidang yang dipanggil author, Anda tidak dapat membuat bidang model lain yang dipanggil authordi kelas mana pun yang mewarisi dari kelas dasar tersebut.

nada
sumber
11
Lihat jawaban saya mengapa tidak mungkin. Orang-orang menyukai ini karena memang masuk akal, hanya saja tidak langsung terlihat.
Tandai
4
@ leo-the-manic saya pikir User._meta.get_field('email').required = Truebisa bekerja, tidak yakin berpikir.
Jens Timmerman
@ leo-the-manic, @JensTimmerman, @utapyngo Menyetel nilai properti kelas Anda tidak akan berpengaruh pada bidang yang diwariskan. Anda harus bertindak berdasarkan _metakelas induk, misalnya MyParentClass._meta.get_field('email').blank = False(untuk membuat emailbidang yang diwariskan wajib di Admin)
Peterino
1
Ups, maaf, kode @ utapyngo di atas sudah benar, tetapi setelah itu harus ditempatkan di luar badan kelas! Mengatur bidang kelas induk seperti yang saya sarankan mungkin memiliki efek samping yang tidak diinginkan.
Peterino
Saya ingin sebuah field di setiap subclass memiliki tipe yang berbeda dari field dengan nama yang sama di kelas induk abstrak untuk menjamin bahwa semua subclass memiliki field dengan nama tertentu. kode utapyngo tidak memenuhi kebutuhan ini.
Daniel
28

Itu tidak mungkin kecuali abstrak, dan inilah alasannya: LongNamedRestaurantjuga a Place, tidak hanya sebagai kelas tetapi juga dalam database. Place-table berisi entri untuk setiap pure Placedan untuk setiap LongNamedRestaurant. LongNamedRestauranthanya membuat tabel tambahan dengan food_typedan referensi ke tabel tempat.

Jika Anda melakukannya Place.objects.all(), Anda juga mendapatkan setiap tempat yang a LongNamedRestaurant, dan itu akan menjadi turunan dari Place(tanpa food_type). Jadi Place.namedan LongNamedRestaurant.nameberbagi kolom database yang sama, dan karena itu harus berjenis sama.

Saya pikir ini masuk akal untuk model normal: setiap restoran adalah sebuah tempat, dan setidaknya harus memiliki semua yang dimiliki tempat itu. Mungkin konsistensi ini juga mengapa tidak mungkin untuk model abstrak sebelum 1,10, meskipun tidak akan memberikan masalah database di sana. Seperti yang dikatakan @lampslave, hal itu dimungkinkan di 1.10. Saya pribadi merekomendasikan perawatan: jika Sub.x menimpa Super.x, pastikan Sub.x adalah subkelas dari Super.x, jika tidak Sub tidak dapat digunakan sebagai pengganti Super.x.

Solusi : Anda dapat membuat model pengguna khusus ( AUTH_USER_MODEL) yang melibatkan cukup banyak duplikasi kode jika Anda hanya perlu mengubah bidang email. Sebagai alternatif, Anda dapat membiarkan email apa adanya dan memastikannya diwajibkan dalam segala bentuk. Ini tidak menjamin integritas database jika aplikasi lain menggunakannya, dan tidak berfungsi sebaliknya (jika Anda ingin membuat nama pengguna tidak diperlukan).

Menandai
sumber
Saya rasa itu karena perubahan di 1.10: "Diizinkan mengganti bidang model yang diwarisi dari kelas dasar abstrak." docs.djangoproject.com/en/2.0/releases/1.10/#models
lampslave
Saya meragukannya karena itu belum keluar pada saat itu, tetapi itu adalah hal yang baik untuk ditambahkan, terima kasih!
Tandai
19

Lihat https://stackoverflow.com/a/6379556/15690 :

class BaseMessage(models.Model):
    is_public = models.BooleanField(default=False)
    # some more fields...

    class Meta:
        abstract = True

class Message(BaseMessage):
    # some fields...
Message._meta.get_field('is_public').default = True
kebiruan
sumber
2
AttributeError: tidak dapat mengatur atribut ((((tapi saya mencoba mengatur pilihan
Alexey
Ini tidak bekerja pada Django 1.11 (ini digunakan untuk bekerja pada versi sebelumnya) ... tanggapan yang diterima bekerja
acaruci
9

Tempelkan kode Anda ke dalam aplikasi baru, tambahkan aplikasi ke INSTALLED_APPS dan jalankan syncdb:

django.core.exceptions.FieldError: Local field 'name' in class 'LongNamedRestaurant' clashes with field of similar name from base class 'Place'

Sepertinya Django tidak mendukung itu.

Brian Luft
sumber
7

Bagian kode yang sangat keren ini memungkinkan Anda untuk 'mengganti' bidang di kelas induk abstrak.

def AbstractClassWithoutFieldsNamed(cls, *excl):
    """
    Removes unwanted fields from abstract base classes.

    Usage::
    >>> from oscar.apps.address.abstract_models import AbstractBillingAddress

    >>> from koe.meta import AbstractClassWithoutFieldsNamed as without
    >>> class BillingAddress(without(AbstractBillingAddress, 'phone_number')):
    ...     pass
    """
    if cls._meta.abstract:
        remove_fields = [f for f in cls._meta.local_fields if f.name in excl]
        for f in remove_fields:
            cls._meta.local_fields.remove(f)
        return cls
    else:
        raise Exception("Not an abstract model")

Ketika field telah dihapus dari kelas induk abstrak, Anda bebas untuk mendefinisikan ulang sesuai kebutuhan.

Ini bukan pekerjaan saya sendiri. Kode asli dari sini: https://gist.github.com/specialunderwear/9d917ddacf3547b646ba

Devin
sumber
6

Mungkin Anda bisa berurusan dengan kontribusi_kelas_kelas:

class LongNamedRestaurant(Place):

    food_type = models.CharField(max_length=25)

    def __init__(self, *args, **kwargs):
        super(LongNamedRestaurant, self).__init__(*args, **kwargs)
        name = models.CharField(max_length=255)
        name.contribute_to_class(self, 'name')

Syncdb berfungsi dengan baik. Saya tidak mencoba contoh ini, dalam kasus saya, saya hanya mengganti parameter kendala jadi ... tunggu & lihat!

JF Simon
sumber
1
juga argumen untuk berkontribusi_ke_kelas tampak aneh (juga salah?) Sepertinya Anda mengetik ini dari memori. Bisakah Anda memberikan kode sebenarnya yang Anda uji? Jika Anda berhasil melakukannya, saya ingin tahu persis bagaimana Anda melakukannya.
Michael Bylstra
Ini tidak berhasil untukku. Akan tertarik dengan contoh kerja juga.
garromark
silakan lihat blog.jupo.org/2011/11/10/django-model-field-injection itu harus dikontribusikan_ke_kelas (<ModelClass>, <fieldToReplace>)
goh
3
Place._meta.get_field('name').max_length = 255di badan kelas harus melakukan trik, tanpa menimpa __init__(). Akan lebih ringkas juga.
Peterino
4

Saya tahu ini pertanyaan lama, tetapi saya memiliki masalah serupa dan menemukan solusi:

Saya memiliki kelas-kelas berikut:

class CommonInfo(models.Model):
    image = models.ImageField(blank=True, null=True, default="")

    class Meta:
        abstract = True

class Year(CommonInfo):
    year = models.IntegerField() 

Tapi saya ingin bidang gambar warisan Tahun menjadi diperlukan sambil menjaga bidang gambar dari superclass nullable. Pada akhirnya saya menggunakan ModelForms untuk menegakkan gambar pada tahap validasi:

class YearForm(ModelForm):
    class Meta:
        model = Year

    def clean(self):
        if not self.cleaned_data['image'] or len(self.cleaned_data['image'])==0:
            raise ValidationError("Please provide an image.")

        return self.cleaned_data

admin.py:

class YearAdmin(admin.ModelAdmin):
    form = YearForm

Tampaknya ini hanya berlaku untuk beberapa situasi (tentunya di mana Anda perlu menegakkan aturan yang lebih ketat di bidang subclass).

Alternatifnya, Anda dapat menggunakan clean_<fieldname>()metode ini sebagai ganti clean(), misalnya jika suatu bidang townakan diminta untuk diisi:

def clean_town(self):
    town = self.cleaned_data["town"]
    if not town or len(town) == 0:
        raise forms.ValidationError("Please enter a town")
    return town
pholz
sumber
1

Anda tidak dapat mengganti bidang Model, tetapi mudah dicapai dengan mengganti / menetapkan metode clean (). Saya memiliki masalah dengan bidang email dan ingin membuatnya unik pada tingkat Model dan melakukannya seperti ini:

def clean(self):
    """
    Make sure that email field is unique
    """
    if MyUser.objects.filter(email=self.email):
        raise ValidationError({'email': _('This email is already in use')})

Pesan kesalahan tersebut kemudian ditangkap oleh kolom Formulir dengan nama "email"

Phoenix49
sumber
Pertanyaannya adalah tentang memperpanjang max_length bidang karakter. Jika ini diberlakukan oleh database, maka "solusi" ini tidak membantu. Solusinya adalah dengan menentukan max_length yang lebih panjang dalam model dasar dan menggunakan metode clean () untuk memaksakan panjang yang lebih pendek di sana.
DylanYoung
0

Solusi saya sesederhana selanjutnya monkey patching, perhatikan bagaimana saya mengubah max_lengthatribut untuk namebidang dalam LongNamedRestaurantmodel:

class Place(models.Model):
   name = models.CharField(max_length=20)

class LongNamedRestaurant(Place):
    food_type = models.CharField(max_length=25)
    Place._meta.get_field('name').max_length = 255
NoamG
sumber