应用升级冷启动场景下的数据导入和导出

       项目中遇到一个比较棘手的问题,之前的应用是没有migrate相关功能的。从下一个版本起,需要实现应用的数据库自动升级。采用的是Flask-Migrate插件。但是要做migrate的前提是有之前的数据库版本文件支撑。

       最理想的情况是没有什么用户数据,简单粗暴的强制用户重新安装并且重新建立新的数据库,这样以后都可以直接调用migrate的相关命令进行升级。这种做法显然不是很负责的,虽然真的很想这么干。所以!作为一个负责人的程序员!必须解决这样的让人头大的,冷启动问题。

       第一种方案,给用户下发migrate版本文件,用户没有初始版本,那我们可以在更新包内给用户一个!貌似很完美的解决了这个问题。但是!用户使用的版本,也许大概可能绝对不会只有一个...下发哪个版本的脚本文件?所以该方案不完美。

       第二种方案,根据数据库和普遍orm的特征,当模型字段少于数据库字段时,正常的数据库操作不受影响。所以,我们可以在做本次升级之前,保留用户的全部数据。升级创建新的数据库和数据表,再将旧的数据全部导入。但是!有个问题就是,当orm的字段多于数据库时,所有的orm操作都会受到影响。假如只支持单数据库,那么可以通过数据库的dumps或者类似命令去直接解决,假如支持用户自行设定数据库,这个问题则比较麻烦。好在问题停留在了代码层,总归是可以解决。Ok,代码这么搞。

from flask_script import Manager
from xxx import session_maker, db
import json, os
from datetime import datetime
from sqlalchemy.exc import ProgrammingError, OperationalError
from modules.database import models
from xxxx import g_init_cfg


def get_rows(model, columns=None):
    """
        获取数据行
    :param model:       模型类
    :param columns:     需要获取的列。因为代码model的字段可能多于数据库表,所以此处的列需要动态剔除不支持的列。
    :return:            组装成字典的数据
    """

    def inner(model, columns):
        """
            内部递归方法,因数据库机制,每次抛错只抛出一个错误,所以需要递归检查并剔除检查出的列。直到剩余的column与数据库表完全兼容。
        :param model:           模型类
        :param columns:         需要获取的列
        :return:                数据库数据的元组列表和最终剩余的有效columns
        """
        with session_maker() as session:
            if not columns:
                ''' 当columns为空时,默认获取模型内所有的字段 '''
                columns = list(model.__mapper__.column_attrs)
            try:
                ''' 将字段类型转化为SqlAlchemy支持的类型,并尝试获取数据 '''
                fields = [getattr(model, column.key) for column in columns]
                rows = session.query(*fields).all()
            except (ProgrammingError, OperationalError) as e:
                ''' 因为各数据库的错误提示不同,根据不同的数据库引擎分别处理不同的错误信息 '''
                engine = g_init_cfg.getEngine()
                if engine == 'sqlserver':
                    if 'Invalid column name' in e.args[0]:
                        ''' 当提示某字段不存在或者不支持,则记录并从columns内剔除该字段 '''
                        column_name = e.args[0].split('\'')[1]
                    elif 'Invalid object name' in e.args[0]:
                        ''' 当提示当前模型没有对应数据表,则直接返回空,无需处理 '''
                        return [], columns
                elif engine == 'postgresql':
                    if 'does not exist' in e.orig.diag.message_primary and 'relation' not in e.orig.diag.message_primary:
                        column_name = e.orig.diag.message_primary.split('.')[1].split()[0]
                    elif 'does not exist' in e.orig.diag.message_primary and 'relation' in e.orig.diag.message_primary:
                        return [], columns
                elif engine == 'sqlite':
                    if 'no such column' in e.args[0]:
                        column_name = e.args[0].split('.')[2]
                    elif 'no such table' in e.args[0]:
                        return [], columns
                for i, column in enumerate(columns):
                    if column.key == column_name:
                        ''' 剔除不支持的字段 '''
                        columns.pop(i)
                ''' 因为上文已经出现过报错,在某些数据库的引擎中,如postgres的引擎会将execute指令认为是事务,导致之后所有操作均不执行
                 此处直接关闭session,再递归进下一步时重新创建'''
                session.close()
                ''' 递归入口 '''
                rows, columns = inner(model, columns)
            return rows, columns

    rows, columns = inner(model, columns)
    if rows:
        ''' 拼装返回的数据为列表 '''
        li = []
        for row in rows:
            mo = dict()
            for idx, field in enumerate(list(row)):
                if isinstance(field, datetime):
                    field = field.strftime('%Y-%m-%d %H:%M:%S')
                mo[columns[idx].key] = field
            li.append(mo)
        return li
    else:
        return []


customCommand = Manager(help='Project auxiliary tool.')


@customCommand.option('-d', '--directory', dest='directory', default='',
                      help=("json script directory (default is "
                            "'Project home directory')"))
def exports(directory=''):
    """
        数据导出 命令
    :param directory:   可设定的导出存放路径,默认为当前目录下 
    :return:
    """
    ''' 获取所有基于db.Model的子类,即所有模型model '''
    models = db.Model.__subclasses__()
    result = dict()
    for model in models:
        result[model.__name__] = get_rows(model)
    f = open(directory + 'models.json', 'w')
    f.write(json.dumps(result))
    f.close()
    print('export data to [%s] success!' % directory + 'models.json')


@customCommand.option('-d', '--directory', dest='directory', default='',
                      help=("json script directory (default is "
                            "'Project home directory')"))
def imports(directory=''):
    """
        数据导入 命令
    :param directory:   数据库文件的存放路径,默认为当前目录下 
    :return: 
    """
    os.path.exists(directory + 'models.json')
    f = open(directory + 'models.json', 'r')
    mo_data = f.read()
    data = json.loads(mo_data)
    for table in data.keys():
        if data.get(table):
            with session_maker() as session:
                instance_li = []
                for value in data[table]:
                    instance = getattr(models, table)(**value)
                    instance_li.append(instance)
                session.add_all(instance_li)
    print('import data success!')

       注释齐全。以上。

你可能感兴趣的:(应用升级冷启动场景下的数据导入和导出)