在Django模型中,存储列表的最有效方法是什么?

191

目前我的代码中有很多类似以下的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)。虽然这并不难,但是... - grieve
这个回答是否能解决您的问题?在Django模型中存储数组是否可能? - Kit
15个回答

152

"过早的优化是万恶之源。"

记住这一点,让我们开始吧!一旦您的应用程序达到一定程度,非规范化数据就非常普遍了。如果做得正确,它可以在稍微增加一些维护成本的情况下节省大量昂贵的数据库查询。

为了返回一个好友名称 list,我们需要创建一个自定义的 Django Field 类,当它被访问时返回一个列表。

David Cramer 在他的博客上发布了一个创建 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()

13
非常感谢你给出的好答案,但我们已经在做类似的事情了。实际上,我们正在将所有值压缩成一个字符串,然后再将它们分开。我想我希望能有更像ListofStringsField这样的东西,它可以构建单独的表并自动创建外键。我不确定在Django中是否可能实现这一点。如果可以并且我找到了答案,我会在stackoverflow上发布它。 - grieve
2
如果是这种情况,那么您正在寻找initcrash的django-denorm。您可以在github上找到它:http://github.com/initcrash/django-denorm/tree/master - jb.
3
可能会出现字符串中逗号的问题。那么对于从 JSON 进行序列化和反序列化呢? - sbeliakov
尝试将my_vals = SeparatedValuesField(blank=True, default="")添加到现有模型中,但由于存在NULL值而遇到完整性错误。默认参数没有正确传递吗? - John Lehmann
1
请注意,在Django 2.1中,不再在读取时调用to_python。因此,为了使其正常工作,您需要添加以下内容:def from_db_value(self, value, expression, connection, context): return self.to_python(value) - theadriangreen
一旦您的应用程序达到一定程度,您是指该应用程序已经部署了吗?在我的数据库课上,我学到了应该仔细考虑模式,因为一旦部署后更改可能会非常复杂和昂贵... - AlwaysLearning

100

将这种关系表述为对 Friends 表的一对多外键关系不是更好吗?我知道myFriends只是字符串,但我认为更好的设计是创建一个Friend模型,并使MyClass包含到结果表的外键关系。


21
这可能是我最终要做的事情,但我真的希望这个基础结构早就建好了。我想我太懒了。 - grieve
1
优雅而最美妙的解释。 - Trect
请参考https://docs.djangoproject.com/en/3.0/topics/db/examples/many_to_one/。 - Jon Ison

69

在Django中存储列表的简单方法是将其转换为JSON字符串,然后将其保存为模型中的Text。您可以通过将(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'>

10
很遗憾,这并不能帮助你使用Django管理界面管理列表。 - GreenAsJade

46
如果您正在使用Django >= 1.9和Postgres,您可以利用ArrayField的优势。

存储数据列表的字段。大多数字段类型都可以使用,只需将另一个字段实例作为基本字段传递即可。您还可以指定大小。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 所提到的,直接查询元素也是可能的。文档 参考资料


2
这样做的一个巨大优势是您可以直接从数组字段查询元素。 - Thane Brimhall
1
@ThaneBrimhall 你说得对。也许我应该更新答案,谢谢。 - wolendranh
很遗憾,MySQL目前没有解决方案。 - Joel G Mathew
3
需要提到的是,这仅适用于PostGres。 - theadriangreen
2
Django 1.8也有ArrayField: https://docs.djangoproject.com/en/1.8/ref/contrib/postgres/fields/ - kontextify

17

由于这是一个旧问题,并且Django技术必须自那以后发生了重大变化,因此此答案反映了Django 1.4版本,并且很可能适用于v1.5。

Django默认使用关系型数据库;您应该利用它们。使用ManyToManyField将好友映射到数据库关系(外键约束)。这样做可以让您使用RelatedManagers来管理好友列表,这些列表使用智能查询集。您可以使用所有可用的方法,例如filtervalues_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

请注意这些关系是对称的:如果约瑟夫是鲍勃的朋友,那么鲍勃也是约瑟夫的朋友。


12

因为在2021年这篇文章是谷歌搜索结果中排名第一的。

MySql文档

PostgreSQL文档

from django.db.models import CharField, Model
from django_mysql.models import ListCharField

class Person(Model):
    name = CharField()
    post_nominals = ListCharField(
        base_field=CharField(max_length=10),
        size=6,
        max_length=(6 * 11)  # 6 * 10 character nominals, plus commas
    )

表单

from django.db.models import IntegerField, Model
from django_mysql.models import ListTextField

class Widget(Model):
    widget_group_ids = ListTextField(
        base_field=IntegerField(),
        size=100,  # Maximum of 100 ids in list
    )

查询

>>> Person.objects.create(name='Horatio', post_nominals=['PhD', 'Esq.', 'III'])
>>> Person.objects.create(name='Severus', post_nominals=['PhD', 'DPhil'])
>>> Person.objects.create(name='Paulus', post_nominals=[])

>>> Person.objects.filter(post_nominals__contains='PhD')
[<Person: Horatio>, <Person: Severus>]

>>> Person.objects.filter(post_nominals__contains='Esq.')
[<Person: Horatio>]

>>> Person.objects.filter(post_nominals__contains='DPhil')
[<Person: Severus>]

>>> Person.objects.filter(Q(post_nominals__contains='PhD') & Q(post_nominals__contains='III'))
[<Person: Horatio>]

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

10

记住,最终这些内容将要存储在关系型数据库中。因此使用关联(relations)确实是解决这个问题的常见方式。如果您坚持要在对象本身中存储列表,您可以将其制作成逗号分隔的字符串并在字符串中进行存储,并提供访问器函数来将该字符串拆分为列表。但是,这样做会限制您的最大字符串数量,并且您将失去有效查询的优势。


3
我对数据库将其存储为关系型数据感到满意,但我希望Django模型已经为我抽象出了这部分内容。从应用程序的角度来看,我总是希望将其视为字符串列表。 - grieve

8

在Django模型中存储字符串列表:

class Bar(models.Model):
  foo = models.TextField(blank=True)
    
  def set_list(self, element):
    self.foo += "," + element if self.foo else element
    
  def get_list(self):
    return self.foo.split(",") if self.foo else None

你可以像这样调用它:

bars = Bar()
bars.set_list("str1")
bars.set_list("str2")
bar_list = bars.get_list()

for bar in bar_list:
   print bar    

8

如果你正在使用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/


1
链接已经失效。 - Mehrdad Salimi

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接