Qual é a maneira mais eficiente de armazenar uma lista nos modelos do Django?

146

Atualmente, tenho muitos objetos python no meu código semelhantes aos seguintes:

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

Agora eu quero transformar isso em um modelo do Django, onde self.myName é um campo de string e self.myFriends é uma lista de strings.

from django.db import models

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

Como a lista é uma estrutura de dados tão comum em python, eu meio que esperava que houvesse um campo de modelo do Django para ela. Eu sei que posso usar um relacionamento ManyToMany ou OneToMany, mas esperava evitar esse indireção extra no código.

Editar:

Eu adicionei esta pergunta relacionada , que as pessoas podem achar útil.

entristecer
fonte
1
@drozzy: Bem, eu provavelmente poderia ter usado uma frase diferente, mas basicamente o que eu quis dizer foi que eu queria passar uma lista de strings e recuperar uma lista de strings. Não quero criar um monte de objetos Friend e chame inst.myFriends.add (friendObj) para cada um deles. Não que isso seria tão difícil, mas ...
lamentar

Respostas:

77

Esse relacionamento não seria melhor expresso como um relacionamento de chave estrangeira um para muitos em uma Friendstabela? Entendo que myFriendssão apenas strings, mas eu pensaria que um design melhor seria criar um Friendmodelo e MyClassconter uma relação de chave estrangeira na tabela resultante.

Andrew Hare
fonte
15
Provavelmente é isso que acabarei fazendo, mas eu realmente esperava que a estrutura subjacente fosse incorporada. Acho que sou preguiçoso.
11409
Elegante e muito bem explicado.
Tessaracter
129

"Otimização prematura é a raiz de todo o mal."

Com isso em mente, vamos fazer isso! Depois que seus aplicativos atingem um determinado ponto, a desnormalização de dados é muito comum. Feito corretamente, ele pode salvar inúmeras pesquisas caras no banco de dados, ao custo de um pouco mais de limpeza.

Para retornar um nome listde amigo, precisamos criar uma classe Django Field personalizada que retornará uma lista quando acessada.

David Cramer postou um guia para criar um SeperatedValueField em seu blog. Aqui está o 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)

A lógica desse código lida com os valores de serialização e desserialização do banco de dados para Python e vice-versa. Agora você pode facilmente importar e usar nosso campo personalizado na classe 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.
fonte
8
+1 para uma ótima resposta, mas já estamos fazendo algo assim. É realmente espremer todos os valores em uma string e depois separá-los. Acho que esperava algo mais parecido com um ListofStringsField, que realmente cria a tabela separada e torna as chaves estrangeiras automaticamente. Não tenho certeza se isso é possível no Django. Se for, e eu encontrar uma resposta, eu a publicarei no stackoverflow.
13449
2
Se for esse o caso, então você está procurando pelo django-denorm do initcrash. Você o encontrará no github: github.com/initcrash/django-denorm/tree/master
jb.
3
+1. Mas possíveis problemas com vírgulas em strings. Que tal serializar e desserializar do json?
sbeliakov
Tentando adicionar isso ao modelo existente, my_vals = SeparatedValuesField(blank=True, default="")mas obtendo o IntegrityError por causa de NULLs. O argumento padrão não está sendo transmitido corretamente?
John Lehmann
1
Note que no Django 2.1 to_pythonnão é mais necessário ler. Assim, para fazer este trabalho você precisa adicionar: def from_db_value(self, value, expression, connection, context): return self.to_python(value)
theadriangreen
46

Uma maneira simples de armazenar uma lista no Django é apenas convertê-la em uma string JSON e salvá-la como Texto no modelo. Em seguida, você pode recuperar a lista convertendo a sequência (JSON) novamente em uma lista python. Aqui está como:

A "lista" seria armazenada no seu modelo do Django da seguinte forma:

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

Na sua visão / código do controlador:

Armazenando a lista no banco de dados:

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 a lista do banco de dados:

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

Conceitualmente, aqui está o que está acontecendo:

>>> 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'>
ladrão de mente
fonte
8
Infelizmente isso não ajudá-lo a gerenciar a lista usando Django admin
GreenAsJade
25

Se você estiver usando Django> = 1.9 com o Postgres, poderá usar as vantagens do ArrayField

Um campo para armazenar listas de dados. A maioria dos tipos de campo pode ser usada; você simplesmente passa outra instância de campo como o base_field. Você também pode especificar um tamanho. ArrayField pode ser aninhado para armazenar matrizes multidimensionais.

Também é possível aninhar 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 o @ thane-brimhall mencionou, também é possível consultar elementos diretamente. Referência da documentação

wolendranh
fonte
2
A grande vantagem disso é que você pode consultar os elementos diretamente do campo da matriz.
precisa saber é o seguinte
@ThaneBrimhall você está certo. Talvez eu deva atualizar a resposta com essas informações. Graças
wolendranh
Infelizmente, não há solução para o mysql
Joel G Mathew
Deve-se mencionar que isso funciona apenas com PostGres.
theadriangreen
1
O Django 1.8 também tem ArrayField: docs.djangoproject.com/en/1.8/ref/contrib/postgres/fields
kontextify
15

Como esta é uma pergunta antiga, e as técnicas do Django devem ter mudado significativamente desde então, esta resposta reflete a versão 1.4 do Django e é provavelmente aplicável à versão 1.5.

O Django por padrão usa bancos de dados relacionais; você deve usá-los. Mapeie amizades para relações de banco de dados (restrições de chave estrangeira) com o uso de ManyToManyField. Isso permite que você use o RelatedManagers para listas de amigos, que usam conjuntos de consultas inteligentes. Você pode usar todos os métodos disponíveis, como filterou values_list.

Usando ManyToManyFieldrelações e propriedades:

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

Você pode acessar a lista de amigos de um usuário desta maneira:

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

Observe, no entanto, que essas relações são simétricas: se Joseph é amigo de Bob, então Bob é amigo de Joseph.

sleblanc
fonte
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
fonte
8

Lembre-se de que isso eventualmente acaba em um banco de dados relacional. Portanto, usar relações é realmente a maneira mais comum de resolver esse problema. Se você absolutamente insistir em armazenar uma lista no próprio objeto, poderá fazê-lo, por exemplo, separado por vírgula, armazená-lo em uma sequência e fornecer funções de acessador que dividem a sequência em uma lista. Com isso, você ficará limitado a um número máximo de strings e perderá consultas eficientes.

Martin v. Löwis
fonte
3
Eu estou bem com o banco de dados armazenando-o como uma relação, eu esperava que os modelos do Django abstraíssem essa parte para mim. Do lado do aplicativo, eu sempre vou querer tratá-lo como uma lista de strings.
11409
7

Caso esteja usando o postgres, você pode usar algo como isto:

class ChessBoard(models.Model):

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

se você precisar de mais detalhes, pode ler no link abaixo: https://docs.djangoproject.com/pt-br/1.9/ref/contrib/postgres/fields/

Marcos Souza
fonte
3

Armazenando uma lista de strings no 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

e você pode chamar assim:

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
fonte
2

Minha solução, pode ser que ajude alguém:

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
fonte
1

O uso da relação um para muitos (FK de Friend para a classe pai) tornará seu aplicativo mais escalável (como você pode estender trivialmente o objeto Friend com atributos adicionais além do nome simples). E assim, este é o melhor caminho

Guarda
fonte
3
Isso não é escalabilidade, é extensibilidade. Muitas vezes, um está à custa do outro. Nesse caso, se você souber que sempre precisará de uma lista de seqüências de caracteres, poderá evitar uma junção cara, tornando seu código mais escalável (ou seja, melhor desempenho da desnormalização).
Dustin Rasener
O acima com algumas ressalvas: 1) você sabe que você nunca quer consulta contra isso) de armazenamento 2 de dados e ainda é mais barato do que o poder de processamento e memória (quem sabe, talvez isso muda com a computação quântica)
Dustin Rasener