Django bidang model dinamis

161

Saya sedang mengerjakan aplikasi multi-penyewa di mana beberapa pengguna dapat menentukan bidang data mereka sendiri (melalui admin) untuk mengumpulkan data tambahan dalam bentuk dan melaporkan data tersebut. Bit terakhir membuat JSONField bukan pilihan yang bagus, jadi saya punya solusi berikut:

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

Perhatikan bagaimana CustomDataField memiliki ForeignKey ke Situs - setiap Situs akan memiliki kumpulan bidang data khusus yang berbeda, tetapi menggunakan database yang sama. Kemudian berbagai bidang data konkret dapat didefinisikan sebagai:

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

Ini mengarah ke penggunaan berikut:

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?

Tapi ini terasa sangat kikuk, terutama dengan kebutuhan untuk secara manual membuat data terkait dan mengaitkannya dengan model konkret. Apakah ada pendekatan yang lebih baik?

Opsi yang telah dibuang sebelumnya:

  • Kustom SQL untuk memodifikasi tabel on-the-fly. Sebagian karena ini tidak akan skala dan sebagian karena terlalu banyak peretasan.
  • Solusi tanpa skema seperti NoSQL. Saya tidak menentang mereka, tetapi mereka masih tidak cocok. Pada akhirnya data ini diketik, dan ada kemungkinan menggunakan aplikasi pelaporan pihak ketiga.
  • JSONField, seperti yang tercantum di atas, karena tidak akan berfungsi dengan baik dengan kueri.
GDorn
sumber

Jawaban:

277

Sampai hari ini, ada empat pendekatan yang tersedia, dua di antaranya membutuhkan backend penyimpanan tertentu:

  1. Django-eav (paket asli tidak lagi dipertahankan tetapi memiliki beberapa garpu yang berkembang )

    Solusi ini didasarkan pada model data Entity Attribute Value , pada dasarnya, ia menggunakan beberapa tabel untuk menyimpan atribut dinamis objek. Bagian hebat dari solusi ini adalah:

    • menggunakan beberapa model Django murni dan sederhana untuk mewakili bidang dinamis, yang membuatnya mudah dipahami dan database-agnostik;
    • memungkinkan Anda untuk secara efektif melampirkan / melepaskan penyimpanan atribut dinamis ke model Django dengan perintah sederhana seperti:

      eav.unregister(Encounter)
      eav.register(Patient)
    • Terintegrasi dengan baik dengan Django admin ;

    • Pada saat yang sama menjadi sangat kuat.

    Kerugian:

    • Tidak terlalu efisien. Ini lebih merupakan kritik terhadap pola EAV itu sendiri, yang mengharuskan penggabungan data secara manual dari format kolom ke satu set pasangan nilai kunci dalam model.
    • Sulit dipertahankan. Menjaga integritas data memerlukan batasan kunci unik multi-kolom, yang mungkin tidak efisien pada beberapa basis data.
    • Anda harus memilih salah satu dari garpu , karena paket resmi tidak lagi dipertahankan dan tidak ada pemimpin yang jelas.

    Penggunaannya cukup mudah:

    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. Bidang Hstore, JSON atau JSONB di PostgreSQL

    PostgreSQL mendukung beberapa tipe data yang lebih kompleks. Sebagian besar didukung melalui paket pihak ketiga, tetapi dalam beberapa tahun terakhir Django telah mengadopsinya ke django.contrib.postgres.fields.

    HStoreField :

    Django-hstore pada awalnya adalah paket pihak ketiga, tetapi Django 1.8 menambahkan HStoreField sebagai built-in, bersama dengan beberapa jenis bidang yang didukung PostgreSQL lainnya.

    Pendekatan ini baik dalam arti memungkinkan Anda memiliki yang terbaik dari kedua dunia: bidang dinamis dan basis data relasional. Namun, hstore tidak ideal untuk performa , terutama jika Anda akan menyimpan ribuan item dalam satu bidang. Ini juga hanya mendukung string untuk nilai.

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

    Dalam shell Django Anda dapat menggunakannya seperti ini:

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

    Anda dapat mengeluarkan kueri yang diindeks terhadap bidang 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 :

    Bidang JSON / JSONB mendukung semua tipe data yang dikodekan JSON, bukan hanya pasangan kunci / nilai, tetapi juga cenderung lebih cepat dan (untuk JSONB) lebih kompak daripada Hstore. Beberapa paket mengimplementasikan JSON / JSONB field termasuk django-pgfields , tetapi pada Django 1.9, JSONField adalah built-in menggunakan JSONB untuk penyimpanan. JSONField mirip dengan HStoreField, dan dapat bekerja lebih baik dengan kamus besar. Ini juga mendukung tipe selain string, seperti bilangan bulat, boolean dan kamus bersarang.

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

    Menciptakan di shell:

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

    Kueri yang diindeks hampir identik dengan HStoreField, kecuali jika nesting mungkin dilakukan. Indeks kompleks mungkin memerlukan pembuatan secara manual (atau migrasi yang dituliskan).

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

    Atau adaptasi NoSQL Django lainnya - dengan itu Anda dapat memiliki model yang sepenuhnya dinamis.

    Pustaka NoSQL Django hebat, tetapi perlu diingat bahwa mereka tidak 100% kompatibel dengan Django, misalnya, untuk bermigrasi ke Django-nonrel dari Django standar, Anda harus mengganti ManyToMany dengan ListField di antara hal-hal lainnya.

    Lihat contoh Django MongoDB ini:

    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, ...}

    Anda bahkan dapat membuat daftar tertanam dari model Django apa pun:

    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-mutan: Model dinamis berdasarkan syncdb dan South-hooks

    Django-mutan mengimplementasikan Foreign Key dan m2m bidang yang sepenuhnya dinamis. Dan terinspirasi oleh solusi luar biasa tetapi agak retas oleh Will Hardy dan Michael Hall.

    Semua ini didasarkan pada kait Selatan Django, yang, menurut ceramah Will Hardy di DjangoCon 2011 (tonton saja!) Namun kuat dan diuji dalam produksi ( kode sumber yang relevan ).

    Pertama yang mengimplementasikan ini adalah Michael Hall .

    Ya, ini ajaib, dengan pendekatan ini Anda dapat mencapai aplikasi, model, dan bidang Django yang sepenuhnya dinamis dengan backend basis data relasional apa pun. Tetapi berapa biayanya? Apakah kestabilan aplikasi akan terganggu karena penggunaan yang berlebihan? Ini adalah pertanyaan yang harus dipertimbangkan. Anda harus memastikan untuk mempertahankan kunci yang tepat untuk memungkinkan perubahan permintaan database secara simultan.

    Jika Anda menggunakan Michael Halls lib, kode Anda akan terlihat seperti ini:

    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',
                   )
Ivan Kharlamov
sumber
3
topik ini baru saja dibicarakan di DjangoCon 2013 Eropa: slideshare.net/schacki/… dan youtube.com/watch?v=67wcGdk4aCc
Aleck Landgraf
Mungkin juga patut dicatat bahwa menggunakan django-pgjson di Postgres> = 9.2 memungkinkan penggunaan langsung bidang json postgresql. Pada Django> = 1,7, filter API untuk kueri relatif waras. Postgres> = 9.4 juga memungkinkan bidang jsonb dengan indeks yang lebih baik untuk kueri yang lebih cepat.
GDorn
1
Diperbarui hari ini untuk mencatat adopsi Django atas HStoreField dan JSONField ke dalam contrib. Ini mencakup beberapa bentuk widget yang tidak luar biasa, tetapi berfungsi jika Anda perlu mengubah data di admin.
GDorn
13

Saya telah berusaha mendorong ide django-dynamo lebih lanjut. Proyek ini masih tidak berdokumen tetapi Anda dapat membaca kode di https://github.com/charettes/django-mutant .

Sebenarnya bidang FK dan M2M (lihat contrib.related) juga berfungsi dan bahkan mungkin untuk menentukan pembungkus untuk bidang khusus Anda sendiri.

Ada juga dukungan untuk opsi model seperti unique_together dan pemesanan plus basis Model sehingga Anda dapat subkelas model proxy, abstrak atau mixin.

Saya benar-benar bekerja pada mekanisme kunci yang tidak ada dalam memori untuk memastikan bahwa definisi model dapat dibagikan di beberapa instance django yang sedang berjalan sambil mencegah mereka menggunakan definisi usang.

Proyek ini masih sangat alfa tetapi merupakan teknologi landasan untuk salah satu proyek saya sehingga saya harus membawanya ke produksi siap. Rencana besar mendukung django-nonrel juga sehingga kita dapat memanfaatkan driver mongodb.

Simon Charette
sumber
1
Hai, Simon! Saya telah menyertakan tautan ke proyek Anda dalam jawaban wiki saya tepat setelah Anda membuatnya di github. :))) Senang bertemu Anda di stackoverflow!
Ivan Kharlamov
4

Penelitian lebih lanjut mengungkapkan bahwa ini adalah kasus yang agak istimewa dari pola desain Nilai Atribut Entitas , yang telah diterapkan untuk Django oleh beberapa paket.

Pertama, ada proyek eav -django asli , yang ada di PyPi.

Kedua, ada garpu yang lebih baru dari proyek pertama, Django- EAV yang terutama merupakan refactor untuk memungkinkan penggunaan EAV dengan model atau model Django sendiri di aplikasi pihak ketiga.

GDorn
sumber
Saya akan memasukkannya ke dalam wiki.
Ivan Kharlamov
1
Saya berpendapat sebaliknya, bahwa EAV adalah kasus khusus pemodelan dinamis. Ini banyak digunakan di komunitas "web semantik" di mana ia disebut "triple" atau "quad" jika itu termasuk ID unik. Namun, sepertinya tidak pernah seefisien mekanisme yang secara dinamis dapat membuat dan memodifikasi tabel SQL.
Cerin
@GDom adalah eav-django pilihan pertama Anda? Maksud saya opsi mana di atas yang Anda pilih?
Moreno
1
@Moreno Pilihan yang tepat akan sangat bergantung pada kasus penggunaan khusus Anda. Saya telah menggunakan EAV dan JsonFields karena berbagai alasan. Yang terakhir ini didukung langsung oleh Django sekarang, jadi untuk proyek baru saya akan menggunakannya terlebih dahulu kecuali saya memiliki kebutuhan khusus untuk dapat melakukan query pada tabel EAV. Perhatikan bahwa Anda dapat melakukan kueri di JsonFields juga.
GDorn