web-dev-qa-db-ja.com

Djangoモデルを使用してJSONデータをリレーショナルデータベースに書き込むための最も洗練されたアプローチ?

Djangoに配置された典型的なリレーショナルデータベースモデルがあります。ここで、典型的なモデルには、いくつかのForeignKeys、いくつかのManyToManyFields、およびDjangoのDateTimeField

外部APIからJSON形式(フラットではない)で受信したデータを保存したい。データがそれぞれのテーブルに保存されるようにしたくありません(1つのフィールドへのjson文字列全体ではありません)。これを行うための最もクリーンでシンプルなアプローチは何ですか?このタスクを簡単にするために利用できるライブラリはありますか?

これが私の質問を明確にするための例です、

モデル-

_class NinjaData(models.Model):
    id = models.IntegerField(primary_key=True, unique=True)
    name = models.CharField(max_length=60)  
    birthdatetime = MyDateTimeField(null=True)
    deathdatetime = MyDatetimeField(null=True)
    skills = models.ManyToManyField(Skills, null=True)
    weapons = models.ManyToManyField(Weapons, null=True)
    master = models.ForeignKey(Master, null=True)

class Skills(models.Model):
    id = models.IntegerField(primary_key=True, unique=True)
    name = models.CharField(max_length=60)
    difficulty = models.IntegerField(null=True)

class Weapons(models.Model):
    id = models.IntegerField(primary_key=True, unique=True)
    name = models.CharField(max_length=60)
    weight = models.FloatField(null=True)

class Master(models.Model):
    id = models.IntegerField(primary_key=True, unique=True)
    name = models.CharField(max_length=60)
    is_awesome = models.NullBooleanField()
_

今、私は通常、外部API(秘密の忍者API)から取得したjson文字列データをこのモデルに保存する必要があります。jsonは次のようになります

JSON-

_{
"id":"1234",
"name":"Hitori",
"birthdatetime":"11/05/1999 20:30:00",
"skills":[
    {
    "id":"3456",
    "name":"stealth",
    "difficulty":"2"
    },
    {
    "id":"678",
    "name":"karate",
    "difficulty":"1"
    }
],
"weapons":[
    {
    "id":"878",
    "name":"shuriken",
    "weight":"0.2"
    },
    {
    "id":"574",
    "name":"katana",
    "weight":"0.5"
    }
],
"master":{
    "id":"4",
    "name":"Schi fu",
    "is_awesome":"true"
    }
}
_

これで、典型的なManyToManyFieldを処理するためのロジックはかなり単純になりました。

論理コード-

_data = json.loads(ninja_json)
ninja = NinjaData.objects.create(id=data['id'], name=data['name'])

if 'weapons' in data:
    weapons = data['weapons']
    for weapon in weapons:
        w = Weapons.objects.get_or_create(**weapon)  # create a new weapon in Weapon table
        ninja.weapons.add(w)

if 'skills' in data:
    ...
    (skipping rest of the code for brevity)
_

私が使用できる多くのアプローチがあります、

  • jsonをモデルインスタンスに変換するすべての作業を行うview関数のロジックの上のコード
  • モデルの___init___メソッドをオーバーライドするロジックの上のコード
  • モデルのsave()メソッドをオーバーライドするロジックの上のコード
  • モデルごとにManagerを作成し、このロジックをcreate、_get_or_create_、filterなどの各メソッド内にコーディングします。
  • ManyToManyFieldを拡張してそこに置き、
  • 外部ライブラリ?

上記のロジックを複数回コーディングせずに、このjson形式のデータをデータベースに保存する単一の最も明白な方法があるかどうかを知りたいのですが、あなたが提案する最もエレガントなアプローチは何ですか?

長い投稿を読んでくれてありがとう、

17
Optimus

私の意見では、必要なコードの最もクリーンな場所は、NinjaDataモデルのカスタムマネージャーの新しいManagerメソッド(例:from_json_string)です。

標準のcreate、get_or_createなどのメソッドをオーバーライドする必要はないと思います。これは、通常とは少し異なることを行っているため、通常どおりに機能し続けることをお勧めします。

更新:ある時点でこれが必要になる可能性があることに気付いたので、ジェネリック関数をコーディングして軽くテストしました。再帰的に通過して他のモデルに影響を与えるため、Managerメソッドとして属しているかどうかはわかりません。おそらく、スタンドアロンのヘルパー関数である必要があります。

def create_or_update_and_get(model_class, data):
    get_or_create_kwargs = {
        model_class._meta.pk.name: data.pop(model_class._meta.pk.name)
    }
    try:
        # get
        instance = model_class.objects.get(**get_or_create_kwargs)
    except model_class.DoesNotExist:
        # create
        instance = model_class(**get_or_create_kwargs)
    # update (or finish creating)
    for key,value in data.items():
        field = model_class._meta.get_field(key)
        if not field:
            continue
        if isinstance(field, models.ManyToManyField):
            # can't add m2m until parent is saved
            continue
        Elif isinstance(field, models.ForeignKey) and hasattr(value, 'items'):
            rel_instance = create_or_update_and_get(field.rel.to, value)
            setattr(instance, key, rel_instance)
        else:
            setattr(instance, key, value)
    instance.save()
    # now add the m2m relations
    for field in model_class._meta.many_to_many:
        if field.name in data and hasattr(data[field.name], 'append'):
            for obj in data[field.name]:
                rel_instance = create_or_update_and_get(field.rel.to, obj)
                getattr(instance, field.name).add(rel_instance)
    return instance

# for example:
from Django.utils.simplejson import simplejson as json

data = json.loads(ninja_json)
ninja = create_or_update_and_get(NinjaData, data)
10
Anentropic

あなたが用語に精通しているかどうかはわかりませんが、基本的にあなたがやろうとしているのは、シリアル化された/文字列からのde-serializeです(この場合はJSON)をPythonモデルオブジェクトにフォーマットします。

私はJSONでこれを行うためのPythonライブラリに精通していないので、推奨/推奨することはできませんが、「python」、「deserialization」、「json」などの用語を使用して検索します、 "object"、および "graph"は シリアル化のためのいくつかのDjangoドキュメント およびgithub上のライブラリ jsonpickle を明らかにするようです。

2
Weston C

私は実際にこれと同じニーズがあり、それを処理するためのカスタムデータベースフィールドを作成しました。以下をプロジェクトのPythonモジュール(たとえば、適切なアプリのfields.pyファイル)に保存し、インポートして使用します。

class JSONField(models.TextField):
    """Specialized text field that holds JSON in the database, which is
    represented within Python as (usually) a dictionary."""

    __metaclass__ = models.SubfieldBase

    def __init__(self, blank=True, default='{}', help_text='Specialized text field that holds JSON in the database, which is represented within Python as (usually) a dictionary.', *args, **kwargs):
        super(JSONField, self).__init__(*args, blank=blank, default=default, help_text=help_text, **kwargs)

    def get_prep_value(self, value):
        if type(value) in (str, unicode) and len(value) == 0:
            value = None
        return json.dumps(value)

    def formfield(self, form_class=JSONFormField, **kwargs):
        return super(JSONField, self).formfield(form_class=form_class, **kwargs)

    def bound_data(self, data, initial):
        return json.dumps(data)

    def to_python(self, value):
        # lists, dicts, ints, and booleans are clearly fine as is
        if type(value) not in (str, unicode):
            return value

        # empty strings were intended to be null
        if len(value) == 0:
            return None

        # NaN should become null; Python doesn't have a NaN value
        if value == 'NaN':
            return None

        # try to tell the difference between a "normal" string
        # and serialized JSON
        if value not in ('true', 'false', 'null') and (value[0] not in ('{', '[', '"') or value[-1] not in ('}', ']', '"')):
            return value

        # okay, this is a JSON-serialized string
        return json.loads(value)

いくつかのこと。まず、Southを使用している場合は、カスタムフィールドがどのように機能するかを説明する必要があります。

from south.modelsinspector import add_introspection_rules
add_introspection_rules([], [r'^feedmagnet\.tools\.fields\.models\.JSONField'])

次に、シリアル化された形式とPythonの間をきれいに行き来するなど、このカスタムフィールドがどこでもNiceを再生できるようにするために多くの作業を行いました。正しく機能しない場所が1つあります。それは、manage.py dumpdataと組み合わせて使用​​する場合です。ここでは、JSONにダンプするのではなく、Pythonを文字列に結合します。これはあなたが望んでいることではありません。これは実際の練習では小さな問題であることがわかりました。

カスタムモデルフィールドの記述 に関するその他のドキュメント。

私は、これがこれを行うための唯一の最良かつ最も明白な方法であると断言します。また、このデータに対してルックアップを実行する必要はないと想定していることに注意してください-例:他の基準に基づいてレコードを取得しますが、これはそれに伴います。 JSON内の何かに基づいてルックアップを実行する必要がある場合は、それが真のSQLフィールドであることを確認してください(そして、インデックスが付けられていることを確認してください!)。

1
Luke Sneeringer