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 want 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".
def __str__( self ): return "something" This will display the objects as something always in the admin interface.
The __str__ method just tells Django what to print when it needs to print out an instance of the any model. It is also what lets your admin panel, go from this. Note: how objects are just plainly numbered.
Each attribute of the model represents a database field. With all of this, Django gives you an automatically-generated database-access API; see Making queries.
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.
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).
"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.
The run-time performance of the view functions is the same.
The one-time class-definition has saved a few lines of code that are executed once during application start-up.
The development cost (i.e., solving this question) is higher.
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".
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With