一个简单的数据库迁移脚本(python实现)

使用python写一个迁移数据库的脚本

最近有一个小项目,是将SQL server中得数据迁移到远程的MySQL库中,因为字段名和数据表现方式都不尽相同,所以操作起来比较繁琐。为了以后能够更快地增加表,甚至能够从MySQL(或是其他什么数据库软件)迁移到SQL server中,所以打算做一个尽量能够通用的数据库迁移脚本。


连接mssqlserver和mariadb

得益于pymssql和pymsql的方法极其相似,可以极其简单地对数据库进行连接操作。

pymssql.connect(ip, user, password, database)

当然,两者的其他操作也十分相似。


设计大致的框架

其实一个脚本,也没什么好设计的。但是为了以后可能存在的迭代更新乃至于增加的需求,秉着“程序员”的思考方式,还是稍微设计一下吧。

1、数据库connector

首先需求就是将数据从一个库移到另一个库,于是很容易就能想到,要先连接两个数据库。那么就先设计一个link_to_databases的模块吧。

这个模块有一个Connector的父类,以及两个分别对应MSSQLSERVERMARIADB的两个Connector子类。超类中有conn连接和cursor指针两个成员变量,以及连接connect和释放资源disconnect两个成员函数。

子类中,为了方便成员变量的获取,将IP地址server、用户名user、密码password以及连接的需要连接的库database在初始化类实例的时候作为成员变量保存。

然后重写connectdisconnect,因为两者有稍微的不同。

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()来验证。

2、数据表models

各数据库的数据表暂时分开来写,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语句。

3、数据交换模块

将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
    ......

交换数据的方法很简单,就是一一对照赋值。但是我发现这边的速度非常慢,应该还有其他可以优化的方法。

4、数据库操作

对数据库的操作,按照此处的需求是,将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()

从上面可以看出,所有的方法都是打开连接->处理->关闭连接,而非再同一个连接下完成,虽然按照需求只是重复了三次。


遇到的问题和注意点

1、 将所有的主键、外键改为UUID

这里遇到一些关于主键的问题,详细不谈了,在这里只做记录,对应MariaDB的系统是由Django完成的,所以在Django中将默认的ID换为UUID的方法如下:

class SomeModel(models.Model):
	uuid = models.UUIDField(primary_key=True, default=uuid.uuid4, editable=False)

2、多张表相互引用时的问题

有两张以上的表互相作为外键或是引用时,删除或是更新甚至清空再保存,都会遇到一些问题,尤其是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()

这样就可以基本解决问题。

3、对于搜索集不同的处理方式

pymysql和pymssql的搜索集的处理方式很不一样,因为pymysql返回的是一个set或是list,但是pymssql返回的结果集是一个字典。

# 这个是pymysql
self.id = queryset[0]

# 这个是pymssql
self.id = queryset.get("ID", "")

4、消除控制台warning

import warnings
warnings.filterwarnings('ignore')

5、遇到的数据库链接错误

在使用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__()方法。


对速率的优化

1、重要的修改

上面说到了,对超过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()

2、代码结构和逻辑的修改

对代码的优化

你可能感兴趣的:(python,python,pymysql,pymssql)