最近有一个小项目,是将SQL server中得数据迁移到远程的MySQL库中,因为字段名和数据表现方式都不尽相同,所以操作起来比较繁琐。为了以后能够更快地增加表,甚至能够从MySQL(或是其他什么数据库软件)迁移到SQL server中,所以打算做一个尽量能够通用的数据库迁移脚本。
得益于pymssql和pymsql的方法极其相似,可以极其简单地对数据库进行连接操作。
pymssql.connect(ip, user, password, database)
当然,两者的其他操作也十分相似。
其实一个脚本,也没什么好设计的。但是为了以后可能存在的迭代更新乃至于增加的需求,秉着“程序员”的思考方式,还是稍微设计一下吧。
首先需求就是将数据从一个库移到另一个库,于是很容易就能想到,要先连接两个数据库。那么就先设计一个link_to_databases
的模块吧。
这个模块有一个Connector的父类,以及两个分别对应MSSQLSERVER
和MARIADB
的两个Connector子类。超类中有conn
连接和cursor
指针两个成员变量,以及连接connect
和释放资源disconnect
两个成员函数。
子类中,为了方便成员变量的获取,将IP地址server
、用户名user
、密码password
以及连接的需要连接的库database
在初始化类实例的时候作为成员变量保存。
然后重写connect
和disconnect
,因为两者有稍微的不同。
Connector:
class Connector:
def __init__(self):
self.conn = None
self.cursor = None
def connect(self):
pass
def disconnect(self):
try:
if self.cursor:
self.cursor.close()
except Exception as err:
print(err)
try:
if self.conn:
self.conn.close()
except Exception as err:
print(err)
因为不想做太多的处理,所以此处就算便try…catch…了。
MSConnector:
class MsConnector(Connector):
def __init__(self, mssql_server, mssql_user, mssql_password, mssql_database):
super(MsConnector, self).__init__()
self.server = mssql_server
self.user = mssql_user
self.password = mssql_password
self.database = mssql_database
def connect(self):
if self.cursor:
self.cursor.close()
self.cursor = None
if self.conn:
self.conn.close()
self.conn = None
self.conn = pymssql.connect(self.server, self.user, self.password, self.database)
self.cursor = self.conn.cursor(as_dict=True)
MariaConnector:
class MariaConnector(Connector):
def __init__(self, mysql_server, mysql_user, mysql_password, mysql_database):
super(MariaConnector, self).__init__()
self.server = mysql_server
self.user = mysql_user
self.password = mysql_password
self.database = mysql_database
def connect(self):
if self.cursor:
self.cursor.close()
self.cursor = None
if self.conn and self.conn.open:
self.conn.close()
self.conn = None
self.conn = pymysql.connect(self.server, self.user, self.password, self.database)
self.cursor = self.conn.cursor()
这里其实没有做好抽象,不过并无大碍。另外,pymysql库中,判断连接是否还在,需要用open()
来验证。
各数据库的数据表暂时分开来写,MS的归MS,Maria的归Maria。
这里以MariaDB为例。
各表的model有各自的字段,这个不表了。这里谈谈超类的成员变量,以及为何需要这些变量。
MariaModel:
class MariaModel:
def __init__(self):
self.column = ""
self.pk = ""
self.filter = ""
def update_from_db(self, queryset):
pass
def __str__(self):
return "MARIADB_MODEL - " + self.column
def get_field(self):
return []
上述,column
是各自表的名称,因为相互对应的两个数据库中的数据表,表名完全不同。为了之后的通用性,使用该字段指向自己model的表名。
pk
并非主键,而是用来指向表中唯一的标志,比如id或是uuid,因为在需求中,在迁移数据的过程中,如果遇到了id相同或是uuid相同的数据,则先删除后再添加。
filter
是用来指代可以筛选的字段,比如时间日期或是其他,不过体现在项目中,只有日期。
update_from_db
是用来将获取到的数据库的数据,保存到model之中,便于使用model统一操作。
get_field
是返回整个表的字段,目前用于操作模块自动生成sql语句。
将MSSQL server的model交换为MariaDB的model之后,才能进行对数据库的操作,所以接下来最重要的就是Translator模块。
Translator模块是需要反复使用的模块,为了提高效率,所以写了一个单例的装饰器。
def singleton(cls):
instances = {
}
def instance(*args, **kwargs):
if cls not in instances:
instances[cls] = cls(*args, **kwargs)
return instances[cls]
return instance
接着写一个简单的工厂类,用来自动调用不同的数据交换类(MS2MYSQL和MYSQL2MS)。
class ModelTranslator:
@classmethod
def dispatch(cls, model):
if isinstance(model, sql_server_models.MsModel):
return getattr(MS2SQL(), str(type(model).__name__))(model)
elif isinstance(model, my_sql_models.MariaModel):
return getattr(SQL2MS(), str(type(model).__name__))(model)
最后就是交换数据了,编写这部分和上面的model类是最耗费时间和精力的体力劳动。因为有大量的字段需要定义并且赋值,而且大同小异,十分之无聊与枯燥,我于是写了几个脚本用于字符串的格式化,来省去写大量代码的时间。最后附上。
@singleton
class MS2SQL:
def Account(self, account: sql_server_models.Account):
pass
def Axe(self, axe: sql_server_models.Axe):
pass
......
交换数据的方法很简单,就是一一对照赋值。但是我发现这边的速度非常慢,应该还有其他可以优化的方法。
对数据库的操作,按照此处的需求是,将MSSQLSERVER的数据取出,再删除MARIADB中相应的数据,再将取出的数据存入MARIADB。
此处的父类定义了一个connector的成员变量和get、delete、save三个成员方法,之后只要其他数据库的connector实现就行了。
get:
def get(self, model_class, filters: list = None):
"""
获取数据库数据
:param model_class: 表模型
:param filters: 过滤条件
:return: 数据集合
"""
self.connector.connect()
if model_class:
table = model_class().column
filter_item = model_class().filter
if issubclass(model_class, MsModel):
sql = "SELECT * FROM [{0}]".format(table)
elif issubclass(model_class, MariaModel):
sql = "SELECT * FROM `{0}`".format(table)
else:
sql = ""
# filter 省略
try:
self.connector.cursor.execute(sql)
results = self.connector.cursor.fetchall()
self.connector.disconnect()
except Exception as err:
self.connector.disconnect()
return []
models = []
for query in results:
model = model_class()
model.update_from_db(query)
models.append(model)
return models
else:
self.connector.disconnect()
print("请输入需要读取的数据模型")
return []
可以很明显的看到,这里的操作会浪费大量的时间和空间,但是这里的效率还算比较快,并没有造成大量时间的浪费。
delete:
def delete(self, model_class, filters: list = None, all_update=False):
"""
删除数据
:param model_class: 数据表模型
:param filters: 条件
:param all_update: 是否完全更新(如果是,需要全部删除)
"""
if not all_update and not model_class().pk:
return
self.connector.connect()
if model_class:
table = model_class().column
filter_item = model_class().filter
self.connector.cursor.execute("SET FOREIGN_KEY_CHECKS = 0;")
self.connector.conn.commit()
sql = "DELETE FROM `{0}`".format(table)
if not all_update:
if sql and filter_item:
if len(filters) == 2:
if filters[0] and filters[1]:
sql = sql + " WHERE `{0}` >= '{1}' and `{0}` <= '{2}'".format(filter_item, filters[0],
filters[1])
elif filters[0] and not filters[1]:
sql = sql + " WHERE `{0}` >= '{1}'".format(filter_item, filters[0])
elif not filters[0] and filters[1]:
sql = sql + " WHERE `{0}` <= '{1}'".format(filter_item, filters[1])
elif len(filters) == 1:
sql = sql + " WHERE `{0}` >= '{1}'".format(filter_item, filters[0])
else:
pass
try:
self.connector.cursor.execute(sql)
self.connector.conn.commit()
self.connector.cursor.execute("SET FOREIGN_KEY_CHECKS = 1;")
self.connector.conn.commit()
self.connector.disconnect()
except Exception as err:
self.connector.disconnect()
Logger.Logger.Instance("ERROR").exception(str(err))
else:
self.connector.disconnect()
这里的删除是MariaDB的connector做的重写。所以只针对MySQL。
save:
def save(self, model_class, context=None):
self.connector.connect()
if model_class:
table = model_class().column
fields = model_class().get_field()
if issubclass(model_class, MariaModel):
self.connector.cursor.execute("SET FOREIGN_KEY_CHECKS = 0;")
self.connector.conn.commit()
for item in context:
sql_insert = "INSERT INTO `{0}`(".format(table)
for field in fields[:-1]:
sql_insert = sql_insert + "`{0}`,".format(field)
sql_insert = sql_insert + "`{0}`) VALUES (".format(fields[-1])
sql_check = "SELECT * FROM `{0}`".format(table)
try:
if isinstance(item, MariaModel):
if item.pk:
sql_check = sql_check + " WHERE `{0}`='{1}'".format(item.pk, getattr(item, item.pk))
count = self.connector.cursor.execute(sql_check)
if count:
sql_delete = "DELETE FROM `{0}` WHERE `{1}`='{2}'".format(table, item.pk,
getattr(item, item.pk))
self.connector.cursor.execute(sql_delete)
self.connector.conn.commit()
for field in fields[:-1]:
sql_insert = sql_insert + "'{0}',".format(getattr(item, field))
sql_insert = sql_insert + "'{0}')".format(getattr(item, fields[-1]))
self.connector.cursor.execute(sql_insert)
self.connector.conn.commit()
else:
Logger.Logger.Instance("ERROR").error("The model is wrong.")
except Exception as err:
Logger.Logger.Instance("ERROR").exception(str(err))
self.connector.cursor.execute("SET FOREIGN_KEY_CHECKS = 1;")
self.connector.conn.commit()
self.connector.disconnect()
print("done")
else:
self.connector.disconnect()
else:
self.connector.disconnect()
从上面可以看出,所有的方法都是打开连接->处理->关闭连接,而非再同一个连接下完成,虽然按照需求只是重复了三次。
这里遇到一些关于主键的问题,详细不谈了,在这里只做记录,对应MariaDB的系统是由Django完成的,所以在Django中将默认的ID换为UUID的方法如下:
class SomeModel(models.Model):
uuid = models.UUIDField(primary_key=True, default=uuid.uuid4, editable=False)
有两张以上的表互相作为外键或是引用时,删除或是更新甚至清空再保存,都会遇到一些问题,尤其是MySql(Maria DB)。
Cannot add or update a child row: a foreign key constraint fails (database.table, CONSTRAINT XXX FOREIGN KEY (xxx) REFERENCES `xxx_table` (`id`))')
会产生类似于上述的错误。
这时候需要在运行sql之前(针对mysql和pymysql):
self.connector.cursor.execute("SET FOREIGN_KEY_CHECKS = 0;")
self.connector.conn.commit()
以及在最后释放连接资源之前(针对mysql和pymysql):
self.connector.cursor.execute("SET FOREIGN_KEY_CHECKS = 1;")
self.connector.conn.commit()
这样就可以基本解决问题。
pymysql和pymssql的搜索集的处理方式很不一样,因为pymysql返回的是一个set或是list,但是pymssql返回的结果集是一个字典。
# 这个是pymysql
self.id = queryset[0]
# 这个是pymssql
self.id = queryset.get("ID", "")
import warnings
warnings.filterwarnings('ignore')
在使用pymssql的时候,有事会产生这种异常
conn = pymssql.connect(config.Host, config.User, config.Pass, config.Database)
File "pymssql.pyx", line 641, in pymssql.connect (pymssql.c:10824)
pymssql.OperationalError: (20017, b'DB-Lib error message 20017, severity 9:\nUnexpected EOF from the server (10.1.2.14:1433)\nDB-Lib error message 20002, severity 9:\nAdaptive Server connection failed (10.1.2.14:1433)\n')
这个问题的本质目前还不清楚,网上有人给出的解决方案是给一个显式编码
conn = pymssql.connect(config.Host, config.User, config.Pass, config.Database,charset="UTF-8")
这个小小的脚本使用的都是最基础的知识点,也没有什么框架和设计模式的知识,虽说有点小家子气和无趣,但是也算是一次实践。原本一直都是用django写一些后端代码的,现在直接用python写这么多,还是头一次。记录在此,希望以后还能有所帮助和感悟。
之后我会补完代码的优化和运行效率的部分。目前项目的整体还不完善,而且运行速度十分有限,字段少的表中,25万条数据大概要1个半小时,而字段多(100以上)的表,4个小时才跑完8万条数据,说明在数据模块交换的地方耗费了大量的时间,如果能有所改进就好。
算是对一些小地方的补充,没什么意义,纯粹作为记录。
import re
if __name__ == "__main__":
context = []
with open("demo.txt") as p:
text = p.readlines()
for line in text:
m = re.search("self.([a-zA-z0-9_]+) = \"\"", line)
if m:
item = m.groups()
context.append(item[0])
print(context)
print()
print()
conut = 0
result = ""
for item in context:
result = result + "\""
result = result + item + ": {" + str(conut) + "},\\t"
if conut == len(context) - 1:
result = result + "\".format(\n"
break
result = result + "\" \\\n"
conut = conut + 1
for item in context:
result = result + "self." + item + ",\n"
result = result + ")"
print(result)
这个脚本将形似于
self.id = ""
self.user_id = ""
self.role_id = ""
的成员变量,转换为
['id', 'user_id', 'role_id']
"id: {0},\t" \
"user_id: {1},\t" \
"role_id: {2},\t".format(
self.id,
self.user_id,
self.role_id,
)
将成员变量提取出来,用于完成update_from_db
方法和__str__()
方法。
上面说到了,对超过20万条的数据,执行速度十分缓慢,而且通过测试,并不是如同在总结中所说的,是数据交换赋值的速度有问题,而是数据库插入操作的问题。对于多条数据,pymysql是推荐使用cursor.executemany(sql_str,values)
来替代cursor.execute(sql_str)
的,也不推荐用户自己拼接字符串。
所以应该使用cursor.executemany(sql_str,values)
。结果显而易见,28万条数据的插入仅仅使用了1分钟便完成了。
def fast_save(self, model_class, context=None):
self.connector.connect()
if model_class:
table = model_class().column
fields = model_class().get_field()
if issubclass(model_class, MariaModel):
self.connector.cursor.execute("SET FOREIGN_KEY_CHECKS = 0;")
self.connector.conn.commit()
sql_insert = "INSERT INTO `{0}`(".format(table)
for field in fields[:-1]:
sql_insert = sql_insert + "`{0}`,".format(field)
sql_insert = sql_insert + "`{0}`) VALUES (".format(fields[-1])
for field in fields[:-1]:
sql_insert = sql_insert + "%s,"
sql_insert = sql_insert + "%s);"
values = []
for item in context:
t = []
for field in fields:
t.append(getattr(item, field))
values.append(tuple(t))
self.connector.cursor.executemany(sql_insert, values)
self.connector.conn.commit()
self.connector.cursor.execute("SET FOREIGN_KEY_CHECKS = 1;")
self.connector.conn.commit()
self.connector.disconnect()
print("done")
else:
self.connector.disconnect()
else:
self.connector.disconnect()