项目中遇到一个比较棘手的问题,之前的应用是没有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!')
注释齐全。以上。