Самый элегантный подход для записи данных JSON в реляционную базу данных с использованием моделей Django?
У меня есть типичная модель реляционной базы данных, изложенная в Django, где типичная модель содержит некоторые ForeignKeys
, некоторые ManyToManyFields
и некоторые поля, которые расширяют Django DateTimeField
.
Я хочу сохранить данные, которые я получаю в формате JSON (не flat) из внешнего api. Я не хочу, чтобы данные сохранялись в соответствующих таблицах (а не всей строке 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()
теперь мне обычно приходится сохранять json-строковые данные, которые я получаю из внешней api (secret ninja 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()
- создайте Менеджер для каждой модели и закодируйте эту логику внутри каждого из своих методов, таких как
create
, get_or_create
, filter
и т.д.
- продолжите
ManyToManyField
и поместите туда,
- внешняя библиотека?
Я хотел бы знать, существует ли единственный наиболее очевидный способ, чтобы сохранить данные в этой форме json в базе данных, не кодируя вышеуказанную логику несколько раз, что было бы самым элегантным подходом, который вы бы предложить?
Спасибо всем за чтение длинного сообщения,
Ответы
Ответ 1
На мой взгляд, самым чистым местом для кода, который вам нужен, является новый метод Менеджера (например, from_json_string) в пользовательском менеджере для модели NinjaData.
Я не думаю, что вы должны переопределить стандартные методы create, get_or_create и т.д., поскольку вы делаете что-то немного отличное от того, что они обычно делают, и хорошо, чтобы они работали нормально.
Update:
Я понял, что, вероятно, захочу это для себя в какой-то момент, поэтому я закодировал и слегка протестировал общую функцию. Поскольку он рекурсивно проходит и влияет на другие модели, я уже не уверен, что он принадлежит методу диспетчера и, вероятно, должен быть автономной вспомогательной функцией.
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
Я не знаю, знакомы ли вы с терминологией, но то, что вы в основном пытаетесь сделать, - де-сериализация из сериализованного/строкового формата (в данном случае JSON) в объекты модели Python.
Я не знаком с библиотеками Python для этого с JSON, поэтому я не могу рекомендовать/одобрять любые, но поиск с использованием таких терминов, как "python", "deserialization", "json", "object" и "graph", кажется, раскрывает некоторую документацию Django для сериализации и библиотеку jsonpickle на github.
Ответ 3
У меня на самом деле была такая же потребность, и я написал собственное поле базы данных для его обработки. Просто сохраните следующее в модуле 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 (и убедитесь, что оно проиндексировано!).