Changes between Initial Version and Version 1 of DynamicModels


Ignore:
Timestamp:
05/09/2007 02:45:14 PM (12 years ago)
Author:
Marty Alchin <gulopine@…>
Comment:

--

Legend:

Unmodified
Added
Removed
Modified
  • DynamicModels

    v1 v1  
     1
     2= Dynamic models =
     3
     4[[TOC]]
     5
     6As of [5163], Django models can now be created dynamically at run-time, rather than being defined in a Python source file. While this may seem useless on the surface, it actually provides a powerful way to prototype a Django project, even by users who don't know a thing about Python! This page lists the basic technique involved in creating models at run-time, as well as some examples of how it could be used. It is not meant to be exhaustive, nor is it meant to be drop-in code for your own project. Read and learn, don't copy.
     7
     8== The basic technique ==
     9
     10Internally, Django uses metaclasses to create models based on a class you provide in your source code. Without getting into too many details, that means that rather than your classes being the actual models, Django receives a description of your class, which it uses to create a model in its place.
     11
     12Thankfully, Python treats classes like any other object, so we can force this process to occur even when the class wasn't defined in source. All we really need is to give Django a description of a class, and let it do all the work of creating the model for us. Here's some bare-bones code.
     13
     14{{{
     15#!python
     16model = type(name, (models.Model,), attrs)
     17}}}
     18
     19As you can see, describing a model for the purposes of metaclasses involves three pieces of information:
     20
     21 * The intended name of the class
     22 * A tuple containing any classes it inherits from
     23 * A dictionary containing the attributes of the class
     24
     25Python then processes that description as if it came from a normal class declaration, automatically triggering Django's metaclass along the way. In fact, these three bits of information correspond exactly to standard definitions, as shown in the following comparison.
     26
     27This:
     28
     29{{{
     30#!python
     31class Person(models.Model):
     32    first_name = models.CharField(maxlength=255)
     33    last_name = models.CharField(maxlength=255)
     34}}}
     35
     36is functionally equivalent to this:
     37
     38{{{
     39#!python
     40Person = type('Person', (models.Model,), {
     41    'first_name': models.CharField(maxlength=255)
     42    'last_name': models.CharField(maxlength=255)
     43})
     44}}}
     45
     46Yes, that is slightly more code than the standard technique, but the advantages of not having to declare a model in Python source should become clear shortly.
     47
     48== A general-purpose approach ==
     49
     50To illustrate in a simple manner how easy it can be to create full models on the fly, consider the following function.
     51
     52{{{
     53#!python
     54def create_model(name, fields=None, app_label='', module='', options=None, admin=None):
     55    "One example of how to create a model dynamically at run-time"
     56
     57    class Meta:
     58        # Using type('Meta', ...) gives a dictproxy error during model creation
     59        pass
     60
     61    if app_label:
     62        # app_label must be set using the Meta inner class
     63        setattr(Meta, 'app_label', app_label)
     64
     65    # Update Meta with any options that were provided
     66    if options is not None:
     67        for key, value in options.items():
     68            setattr(Meta, key, value)
     69
     70    # Set up a dictionary to simulate declarations within a class
     71    attrs = {'__module__': module, 'Meta': Meta}
     72
     73    # Add in any fields that were provided
     74    if fields:
     75        attrs.update(fields)
     76
     77    # Create an Admin inner class if admin options were provided
     78    if admin is not None:
     79        class Admin:
     80            pass
     81        for key, value in admin:
     82            setattr(Admin, key, value)
     83        attrs['Admin'] = Admin
     84
     85    # Create the class, which automatically triggers ModelBase processing
     86    return type(name, (models.Model,), attrs)
     87}}}
     88
     89This function provides everything necessary to make a fully functional Django model from scratch, even given data that's not available until the application is up and running. The arguments it takes work as follows:
     90
     91 * {{{name}}} - The name of the model to be created
     92 * {{{fields}}} - A dictionary of fields the model will have (managers and methods would go in this dictionary as well)
     93 * {{{app_label}}} - A custom application label for the model (this does not have to exist in your project, but see the [[#Admininterface Admin drawback]] below)
     94 * {{{module}}} - An arbitrary module name to use as the model's source (prior to [5163], this had to be a real module path that had in fact been loaded)
     95 * {{{options}}} - A dictionary of options, as if they were provided to the inner {{{Meta}}} class
     96 * {{{admin}}} - A dictionary of admin options, as if they were provided to the inner {{{Admin}}} class (again, see [[#Admininterface Admin drawback]] below)
     97
     98Models can be created using this class with any number of features, as shown by the examples below.
     99
     100{{{
     101#!python
     102>>> model = create_model('Empty')
     103>>> model.__module__
     104''
     105>>> model._meta.app_label
     106''
     107>>> len(model._meta.fields) # Remember, an "id" field is created automatically for each model
     1081
     109}}}
     110
     111{{{
     112#!python
     113>>> fields = {
     114...     'first_name': models.CharField(maxlength=255),
     115...     'last_name': models.CharField(maxlength=255),
     116...     '__str__': lambda self: '%s %s' (self.first_name, self.last_name),
     117... }
     118>>> options = {
     119...     'ordering' = ['last_name', 'first_name'],
     120...     'verbose_name' = 'valued customer',
     121... }
     122>>> admin = {} # An empty dictionary is equivalent to "class Admin: pass"
     123>>> model = create_model('Person', fields,
     124...     options=options,
     125...     admin=admin,
     126...     app_label='fake_app',
     127...     module='fake_project.fake_app.no_models',
     128... )
     129>>> model._meta.verbose_name_plural
     130'valued customers'
     131>>> len(model._meta.fields) # Remember, an "id" field is created automatically for each model
     1323
     133}}}
     134
     135== A database-driven approach ==
     136
     137Much more useful, however, is the ability to manage models using data contained in other models. This would allow model prototypes to be created and shared using the standard admin interface, even by users with no Python knowledge. Consider the following models.
     138
     139{{{
     140#!python
     141from django.core.validators import ValidationError
     142
     143class App(models.Model):
     144    name = models.CharField(maxlength=255)
     145    module = models.CharField(maxlength=255)
     146
     147    def __str__(self):
     148        return self.name
     149
     150class Model(models.Model):
     151    app = models.ForeignKey(App, related_name='models')
     152    name = models.CharField(maxlength=255)
     153
     154    def __str__(self):
     155        return self.name
     156
     157    def get_django_model(self):
     158        "Returns a functional Django model based on current data"
     159        # Get all associated fields into a list ready for dict()
     160        fields = [(f.name, f.get_django_field()) for f in self.fields.all()]
     161
     162        # Use the create_model function defined above
     163        return create_model(self.name, dict(fields), self.app.name, self.app.module)
     164
     165    class Meta:
     166        unique_together = (('app', 'name'),)
     167
     168def is_valid_field(self, field_data, all_data):
     169    if hasattr(models, field_data) and issubclass(getattr(models, field_data), models.Field):
     170        # It exists and is a proper field type
     171        return
     172    raise ValidationError("This is not a valid field type.")
     173
     174class Field(models.Model):
     175    model = models.ForeignKey(Model, related_name='fields')
     176    name = models.CharField(maxlength=255)
     177    type = models.CharField(maxlength=255, validator_list=[is_valid_field])
     178
     179    get_django_field(self):
     180        "Returns the correct field type, instantiated with applicable settings"
     181        # Get all associated settings into a list ready for dict()
     182        settings = [(s.name, s.value) for s in field.settings.all()]
     183
     184        # Instantiate the field with the settings as **kwargs
     185        attrs[field.name] = getattr(models, field.name)(**dict(settings))
     186
     187    class Meta:
     188        unique_together = (('model', 'name'),)
     189
     190class Setting(models.Model):
     191    field = models.ForeignKey(field, related_name='settings')
     192    name = models.CharField(maxlength=255)
     193    value = models.CharField(maxlength=255)
     194
     195    class Meta:
     196        unique_together = (('field', 'name'),)
     197}}}
     198
     199This doesn't take {{{Meta}}} or {{{Admin}}} options into account, nor does it allow for the creation of any model methods, but it's a decent example of how it could be done. Now a model can be created based on a database-backed description, as simply as this (but see the [[#Syncdb syncdb]] section for more details):
     200
     201{{{
     202#!python
     203>>> model = Model.objects.get(app__name='fake_project', name='FakeModel')
     204>>> model
     205<Model: FakeModel>
     206>>> model._meta.app_label
     207'fake_project'
     208>>> model.objects.count()
     20945
     210}}}
     211
     212== Drawbacks ==
     213
     214While dynamic model creation has incredible potential, it should not be considered a recommended way to go about model declarations. Most projects won't find any value in it, and those that do have a bit of work making sure their definition scheme is robust enough for their needs. In addition to the decision of whether to use them, there are a few other details that somewhat hinder their use.
     215
     216=== Syncdb ===
     217
     218The standard {{{syncdb}}} function provided by {{{manage.py}}} relies on crawling through apps that are registered during Django's startup. This means that dynamic models (likely) won't even be found by the time {{{syncdb}}} runs. Even if a dynamic model is found and loaded, its {{{app_label}}} (whether calculated based on its {{{__module__}}} or provided through {{{Meta}}}) '''must match an entry in your {{{INSTALLED_APPS}}} setting''' in order to be processed properly.
     219
     220This is by design, and should not be considered a bug of any kind. It's just a fact of life when dealing with models in this manner. Without having a table in the database, however, dynamic models will be unable to perform any queries; they just sit there looking shiny. One workaround for basic models uses an internal portion of {{{django.core.management}}} to install a basic table definition to the database.
     221
     222{{{
     223#!python
     224def install(model):
     225    from django.core import management
     226    from django.db import connection
     227
     228    # Standard syncdb expects models to be in reliable locations,
     229    # so dynamic models need to bypass django.core.management.syncdb.
     230    # On the plus side, this allows individual models to be installed
     231    # without installing the entire project structure.
     232    # On the other hand, this means that things like relationships and
     233    # indexes will have to be handled manually.
     234    # This installs only the basic table definition.
     235
     236    cursor = connection.cursor()
     237    statements, pending = management._get_sql_model_create(model)
     238    for sql in statements:
     239        cursor.execute(sql)
     240}}}
     241
     242As the comments indicate, this will only create the single table definition, so proceed with caution if you use this function. For a production environment, a more robust solution should be found.
     243
     244<speculation type="wild">Since this involves {{{app_label}}}, #3591 ''might'' help, but more research is necessary on that.</speculation>
     245
     246=== Admin interface ===
     247
     248Django's built-in admin interface also relies on your project's {{{INSTALLED_APPS}}} setting, so models created dynamically will not show up in the admin unless their {{{app_label}}}s match up with packages listed in {{{INSTALLED_APPS}}}. This is again by design, and should not be considered a bug. Applications which make use of dynamic models should either create a placeholder app and put it in the project settings, or create a custom data interface for accessing the dynamic models.
     249
     250{{{
     251#!sh
     252manage.py startapp my_placeholder
     253}}}
     254
     255{{{
     256#!python
     257INSTALLED_APPS = (
     258    ...
     259    ('myproject.myapp.my_placeholder'),
     260    ...
     261)
     262}}}
     263
     264{{{
     265#!python
     266model = create_model('Model', app_label='my_placeholder')
     267}}}
     268
     269'''HINT:''' Use a placeholder app unless you ''really'' need more flexibility.
Back to Top