以编程方式指定Django模型属性

11 投票
4 回答
6666 浏览
提问于 2025-04-15 20:45

我想要在Django模型中以编程方式添加属性,也就是在定义模型类的时候就添加。这个模型在运行时不会再改变。比如说,我想定义一个Car(汽车)模型类,并且想要根据给定的货币列表为每种货币添加一个price(价格)属性(数据库列)。这个货币列表应该是一个常量,在运行时不会改变。我不想为这些价格创建一个相关的模型。

那么,最好的方法是什么呢?

我曾经有一个想法,觉得可以实现,但结果并不是这样。我尝试用上面的汽车例子来做:

from django.db import models

class Car(models.Model):
    name = models.CharField(max_length=50)

currencies = ['EUR', 'USD']
for currency in currencies:
    Car.add_to_class('price_%s' % currency.lower(), models.IntegerField())

乍一看,这似乎是有效的:

$ ./manage.py syncdb
Creating table shop_car

$ ./manage.py dbshell
shop=# \d shop_car
                                  Table "public.shop_car"
  Column   |         Type          |                       Modifiers                       
-----------+-----------------------+-------------------------------------------------------
 id        | integer               | not null default nextval('shop_car_id_seq'::regclass)
 name      | character varying(50) | not null
 price_eur | integer               | not null
 price_usd | integer               | not null
Indexes:
    "shop_car_pkey" PRIMARY KEY, btree (id)

但是当我尝试创建一个新的汽车时,它就不再有效了:

>>> from shop.models import Car
>>> mycar = Car(name='VW Jetta', price_eur=100, price_usd=130)
>>> mycar
<Car: Car object>
>>> mycar.save()
Traceback (most recent call last):
  File "<console>", line 1, in <module>
  File "/Library/Frameworks/Python.framework/Versions/2.6/lib/python2.6/site-packages/django/db/models/base.py", line 410, in save
    self.save_base(force_insert=force_insert, force_update=force_update)
  File "/Library/Frameworks/Python.framework/Versions/2.6/lib/python2.6/site-packages/django/db/models/base.py", line 495, in save_base
    result = manager._insert(values, return_id=update_pk)
  File "/Library/Frameworks/Python.framework/Versions/2.6/lib/python2.6/site-packages/django/db/models/manager.py", line 177, in _insert
    return insert_query(self.model, values, **kwargs)
  File "/Library/Frameworks/Python.framework/Versions/2.6/lib/python2.6/site-packages/django/db/models/query.py", line 1087, in insert_query
    return query.execute_sql(return_id)
  File "/Library/Frameworks/Python.framework/Versions/2.6/lib/python2.6/site-packages/django/db/models/sql/subqueries.py", line 320, in execute_sql
    cursor = super(InsertQuery, self).execute_sql(None)
  File "/Library/Frameworks/Python.framework/Versions/2.6/lib/python2.6/site-packages/django/db/models/sql/query.py", line 2369, in execute_sql
    cursor.execute(sql, params)
  File "/Library/Frameworks/Python.framework/Versions/2.6/lib/python2.6/site-packages/django/db/backends/util.py", line 19, in execute
    return self.cursor.execute(sql, params)
ProgrammingError: column "price_eur" specified more than once
LINE 1: ...NTO "shop_car" ("name", "price_eur", "price_usd", "price_eur...
                                                             ^

显然,我的代码似乎运行了好几次,导致“price_eur”这个属性被添加了多次。

补充说明:最开始我用的说法是“在运行时”(“我想在运行时以编程方式向Django模型添加属性。”)。这个说法并不是很好。我真正想要的是在“模型定义时”或“类创建时”添加这些字段。

4 个回答

2

“我想在创建Django模型时,程序化地添加属性。”

别这么做。“程序化”地添加列是个傻主意,容易让人困惑。对你这个开发者来说,可能觉得没问题,因为你对Django的细节很了解。

但对我们这些维护者来说,这段代码会(a)毫无意义,(b)需要用简单明了的代码替换掉,做到同样的事情,越简单越好。

记住,维护者可不是好惹的,他们知道你住哪儿。为了他们的方便,写简单明了的代码吧。

没有理由用一个复杂的循环来替代一堆简单的属性定义。这样做没有任何好处,也没有节省。

  1. 视图函数的运行性能是一样的。

  2. 一次性定义类虽然节省了几行代码,但这些代码只在应用启动时执行一次。

  3. 开发成本(也就是解决这个问题的成本)更高。

  4. 维护成本(也就是把这段代码交给别人维护的成本)非常高。最终这段代码会被更简单明了的代码替换掉。

即使你有上百种货币,这个主意依然是个坏主意。

“我不想为这些价格建立一个相关模型。”

为什么不呢?相关模型(1)简单,(2)明显,(3)标准,(4)可扩展。它在运行时或开发时几乎没有成本,当然也没有复杂性。

“比如说,我有一个汽车模型类,想为每种货币添加一个价格属性(数据库列),给定一个货币列表。”

这根本不是一个新属性。

这只是一个表中新的值,表的键是汽车模型和货币。

你的类看起来大概是这样的:

class Price( models.Model ):
    car = models.ForeignKey( Car )
    currency = models.ForeignKey( Currency )
    amount = models.DecimalField()

问题的前一个版本

“我想在运行时为Django模型添加属性。”

别这么做。绝对没有任何情况下你需要在“运行时”向数据库添加属性。

5

我的解决方案虽然有很多不好的地方,但总算是能用:

from django.db import models

currencies = ["EUR", "USD"]

class Car(models.Model):

    name = models.CharField(max_length=50)

    for currency in currencies:
        locals()['price_%s' % currency.lower()] = models.IntegerField()

在我需要做这个的地方,我只能在这种方法和维护一个有200多个列的表格之间选择(我知道这样不好,但我没有办法改变这个情况)。

4

虽然这个方法还是不太完美,但比起用 locals 来说要好一些,可以使用 Field.contribute_to_class

for currency in currencies:
    models.IntegerField().contribute_to_class(Car, 'price_%s' % currency.lower())

我在 MPTT 中用过这个方法(我一直是个很糟糕的维护者 *躲起来*)

补充:仔细想想,你的代码其实是可以正常工作的(Car.add_to_class 会自动调用字段的 contribute_to_class),但问题似乎在于添加额外字段的代码被执行了多次,这导致你的模型认为需要保存多个同名字段。你需要在这里加点东西,确保只动态添加一次字段。

django.db.models.fields.Field.contribute_to_class 会调用 django.db.models.options.Options.add_field(对象的 _meta 属性是一个 Options 的实例),而这个方法并不会检查是否已经存在同名的字段,它会很高兴地把字段信息添加到它知道的字段列表中。

撰写回答