Самый элегантный подход для записи данных JSON в реляционную базу данных с использованием Django Models? - PullRequest
17 голосов
/ 03 декабря 2011

У меня есть типичная модель реляционной базы данных, выложенная в Django, где типичная модель содержит некоторые ForeignKeys, некоторые ManyToManyFields и некоторые поля, расширяющие DateTimeField Django.

Я хочу сохранить данные, которые я получаю в формате JSON (не плоский) из внешнего API.Я не хочу, чтобы данные сохранялись в соответствующих таблицах (а не во всей строке json в одном поле).Какой самый простой и понятный подход для этого?Есть ли библиотека, позволяющая упростить эту задачу?

Вот пример, поясняющий мой вопрос:

Models-

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

сейчас,Обычно мне нужно сохранить данные строки json, которые я получаю из внешнего API (секретного API ниндзя), в эту модель, 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)

Есть много подходов, которые я мог бы использовать,

  • код над логикой в ​​функции view, которая выполняет всю работу по преобразованию json в экземпляры модели
  • код над логикой переопределяющей модели __init__ метод
  • код над логикой переопределяющей моделиsave() method
  • создать менеджера для каждой модели и закодировать эту логику в каждом из ее методов, таких как create, get_or_create, filter и т. Д.
  • extension ManyToManyField иположить туда,
  • внешнюю библиотеку?

Я бы хотелзнаете, если существует единственный наиболее очевидный способ для сохранения данных в этой json-форме в базу данных без многократного кодирования вышеуказанной логики, какой самый элегантный подход вы бы предложили?

Спасибо всем за чтение длинного поста,

Ответы [ 3 ]

10 голосов
/ 04 декабря 2011

На мой взгляд, самое чистое место для кода, который вам нужен, - это новый метод Manager (например, from_json_string) в пользовательском менеджере для модели NinjaData.

Я не думаю, что вам следует переопределять стандартные методы 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)
2 голосов
/ 04 декабря 2011

Я не знаю, знакомы ли вы с терминологией, но в основном вы пытаетесь сделать десериализацию из сериализованного / строкового формата (в данном случае JSON) вОбъекты модели Python.

Я не знаком с библиотеками Python для того, чтобы делать это с JSON, поэтому я не могу рекомендовать / одобрять какие-либо, но поиск с использованием таких терминов, как "python", "deserialization", "json", "object", и" graph ", кажется, показывает некоторую документацию Django для сериализации и библиотеку jsonpickle на github.

1 голос
/ 03 декабря 2011

У меня фактически была такая же потребность, и я написал настраиваемое поле базы данных для ее обработки. Просто сохраните следующее в модуле 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)

Пара вещей. Во-первых, если вы используете Юг, вам нужно объяснить ему, как работает ваше настраиваемое поле:

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

Во-вторых, хотя я проделал большую работу, чтобы убедиться, что это настраиваемое поле играет хорошо везде, например, аккуратно переходя между сериализованным форматом и Python. Есть одно место, где он не совсем работает должным образом, это когда он используется вместе с manage.py dumpdata, где он объединяет Python в строку, а не выгружает его в JSON, а это не то, что вам нужно. Я обнаружил, что это незначительная проблема на практике.

Дополнительная документация по написанию пользовательских полей модели .

Я утверждаю, что это единственный лучший и наиболее очевидный способ сделать это. Обратите внимание, что я также предполагаю, что вам не нужно выполнять поиск на этих данных - например, вы будете получать записи на основе других критериев, и это будет сопровождаться этим. Если вам нужно выполнить поиск на основе чего-либо в вашем JSON, убедитесь, что это истинное поле SQL (и убедитесь, что оно проиндексировано!).

...