开发者

Programmatically specifying Django model attributes

I would like to add attributes to a Django models programmatically. At class creation time (the time of the definition of the model class). The model is not going to change after that in run time. For instance, lets say I want to define a Car model class and want to add one price attribute (database column) per currency, given a list of currencies. (This list of currencies should be considered a constant that won't change runtime. I don't w开发者_如何学Cant a related model for these prices.)

What would be the best way to do this?

I had an approach that I thought would work, but it didn't exactly. This is how I tried doing it, using the car example above:

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

This does seem to work pretty well at first sight:

$ ./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)

But when I try to create a new Car, it doesn't really work anymore:

>>> 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...
                                                             ^

Apparently, though, my code seem to run several times, causing the "price_eur" attribute to be added several times.

Comment: Initially I used the wording "at run time" ("I would like to add attributes to a Django models programmatically, at run time."). This wording wasn't the best. What I really want to was to add those fields at "model definition time" or "class creation time".


My solution is something which is bad from various reasons, but it works:

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

In the place where I had to do this I had choice between something like that and maintaining table with more than 200 columns (I know, how bad it was, but I had no influence on it).


The still not nice, but nicer than using locals solution is to use Field.contribute_to_class:

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

I used it for MPTT (for which I've been a very bad maintainer *hides*)

Edit: On second thoughts, your code was working fine (Car.add_to_class calls the field's contribute_to_class for you) but the problem seems to be that the code to add the additional fields is being executed multiple times, so your model thinks it needs to save multiple fields with the same name. You need to put something in there to make sure you only dynamically add the fields once.

django.db.models.fields.Field.contribute_to_class calls django.db.models.options.Options.add_field (the object's _meta attribute is an instance of an Options), which doesn't check to see if a field with that name already exists and happily adds the field details to the list of fields it knows about.


"I would like to add attributes to a Django models programmatically. At class creation time"

Don't. "Programmatically" adding columns is silly and confusing. It seems fine to you -- the developer -- who deeply gets the nuances of Django.

For us, the maintainers, that code will (a) make no sense and (b) have to be replaced with simple, obvious code that does the same job the simplest most obvious way.

Remember, the maintainers are violent sociopaths who know where you live. Pander to them with simple and obvious code.

There's no reason to replace a cluster of simple attribute definitions with a tricky-looking loop. There's no improvement and no savings.

  1. The run-time performance of the view functions is the same.

  2. The one-time class-definition has saved a few lines of code that are executed once during application start-up.

  3. The development cost (i.e., solving this question) is higher.

  4. The maintenance cost (i.e., turning this code over to someone else to keep it running) is astronomically high. The code will simply be replaced with something simpler and more obvious.

Even if you have 100's of currencies, this is still a perfectly bad idea.

"I don't want a related model for these prices"

Why not? A related model is (1) simple, (2) obvious, (3) standard, (4) extensible. It has almost no measurable cost at run time or development time and certainly no complexity.

"For instance, lets say I have a Car model class and want to add one price attribute (database column) per currency, given a list of currencies."

That is not a new attribute at all.

That is a new value (in a row) of a table that has Car Model and Currency as the keys.

You class looks something like this:

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

Previous Version of the question

"I would like to add attributes to a Django models programmatically, at run time."

Don't. There are absolutely no circumstances under which you ever want to add attributes to a database "at run time".


You cannot do this at run time. It would change the database model and the database is changed via the command syncdb and somehow this feels really ugly.

Why not create a second model Price that holds the price for different currencies. Or if possible convert the price on the fly to a specific currency.

0

上一篇:

下一篇:

精彩评论

暂无评论...
验证码 换一张
取 消

最新问答

问答排行榜