如何从Django模型类继承并覆盖以创建一个listOfStringsField?

8
我希望创建一种新的Django模型字段类型,基本上是一个字符串列表。因此,在您的模型代码中,您将拥有以下内容:

models.py:

from django.db import models

class ListOfStringsField(???):
    ???

class myDjangoModelClass():
    myName = models.CharField(max_length=64)
    myFriends = ListOfStringsField() # 

other.py:

myclass = myDjangoModelClass()
myclass.myName = "bob"
myclass.myFriends = ["me", "myself", "and I"]

myclass.save()

id = myclass.id

loadedmyclass = myDjangoModelClass.objects.filter(id__exact=id)

myFriendsList = loadedclass.myFriends
# myFriendsList is a list and should equal ["me", "myself", "and I"]

如果按照以下规定,你会如何编写此字段类型?

  • 我们不想创建一个将所有字符串压缩在一起并用令牌分隔它们的字段,比如this。这是一种好的解决方案,在某些情况下非常有效,但我们希望保持字符串数据的规范化,以便除Django之外的工具可以查询数据。
  • 该字段应自动创建任何需要存储字符串数据的辅助表。
  • 辅助表最好只有每个唯一字符串的一个副本。这是可选的,但最好有。

查看Django代码,似乎我应该做类似于ForeignKey正在做的事情,但文档很少。

这引出了以下问题:

  • 这能做到吗?
  • 是否已经完成了(如果完成了,那在哪里)?
  • Django有关于如何扩展和覆盖他们的模型类,特别是他们的关系类的文档吗?我没有看到太多关于他们代码这方面的文档,但是有this

这来自于问题


1
肯定是可能的,尽管我不知道具体细节。我建议阅读他们的邮件列表并查看文档,但最终您可能需要深入研究Django源代码。 - AlbertoPL
我正在研究源代码。如果我真的能够找到一些可行的东西,我会尝试在这里发布它。 - grieve
我已经发布了我的尝试,似乎可以工作。 - grieve
所以我的下面的尝试确实可以工作,但是非常脆弱且容易出错。 :( - grieve
5个回答

7

关于创建自定义字段的文档在这里有非常好的说明。

但是,我认为你可能想得太多了。听起来你只是想要一个标准外键,但额外能够将所有元素作为单个列表检索。所以最简单的方法就是使用ForeignKey,并在模型上定义一个get_myfield_as_list方法:

class Friends(model.Model):
    name = models.CharField(max_length=100)
    my_items = models.ForeignKey(MyModel)

class MyModel(models.Model):
    ...

    def get_my_friends_as_list(self):
        return ', '.join(self.friends_set.values_list('name', flat=True))

现在,在 MyModel 的实例上调用 get_my_friends_as_list() 方法将会返回一个字符串列表,就像你需要的那样。

我本以为ForeignKey应该属于Friends类。我有什么遗漏吗?是的,我可能过于深思熟虑了,但如果我能创建我想要的字段,我认为它会普遍有用。 - grieve
我认为在这里使用外键(FK)是一个不错的选择。赞同将获取朋友列表的方法添加进去。 - googletorp

5
我认为你的方法是错误的。试图让Django字段创建一个辅助数据库表几乎肯定是错误的方法。这样做会非常困难,并且如果你试图使你的解决方案普遍有用,很可能会让第三方开发人员感到困惑。
如果你想将非规范化的数据块存储在单个列中,我建议采用类似于你链接到的方法,将Python数据结构序列化并存储在TextField中。如果你希望除了Django之外的工具也能够操作数据,那么可以将其序列化为JSON(或其他具有广泛语言支持的格式)。
from django.db import models
from django.utils import simplejson

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

    def to_python(self, value):
        if value is None: 
            return None
        if not isinstance(value, basestring): 
            return value
        return simplejson.loads(value)

    def get_db_prep_save(self, value):
        if value is None: 
            return None
        return simplejson.dumps(value)

如果您只需要一个类似于Django Manager的描述符,让您可以操作与模型关联的字符串列表,那么您可以手动创建一个联接表,并使用描述符来管理关系。这不完全是您所需的,但这段代码可以帮助您入门

我刚刚有机会查看了您发布的链接。我认为它在实现方面比我目前拥有的更接近我想要做的事情。谢谢! - grieve
如果只是为了去规范化,那么请使用这种方法进行反规范化!否则,请规范化! - Soviut
请注意,get_db_prep_save现在需要4个参数(自Django 1.5起)。 - GreenAsJade

5

你所描述的听起来与标签非常相似。
那为什么不使用django tagging呢?
它非常好用,您可以独立安装它,它的API也很容易使用。


此外,如果它不能按照原帖作者的要求工作,很容易查看它是如何完成的并根据自己的需求进行更改。 - Dave Vogt
不错!我需要进一步研究一下。 - grieve

2
感谢所有提供答案的人。即使我没有直接使用你们的答案,例子和链接也让我朝着正确的方向前进了。
我不确定这是否可以用于生产环境,但到目前为止,在我进行的所有测试中,它似乎都在正常工作。
class ListValueDescriptor(object):

   def __init__(self, lvd_parent, lvd_model_name, lvd_value_type, lvd_unique, **kwargs):
      """
         This descriptor object acts like a django field, but it will accept
         a list of values, instead a single value.
         For example:
            # define our model
            class Person(models.Model):
               name = models.CharField(max_length=120)
               friends = ListValueDescriptor("Person", "Friend", "CharField", True, max_length=120)

            # Later in the code we can do this
            p = Person("John")
            p.save() # we have to have an id
            p.friends = ["Jerry", "Jimmy", "Jamail"]
            ...
            p = Person.objects.get(name="John")
            friends = p.friends
            # and now friends is a list.
         lvd_parent - The name of our parent class
         lvd_model_name - The name of our new model
         lvd_value_type - The value type of the value in our new model
                        This has to be the name of one of the valid django
                        model field types such as 'CharField', 'FloatField',
                        or a valid custom field name.
         lvd_unique - Set this to true if you want the values in the list to
                     be unique in the table they are stored in. For
                     example if you are storing a list of strings and
                     the strings are always "foo", "bar", and "baz", your
                     data table would only have those three strings listed in
                     it in the database.
         kwargs - These are passed to the value field.
      """
      self.related_set_name = lvd_model_name.lower() + "_set"
      self.model_name = lvd_model_name
      self.parent = lvd_parent
      self.unique = lvd_unique

      # only set this to true if they have not already set it.
      # this helps speed up the searchs when unique is true.
      kwargs['db_index'] = kwargs.get('db_index', True)

      filter = ["lvd_parent", "lvd_model_name", "lvd_value_type", "lvd_unique"]

      evalStr = """class %s (models.Model):\n""" % (self.model_name)
      evalStr += """    value = models.%s(""" % (lvd_value_type)
      evalStr += self._params_from_kwargs(filter, **kwargs) 
      evalStr += ")\n"
      if self.unique:
         evalStr += """    parent = models.ManyToManyField('%s')\n""" % (self.parent)
      else:
         evalStr += """    parent = models.ForeignKey('%s')\n""" % (self.parent)
      evalStr += "\n"
      evalStr += """self.innerClass = %s\n""" % (self.model_name)

      print evalStr

      exec (evalStr) # build the inner class

   def __get__(self, instance, owner):
      value_set = instance.__getattribute__(self.related_set_name)
      l = []
      for x in value_set.all():
         l.append(x.value)

      return l

   def __set__(self, instance, values):
      value_set = instance.__getattribute__(self.related_set_name)
      for x in values:
         value_set.add(self._get_or_create_value(x))

   def __delete__(self, instance):
      pass # I should probably try and do something here.


   def _get_or_create_value(self, x):
      if self.unique:
         # Try and find an existing value
         try:
            return self.innerClass.objects.get(value=x)
         except django.core.exceptions.ObjectDoesNotExist:
            pass

      v = self.innerClass(value=x)
      v.save() # we have to save to create the id.
      return v

   def _params_from_kwargs(self, filter, **kwargs):
      """Given a dictionary of arguments, build a string which 
      represents it as a parameter list, and filter out any
      keywords in filter."""
      params = ""
      for key in kwargs:
         if key not in filter:
            value = kwargs[key]
            params += "%s=%s, " % (key, value.__repr__())

      return params[:-2] # chop off the last ', '

class Person(models.Model):
   name = models.CharField(max_length=120)
   friends = ListValueDescriptor("Person", "Friend", "CharField", True, max_length=120)

我认为,如果将这个功能深入到Django代码中,并使其更像ManyToManyField或ForeignKey,那么它的表现会更好。


我注意到在这个类中,你必须先保存再添加,因为ID直到保存后才存在。我认为如果从RelatedField和Field继承,这个问题可以得到解决/纠正,但我仍在努力理解那段代码。 - grieve
经过更多的尝试,这种方法似乎可行,但是非常脆弱,特别是在命名空间方面,它必须存在于models.py中。我会继续努力,并希望开发出更清晰的版本。 - grieve

-1

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