8đź‘Ť
Yes, it is possible. Here is an example:
models.py
from django.db import models
# Create your models here.
class NodeA(models.Model):
name_a = models.CharField(max_length=75, blank=True, null=True)
class Meta:
db_table = 'Nodes'
managed = False
class NodeB(models.Model):
name_b = models.CharField(max_length=75, blank=True, null=True)
class Meta:
db_table = 'Nodes'
managed = False
class NodeC(models.Model):
name_c = models.CharField(max_length=75, blank=True, null=True)
class Meta:
db_table = 'Nodes'
managed = False
Database schema (SQLITE)
Nodes {
id integer primary key
name_a TEXT
name_b TEXT
name_c TEXT }
Proof of concept
import NodeA, NodeB, NodeC
a = NodeA()
a.name_a = 'Node A'
a.save()
b = NodeB()
b.name_b = 'Node B'
b.save()
c = NodeC()
c.name_c = 'Node C'
c.save()
This produces:
id name_a name_b name_c
1 Node A
2 Node B
3 Node C
6đź‘Ť
I use a somewhat different approach, which plays nicely with south by creating a perspective. A perspective is a proxy which renames some fields in the model, but keep the name of the column.
For me it was an example to show the flexibility of the django ORM. I am not sure if you want to use this in production code. Therefore it is not tested enough, but it will give you some idea.
The idea
A perspective let the user creating different models to one table, which can have their own methods and have different field names, but share the underlying model and table.
It can store different types in the same table, which can be handy for logging or event systems. Every perspective can only see it’s own entries, because it is filtered on a field name action_type.
The models are unmanaged, but have a custom manager, so south doesn’t create new tables for it.
Usage
The implementation is a class decorator, which modifies the meta data of the django model. It takes in an “base” model and a dictionary of aliased fields.
Let first look at an example:
class UserLog(models.Model):
"""
A user action log system, user is not in this class, because it clutters import
"""
date_created = models.DateTimeField(_("Date created"), auto_now_add=True)
# Action type is obligatory
action_type = models.CharField(_("Action Type"), max_length=255)
integer_field1 = models.IntegerField()
integer_field2 = models.IntegerField()
char_field1 = models.CharField(max_length=255)
char_field2 = models.CharField(max_length=255)
@ModelPerspective({
'x': 'integer_field1',
'y': 'integer_field2',
'target': 'char_field1'
}, UserLog)
class UserClickLog(models.Model):
pass
This creates a model, which maps the property x to integer_field1, y to integer_field2 and target to char_field1 and where the underlying table is the same as the table as UserLog.
Usage is not different then any other model and south will only create the UserLog table.
Now let’s look how to implement this.
Implementation
How does it work?
If the class is evaluated the decorator receives the class. This will monkey patch the class, so it instances will reflect the base table as you provided.
Adding the aliases
If we walk somewhat deeper into the code. The aliased dictionary is read and for every field the base field is looked up. If we found the field in the base table, the name is changed. This has a small side effect, that it also changes the column. So we have to retrieve the field column from the base field. Then the field is added to the class with the contribute_to_class method, which take care of all the bookkeeping.
Then all not aliased properties are added to the model. This is not needed perse, but I have chosen to add them.
Setting the properties
Now we have all the fields, we have to set a couple of properties. The managed property will trick south in ignoring the table, but it has a side effect. The class will not have a manager. (We will fix that later). We also copy the table name (db_table) from the base model and make the action_type field default to the class name.
The last thing we need to do is to provide a manager. Some care has to be taken, because django states there is only one QuerySet manager. We solve this by copying the manager with deepcopy and then add a filter statement, which filters on the class name.
deepcopy(QuerySet()).filter(action_type = cls.class.name)
This let our table return only relevant records. Now wrap it up into a decorator and it is done.
This is the code:
from django.db import models
from django.db.models.query import QuerySet
def ModelPerspective(aliases, model):
"""
This class decorator creates a perspective from a model, which is
a proxy with aliased fields.
First it will loop over all provided aliases
these are pairs of new_field, old_field.
Then it will copy the old_fields found in the
class to the new fields and change their name,
but keep their columnnames.
After that it will copy all the fields, which are not aliased.
Then it will copy all the properties of the model to the new model.
Example:
@ModelPerspective({
'lusername': 'username',
'phonenumber': 'field1'
}, User)
class Luser(models.Model):
pass
"""
from copy import deepcopy
def copy_fields(cls):
all_fields = set(map(lambda x: x.name, model._meta.fields))
all_fields.remove('id')
# Copy alias fields
for alias_field in aliases:
real_field = aliases[alias_field]
# Get field from model
old_field = model._meta.get_field(real_field)
oldname, columnname = old_field.get_attname_column()
new_field = deepcopy(old_field)
# Setting field properties
new_field.name = alias_field
new_field.db_column = columnname
new_field.verbose_name = alias_field
new_field.contribute_to_class(cls, "_%s" % alias_field)
all_fields.remove(real_field)
for field in all_fields:
new_field = deepcopy(model._meta.get_field(field))
new_field.contribute_to_class(cls, "_%s" % new_field.name)
def copy_properties(cls):
# Copy db table
cls._meta.db_table = model._meta.db_table
def create_manager(cls):
from copy import deepcopy
field = cls._meta.get_field('action_type')
field.default = cls.__name__
# Only query on relevant records
qs = deepcopy(cls.objects)
cls.objects = qs.filter(action_type=cls.__name__)
def wrapper(cls):
# Set it unmanaged
cls._meta.managed = False
copy_properties(cls)
copy_fields(cls)
create_manager(cls)
return cls
return wrapper
Is this ready for production?
I wouldn’t use it in production code, for me it was an exercise to show the flexibility of django, but with sufficient testing it could be used in code if one wants.
Another argument against using in production would be that the code uses a fair amount of internal workings of the django ORM. I am not sure it the api will suffices to be stable.
And this solution is not the best solution you could think up. There are more possibilities to solve this problem of storing dynamic fields in a database.
- Django DB level default value for a column
- Gunicorn will not bind to my application
- 'CheckoutView' object has no attribute 'object'