¿Cuál es la forma más eficiente de almacenar una lista en los modelos Django?

146

Actualmente tengo muchos objetos de python en mi código similares a los siguientes:

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

Ahora quiero convertir esto en un modelo de Django, donde self.myName es un campo de cadena y self.myFriends es una lista de cadenas.

from django.db import models

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

Dado que la lista es una estructura de datos tan común en python, esperaba que hubiera un campo modelo de Django para ella. Sé que puedo usar una relación ManyToMany o OneToMany, pero esperaba evitar esa indirección adicional en el código.

Editar:

Agregué esta pregunta relacionada , que la gente puede encontrar útil.

afligirse
fuente
1
@drozzy: Bueno, probablemente podría haber usado una frase diferente, pero básicamente lo que quise decir es que quiero pasar una lista de cadenas y recuperar una lista de cadenas. No quiero crear un montón de objetos Friend, y llamar a inst.myFriends.add (friendObj) para cada uno de ellos. No es que sea tan difícil, pero ...
lamento el

Respuestas:

77

¿Esta relación no se expresaría mejor como una relación de clave externa de uno a muchos con una Friendstabla? Entiendo que myFriendsson solo cadenas, pero creo que un mejor diseño sería crear un Friendmodelo y MyClasscontener una relación de clave externa para la tabla resultante.

Andrew Hare
fuente
15
Esto es probablemente lo que terminaré haciendo, pero realmente esperaba que la estructura subyacente para esto se hubiera incorporado. Supongo que soy demasiado flojo.
duelo
Elegante y muy bellamente explicado.
Tessaracter
129

"La optimización temprana es la raíz de todo mal."

Con eso en mente, ¡hagamos esto! Una vez que sus aplicaciones llegan a cierto punto, la desnormalización de datos es muy común. Hecho correctamente, puede ahorrar numerosas búsquedas costosas de bases de datos a costa de un poco más de limpieza.

Para devolver un nombre listde amigo, necesitaremos crear una clase de campo Django personalizada que devolverá una lista cuando se acceda.

David Cramer publicó una guía para crear un SeperatedValueField en su blog. Aquí está el código:

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)

La lógica de este código trata de serializar y deserializar valores de la base de datos a Python y viceversa. Ahora puede importar y usar fácilmente nuestro campo personalizado en la clase de modelo:

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

class Person(models.Model):
    name = models.CharField(max_length=64)
    friends = SeparatedValuesField()
jb.
fuente
8
+1 para una gran respuesta, pero ya estamos haciendo algo como esto. Realmente es aplastar todos los valores en una cadena y luego dividirlos. Supongo que esperaba algo más como un ListofStringsField, que realmente construye la tabla separada y crea las claves foráneas automáticamente. No estoy seguro de si eso es posible en Django. Si es así, y encuentro una respuesta, la publicaré en stackoverflow.
duelo
2
Si ese es el caso, entonces está buscando django-denorm de initcrash. Lo encontrará en github: github.com/initcrash/django-denorm/tree/master
jb.
3
+1. Pero posibles problemas con comas en cadenas. ¿Qué pasa con la serialización y deserialización de json?
sbeliakov
Intentando agregar esto al modelo existente con my_vals = SeparatedValuesField(blank=True, default="")pero obteniendo IntegrityError debido a NULLs. ¿El argumento predeterminado no se pasa correctamente?
John Lehmann
1
Tenga en cuenta que en Django 2.1 to_pythonya no se requiere lectura. Por lo tanto, para que esto funcione, debe agregar: def from_db_value(self, value, expression, connection, context): return self.to_python(value)
theadriangreen
46

Una forma sencilla de almacenar una lista en Django es simplemente convertirla en una cadena JSON y luego guardarla como Texto en el modelo. Luego puede recuperar la lista convirtiendo la cadena (JSON) nuevamente en una lista de Python. Así es cómo:

La "lista" se almacenaría en su modelo de Django así:

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

En su código de vista / controlador:

Almacenar la lista en la base de datos:

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

Recuperando la lista de la base de datos:

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

Conceptualmente, esto es lo que está sucediendo:

>>> 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'>
ladronzuelo
fuente
8
Por desgracia, esto no ayuda a gestionar la lista usando Django administrador
GreenAsJade
25

Si está utilizando Django> = 1.9 con Postgres, puede aprovechar las ventajas de ArrayField

Un campo para almacenar listas de datos. Se pueden usar la mayoría de los tipos de campo, simplemente pasa otra instancia de campo como base_field. También puede especificar un tamaño. ArrayField se puede anidar para almacenar matrices multidimensionales.

También es posible anidar campos de matriz:

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

Como @ thane-brimhall mencionó, también es posible consultar elementos directamente. Referencia de la documentación

Wolendranh
fuente
2
La gran ventaja de esto es que puede consultar los elementos directamente desde el campo de matriz.
Thane Brimhall
@ThaneBrimhall tienes razón. Tal vez debería actualizar la respuesta con esta información. Gracias
wolendranh
Lamentablemente, no hay solución para mysql
Joel G Mathew
Cabe mencionar que esto solo funciona con PostGres.
theadriangreen
1
Django 1.8 también tiene ArrayField: docs.djangoproject.com/en/1.8/ref/contrib/postgres/fields
kontextify
15

Como esta es una pregunta antigua, y las técnicas de Django deben haber cambiado significativamente desde entonces, esta respuesta refleja la versión 1.4 de Django, y probablemente sea aplicable para la versión 1.5.

Django por defecto usa bases de datos relacionales; deberías usarlos. Asigne amistades a relaciones de bases de datos (restricciones de clave externa) con el uso de ManyToManyField. Hacerlo le permite usar RelatedManagers para listas de amigos, que usan conjuntos de consultas inteligentes. Puede usar todos los métodos disponibles como filtero values_list.

Usando ManyToManyFieldrelaciones y propiedades:

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

Puede acceder a la lista de amigos de un usuario de esta manera:

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

Sin embargo, tenga en cuenta que estas relaciones son simétricas: si Joseph es amigo de Bob, entonces Bob es amigo de Joseph.

sleblanc
fuente
9
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')
Andriy Drozdyuk
fuente
8

Recuerde que esto finalmente tiene que terminar en una base de datos relacional. Entonces, usar las relaciones es realmente la forma común de resolver este problema. Si insiste absolutamente en almacenar una lista en el propio objeto, podría hacerlo, por ejemplo, separado por comas, y almacenarlo en una cadena, y luego proporcionar funciones de acceso que dividen la cadena en una lista. Con eso, estará limitado a un número máximo de cadenas y perderá consultas eficientes.

Martin v. Löwis
fuente
3
Estoy de acuerdo con que la base de datos lo almacene como una relación, esperaba que los modelos de Django ya me hubieran abstraído esa parte. Desde el lado de la aplicación, siempre voy a querer tratarlo como una lista de cadenas.
llorar
3

Almacenar una lista de cadenas en el modelo 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

y puedes llamarlo así:

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."      
Ahtisham
fuente
2

Mi solución, puede ser que ayude a alguien:

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)
stefanitsky
fuente
1

El uso de la relación uno a muchos (FK de amigo a clase principal) hará que su aplicación sea más escalable (ya que puede extender trivialmente el objeto amigo con atributos adicionales más allá del nombre simple). Y esta es la mejor manera.

Guardia
fuente
3
Eso no es escalabilidad, es extensibilidad. A menudo, uno es a costa del otro. En este caso, si sabe que siempre necesitará una lista de cadenas, puede evitar una unión costosa, lo que hace que su código sea más escalable (es decir, más eficaz desde la desnormalización).
Dustin Rasener
Lo anterior con un par de advertencias: 1) sabes que nunca quieres consultar esos datos y 2) el almacenamiento sigue siendo más barato que el poder de procesamiento y la memoria (quién sabe, tal vez esto cambie con la computación cuántica)
Dustin Rasener