How to add a new field to a model with new Django

2019-01-21 08:18发布

I'm using the contribute_to_class method but I don't know how to create the field in the database with new migrations.

3条回答
何必那么认真
2楼-- · 2019-01-21 08:39

You can create like this:

from django.db.models import CharField
from django.db.models.signals import class_prepared

def add_field(sender, **kwargs):
    """
    class_prepared signal handler that checks for the model named
    MyModel as the sender, and adds a CharField
    to it.
    """
    if sender.__name__ == "MyModel":
        field = CharField("New field", max_length=100)
        field.contribute_to_class(sender, "new_field")

class_prepared.connect(add_field)

See "Django Model Field Injection" for more information.

查看更多
时光不老,我们不散
3楼-- · 2019-01-21 08:55

To answer your question, with the new migration introduced in Django 1.7, in order to add a new field to a model you can simply add that field to your model and initialize migrations with ./manage.py makemigrations and then run ./manage.py migrate and the new field will be added to your DB.

To avoid dealing with errors for your existing models however, you can use the --fake:

  1. Initialize migrations for your existing models:

    ./manage.py makemigrations myapp
    
  2. Fake migrations for existing models:

    ./manage.py migrate --fake myapp
    
  3. Add the new field to myapp.models:

    from django.db import models
    
    class MyModel(models.Model):
        ... #existing fields
        newfield = models.CharField(max_length=100) #new field
    
  4. Run makemigrations again (this will add a new migration file in migrations folder that add the newfield to db):

    ./manage.py makemigrations myapp
    
  5. Run migrate again:

    ./manage.py migrate myapp
    
查看更多
啃猪蹄的小仙女
4楼-- · 2019-01-21 08:55

To be able to do that and have the migration file located in the application where I actually add the field as opposed to having the migration located inside the application to which the model belongs, I had to write my own Migration base class.

If you use contribute_to_class inside the same application as the original model, @nima's answer works perfectly, although I don't see the point of using contribute_to_class then.

Here is the code. It is Django's original code adapted to migrate a model from self.migrated_app instead of self.app_label:

from django.db import migrations


class Migration(migrations.Migration):

  migrated_app = None

  def __init__(self, name, app_label):
    super(Migration,self).__init__(name, app_label)
    if self.migrated_app is None:
      self.migrated_app = self.app_label

  def mutate_state(self, project_state):
    new_state = project_state.clone()
    for operation in self.operations:
        operation.state_forwards(self.migrated_app, new_state)
    return new_state

  def apply(self, project_state, schema_editor, collect_sql=False):
    for operation in self.operations:
      if collect_sql and not operation.reduces_to_sql:
        schema_editor.collected_sql.append("--")
        schema_editor.collected_sql.append("-- MIGRATION NOW PERFORMS OPERATION THAT CANNOT BE WRITTEN AS SQL:")
        schema_editor.collected_sql.append("-- %s" % operation.describe())
        schema_editor.collected_sql.append("--")
        continue
      new_state = project_state.clone()
      operation.state_forwards(self.migrated_app, new_state)
      if not schema_editor.connection.features.can_rollback_ddl and operation.atomic:
        with atomic(schema_editor.connection.alias):
          operation.database_forwards(self.migrated_app, schema_editor, project_state, new_state)
      else:
        operation.database_forwards(self.migrated_app, schema_editor, project_state, new_state)
      project_state = new_state
    return project_state

  def unapply(self, project_state, schema_editor, collect_sql=False):
    to_run = []
    for operation in self.operations:
      if collect_sql and not operation.reduces_to_sql:
        schema_editor.collected_sql.append("--")
        schema_editor.collected_sql.append("-- MIGRATION NOW PERFORMS OPERATION THAT CANNOT BE WRITTEN AS SQL:")
        schema_editor.collected_sql.append("-- %s" % operation.describe())
        schema_editor.collected_sql.append("--")
        continue
      if not operation.reversible:
        raise Migration.IrreversibleError("Operation %s in %s is not reversible" % (operation, self))
      new_state = project_state.clone()
      operation.state_forwards(self.migrated_app, new_state)
      to_run.append((operation, project_state, new_state))
      project_state = new_state
    to_run.reverse()
    for operation, to_state, from_state in to_run:
      if not schema_editor.connection.features.can_rollback_ddl and operation.atomic:
        with atomic(schema_editor.connection.alias):
          operation.database_backwards(self.migrated_app, schema_editor, from_state, to_state)
      else:
        operation.database_backwards(self.migrated_app, schema_editor, from_state, to_state)
    return project_state

With this new Migration class located in base.utils a hand-written migration would look like this. You can also let Django write the migration for you inside the "wrong" application, move the file and update it to use the custom Migration class:

# -*- coding: utf-8 -*-
from __future__ import unicode_literals

from django.db import models, migrations
from base.utils import Migration
import dynamicsites.fields


class Migration(Migration):

    dependencies = [
        ('sites', '0001_initial'),
        ('base', '0001_initial'),
    ]

    migrated_app = 'sites'

    operations = [
        migrations.AddField(
            model_name='site',
            name='folder_name',
            field=dynamicsites.fields.FolderNameField(default='', help_text=b"Folder name for this site's files.  The name may only consist of lowercase characters, numbers (0-9), and/or underscores", max_length=64, blank=True),
            preserve_default=False,
        ),
        migrations.AddField(
            model_name='site',
            name='subdomains',
            field=dynamicsites.fields.SubdomainListField(default=(), help_text=b'Comma separated list of subdomains this site supports.  Leave blank to support all subdomains', blank=True),
            preserve_default=False,
        ),
    ]

Custom migration class for Django 1.8

from django.db import migrations


class Migration(migrations.Migration):

  migrated_app = None

  def __init__(self, name, app_label):
    super(Migration,self).__init__(name, app_label)
    if self.migrated_app is None:
      self.migrated_app = self.app_label

  def __eq__(self, other):
    if not isinstance(other, Migration):
      if not isinstance(other, migrations.Migration):
        return False
      return (self.name == other.name) and (self.migrated_app == other.app_label)
    return (self.name == other.name) and (self.migrated_app == other.migrated_app)

  def __hash__(self):
    return hash("%s.%s" % (self.app_label, self.name))

  def mutate_state(self, project_state, preserve=True):
    new_state = project_state
    if preserve:
      new_state = project_state.clone()

    for operation in self.operations:
      operation.state_forwards(self.migrated_app, new_state)
    return new_state

  def apply(self, project_state, schema_editor, collect_sql=False):
    for operation in self.operations:
      if collect_sql and not operation.reduces_to_sql:
        schema_editor.collected_sql.append("--")
        schema_editor.collected_sql.append("-- MIGRATION NOW PERFORMS OPERATION THAT CANNOT BE "
                                           "WRITTEN AS SQL:")
        schema_editor.collected_sql.append("-- %s" % operation.describe())
        schema_editor.collected_sql.append("--")
        continue
      old_state = project_state.clone()
      operation.state_forwards(self.migrated_app, project_state)
      if not schema_editor.connection.features.can_rollback_ddl and operation.atomic:
        with atomic(schema_editor.connection.alias):
          operation.database_forwards(self.migrated_app, schema_editor, old_state, project_state)
      else:
        operation.database_forwards(self.migrated_app, schema_editor, old_state, project_state)
    return project_state

  def unapply(self, project_state, schema_editor, collect_sql=False):
    to_run = []
    new_state = project_state
    for operation in self.operations:
      if not operation.reversible:
        raise Migration.IrreversibleError("Operation %s in %s is not reversible" % (operation, self))
      new_state = new_state.clone()
      old_state = new_state.clone()
      operation.state_forwards(self.migrated_app, new_state)
      to_run.insert(0, (operation, old_state, new_state))

    for operation, to_state, from_state in to_run:
      if collect_sql:
        if not operation.reduces_to_sql:
          schema_editor.collected_sql.append("--")
          schema_editor.collected_sql.append("-- MIGRATION NOW PERFORMS OPERATION THAT CANNOT BE "
                                             "WRITTEN AS SQL:")
          schema_editor.collected_sql.append("-- %s" % operation.describe())
          schema_editor.collected_sql.append("--")
          continue
      if not schema_editor.connection.features.can_rollback_ddl and operation.atomic:
        with atomic(schema_editor.connection.alias):
          operation.database_backwards(self.migrated_app, schema_editor, from_state, to_state)
      else:
        operation.database_backwards(self.migrated_app, schema_editor, from_state, to_state)
      return project_state
查看更多
登录 后发表回答