1. ホーム
  2. python

[解決済み] Djangoの動的モデルフィールド

2022-04-23 13:51:14

質問

私は今 マルチテナント アプリケーションで、一部のユーザーが独自のデータフィールドを定義して(管理者経由で)フォームに追加データを収集し、データについてレポートを作成することができます。 後者の場合、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がSiteに対してForeignKeyを持つことに注意してください。各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でテーブルをオンザフライで変更する。 これではスケールしないし、ハックしすぎという理由もある。
  • NoSQLのようなスキーマレスソリューション。 反対はしないが、やはり相性が悪い。 最終的にはこのデータ を入力し、サードパーティのレポートアプリケーションを使用する可能性があります。
  • JSONFieldは、上記の通り、クエリとの相性が悪いので。

どのように解決するのですか?

現在のところ、4つのアプローチがあり、そのうち2つは特定のストレージバックエンドを必要とします。

  1. Django-eav (オリジナルのパッケージはもう管理されていませんが、いくつかの フォーク繁盛 )

    このソリューションは エンティティの属性値 データモデルで、基本的には、オブジェクトの動的属性を格納するためにいくつかのテーブルを使用します。このソリューションの優れた点は、以下の通りです。

    • は、いくつかの純粋でシンプルな Django モデルを使って動的なフィールドを表現しています。
    • のような簡単なコマンドで、Djangoモデルに動的属性ストレージを効果的にアタッチ/デタッチすることができます。

      eav.unregister(Encounter)
      eav.register(Patient)
      
      
    • Django の管理画面とうまく統合される ;

    • 同時に、本当にパワフルであること。

    デメリット

    • あまり効率的でない。 これはむしろEAVパターン自体への批判であり、カラム形式からモデル内のキーと値のペアのセットにデータを手動でマージする必要があります。
    • メンテナンスが大変。 データの整合性を保つには、複数列のユニークキー制約が必要であり、データベースによっては非効率的な場合がある。
    • を選択する必要があります。 フォークの1つ 公式パッケージはもはやメンテナンスされておらず、明確なリーダーが存在しないからです。

    使い方はいたって簡単です。

    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. PostgreSQLのHstore、JSONまたはJSONBフィールド

    PostgreSQLはさらに複雑なデータ型をいくつかサポートしています。 ほとんどはサードパーティのパッケージでサポートされていますが、近年 Django はそれらを django.contrib.postgres.fields に採用しました。

    HStoreField :

    Django-hstore はもともとサードパーティ製のパッケージでしたが、Django 1.8で HStoreField を、PostgreSQLがサポートする他のいくつかのフィールド型と一緒に組み込みで使用することができます。

    この方法は、ダイナミックフィールドとリレーショナルデータベースという、両方の良いところを取り入れることができるという意味で優れています。しかし、hstoreは パフォーマンス的に理想的ではない 特に、1つのフィールドに何千ものアイテムを格納することになる場合は、注意が必要です。 また、値として文字列しかサポートしていません。

    #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エンコード可能なあらゆるデータ型をサポートし、Hstoreよりも高速で(JSONBでは)コンパクトになる傾向があります。 いくつかのパッケージが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を リストフィールド などがあります。

    この 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(ジャンゴミュータント は、完全に動的な外部キーと m2m フィールドを実装しています。また、以下のような素晴らしい、しかしややハック的なソリューションに触発されています。 ウィル・ハーディー やMichael Hallのような有名な作家の作品もあります。

    これらはすべてDjango South hooksをベースにしており、それによると DjangoCon 2011 での Will Hardy の講演内容 (ご覧ください!) は、それにもかかわらず、堅牢で実運用でテストされている ( 関連するソースコード ).

    最初に これを実装する マイケル・ホール .

    そう、これはマジックです。これらのアプローチで、あなたは実現することができます。 完全に動的な 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',
                   )