views:

447

answers:

4

Hi!

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

+4  A: 

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

S.Lott
Why the cocky attitude? I understand your view here completely. But in this case, I wanted the currencies as columns in the database - not as a related model. Using a separate model (and database table) introduces a SQL join that's simply not OK in this case where there is a fixed set of currencies that changes really seldom (like about once a year or so). Adding a second level of abstraction is nice, but also introduces overhead both in terms of performance and complexity.
mojbro
I agree my choice of words here, "at run time", is not the best. But I completely disagree on your opinion that there are absolutely no circumstances where you'd like to programmatically alter the model definitions.
mojbro
@mojbro: You're missing the point. The point is not one of performance. It's a matter of definition. When using the relational model, there is absolutely no reason to try to add columns at run time. If you want to change your question, I can change my answer. But the relational model was very carefully designed to make adding attributes at run time completely unnecessary. Any attempt to add attributes at run time means, one overlooked a relational design pattern.
S.Lott
@mojbro: If you have a fixed set of currencies and want to denormalize them into columns, you're not adding anything at **run-time**. Please correct your question to describe what you really want to do.
S.Lott
Okay, I just give up. You're totally focused on the wording "run time", even though I specifically comment that the wording "run time" wasn't the best. What I really want is code that produces the model definition. At the time when the model is defined (which happens in run time in Python, by the way). A better wording might be "at compile time" (which really isn't the case in Python).
mojbro
And, by the way, I totally understand your point of view here. But defining some data structures "flat" rather than nested, isn't necessarily always a bad thing.
mojbro
@S.Lott: I've updated the question now. Hope it's clearer.
mojbro
@mojbro: (1) comments are not the question; the question said "run time" and that's simply not necessary. (2) denormalization has nothing to do with "run time", which is what the question said. (3) You have to make the question correct; comments don't count.
S.Lott
Count? What are you counting? And why do you even use comments then? I was just trying to get help with an issue.
mojbro
@mojbro: You cannot claim the comments are part of the question. I'm trying to make this clear to you. Please do not simply add comments to a question and claim that "changes" the question. I was focused on the question -- as written -- until you rewrote the question. Merely throwing comments around is not a change to the question. I have no idea what "counting" you're talking about. You OWN the question. It's YOUR question. Your use of comments in an answer makes the question hard for others to follow.
S.Lott
@S.Lott: Of course you are right. The question is the question. But what I was referring to was your own comment of this answer. In the conversation we were having. And yes, your new answer is good (even though I don't agree fully, but that's a different matter). But this is just getting silly, so I think we should just stop here. Thanks for the edit anyway - it was good.
mojbro
"what I was referring to"? What reference are you talking about? Without a quote, I cannot possibly understand that comment.
S.Lott
OK, I give up. "But this is just getting silly, so I think we should just stop here."
mojbro
@mojbro: I don't get it. I'd like to fix my comment or my answer. Clearly, some part was confusing. You said something was confusing. Now you're saying you refuse to state what specifically was confusing. How can I improve if you cannot identify the error or confusion? What's the point of saying you refuse to help? Why not silently refuse to help? I'm baffled by this. Can you provide the specific thing that was confusing or erroneous so I can fix it?
S.Lott
OK. I wanted to add fields to a Django model class at class creation time, but wrote "run time". That was obviously misleading. I write to you in the comment field of this answer, that "I agree my choice of words here, 'at run time', is not the best.". I thought it was obvious (it obviously wasn't) that what I wanted to do was to add these fields at class creation time. Then you state: "If you have a fixed set of currencies and want to denormalize them into columns, you're not adding anything at run-time." (to be continued)
mojbro
I probably misinterpreted your answer - but I read it like you weren't interested in helping me out (answering how I can create my model class programmatically, rather than by typing it in), but rather trying to teach me about model design patterns - whether to use a normalized or denormalized structure. But that wasn't the question. I realize you probably really didn't understand the question. But I interpreted it like you on purpose chose not to understand what I meant.
mojbro
And by the way - the number of currencies I would use are actually three. Might be four in the future. Why didn't I want it nested? I understand your arguments against flat structures, and even more your arguments against the readability issues of the code. And I believe you're right. I just got pissed by the (probably incorrect) interpretation of your answers that you simply didn't want to answer my question, but rather show off your ideas about design patterns.
mojbro
Hope that clears out my strange reaction.
mojbro
@mojbro: "you probably really didn't understand the question". Always, the default assumption. I'm not you. I can't know what's in your head. You're not the first to claim that your question was good and the answers are coming from idiots. If you aren't getting good answers, you need to fix your question.
S.Lott
Am I blaming you for not understanding the question? No. What I was trying to say is: **my question written in a bad way**, so **you probably didn't understand what I meant** (of course, because I wrote the question in a bad way), and **i misinterpreted your anwser** (because I thought you had understood my question). When did I claim my question was good? When did I say the answers was coming from idiots? I was simply stating that there was a misunderstanding going on. I just don't get the angry attitude...
mojbro
A: 

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.

Felix Kling
I understand your opinion here, but there are reasons why I want to do it. A related model simply reduces performance too much to be acceptable in my case. Hard-coding the currencies at multiple places aren't good when the shop want to add a new currency. This happens very seldom, and when that happens, it's completely the database will of course have to be changed. Just like when you edit the model. The "CURRENCIES" list is more or less part of the model definition here.
mojbro
+1  A: 

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

Łukasz
Great! I didn't know you could do that (use a for loop in the class definition). That solves my problem perfectly. I agree this is bad - manipulating models at run time can be seen as bad for several reasons - but I still think there are times it is necessary.
mojbro
+2  A: 

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.

insin
Great! I think my main issue then is that I used add_to_class rather than contribute_to_class. And yup, I agree it's a bit ugly. My entire question is a bit ugly I suppose.
mojbro
Hey, you gotta do what you gotta do :)
insin
I think I've gotta read up on the Django internals a bit.
mojbro
Thanks for the edit! You're right here. The `add_to_class` should work, and for some reason my code run several times. Now I have a check for that and everything works just fine. Thanks!
mojbro