Django ฟิลด์โมเดลแบบไดนามิก


161

ผมทำงานในหลายอาศัยแอพลิเคชันที่ผู้ใช้บางคนสามารถกำหนดเขตข้อมูลของตัวเอง (ผ่านผู้ดูแลระบบ) เพื่อรวบรวมข้อมูลเพิ่มเติมในรูปแบบและรายงานเกี่ยวกับข้อมูล บิตหลังทำให้ JSONField ไม่ใช่ตัวเลือกที่ยอดเยี่ยมดังนั้นฉันจึงมีวิธีแก้ไขปัญหาต่อไปนี้:

class CustomDataField(models.Model):
    """
    Abstract specification for arbitrary data fields.
    Not used for holding data itself, but metadata about the fields.
    """
    site = models.ForeignKey(Site, default=settings.SITE_ID)
    name = models.CharField(max_length=64)

    class Meta:
        abstract = True

class CustomDataValue(models.Model):
    """
    Abstract specification for arbitrary data.
    """
    value = models.CharField(max_length=1024)

    class Meta:
        abstract = True

โปรดสังเกตว่า CustomDataField มี ForeignKey to Site - แต่ละไซต์จะมีชุดข้อมูลแบบกำหนดเองที่แตกต่างกัน แต่ใช้ฐานข้อมูลเดียวกัน จากนั้นเขตข้อมูลที่เป็นรูปธรรมต่างๆสามารถกำหนดเป็น:

class UserCustomDataField(CustomDataField):
    pass

class UserCustomDataValue(CustomDataValue):
    custom_field = models.ForeignKey(UserCustomDataField)
    user = models.ForeignKey(User, related_name='custom_data')

    class Meta:
        unique_together=(('user','custom_field'),)

สิ่งนี้นำไปสู่การใช้งานต่อไปนี้:

custom_field = UserCustomDataField.objects.create(name='zodiac', site=my_site) #probably created in the admin
user = User.objects.create(username='foo')
user_sign = UserCustomDataValue(custom_field=custom_field, user=user, data='Libra')
user.custom_data.add(user_sign) #actually, what does this even do?

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

ตัวเลือกที่ถูกลบทิ้งล่วงหน้าแล้ว:

  • SQL แบบกำหนดเองเพื่อปรับเปลี่ยนตารางแบบทันที ส่วนหนึ่งเป็นเพราะสิ่งนี้จะไม่ขยายขนาดและส่วนหนึ่งเป็นเพราะแฮ็คมากเกินไป
  • Schema-less solutions เช่น NoSQL ฉันไม่มีอะไรกับพวกเขา แต่พวกเขายังคงไม่เหมาะสม ในที่สุดข้อมูลนี้จะถูกพิมพ์และความเป็นไปได้ที่จะมีการใช้แอปพลิเคชันการรายงานของบุคคลที่สาม
  • JSONField ดังที่แสดงไว้ด้านบนเนื่องจากจะไม่สามารถทำงานได้ดีกับข้อความค้นหา

6
Pre-emptively นี้ไม่ได้ใด ๆ ของคำถามเหล่านี้: stackoverflow.com/questions/7801729/... stackoverflow.com/questions/2854656/...
GDorn

คำตอบ:


278

ณ วันนี้มีสี่วิธีที่ใช้ได้ซึ่งสองวิธีต้องการแบ็กเอนด์หน่วยเก็บข้อมูลที่แน่นอน:

  1. Django-eav (แพคเกจเดิมไม่ได้ถูก mantained อีกต่อไป แต่มีส้อมที่เจริญรุ่งเรืองบางส่วน)

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

    • ใช้แบบจำลอง Django ที่บริสุทธิ์และเรียบง่ายหลายแบบเพื่อเป็นตัวแทนเขตข้อมูลแบบไดนามิกซึ่งทำให้ง่ายต่อการเข้าใจและไม่เชื่อเรื่องฐานข้อมูล
    • ช่วยให้คุณสามารถแนบ / แยกการจัดเก็บแอตทริบิวต์แบบไดนามิกกับรุ่น Django ได้อย่างมีประสิทธิภาพด้วยคำสั่งง่ายๆเช่น:

      eav.unregister(Encounter)
      eav.register(Patient)
    • ทำงานร่วมกับผู้ดูแลระบบ Django อย่างดี ;

    • ในขณะเดียวกันก็มีพลังจริงๆ

    ข้อเสีย:

    • ไม่ค่อยมีประสิทธิภาพ นี่เป็นคำวิจารณ์เพิ่มเติมของรูปแบบ EAV ซึ่งต้องการรวมข้อมูลจากรูปแบบคอลัมน์เข้ากับชุดของคู่คีย์ - ค่าในแบบจำลองด้วยตนเอง
    • บำรุงรักษายาก การรักษาความถูกต้องของข้อมูลจำเป็นต้องมีข้อ จำกัด คีย์ที่ไม่ซ้ำกันหลายคอลัมน์ซึ่งอาจไม่มีประสิทธิภาพในฐานข้อมูลบางตัว
    • คุณจะต้องเลือกหนึ่งในส้อมเนื่องจากแพ็คเกจอย่างเป็นทางการจะไม่ได้รับการบำรุงรักษาอีกต่อไปและไม่มีผู้นำที่ชัดเจน

    การใช้งานค่อนข้างตรงไปตรงมา:

    import eav
    from app.models import Patient, Encounter
    
    eav.register(Encounter)
    eav.register(Patient)
    Attribute.objects.create(name='age', datatype=Attribute.TYPE_INT)
    Attribute.objects.create(name='height', datatype=Attribute.TYPE_FLOAT)
    Attribute.objects.create(name='weight', datatype=Attribute.TYPE_FLOAT)
    Attribute.objects.create(name='city', datatype=Attribute.TYPE_TEXT)
    Attribute.objects.create(name='country', datatype=Attribute.TYPE_TEXT)
    
    self.yes = EnumValue.objects.create(value='yes')
    self.no = EnumValue.objects.create(value='no')
    self.unkown = EnumValue.objects.create(value='unkown')
    ynu = EnumGroup.objects.create(name='Yes / No / Unknown')
    ynu.enums.add(self.yes)
    ynu.enums.add(self.no)
    ynu.enums.add(self.unkown)
    
    Attribute.objects.create(name='fever', datatype=Attribute.TYPE_ENUM,\
                                           enum_group=ynu)
    
    # When you register a model within EAV,
    # you can access all of EAV attributes:
    
    Patient.objects.create(name='Bob', eav__age=12,
                               eav__fever=no, eav__city='New York',
                               eav__country='USA')
    # You can filter queries based on their EAV fields:
    
    query1 = Patient.objects.filter(Q(eav__city__contains='Y'))
    query2 = Q(eav__city__contains='Y') |  Q(eav__fever=no)
  2. เขตข้อมูล Hstore, JSON หรือ JSONB ใน PostgreSQL

    PostgreSQL รองรับประเภทข้อมูลที่ซับซ้อนมากขึ้น ส่วนใหญ่ได้รับการสนับสนุนผ่านแพ็คเกจของบุคคลที่สาม แต่ในช่วงไม่กี่ปีที่ผ่านมา Django ได้นำไปใช้กับ django.contrib.postgres.fields

    HStoreField :

    Django-hstoreเดิมเป็นแพ็คเกจของบุคคลที่สาม แต่ Django 1.8 ได้เพิ่มHStoreFieldในตัวพร้อมกับ PostgreSQL ที่รองรับประเภทฟิลด์อื่น ๆ

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

    #app/models.py
    from django.contrib.postgres.fields import HStoreField
    class Something(models.Model):
        name = models.CharField(max_length=32)
        data = models.HStoreField(db_index=True)

    ในเปลือกของ Django คุณสามารถใช้มันได้:

    >>> instance = Something.objects.create(
                     name='something',
                     data={'a': '1', 'b': '2'}
               )
    >>> instance.data['a']
    '1'        
    >>> empty = Something.objects.create(name='empty')
    >>> empty.data
    {}
    >>> empty.data['a'] = '1'
    >>> empty.save()
    >>> Something.objects.get(name='something').data['a']
    '1'

    คุณสามารถออกแบบสอบถามที่จัดทำดัชนีไว้กับเขตข้อมูล hstore:

    # equivalence
    Something.objects.filter(data={'a': '1', 'b': '2'})
    
    # subset by key/value mapping
    Something.objects.filter(data__a='1')
    
    # subset by list of keys
    Something.objects.filter(data__has_keys=['a', 'b'])
    
    # subset by single key
    Something.objects.filter(data__has_key='a')    

    JSONField :

    ฟิลด์ JSON / JSONB รองรับประเภทข้อมูลที่เข้ารหัสได้ JSON ไม่เพียง แต่คู่คีย์ / ค่าเท่านั้น แต่ยังมีแนวโน้มที่จะเร็วกว่าและ (สำหรับ JSONB) จะกะทัดรัดกว่า Hstore แพ็คเกจจำนวนมากใช้ฟิลด์ JSON / JSONB ซึ่งรวมถึงdjango-pgfieldsแต่ในขณะที่ Django 1.9 นั้นJSONFieldเป็นแบบในตัวโดยใช้ JSONB สำหรับการจัดเก็บ JSONFieldคล้ายกับ HStoreField และอาจทำงานได้ดีขึ้นกับพจนานุกรมขนาดใหญ่ นอกจากนี้ยังรองรับประเภทอื่น ๆ นอกเหนือจากสตริงเช่นจำนวนเต็มบูลีนและพจนานุกรมที่ซ้อนกัน

    #app/models.py
    from django.contrib.postgres.fields import JSONField
    class Something(models.Model):
        name = models.CharField(max_length=32)
        data = JSONField(db_index=True)

    การสร้างในเชลล์:

    >>> instance = Something.objects.create(
                     name='something',
                     data={'a': 1, 'b': 2, 'nested': {'c':3}}
               )

    ข้อความค้นหาที่จัดทำดัชนีเกือบจะเหมือนกับ HStoreField ยกเว้นการซ้อนเป็นไปได้ ดัชนีที่ซับซ้อนอาจต้องสร้างด้วยตนเอง (หรือการย้ายสคริปต์)

    >>> Something.objects.filter(data__a=1)
    >>> Something.objects.filter(data__nested__c=3)
    >>> Something.objects.filter(data__has_key='a')
  3. Django MongoDB

    หรือการดัดแปลง NoSQL Django อื่น ๆ - กับพวกเขาคุณสามารถมีรูปแบบไดนามิกอย่างเต็มที่

    ห้องสมุด NoSQL Django นั้นยอดเยี่ยม แต่โปรดจำไว้ว่าพวกเขาไม่ใช่ 100% ที่เข้ากันได้กับ Django เช่นการโยกย้ายไปยังDjango nonrelจาก Django มาตรฐานคุณจะต้องแทนที่ ManyToMany ด้วยListFieldเหนือสิ่งอื่นใด

    ชำระเงินตัวอย่าง Django MongoDB นี้:

    from djangotoolbox.fields import DictField
    
    class Image(models.Model):
        exif = DictField()
    ...
    
    >>> image = Image.objects.create(exif=get_exif_data(...))
    >>> image.exif
    {u'camera_model' : 'Spamcams 4242', 'exposure_time' : 0.3, ...}

    คุณสามารถสร้างรายการแบบฝังของรุ่น Django ได้:

    class Container(models.Model):
        stuff = ListField(EmbeddedModelField())
    
    class FooModel(models.Model):
        foo = models.IntegerField()
    
    class BarModel(models.Model):
        bar = models.CharField()
    ...
    
    >>> Container.objects.create(
        stuff=[FooModel(foo=42), BarModel(bar='spam')]
    )
  4. Django-mutant: โมเดลไดนามิกที่อิงตาม syncdb และ South-hooks

    Django-mutantใช้งาน Foreign Key แบบไดนามิกและฟิลด์ m2m และได้รับแรงบันดาลใจจากโซลูชั่นที่เหลือเชื่อ แต่ค่อนข้างแฮ็คโดยWill Hardyและ Michael Hall

    ทั้งหมดนี้มาจาก Django South hooks ซึ่งตามการพูดคุยของ Will Hardy ที่ DjangoCon 2011 (ดูมัน!)ยังมีประสิทธิภาพและทดสอบในการผลิต ( ซอร์สโค้ดที่เกี่ยวข้อง) )

    คนแรกที่จะดำเนินการนี้เป็นไมเคิลฮอลล์ไมเคิลฮอลล์

    ใช่นี่คือความมหัศจรรย์ด้วยวิธีการเหล่านี้คุณสามารถใช้แอป Django แบบไดนามิกและฟิลด์ด้วยฐานข้อมูลเชิงสัมพันธ์ แต่ราคาเท่าไหร่ ความเสถียรของแอพพลิเคชั่นจะทุกข์ทรมานเมื่อใช้งานหนักหรือไม่? นี่คือคำถามที่ต้องพิจารณา คุณต้องแน่ใจว่าได้ล็อคที่เหมาะสมเพื่อให้คำขอฐานข้อมูลการเปลี่ยนแปลงพร้อมกัน

    หากคุณใช้งาน Michael Halls รหัสของคุณจะเป็นดังนี้:

    from dynamo import models
    
    test_app, created = models.DynamicApp.objects.get_or_create(
                          name='dynamo'
                        )
    test, created = models.DynamicModel.objects.get_or_create(
                      name='Test',
                      verbose_name='Test Model',
                      app=test_app
                   )
    foo, created = models.DynamicModelField.objects.get_or_create(
                      name = 'foo',
                      verbose_name = 'Foo Field',
                      model = test,
                      field_type = 'dynamiccharfield',
                      null = True,
                      blank = True,
                      unique = False,
                      help_text = 'Test field for Foo',
                   )
    bar, created = models.DynamicModelField.objects.get_or_create(
                      name = 'bar',
                      verbose_name = 'Bar Field',
                      model = test,
                      field_type = 'dynamicintegerfield',
                      null = True,
                      blank = True,
                      unique = False,
                      help_text = 'Test field for Bar',
                   )

3
หัวข้อนี้ถูกพูดคุยเมื่อเร็ว ๆ นี้เกี่ยวกับที่ DjangoCon 2013 ยุโรป: slideshare.net/schacki/และyoutube.com/watch?v=67wcGdk4aCc
Aleck Landgraf

การใช้django-pgjsonบน Postgres> = 9.2 นั้นอาจช่วยให้สามารถใช้เขตข้อมูล json ของ postgresql ได้โดยตรง บน Django> = 1.7 ตัวกรอง API สำหรับการสืบค้นค่อนข้างมีเหตุผล Postgres> = 9.4 ยังอนุญาตให้เขต jsonb พร้อมดัชนีที่ดีขึ้นสำหรับการสืบค้นที่เร็วขึ้น
GDorn

1
อัปเดตในวันนี้เพื่อรับทราบการยอมรับของ Hango Hield และ JSONField ในการควบคุม มันมีวิดเจ็ตรูปแบบบางตัวที่ไม่ได้ยอดเยี่ยม แต่ใช้งานได้หากคุณต้องการปรับแต่งข้อมูลในผู้ดูแลระบบ
GDorn

13

ฉันพยายามผลักดันไอเดีย django-dynamo ต่อไป โครงการยังไม่มีเอกสาร แต่คุณสามารถอ่านรหัสได้ที่https://github.com/charettes/django-mutant https://github.com/charettes/django-mutant

ที่จริงแล้วฟิลด์ FK และ M2M (ดูที่ contrib.related) สามารถใช้งานได้และเป็นไปได้ที่จะกำหนด wrapper สำหรับฟิลด์ที่คุณกำหนดเอง

นอกจากนี้ยังมีการสนับสนุนตัวเลือกรูปแบบเช่น unique_together และการสั่งซื้อรวมถึงฐานแบบจำลองเพื่อให้คุณสามารถพร็อกซีโมเดลย่อยคลาสนามธรรมหรือมิกซ์อิน

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

โครงการนี้ยังคงเป็นอัลฟามาก แต่เป็นเทคโนโลยีที่สำคัญสำหรับหนึ่งในโครงการของฉันดังนั้นฉันจะต้องนำมันไปผลิตพร้อม แผนใหญ่รองรับ django-nonrel เพื่อให้เราสามารถใช้ประโยชน์จากคนขับ MongoDB


1
สวัสดี Simon! ฉันได้รวมลิงค์ไปยังโครงการของคุณในคำตอบ wikiหลังจากที่คุณสร้างขึ้นใน gitub :))) ดีใจที่ได้พบคุณใน stackoverflow!
Ivan Kharlamov

4

การวิจัยเพิ่มเติมแสดงให้เห็นว่านี่เป็นกรณีพิเศษของรูปแบบการออกแบบค่าเอนทิตีคุณสมบัติซึ่งได้รับการดำเนินการสำหรับ Django โดยสองแพคเกจ

เริ่มแรกมีโปรเจ็กต์ eav-djangoดั้งเดิมซึ่งอยู่ใน PyPi

ประการที่สองมีโปรเจ็กต์แรกที่ล่าสุดคือdjango-eavซึ่งเป็น refactor หลักที่อนุญาตให้ใช้ EAV กับรุ่นหรือรุ่นของ django ในแอปของบุคคลที่สาม


ฉันจะรวมไว้ในวิกิ
Ivan Kharlamov

1
ฉันจะเถียงอีกวิธีหนึ่งว่า EAV เป็นกรณีพิเศษของการสร้างแบบจำลองแบบไดนามิก มันถูกใช้อย่างหนักในชุมชน "semantic web" ซึ่งเรียกว่า "สาม" หรือ "รูปสี่เหลี่ยม" ถ้ามันมี ID เฉพาะ อย่างไรก็ตามมันไม่น่าจะมีประสิทธิภาพเท่ากลไกที่สามารถสร้างและปรับเปลี่ยนตาราง SQL แบบไดนามิกได้
Cerin

@GDom eav-django เป็นตัวเลือกแรกของคุณหรือไม่ ฉันหมายถึงตัวเลือกใดข้างต้นที่คุณเลือก?
Moreno

1
@ Moreno ทางเลือกที่เหมาะสมจะขึ้นอยู่กับกรณีการใช้งานเฉพาะของคุณเป็นอย่างมาก ฉันใช้ทั้ง EAV และ JsonFields ด้วยเหตุผลที่ต่างกัน Django ได้รับการสนับสนุนโดยตรงในตอนนี้ดังนั้นสำหรับโครงการใหม่ฉันจะใช้สิ่งนั้นก่อนเว้นแต่ว่าฉันมีความต้องการเฉพาะที่จะสามารถสืบค้นในตาราง EAV ได้ โปรดทราบว่าคุณสามารถสืบค้น JsonFields ได้เช่นกัน
GDorn
โดยการใช้ไซต์ของเรา หมายความว่าคุณได้อ่านและทำความเข้าใจนโยบายคุกกี้และนโยบายความเป็นส่วนตัวของเราแล้ว
Licensed under cc by-sa 3.0 with attribution required.