Skip to main content
 首页 » 编程设计

python之Django:动态模型字段和迁移

2024年10月01日3552php

我无法理解 Django 模型字段的工作原理。我想要实现的是类似于 PriceField (DecimalField) 的东西,它动态创建/注入(inject)另一个模型字段,比方说货币 (CharField) 字段。

我在 https://blog.elsdoerfer.name/2008/01/08/fuzzydates-or-one-django-model-field-multiple-database-columns/ 上阅读了关于此主题的有趣博客文章.我认为(并希望)我已经理解了这些文章的核心信息。但由于它们中的大多数都有点过时,我不知道它们是否仍然适用于当前的 django 版本和我的以下代码。

我使用 Django 1.11.4、Python 3.6.2 以及使用 ./manage.py startapp testing 创建的干净应用。 models.py中的代码:

from django.db import models 
from django.db.models import signals 
 
 
_currency_field_name = lambda name: '{}_extension'.format(name) 
 
 
class PriceField(models.DecimalField): 
 
    def contribute_to_class(self, cls, name): 
        # add the extra currency field (CharField) to the class 
        if not cls._meta.abstract: 
            currency_field = models.CharField( 
                max_length=3,  
                editable=False, 
                null=True,  
                blank=True 
            ) 
            cls.add_to_class(_currency_field_name(name), currency_field) 
        # add the original price field (DecimalField) to the class 
        super().contribute_to_class(cls, name) 
 
        # TODO: set the descriptor 
        # setattr(cls, self.name, FooDescriptor(self)) 
 
 
class FooModel(models.Model): 
 
    price = PriceField('agrhhhhh', decimal_places=3, max_digits=10, blank=True, null=True) 

如果我尝试为该模型创建迁移,就会出现问题。如果执行 python manage.py makemigrations,将显示以下消息:

Migrations for 'testing': 
  testing/migrations/0001_initial.py 
    - Create model FooModel 

迁移文件 0001_initial.py 包含以下内容:

# Generated by Django 1.11.4 on 2017-09-11 18:02 
from __future__ import unicode_literals 
 
from django.db import migrations, models 
import testing.models 
 
 
class Migration(migrations.Migration): 
 
    initial = True 
 
    dependencies = [ 
    ] 
 
    operations = [ 
        migrations.CreateModel( 
            name='FooModel', 
            fields=[ 
                ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), 
                ('price', testing.models.PriceField(blank=True, decimal_places=3, max_digits=10, null=True, verbose_name='agrhhhhh')), 
                ('price_extension', models.CharField(blank=True, editable=False, max_length=3, null=True)), 
            ], 
        ), 
    ] 

到目前为止,对我来说这看起来还不错。但是如果我然后执行 ./manage.py migrate testing,django 会喊:

Operations to perform: 
Apply all migrations: testing 
Running migrations: 
Applying testing.0001_initial...Traceback (most recent call last): 
File "/usr/local/var/pyenv/versions/stockmanagement-3.6.2/lib/python3.6/site-packages/django/db/backends/utils.py", line 63, in execute 
    return self.cursor.execute(sql) 
File "/usr/local/var/pyenv/versions/stockmanagement-3.6.2/lib/python3.6/site-packages/django/db/backends/sqlite3/base.py", line 326, in execute 
    return Database.Cursor.execute(self, query) 
sqlite3.OperationalError: duplicate column name: price_extension 

当迁移文件中只定义了一个这样的字段时,为什么它会在重复的列名:price_extension 上出错?这个重复的字段是从哪里来的,这种情况有没有解决办法?谢谢!

编辑1

此异常不仅发生在已经存在的数据库中,而且当我从头开始使用空数据库(删除 SQLite 文件)时也会发生。迁移命令失败后,这是数据库的结构:

./manage.py dbshell 
sqlite> .tables 
django_migrations 
sqlite> .schema 
CREATE TABLE "django_migrations" ("id" integer NOT NULL PRIMARY KEY AUTOINCREMENT, "app" varchar(255) NOT NULL, "name" varchar(255) NOT NULL, "applied" datetime NOT NULL); 
sqlite> select * from django_migrations; 
sqlite> 

和完整的堆栈跟踪:

./manage.py migrate testing 
Operations to perform: 
  Apply all migrations: testing 
Running migrations: 
  Applying testing.0001_initial...Traceback (most recent call last): 
  File "/usr/local/var/pyenv/versions/stockmanagement-3.6.2/lib/python3.6/site-packages/django/db/backends/utils.py", line 63, in execute 
    return self.cursor.execute(sql) 
  File "/usr/local/var/pyenv/versions/stockmanagement-3.6.2/lib/python3.6/site-packages/django/db/backends/sqlite3/base.py", line 326, in execute 
    return Database.Cursor.execute(self, query) 
sqlite3.OperationalError: duplicate column name: price_extension 
 
The above exception was the direct cause of the following exception: 
 
Traceback (most recent call last): 
  File "./manage.py", line 22, in <module> 
    execute_from_command_line(sys.argv) 
  File "/usr/local/var/pyenv/versions/stockmanagement-3.6.2/lib/python3.6/site-packages/django/core/management/__init__.py", line 363, in execute_from_command_line 
    utility.execute() 
  File "/usr/local/var/pyenv/versions/stockmanagement-3.6.2/lib/python3.6/site-packages/django/core/management/__init__.py", line 355, in execute 
    self.fetch_command(subcommand).run_from_argv(self.argv) 
  File "/usr/local/var/pyenv/versions/stockmanagement-3.6.2/lib/python3.6/site-packages/django/core/management/base.py", line 283, in run_from_argv 
    self.execute(*args, **cmd_options) 
  File "/usr/local/var/pyenv/versions/stockmanagement-3.6.2/lib/python3.6/site-packages/django/core/management/base.py", line 330, in execute 
    output = self.handle(*args, **options) 
  File "/usr/local/var/pyenv/versions/stockmanagement-3.6.2/lib/python3.6/site-packages/django/core/management/commands/migrate.py", line 204, in handle 
    fake_initial=fake_initial, 
  File "/usr/local/var/pyenv/versions/stockmanagement-3.6.2/lib/python3.6/site-packages/django/db/migrations/executor.py", line 115, in migrate 
    state = self._migrate_all_forwards(state, plan, full_plan, fake=fake, fake_initial=fake_initial) 
  File "/usr/local/var/pyenv/versions/stockmanagement-3.6.2/lib/python3.6/site-packages/django/db/migrations/executor.py", line 145, in _migrate_all_forwards 
    state = self.apply_migration(state, migration, fake=fake, fake_initial=fake_initial) 
  File "/usr/local/var/pyenv/versions/stockmanagement-3.6.2/lib/python3.6/site-packages/django/db/migrations/executor.py", line 244, in apply_migration 
    state = migration.apply(state, schema_editor) 
  File "/usr/local/var/pyenv/versions/stockmanagement-3.6.2/lib/python3.6/site-packages/django/db/migrations/migration.py", line 129, in apply 
    operation.database_forwards(self.app_label, schema_editor, old_state, project_state) 
  File "/usr/local/var/pyenv/versions/stockmanagement-3.6.2/lib/python3.6/site-packages/django/db/migrations/operations/models.py", line 97, in database_forwards 
    schema_editor.create_model(model) 
  File "/usr/local/var/pyenv/versions/stockmanagement-3.6.2/lib/python3.6/site-packages/django/db/backends/base/schema.py", line 303, in create_model 
    self.execute(sql, params or None) 
  File "/usr/local/var/pyenv/versions/stockmanagement-3.6.2/lib/python3.6/site-packages/django/db/backends/base/schema.py", line 120, in execute 
    cursor.execute(sql, params) 
  File "/usr/local/var/pyenv/versions/stockmanagement-3.6.2/lib/python3.6/site-packages/django/db/backends/utils.py", line 80, in execute 
    return super(CursorDebugWrapper, self).execute(sql, params) 
  File "/usr/local/var/pyenv/versions/stockmanagement-3.6.2/lib/python3.6/site-packages/cachalot/monkey_patch.py", line 113, in inner 
    out = original(cursor, sql, *args, **kwargs) 
  File "/usr/local/var/pyenv/versions/stockmanagement-3.6.2/lib/python3.6/site-packages/django/db/backends/utils.py", line 65, in execute 
    return self.cursor.execute(sql, params) 
  File "/usr/local/var/pyenv/versions/stockmanagement-3.6.2/lib/python3.6/site-packages/django/db/utils.py", line 94, in __exit__ 
    six.reraise(dj_exc_type, dj_exc_value, traceback) 
  File "/usr/local/var/pyenv/versions/stockmanagement-3.6.2/lib/python3.6/site-packages/django/utils/six.py", line 685, in reraise 
    raise value.with_traceback(tb) 
  File "/usr/local/var/pyenv/versions/stockmanagement-3.6.2/lib/python3.6/site-packages/django/db/backends/utils.py", line 63, in execute 
    return self.cursor.execute(sql) 
  File "/usr/local/var/pyenv/versions/stockmanagement-3.6.2/lib/python3.6/site-packages/django/db/backends/sqlite3/base.py", line 326, in execute 
    return Database.Cursor.execute(self, query) 
django.db.utils.OperationalError: duplicate column name: price_extension 

编辑2

可以在以下位置找到包含上述代码的 git 存储库:https://github.com/hetsch/django_testing .如果克隆此存储库(没有任何 DB 的干净项目)、调用 makemigrations 然后调用 migrate,也会发生此错误。

请您参考如下方法:

根据 Django 票 #22555 https://code.djangoproject.com/ticket/22555 ,官方不支持这种添加字段的方法。尽管如此,我还是通过以下简单修复使其工作:

def contribute_to_class(self, cls, name): 
    # add the extra currency field (CharField) to the class 
    # and prevent adding another field instance if the 
    # field was allready attached. 
    if not cls._meta.abstract and not hasattr(cls, _currency_field_name(name)): 
        currency_field = models.CharField( 
            max_length=3,  
            editable=False, 
            null=True,  
            blank=True 
        ) 
        cls.add_to_class(_currency_field_name(name), currency_field)