34

我不想破坏我网站上的所有用户。但我想利用 Django 1.5 的自定义可插拔用户模型。这是我的新用户模型:

class SiteUser(AbstractUser):
    site = models.ForeignKey(Site, null=True)

在新安装时,一切都适用于我的新模型(我有其他代码,以及这样做的充分理由——所有这些在这里都无关紧要)。但是,如果我将它放在我的实时站点上并进行同步数据库和迁移,我将失去所有用户,或者至少他们将位于与为我的新模型创建的新表不同的孤立表中。

我对 South 很熟悉,但根据这篇文章和我的一些试验,它的数据迁移目前似乎不适合这种特定的迁移。因此,我正在寻找某种方法来让 South 为此工作或进行一些非 South 迁移(原始 SQL、dumpdata/loaddata 或其他),我可以在我的每台服务器(Postgres 9.2)上运行以迁移用户一旦创建了新表,而旧的 auth.User 表仍在数据库中。

4

5 回答 5

49

South 能够为您完成迁移,但您需要聪明并分阶段进行。这是分步指南:(本指南假定您是子类AbstractUser,而不是AbstractBaseUser

  1. 在进行切换之前,请确保在包含您的自定义用户模型的应用程序中启用了南支持(为了指南,我们将调用它accounts和模型User)。此时,您应该还没有自定义用户模型。

    $ ./manage.py schemamigration accounts --initial
    Creating migrations directory at 'accounts/migrations'...
    Creating __init__.py in 'accounts/migrations'...
    Created 0001_initial.py.
    
    $ ./manage.py migrate accounts [--fake if you've already syncdb'd this app]
     Running migrations for accounts:
     - Migrating forwards to 0001_initial.
     > accounts:0001_initial
     - Loading initial data for accounts.
    
  2. 在帐户应用程序中创建一个新的空白用户迁移。

    $ ./manage.py schemamigration accounts --empty switch_to_custom_user
    Created 0002_switch_to_custom_user.py.
    
  3. 在应用程序中创建您的自定义User模型accounts,但请确保将其定义为:

    class SiteUser(AbstractUser): pass
    
  4. 使用以下代码填写空白迁移。

    # encoding: utf-8
    from south.db import db
    from south.v2 import SchemaMigration
    
    class Migration(SchemaMigration):
    
        def forwards(self, orm):
            # Fill in the destination name with the table name of your model
            db.rename_table('auth_user', 'accounts_user')
            db.rename_table('auth_user_groups', 'accounts_user_groups')
            db.rename_table('auth_user_user_permissions', 'accounts_user_user_permissions')
    
        def backwards(self, orm):
            db.rename_table('accounts_user', 'auth_user')
            db.rename_table('accounts_user_groups', 'auth_user_groups')
            db.rename_table('accounts_user_user_permissions', 'auth_user_user_permissions')
    
        models = { ....... } # Leave this alone
    
  5. 运行迁移

    $ ./manage.py migrate accounts
     - Migrating forwards to 0002_switch_to_custom_user.
     > accounts:0002_switch_to_custom_user
     - Loading initial data for accounts.
    
  6. 现在对您的用户模型进行任何更改。

    # settings.py
    AUTH_USER_MODEL = 'accounts.User'
    
    # accounts/models.py
    class SiteUser(AbstractUser):
        site = models.ForeignKey(Site, null=True)
    
  7. 为此更改创建并运行迁移

    $ ./manage.py schemamigration accounts --auto
     + Added field site on accounts.User
    Created 0003_auto__add_field_user_site.py.
    
    $ ./manage.py migrate accounts
     - Migrating forwards to 0003_auto__add_field_user_site.
     > accounts:0003_auto__add_field_user_site
     - Loading initial data for accounts.
    

老实说,如果您已经对您的设置有很好的了解并且已经使用了 south,那么它应该就像将以下迁移添加到您的帐户模块一样简单。

# encoding: utf-8
from south.db import db
from south.v2 import SchemaMigration
from django.db import models

class Migration(SchemaMigration):

    def forwards(self, orm):
        # Fill in the destination name with the table name of your model
        db.rename_table('auth_user', 'accounts_user')
        db.rename_table('auth_user_groups', 'accounts_user_groups')
        db.rename_table('auth_user_permissions', 'accounts_user_permissions')
        # == YOUR CUSTOM COLUMNS ==
        db.add_column('accounts_user', 'site_id',
            models.ForeignKey(orm['sites.Site'], null=True, blank=False)))

    def backwards(self, orm):
        db.rename_table('accounts_user', 'auth_user')
        db.rename_table('accounts_user_groups', 'auth_user_groups')
        db.rename_table('accounts_user_user_permissions', 'auth_user_user_permissions')
        # == YOUR CUSTOM COLUMNS ==
        db.remove_column('accounts_user', 'site_id')

    models = { ....... } # Leave this alone

编辑 2/5/13:为 auth_user_group 表添加了重命名。由于 db 约束,FK 将自动更新以指向正确的表,但 M2M 字段的表名是从 2 个最终表的名称生成的,并且需要以这种方式手动更新。

编辑 2:感谢 @Tuttle 和 @pix0r 的更正。

于 2013-02-25T02:49:34.917 回答
16

我这样做的非常懒惰的方式:

  1. 创建一个新模型(用户),扩展 AbstractUser。在新模型中,在它的 Meta 中,覆盖db_table并设置为“auth_user”。

  2. 使用 South 创建初始迁移。

  3. 迁移,但伪造迁移,--fake在运行迁移时使用。

  4. 添加新字段,创建迁移,正常运行。

这超出了懒惰,但有效。您现在有一个 1.5 兼容的用户模型,它只使用旧的用户表。您还拥有适当的迁移历史记录。

您可以稍后通过手动迁移来重命名表来解决此问题。

于 2013-02-25T06:47:59.580 回答
4

我认为您已经正确地确定了像 South 这样的迁移框架是正确的方法。假设您使用的是 South,您应该能够使用数据迁移功能将旧用户移植到新模型。

具体来说,我会添加一个forwards方法来将用户表中的所有行复制到新表中。类似于以下内容:

def forwards(self, orm):
    for user in orm.User.objects.all():
        new_user = SiteUser(<initialize your properties here>)
        new_user.save()

您也可以使用该bulk_create方法来加快速度。

于 2013-02-15T22:11:16.577 回答
3

我厌倦了与 South 的斗争,所以实际上我最终以不同的方式做这件事,并且对于我的特殊情况来说效果很好:

首先,我使用 ./manage.py 转储数据,修复转储,然后使用 ./manage.py 加载数据,它有效。然后我意识到我可以用一个独立的脚本来做基本相同的事情,它只加载必要的 django 设置并直接进行序列化/反序列化。

自包含的python脚本

## userconverter.py ##

import json
from django.conf import settings

settings.configure(
    DATABASES={ 
            # copy DATABASES configuration from your settings file here, or import it directly from your settings file (but not from django.conf.settings) or use dj_database_url
            },
    SITE_ID = 1, # because my custom user implicates contrib.sites (which is why it's in INSTALLED_APPS too)
    INSTALLED_APPS = ['django.contrib.sites', 'django.contrib.auth', 'myapp'])

# some things you have to import after you configure the settings
from django.core import serializers
from django.contrib.auth.models import User

# this isn't optimized for huge amounts of data -- use streaming techniques rather than loads/dumps if that is your case
old_users = json.loads(serializers.serialize('json', User.objects.all()))
for user in old_users:
    user['pk'] = None
    user['model'] = "myapp.siteuser"
    user['fields']["site"] = settings['SITE_ID']

for new_user in serializers.deserialize('json', json.dumps(old_users)):
    new_user.save()

使用转储数据/加载数据

我做了以下事情:

1) ./manage.py 转储数据 auth.User

2) 将 auth.user 数据转换为新用户的脚本。(或者只是在你最喜欢的文本编辑器或 grep 中手动搜索和替换)我的看起来像:

def convert_user_dump(filename, site_id):
    file = open(filename, 'r')
    contents = file.read()
    file.close()
    user_list = json.loads(contents)
    for user in user_list:
        user['pk'] = None  # it will auto-increment
        user['model'] = "myapp.siteuser"
        user['fields']["site"] = side_id
    contents = json.dumps(user_list)
    file = open(filename, 'w')
    file.write(contents)
    file.close()

3) ./manage.py 加载数据文件名

4) 设置 AUTH_USER_MODEL

*旁注:无论您使用哪种技术(South、序列化/修改/反序列化或其他),进行此类迁移的一个关键部分是,只要您在当前设置中将 AUTH_USER_MODEL 设置为自定义模型,django将您与 auth.User 断开连接,即使该表仍然存在。*

于 2013-02-25T17:33:39.797 回答
2

我们决定在我们的 Django 1.6/Django-CMS 3 项目中切换到自定义用户模型,可能有点晚了,因为我们的数据库中有我们不想丢失的数据(一些 CMS 页面等)。

在我们将 AUTH_USER_MODEL 切换到我们的自定义模型之后,我们遇到了很多我们没有预料到的问题,因为很多其他表都有旧表的外键,而旧auth_user表没有被删除。因此,尽管表面上看起来一切正常,但实际上发生了很多事情:发布页面、向页面添加图像、添加用户等,因为他们试图在仍然有一个外键的表中创建一个条目auth_user,但实际上并没有将匹配的记录插入auth_user.

我们找到了一种快速而肮脏的方法来重建所有表和关系,并复制我们的旧数据(用户除外):

  • 对您的数据库进行完整备份mysqldump
  • 在没有CREATE TABLE语句的情况下进行另一次备份,并排除重建后不存在或将syncdb --migrate在新数据库上填充的一些表:
    • south_migrationhistory
    • auth_user
    • auth_user_groups
    • auth_user_user_permissions
    • auth_permission
    • django_content_types
    • django_site
    • 属于您从项目中删除的应用程序的任何其他表(您可能只有通过试验才能发现)
  • 删除数据库
  • 重新创建数据库(例如manage.py syncdb --migrate
  • 创建空数据库的转储(以便更快地再次循环此循环)
  • 尝试加载您在上面创建的数据转储
  • 如果由于重复的主键或缺少表而无法加载,则:
    • 使用文本编辑器编辑转储
    • 删除锁定、转储和解锁该表的语句
    • 重新加载空的数据库转储
    • 尝试再次加载数据转储
    • 重复直到数据转储加载没有错误

我们运行的命令(对于 MySQL)是:

mysqldump <database> > ~/full-backup.sql
mysqldump <database> \
    --no-create-info \
    --ignore-table=<database>.south_migrationhistory \
    --ignore-table=<database>.auth_user \
    --ignore-table=<database>.auth_user_groups \
    --ignore-table=<database>.auth_user_user_permissions \
    --ignore-table=<database>.auth_permission \
    --ignore-table=<database>.django_content_types \
    --ignore-table=<database>.django_site \
> ~/data-backup.sql

./manage.py sqlclear
./manage.py syncdb --migrate
mysqldump <database> > ~/empty-database.sql

./manage.py dbshell < ~/data-backup.sql

(edit ~/data-backup.sql to remove data dumped from a table that no longer exists)

./manage.py dbshell < ~/empty-database.sql
./manage.py dbshell < ~/data-backup.sql

(repeat until clean)
于 2014-08-13T17:18:49.630 回答