Каков наиболее эффективный способ хранения списка в моделях Django?

151

В настоящее время в моем коде много объектов Python, похожих на следующие:

class MyClass():
  def __init__(self, name, friends):
      self.myName = name
      self.myFriends = [str(x) for x in friends]

Теперь я хочу превратить это в модель Django, где self.myName - это строковое поле, а self.myFriends - это список строк.

from django.db import models

class myDjangoModelClass():
    myName = models.CharField(max_length=64)
    myFriends = ??? # what goes here?

Поскольку список - это такая распространенная структура данных в Python, я ожидал, что для него будет поле модели Django. Я знаю, что могу использовать отношения ManyToMany или OneToMany, но я надеялся избежать этого дополнительного косвенного обращения в коде.

Редактировать:

Я добавил этот связанный вопрос , который может оказаться полезным.

горевать
источник
1
@drozzy: Ну, я, наверное, мог бы использовать другую фразу, но в основном я имел в виду, что хочу передать список строк и получить обратно список строк. Я не хочу создавать кучу объектов Friend и вызывать inst.myFriends.add (friendObj) для каждого из них. Не то чтобы это было бы так сложно, но ...
горе

Ответы:

82

Разве это отношение не было бы лучше выражено как отношение внешнего ключа «один ко многим» к Friendsтаблице? Я понимаю, что myFriendsэто просто строки, но я думаю, что лучше было бы создать Friendмодель, которая MyClassсодержала бы привязку внешнего ключа к результирующей таблице.

Эндрю Хэйр
источник
15
Вероятно, именно этим я и займусь, но я действительно надеялся, что базовая структура для этого была бы встроена. Думаю, я слишком ленив.
горевать
Элегантно и прекрасно объяснено.
Dheeraj M Pai
131

«Преждевременная оптимизация - корень всех зол».

Имея это в виду, давайте сделаем это! Как только ваши приложения достигают определенной точки, денормализация данных становится очень распространенным явлением. При правильном выполнении он может сэкономить множество дорогостоящих поисков в базе данных за счет немного большего количества операций.

Чтобы вернуть список listимен друзей, нам нужно создать собственный класс Django Field, который будет возвращать список при доступе.

Дэвид Крамер опубликовал руководство по созданию SeperatedValueField в своем блоге. Вот код:

from django.db import models

class SeparatedValuesField(models.TextField):
    __metaclass__ = models.SubfieldBase

    def __init__(self, *args, **kwargs):
        self.token = kwargs.pop('token', ',')
        super(SeparatedValuesField, self).__init__(*args, **kwargs)

    def to_python(self, value):
        if not value: return
        if isinstance(value, list):
            return value
        return value.split(self.token)

    def get_db_prep_value(self, value):
        if not value: return
        assert(isinstance(value, list) or isinstance(value, tuple))
        return self.token.join([unicode(s) for s in value])

    def value_to_string(self, obj):
        value = self._get_val_from_obj(obj)
        return self.get_db_prep_value(value)

Логика этого кода связана с сериализацией и десериализацией значений из базы данных в Python и наоборот. Теперь вы можете легко импортировать и использовать наше настраиваемое поле в классе модели:

from django.db import models
from custom.fields import SeparatedValuesField 

class Person(models.Model):
    name = models.CharField(max_length=64)
    friends = SeparatedValuesField()
jb.
источник
8
+1 за отличный ответ, но мы уже делаем что-то подобное. Он действительно сжимает все значения в одну строку, а затем разделяет их. Думаю, я надеялся на что-то большее, чем ListofStringsField, который фактически строит отдельную таблицу и автоматически создает внешние ключи. Я не уверен, возможно ли это в Django. Если это так, и я найду ответ, я отправлю его в stackoverflow.
горевать
2
В таком случае вам нужен django-denorm от initcrash. Вы найдете его на github: github.com/initcrash/django-denorm/tree/master
jb.
3
+1. Но возможны проблемы с запятыми в строках. А как насчет сериализации и десериализации из json?
sbeliakov
Попытка добавить это к существующей модели, my_vals = SeparatedValuesField(blank=True, default="")но с получением IntegrityError из-за NULL. Аргумент по умолчанию не передается правильно?
Джон Леманн
1
Обратите внимание, что в Django 2.1 to_pythonбольше не вызывается чтение. Таким образом, чтобы эта работа работала, вам нужно добавить: def from_db_value(self, value, expression, connection, context): return self.to_python(value)
theadriangreen
48

Простой способ сохранить список в Django - просто преобразовать его в строку JSON, а затем сохранить его как текст в модели. Затем вы можете получить список, преобразовав строку (JSON) обратно в список Python. Вот как:

«Список» будет храниться в вашей модели Django следующим образом:

class MyModel(models.Model):
    myList = models.TextField(null=True) # JSON-serialized (text) version of your list

В вашем коде представления / контроллера:

Сохранение списка в базе данных:

import simplejson as json # this would be just 'import json' in Python 2.7 and later
...
...

myModel = MyModel()
listIWantToStore = [1,2,3,4,5,'hello']
myModel.myList = json.dumps(listIWantToStore)
myModel.save()

Получение списка из базы данных:

jsonDec = json.decoder.JSONDecoder()
myPythonList = jsonDec.decode(myModel.myList)

По идее, вот что происходит:

>>> myList = [1,2,3,4,5,'hello']
>>> import simplejson as json
>>> myJsonList = json.dumps(myList)
>>> myJsonList
'[1, 2, 3, 4, 5, "hello"]'
>>> myJsonList.__class__
<type 'str'>
>>> jsonDec = json.decoder.JSONDecoder()
>>> myPythonList = jsonDec.decode(myJsonList)
>>> myPythonList
[1, 2, 3, 4, 5, u'hello']
>>> myPythonList.__class__
<type 'list'>
умник
источник
8
К сожалению, это не поможет вам управлять списком с помощью администратора django
GreenAsJade
28

Если вы используете Django> = 1.9 с Postgres, вы можете использовать преимущества ArrayField

Поле для хранения списков данных. Можно использовать большинство типов полей, вы просто передаете другой экземпляр поля как base_field. Вы также можете указать размер. ArrayField может быть вложенным для хранения многомерных массивов.

Также можно вкладывать поля массива:

from django.contrib.postgres.fields import ArrayField
from django.db import models

class ChessBoard(models.Model):
    board = ArrayField(
        ArrayField(
            models.CharField(max_length=10, blank=True),
            size=8,
        ),
        size=8,
    )

Как упоминал @ thane-brimhall, также можно напрямую запрашивать элементы. Справочная документация

Wolendranh
источник
2
Большим преимуществом этого является то, что вы можете запрашивать элементы прямо из поля массива.
Тейн Бримхолл
@ThaneBrimhall, ты прав. Возможно, мне следует обновить ответ этой информацией. Спасибо
wolendranh
К сожалению, для mysql нет решения
Джоэл Г. Мэтью
Следует отметить, что это работает только с PostGres.
theadriangreen
1
Django 1.8 также имеет ArrayField: docs.djangoproject.com/en/1.8/ref/contrib/postgres/fields
kontextify
15

Поскольку это старый вопрос, и методы Django должны были значительно измениться с тех пор, этот ответ отражает версию Django 1.4 и, скорее всего, применим к версии 1.5.

Django по умолчанию использует реляционные базы данных; вы должны использовать их. Сопоставьте дружеские отношения с отношениями базы данных (ограничения внешнего ключа) с помощью ManyToManyField. Это позволяет использовать связанные менеджеры для списков друзей, в которых используются интеллектуальные наборы запросов. Вы можете использовать все доступные методы, такие как filterили values_list.

Использование ManyToManyFieldотношений и свойств:

class MyDjangoClass(models.Model):
    name = models.CharField(...)
    friends = models.ManyToManyField("self")

    @property
    def friendlist(self):
        # Watch for large querysets: it loads everything in memory
        return list(self.friends.all())

Вы можете получить доступ к списку друзей пользователя следующим образом:

joseph = MyDjangoClass.objects.get(name="Joseph")
friends_of_joseph = joseph.friendlist

Однако обратите внимание, что эти отношения симметричны: если Джозеф - друг Боба, то Боб - друг Джозефа.

слеблан
источник
10
class Course(models.Model):
   name = models.CharField(max_length=256)
   students = models.ManyToManyField(Student)

class Student(models.Model):
   first_name = models.CharField(max_length=256)
   student_number = models.CharField(max_length=128)
   # other fields, etc...

   friends = models.ManyToManyField('self')
Андрей Дроздюк
источник
8

Помните, что в конечном итоге это должно оказаться в реляционной базе данных. Так что использование отношений действительно является обычным способом решения этой проблемы. Если вы абсолютно настаиваете на хранении списка в самом объекте, вы можете сделать его, например, разделенным запятыми и сохранить его в строке, а затем предоставить функции доступа, которые разбивают строку на список. При этом вы будете ограничены максимальным количеством строк, и вы потеряете эффективные запросы.

Мартин против Лёвиса
источник
3
Меня устраивает, что база данных хранит это как отношение, я надеялся, что модели Django уже абстрагировали эту часть для меня. Со стороны приложения я всегда хочу рассматривать его как список строк.
горевать
7

Если вы используете postgres, вы можете использовать что-то вроде этого:

class ChessBoard(models.Model):

    board = ArrayField(
        ArrayField(
            models.CharField(max_length=10, blank=True),
            size=8,
        ),
        size=8,
    )

если вам нужно больше информации, вы можете прочитать по ссылке ниже: https://docs.djangoproject.com/pt-br/1.9/ref/contrib/postgres/fields/

Маркос Соуза
источник
4

Сохранение списка строк в модели Django:

class Bar(models.Model):
    foo = models.TextField(blank=True)

    def set_list(self, element):
        if self.foo:
            self.foo = self.foo + "," + element
        else:
            self.foo = element

    def get_list(self):
        if self.foo:
            return self.foo.split(",")
        else:
            None

и вы можете назвать это так:

bars = Bar()
bars.set_list("str1")
bars.set_list("str2")
list = bars.get_list()
if list is not None:
    for bar in list:
        print bar
else:
    print "List is empty."      
Ахтишем
источник
1

Использование отношения «один ко многим» (FK от Friend к родительскому классу) сделает ваше приложение более масштабируемым (поскольку вы можете тривиально расширить объект Friend с помощью дополнительных атрибутов, помимо простого имени). Таким образом, это лучший способ

Охранник
источник
3
Это не масштабируемость, это расширяемость. Часто одно происходит за счет другого. В этом случае, если вы знаете, что вам всегда будет нужен список строк, вы можете избежать дорогостоящего соединения, тем самым сделав ваш код более масштабируемым (т.е. более производительным от денормализации).
Dustin Rasener
Вышеупомянутое с парой предостережений: 1) вы знаете, что никогда не хотите запрашивать эти данные и 2) хранение по-прежнему дешевле, чем вычислительная мощность и память (кто знает, может быть, это изменится с квантовыми вычислениями)
Дастин Разенер,
1

Мое решение, может быть, это кому-то поможет:

import json
from django.db import models


class ExampleModel(models.Model):
    _list = models.TextField(default='[]')

    @property
    def list(self):
        return json.loads(self._list)

    @list.setter
    def list(self, value):
        self._list = json.dumps(self.list + value)
Стефаницкий
источник