ใน Django - Model Inheritance - อนุญาตให้คุณลบล้างแอตทริบิวต์ของโมเดลแม่หรือไม่?


102

ฉันต้องการทำสิ่งนี้:

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)

นี่เป็นเวอร์ชันที่ฉันต้องการใช้ (แม้ว่าฉันจะเปิดรับข้อเสนอแนะก็ตาม): http://docs.djangoproject.com/en/dev/topics/db/models/#id7

สิ่งนี้รองรับใน Django หรือไม่ ถ้าไม่มีวิธีที่จะได้ผลลัพธ์ที่คล้ายกันหรือไม่?


คุณสามารถโปรดยอมรับคำตอบลงร้องจาก Django 1.10 มันเป็นไปได้ :)
Holms

@holms เฉพาะในกรณีที่คลาสพื้นฐานเป็นนามธรรม!
Micah Walter

คำตอบ:


66

คำตอบที่อัปเดต: ตามที่มีคนระบุไว้ในความคิดเห็นคำตอบเดิมไม่ได้ตอบคำถามอย่างถูกต้อง อันที่จริงมีเพียงLongNamedRestaurantโมเดลที่สร้างขึ้นในฐานข้อมูลเท่านั้นPlaceไม่ได้

วิธีแก้ปัญหาคือการสร้างแบบจำลองนามธรรมที่แสดงถึง "สถานที่" เช่น AbstractPlaceและรับมรดกจากมัน:

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)

โปรดอ่านคำตอบของ @Mark เขาให้คำอธิบายที่ดีว่าทำไมคุณไม่สามารถเปลี่ยนแอตทริบิวต์ที่สืบทอดมาจากคลาสที่ไม่ใช่นามธรรม

(โปรดทราบว่าสิ่งนี้ทำได้เฉพาะเมื่อ Django 1.10: ก่อน Django 1.10 การแก้ไขแอตทริบิวต์ที่สืบทอดมาจากคลาสนามธรรมไม่สามารถทำได้)

คำตอบเดิม

ตั้งแต่ Django 1.10 เป็นไปได้ ! คุณต้องทำสิ่งที่คุณขอ:

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)

8
สถานที่ต้องเป็นนามธรรมไม่ใช่เหรอ?
DylanYoung

4
ฉันไม่คิดว่าฉันจะตอบคำถามอื่นเพราะฉันแค่บอกว่ารหัสที่โพสต์ในคำถามนั้นใช้งานได้แล้วตั้งแต่ Django 1.10 สังเกตว่าตามลิงค์ที่เขาโพสต์เกี่ยวกับสิ่งที่เขาต้องการใช้เขาลืมที่จะทำให้คลาส Place เป็นนามธรรม
qmarlats

2
ไม่แน่ใจว่าเหตุใดจึงเป็นคำตอบที่ยอมรับ ... OP ใช้การสืบทอดแบบหลายตาราง คำตอบนี้ใช้ได้กับคลาสพื้นฐานนามธรรมเท่านั้น
MrName

1
คลาสนามธรรมมีให้บริการนานก่อน Django 1.10
rbennell

2
@NoamG ในคำตอบเดิมของฉันPlaceเป็นนามธรรมจึงไม่ได้สร้างในฐานข้อมูล แต่ OP ต้องการทั้งสองอย่างPlaceและLongNamedRestaurantสร้างในฐานข้อมูล ดังนั้นฉันจึงปรับปรุงคำตอบของฉันเพื่อเพิ่มAbstractPlaceโมเดลซึ่งเป็นโมเดล "ฐาน" (เช่นนามธรรม) ทั้งสองPlaceและLongNamedRestaurantสืบทอดจาก ตอนนี้ทั้งสองPlaceและLongNamedRestaurantถูกสร้างขึ้นในฐานข้อมูลตามที่ OP ขอ
qmarlats

61

ไม่มันไม่ใช่ :

ไม่อนุญาตให้ใช้ชื่อฟิลด์ "การซ่อน"

ในการสืบทอดคลาส Python ปกติอนุญาตให้คลาสลูกแทนที่แอตทริบิวต์ใด ๆ จากคลาสแม่ ใน Django สิ่งนี้ไม่ได้รับอนุญาตสำหรับแอตทริบิวต์ที่เป็นFieldอินสแตนซ์ (อย่างน้อยไม่ใช่ในขณะนี้) หากคลาสฐานมีฟิลด์ที่เรียกauthorคุณจะไม่สามารถสร้างฟิลด์โมเดลอื่นที่เรียกว่าauthorในคลาสใด ๆ ที่สืบทอดมาจากคลาสพื้นฐานนั้น


11
ดูคำตอบของฉันว่าทำไมถึงเป็นไปไม่ได้ คนชอบสิ่งนี้เพราะมันสมเหตุสมผล แต่ก็ไม่ชัดเจนในทันที
ทำเครื่องหมาย

4
@ leo-the-manic ฉันคิดว่าUser._meta.get_field('email').required = Trueสามารถทำงานได้ไม่แน่ใจว่าคิด
Jens Timmerman

@ leo-the-manic, @JensTimmerman, @utapyngo การตั้งค่าคุณสมบัติของคลาสของคุณจะไม่มีผลกับฟิลด์ที่สืบทอดมา คุณต้องดำเนินการ_metaกับคลาสแม่เช่นMyParentClass._meta.get_field('email').blank = False(เพื่อให้emailฟิลด์ที่สืบทอดมาบังคับในผู้ดูแลระบบ)
Peterino

1
อ๊ะขออภัยรหัสของ @ utapyngo ด้านบนนี้ถูกต้อง แต่จะต้องวางไว้นอกเนื้อหาของชั้นเรียนหลังจากนั้น! การตั้งค่าฟิลด์คลาสหลักตามที่ฉันแนะนำอาจมีผลข้างเคียงที่ไม่ต้องการ
Peterino

ฉันต้องการให้เขตข้อมูลในแต่ละคลาสย่อยเป็นประเภทที่แตกต่างจากเขตข้อมูลที่มีชื่อเดียวกันในคลาสแม่แบบนามธรรมเพื่อรับประกันว่าคลาสย่อยทั้งหมดทั้งหมดมีเขตข้อมูลที่มีชื่อที่แน่นอน รหัสของ utapyngo ไม่ตรงตามความต้องการนี้
Daniel

28

เป็นไปไม่ได้นอกจากนามธรรมและนี่คือเหตุผล: LongNamedRestaurantยังเป็น a Placeไม่เพียง แต่เป็นคลาส แต่ยังอยู่ในฐานข้อมูลด้วย สถานที่โต๊ะมีรายการสำหรับทุกบริสุทธิ์และทุกPlace เพียงแค่สร้างตารางพิเศษที่มีและการอ้างอิงไปยังตารางสถานที่LongNamedRestaurantLongNamedRestaurantfood_type

ถ้าคุณทำPlace.objects.all()คุณจะได้รับทุกสถานที่ที่เป็น a LongNamedRestaurantและจะเป็นตัวอย่างของPlace(โดยไม่มีfood_type) ดังนั้นPlace.nameและLongNamedRestaurant.nameแบ่งปันคอลัมน์ฐานข้อมูลเดียวกันดังนั้นจึงต้องเป็นประเภทเดียวกัน

ฉันคิดว่านี่เป็นเหตุผลสำหรับคนรุ่นปกติร้านอาหารทุกแห่งเป็นสถานที่และอย่างน้อยก็ควรมีทุกอย่าง ความสอดคล้องนี้อาจเป็นสาเหตุที่ทำให้โมเดลนามธรรมก่อน 1.10 ไม่สามารถทำได้แม้ว่าจะไม่ทำให้เกิดปัญหากับฐานข้อมูลก็ตาม ตามที่ @lampslave กล่าวไว้มันเป็นไปได้ใน 1.10 ฉันอยากจะแนะนำการดูแลเป็นการส่วนตัว: ถ้า Sub.x แทนที่ Super.x ตรวจสอบให้แน่ใจว่า Sub.x เป็นคลาสย่อยของ Super.x มิฉะนั้นจะใช้ Sub แทน Super ไม่ได้

วิธีแก้ปัญหา : คุณสามารถสร้างรูปแบบผู้ใช้ที่กำหนดเอง ( AUTH_USER_MODEL) ซึ่งเกี่ยวข้องกับการทำสำเนาโค้ดไม่น้อยหากคุณต้องการเพียงแค่เปลี่ยนฟิลด์อีเมลเท่านั้น หรือคุณสามารถทิ้งอีเมลไว้ได้และตรวจสอบให้แน่ใจว่าจำเป็นต้องใช้ในทุกรูปแบบ สิ่งนี้ไม่รับประกันความสมบูรณ์ของฐานข้อมูลหากแอปพลิเคชันอื่นใช้งานและไม่ได้ผลในทางอื่น (หากคุณไม่ต้องการกำหนดชื่อผู้ใช้)


ฉันเดาว่าเป็นเพราะการเปลี่ยนแปลงใน 1.10: "อนุญาตให้ลบล้างฟิลด์โมเดลที่สืบทอดมาจากคลาสพื้นฐานนามธรรม" docs.djangoproject.com/th/2.0/releases/1.10/#models
lampslave

ฉันสงสัยตั้งแต่ตอนนั้นยังไม่ออก แต่นั่นเป็นสิ่งที่ดีที่จะเพิ่มขอบคุณ!
มาร์ค

19

ดู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

2
AttributeError: ไม่สามารถตั้งค่าแอตทริบิวต์ได้ ((((แต่ฉันกำลังพยายามกำหนดตัวเลือก
Alexey

สิ่งนี้ใช้ไม่ได้กับ Django 1.11 (เคยทำงานกับเวอร์ชันก่อนหน้า) ... คำตอบที่ยอมรับใช้งานได้
acaruci

9

วางรหัสของคุณลงในแอปใหม่เพิ่มแอปใน INSTALLED_APPS แล้วรัน syncdb:

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

ดูเหมือน Django จะไม่สนับสนุนสิ่งนั้น


7

โค้ด supercool นี้ช่วยให้คุณสามารถ 'ลบล้าง' ฟิลด์ในคลาสพาเรนต์นามธรรมได้

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")

เมื่อฟิลด์ถูกลบออกจากคลาสพาเรนต์ที่เป็นนามธรรมคุณมีอิสระที่จะกำหนดฟิลด์ใหม่ได้ตามที่คุณต้องการ

นี่ไม่ใช่ผลงานของตัวเอง รหัสต้นฉบับจากที่นี่: https://gist.github.com/specialunderwear/9d917ddacf3547b646ba


6

บางทีคุณอาจจัดการกับ Contrib_to_class:

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 ทำงานได้ดี ฉันไม่ได้ลองตัวอย่างนี้ในกรณีของฉันฉันเพียงแค่แทนที่พารามิเตอร์ข้อ จำกัด ดังนั้น ... รอดู!


1
นอกจากนี้ข้อโต้แย้งในการมีส่วนร่วม _to_class ก็ดูแปลก ๆ (ผิดด้วยหรือ) ดูเหมือนว่าคุณพิมพ์สิ่งนี้จากหน่วยความจำ คุณช่วยระบุรหัสจริงที่คุณทดสอบได้ไหม ถ้าคุณทำให้สิ่งนี้ได้ผลฉันอยากรู้ว่าคุณทำได้อย่างไร
Michael Bylstra

สิ่งนี้ไม่ได้ผลสำหรับฉัน จะมีความสนใจในตัวอย่างการทำงานเช่นกัน
garromark

โปรดดูblog.jupo.org/2011/11/10/django-model-field-injectionควรมีส่วนร่วม _to_class (<ModelClass>, <fieldToReplace>)
goh

3
Place._meta.get_field('name').max_length = 255__init__()ในร่างกายของชั้นเรียนควรทำเคล็ดลับโดยไม่ต้องเอาชนะ จะกระชับมากขึ้นด้วย
Peterino

4

ฉันรู้ว่ามันเป็นคำถามเก่า แต่ฉันมีปัญหาที่คล้ายกันและพบวิธีแก้ปัญหา:

ฉันมีชั้นเรียนต่อไปนี้:

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

    class Meta:
        abstract = True

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

แต่ฉันต้องการให้ฟิลด์รูปภาพที่สืบทอดมาของปีเป็นสิ่งจำเป็นในขณะที่ทำให้ฟิลด์รูปภาพของคลาสซูเปอร์คลาสเป็นโมฆะ ในท้ายที่สุดฉันใช้ ModelForms เพื่อบังคับใช้รูปภาพในขั้นตอนการตรวจสอบความถูกต้อง:

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

ดูเหมือนว่าจะใช้ได้กับบางสถานการณ์เท่านั้น (แน่นอนว่าคุณต้องบังคับใช้กฎที่เข้มงวดขึ้นในฟิลด์คลาสย่อย)

หรือคุณสามารถใช้clean_<fieldname>()วิธีนี้แทนclean()เช่นหากtownต้องกรอกข้อมูลในฟิลด์:

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

1

คุณไม่สามารถแทนที่ฟิลด์ Model แต่ทำได้อย่างง่ายดายโดยการแทนที่ / ระบุเมธอด clean () ฉันมีปัญหากับฟิลด์อีเมลและต้องการทำให้ไม่ซ้ำกันในระดับรุ่นและทำเช่นนี้:

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')})

จากนั้นข้อความแสดงข้อผิดพลาดจะถูกบันทึกโดยช่องฟอร์มที่มีชื่อ "อีเมล"


คำถามเกี่ยวกับการขยาย max_length ของฟิลด์ถ่าน หากฐานข้อมูลบังคับใช้ "โซลูชัน" นี้จะไม่ช่วย วิธีแก้ปัญหาคือระบุ max_length ที่ยาวขึ้นในโมเดลพื้นฐานและใช้เมธอด clean () เพื่อบังคับใช้ความยาวที่สั้นกว่าที่นั่น
DylanYoung

0

วิธีแก้ปัญหาของฉันง่ายเหมือนmonkey patchingเดิมสังเกตว่าฉันเปลี่ยนmax_lengthแอตทริบิวต์สำหรับnameฟิลด์ในLongNamedRestaurantโมเดลอย่างไร:

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
โดยการใช้ไซต์ของเรา หมายความว่าคุณได้อ่านและทำความเข้าใจนโยบายคุกกี้และนโยบายความเป็นส่วนตัวของเราแล้ว
Licensed under cc by-sa 3.0 with attribution required.