Quelle est la manière la plus efficace de stocker une liste dans les modèles Django?

146

Actuellement, j'ai beaucoup d'objets python dans mon code similaires à ce qui suit:

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

Maintenant, je veux transformer cela en un modèle Django, où self.myName est un champ de chaîne et self.myFriends est une liste de chaînes.

from django.db import models

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

Étant donné que la liste est une structure de données si commune en python, je m'attendais à ce qu'il y ait un champ de modèle Django pour cela. Je sais que je peux utiliser une relation ManyToMany ou OneToMany, mais j'espérais éviter cette indirection supplémentaire dans le code.

Éditer:

J'ai ajouté cette question connexe , que les gens peuvent trouver utile.

pleurer
la source
1
@drozzy: Eh bien, j'aurais probablement pu utiliser une phrase différente, mais en gros ce que je voulais dire, c'est que je voulais passer une liste de chaînes et récupérer une liste de chaînes. Je ne veux pas créer un tas d'objets Friend et appeler inst.myFriends.add (friendObj) pour chacun d'eux. Non pas que ce serait si difficile, mais ...
pleurez le

Réponses:

78

Cette relation ne serait-elle pas mieux exprimée comme une relation de clé étrangère un-à-plusieurs à une Friendstable? Je comprends que ce ne myFriendssont que des chaînes, mais je pense qu'une meilleure conception serait de créer un Friendmodèle et de MyClasscontenir une relation de clé étrangère vers la table résultante.

Andrew Hare
la source
15
C'est probablement ce que je finirai par faire, mais j'espérais vraiment que la structure sous-jacente aurait été intégrée. Je suppose que je suis trop paresseux.
pleurer le
Élégant et très joliment expliqué.
Tessaracter
129

"L'optimisation prématurée est la racine de tout Mal."

Avec cela fermement à l'esprit, faisons ceci! Une fois que vos applications ont atteint un certain point, la dénormalisation des données est très courante. Fait correctement, il peut économiser de nombreuses recherches coûteuses dans la base de données au prix d'un peu plus d'entretien.

Pour renvoyer un listdes noms d'amis, nous devrons créer une classe Django Field personnalisée qui renverra une liste lors de l'accès.

David Cramer a publié un guide pour créer un SeperatedValueField sur son blog. Voici le code:

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 logique de ce code traite de la sérialisation et de la désérialisation des valeurs de la base de données vers Python et vice versa. Maintenant, vous pouvez facilement importer et utiliser notre champ personnalisé dans la classe de modèle:

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

class Person(models.Model):
    name = models.CharField(max_length=64)
    friends = SeparatedValuesField()
jb.
la source
8
+1 pour une excellente réponse, mais nous faisons déjà quelque chose comme ça. C'est vraiment écraser toutes les valeurs en une seule chaîne, puis les diviser. Je suppose que j'espérais quelque chose de plus comme un ListofStringsField, qui construit en fait la table séparée et crée automatiquement les clés étrangères. Je ne sais pas si cela est possible dans Django. Si c'est le cas, et je trouve une réponse, je la posterai sur stackoverflow.
pleurer le
2
Si tel est le cas, vous recherchez le django-denorm de initcrash. Vous le trouverez sur github: github.com/initcrash/django-denorm/tree/master
jb.
3
+1. Mais problèmes possibles avec des virgules dans les chaînes. Qu'en est-il de la sérialisation et de la désérialisation de json?
sbeliakov
Essayer d'ajouter ceci au modèle existant avec my_vals = SeparatedValuesField(blank=True, default="")mais obtenant IntegrityError en raison de NULL. L'argument par défaut n'est-il pas transmis correctement?
John Lehmann
1
Notez que dans Django 2.1 to_pythonn'est plus appelé en lecture. Ainsi, pour faire ce travail, vous devez ajouter: def from_db_value(self, value, expression, connection, context): return self.to_python(value)
theadriangreen
46

Un moyen simple de stocker une liste dans Django consiste simplement à la convertir en chaîne JSON, puis à l'enregistrer en tant que texte dans le modèle. Vous pouvez ensuite récupérer la liste en reconvertissant la chaîne (JSON) en une liste python. Voici comment:

La "liste" serait stockée dans votre modèle Django comme ceci:

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

Dans votre vue / code contrôleur:

Stockage de la liste dans la base de données:

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

Récupération de la liste de la base de données:

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

Conceptuellement, voici ce qui se passe:

>>> 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'>
voleur d'esprit
la source
8
Malheureusement, cela ne vous aide pas à gérer la liste à l'aide de django admin
GreenAsJade
25

Si vous utilisez Django> = 1.9 avec Postgres, vous pouvez profiter des avantages d' ArrayField

Un champ pour stocker des listes de données. La plupart des types de champ peuvent être utilisés, vous passez simplement une autre instance de champ comme base_field. Vous pouvez également spécifier une taille. ArrayField peut être imbriqué pour stocker des tableaux multidimensionnels.

Il est également possible d'imbriquer des champs de tableau:

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

Comme @ thane-brimhall l'a mentionné, il est également possible d'interroger des éléments directement. Référence de la documentation

Wolendranh
la source
2
Le gros avantage de ceci est que vous pouvez interroger les éléments directement à partir du champ de tableau.
Thane Brimhall
@ThaneBrimhall vous avez raison. Peut-être que je devrais mettre à jour la réponse avec ces informations. Merci
wolendranh
Malheureusement, pas de solution pour mysql
Joel G Mathew
Il convient de mentionner que cela ne fonctionne qu'avec PostGres.
theadriangreen
1
Django 1.8 a aussi ArrayField: docs.djangoproject.com/en/1.8/ref/contrib/postgres/fields
kontextify
15

Comme il s'agit d'une vieille question et que les techniques de Django ont dû changer considérablement depuis, cette réponse reflète Django version 1.4, et est probablement applicable pour la v 1.5.

Django utilise par défaut des bases de données relationnelles; vous devriez les utiliser. Mappez les amitiés aux relations de base de données (contraintes de clé étrangère) avec l'utilisation de ManyToManyField. Cela vous permet d'utiliser RelatedManagers pour les listes d'amis, qui utilisent des ensembles de requêtes intelligents. Vous pouvez utiliser toutes les méthodes disponibles telles quefilter ou values_list.

En utilisant ManyToManyField relations et des propriétés:

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

Vous pouvez accéder à la liste d'amis d'un utilisateur de cette façon:

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

Notez cependant que ces relations sont symétriques: si Joseph est un ami de Bob, alors Bob est un ami de Joseph.

Sleblanc
la source
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
la source
8

N'oubliez pas que cela doit finir par aboutir dans une base de données relationnelle. Donc, utiliser les relations est vraiment le moyen le plus courant de résoudre ce problème. Si vous insistez absolument pour stocker une liste dans l'objet lui-même, vous pouvez la créer, par exemple, séparée par des virgules, et la stocker dans une chaîne, puis fournir des fonctions d'accesseur qui divisent la chaîne en une liste. Avec cela, vous serez limité à un nombre maximum de chaînes et vous perdrez des requêtes efficaces.

Martin c.Löwis
la source
3
Je suis d'accord avec la base de données qui le stocke sous forme de relation, j'espérais que les modèles Django ont déjà extrait cette partie pour moi. Du côté de l'application, je vais toujours vouloir le traiter comme une liste de chaînes.
pleurer le
7

Si vous utilisez postgres, vous pouvez utiliser quelque chose comme ceci:

class ChessBoard(models.Model):

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

si vous avez besoin de plus de détails, vous pouvez lire le lien ci-dessous: https://docs.djangoproject.com/pt-br/1.9/ref/contrib/postgres/fields/

Marcos Souza
la source
3

Stockage d'une liste de chaînes dans le modèle 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

et vous pouvez l'appeler comme ceci:

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
la source
1

L'utilisation de la relation un-à-plusieurs (FK de la classe Friend vers la classe parente) rendra votre application plus évolutive (car vous pouvez étendre trivialement l'objet Friend avec des attributs supplémentaires au-delà du simple nom). Et c'est donc la meilleure façon

Garde
la source
3
Ce n'est pas de l'évolutivité, c'est de l'extensibilité. L'un est souvent au détriment de l'autre. Dans ce cas, si vous savez que vous aurez toujours besoin d'une liste de chaînes, vous pouvez éviter une jointure coûteuse, rendant ainsi votre code plus évolutif (c'est-à-dire plus performant à partir de la dénormalisation).
Dustin Rasener
Ce qui précède avec quelques mises en garde: 1) vous savez que vous ne voulez jamais interroger ces données et 2) le stockage est toujours moins cher que la puissance de traitement et la mémoire (qui sait, peut-être que cela change avec l'informatique quantique)
Dustin Rasener
1

Ma solution, peut-être qu'elle aide quelqu'un:

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
la source