├── INSTALL ├── doc ├── main.png └── INCREASE-ETL-METHOD.md ├── requirements.txt ├── __init__.py ├── dbwriter ├── __init__.py ├── base_writer.py └── mysql_writer.py ├── dbreader ├── __init__.py ├── base_reader.py ├── mysql_reader.py ├── oracle_reader.py └── sqlserver_reader.py ├── config.ini ├── logger_file.py ├── config_file.py ├── README.md ├── data_increment.py ├── INCREASE-ETL-METHOD.md └── data_migration.py /INSTALL: -------------------------------------------------------------------------------- 1 | yum -y install python-devel 2 | yum -y install freetds-devel.x86_64 -------------------------------------------------------------------------------- /doc/main.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tangyibo/DataMigrationTool/HEAD/doc/main.png -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | setuptools 2 | pymysql 3 | cx_Oracle 4 | Cython 5 | pymssql 6 | mysql-replication -------------------------------------------------------------------------------- /__init__.py: -------------------------------------------------------------------------------- 1 | # -*- coding: UTF-8 -*- 2 | import dbreader 3 | import dbwriter 4 | from data_migration import * 5 | 6 | __all__ = ["DataMigration","dbreader","dbwriter"] 7 | 8 | -------------------------------------------------------------------------------- /dbwriter/__init__.py: -------------------------------------------------------------------------------- 1 | # -*- coding: UTF-8 -*- 2 | from base_writer import WriterBase 3 | from mysql_writer import WriterMysql 4 | 5 | __all__ = ["WriterBase", "WriterMysql"] 6 | -------------------------------------------------------------------------------- /dbreader/__init__.py: -------------------------------------------------------------------------------- 1 | # -*- coding: UTF-8 -*- 2 | from base_reader import ReaderBase 3 | from mysql_reader import ReaderMysql 4 | from oracle_reader import ReaderOracle 5 | from sqlserver_reader import ReaderSqlserver 6 | 7 | __all__ = ["ReaderBase", "ReaderMysql", "ReaderOracle", "ReaderSqlserver"] 8 | -------------------------------------------------------------------------------- /config.ini: -------------------------------------------------------------------------------- 1 | [source] 2 | ;数据库类型,支持的类型包括:oracle,sqlserver,mysql 3 | type=oracle 4 | ;源端数据库ip地址 5 | host=172.16.90.252 6 | ;源端数据库端口 7 | port=1521 8 | ;源端数据库用户 9 | user=yi_bo 10 | ;源端数据库用户密码 11 | passwd=yi_bo 12 | ;源端数据库名称 13 | dbname=orcl 14 | ;源端表名列表,表名间用英文逗号分隔 15 | tbname=OFUSER_CAS 16 | 17 | [destination] 18 | ;目标数据库类型必须为mysql,即type=mysql 19 | ;目的mysql 数据库ip地址 20 | host=172.16.13.63 21 | ;目的mysql 数据库端口 22 | port=3306 23 | ;目的mysql 数据库用户 24 | user=canal 25 | ;目的mysql 数据库用户密码 26 | passwd=canal 27 | ;目的mysql 数据库名称 28 | dbname=whistle_cas 29 | ;目的mysql 表名列表,表名间用英文逗号分隔 30 | tbname=ofuser_test 31 | -------------------------------------------------------------------------------- /logger_file.py: -------------------------------------------------------------------------------- 1 | # -*- coding: UTF-8 -*- 2 | 3 | import sys, os 4 | import ConfigParser 5 | import logging 6 | import logging.handlers 7 | 8 | __all__ = ["logger"] 9 | 10 | # 加载日志配置 11 | real_dir = os.path.dirname(os.path.realpath(__file__)) 12 | prog_name = os.path.basename(sys.argv[0]) 13 | name, suffix = os.path.splitext(prog_name) 14 | try: 15 | os.mkdir("%s/log" % real_dir) 16 | except Exception, e: 17 | pass 18 | 19 | logfilename = "%s/log/log-%s.log" % (real_dir, name) 20 | handler = logging.handlers.RotatingFileHandler(logfilename, maxBytes=1024 * 1024, backupCount=10) 21 | formatter = logging.Formatter('%(asctime)s - [%(levelname)-8s] - %(message)s %(filename)s:%(lineno)s') 22 | handler.setFormatter(formatter) 23 | logger = logging.getLogger(logfilename) 24 | logger.addHandler(handler) 25 | logger.setLevel(logging.DEBUG) 26 | -------------------------------------------------------------------------------- /dbwriter/base_writer.py: -------------------------------------------------------------------------------- 1 | # -*- coding: UTF-8 -*- 2 | 3 | 4 | class WriterBase(object): 5 | 6 | # 构造函数 7 | def __init__(self, host, port, dbname, username, password,magic_field_name): 8 | self._host = host 9 | self._port = port 10 | self._dbname = dbname 11 | self._username = username 12 | self._password = password 13 | 14 | self._magic_field_name=magic_field_name 15 | 16 | self._connection = None 17 | 18 | def connect(self): 19 | pass 20 | 21 | def close(self): 22 | pass 23 | 24 | def drop_table(self, table_name): 25 | pass 26 | 27 | def create_table(self, create_table_sql): 28 | pass 29 | 30 | def insert_value(self, insert_sql, rows): 31 | pass 32 | 33 | # 装饰器 host 34 | @property 35 | def host(self): 36 | return self._host 37 | 38 | # 装饰器 port 39 | @property 40 | def port(self): 41 | return self._port 42 | 43 | # 装饰器 dbname 44 | @property 45 | def dbname(self): 46 | return self._dbname 47 | 48 | # 装饰器 username 49 | @property 50 | def username(self): 51 | return self._username 52 | 53 | # 装饰器 password 54 | @property 55 | def password(self): 56 | return self._password 57 | 58 | # 装饰器 connection 59 | @property 60 | def connection(self): 61 | return self._connection 62 | 63 | # 装饰器 magic_field_name 64 | @property 65 | def magic_field_name(self): 66 | return self._magic_field_name -------------------------------------------------------------------------------- /dbreader/base_reader.py: -------------------------------------------------------------------------------- 1 | # -*- coding: UTF-8 -*- 2 | 3 | class ReaderBase(object): 4 | 5 | # 构造函数 6 | def __init__(self, host, port, dbname, username, password,magic_field_name): 7 | self._host = host 8 | self._port = port 9 | self._dbname = dbname 10 | self._username = username 11 | self._password = password 12 | 13 | self._magic_field_name=magic_field_name 14 | 15 | self._connection = None 16 | 17 | # 与数据库建立连接,设置self._connection的值 18 | def connect(self): 19 | pass 20 | 21 | # 关闭与数据库的连接 22 | def close(self): 23 | pass 24 | 25 | # 查询表内所有的数据 26 | def find_all(self, cursor, sql): 27 | pass 28 | 29 | # 获取表的创建语句 30 | def get_mysql_create_table_sql(self, curr_table_name, new_table_name=None): 31 | return False, "not implement", None 32 | 33 | # 装饰器 host 34 | @property 35 | def host(self): 36 | return self._host 37 | 38 | # 装饰器 port 39 | @property 40 | def port(self): 41 | return self._port 42 | 43 | # 装饰器 dbname 44 | @property 45 | def dbname(self): 46 | return self._dbname 47 | 48 | # 装饰器 username 49 | @property 50 | def username(self): 51 | return self._username 52 | 53 | # 装饰器 password 54 | @property 55 | def password(self): 56 | return self._password 57 | 58 | # 装饰器 connection 59 | @property 60 | def connection(self): 61 | return self._connection 62 | 63 | # 装饰器 magic_field_name 64 | @property 65 | def magic_field_name(self): 66 | return self._magic_field_name -------------------------------------------------------------------------------- /config_file.py: -------------------------------------------------------------------------------- 1 | # -*- coding: UTF-8 -*- 2 | import os 3 | import ConfigParser 4 | 5 | class ConfigFile: 6 | 7 | def __init__(self,filename): 8 | self.__cf = ConfigParser.ConfigParser() 9 | self._filename=filename 10 | self.__parse() 11 | 12 | # 装饰器 filename 13 | @property 14 | def filename(self): 15 | return self._filename 16 | 17 | def __parse(self): 18 | if not os.path.exists: 19 | raise RuntimeError('file not exist:%s' % self._filename) 20 | 21 | self.__cf.read(self._filename) 22 | 23 | self.source_db_type=self.__cf.get("source", "type") 24 | self.source_db_host = self.__cf.get("source", "host") 25 | self.source_db_port = int(self.__cf.get("source", "port")) 26 | self.source_db_user = self.__cf.get("source", "user") 27 | self.source_db_passwd = self.__cf.get("source", "passwd") 28 | self.source_db_dbname = self.__cf.get("source", "dbname") 29 | 30 | self.destination_mysql_host = self.__cf.get("destination", "host") 31 | self.destination_mysql_port = int(self.__cf.get("destination", "port")) 32 | self.destination_mysql_user = self.__cf.get("destination", "user") 33 | self.destination_mysql_passwd = self.__cf.get("destination", "passwd") 34 | self.destination_mysql_dbname = self.__cf.get("destination", "dbname") 35 | 36 | source_db_tables_list = self.__cf.get("source", "tbname").split(",") 37 | destination_mysql_tables_list = self.__cf.get("destination", "tbname").split(",") 38 | 39 | src_len = len(source_db_tables_list) 40 | dest_len = len(destination_mysql_tables_list) 41 | if src_len != dest_len: 42 | raise RuntimeError('source table list count(%d) not equal destination table list count(%d)', 43 | (src_len, dest_len)) 44 | 45 | self.mysql_table_map = dict(zip(source_db_tables_list, destination_mysql_tables_list)) 46 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | 数据迁移工具(Data Migration Tool,DMT) 2 | =================================== 3 | 4 | 此工具已经不再维护,新项目请见:https://gitee.com/inrgihc/dbswitch 5 | 6 | 一、功能简介 7 | ----------- 8 | - 将远程数据库(包括Oracle、SqlServer、MySQL)中的表拉取迁移到本地 9 | 的MySQL数据库中,提供MySQL自动建表与数据(增量)导入功能。 10 | - 基于MySQL的binlog日志计算在数据迁移同步过程中的增量数据; 11 | 12 | 二、实现逻辑 13 | ----------- 14 | - 根据配置的源端数据库类型(Oracle、SqlServer、MySQL),相应的去读取库中的表结构信息,并生成 15 | MySQL的建表语句,到目的端MySQL执行建表; 16 | - 通过读取源端数据库表中的数据,将其增量推送到目的端库MySQL中; 17 | - 利用MySQL的binlog机制接收在数据同步过程中的增量数据,以备业务使用。 18 | 19 | 数据迁移同步部分的逻辑代码如下: 20 | 21 | (1)为要同步的表增加一个专用扩展列T(时间类型) 22 | 23 | (2)在迁移同步每张表前,先记录下MySQL的当前时间(用于后面的delete同步) 24 | 25 | (3)利用MySQL的insert into ... on duplicate update ...语句,将源表中的数据灌如到目标表中。 26 | 27 | (4)利用扩展列T,同步需要删除的数据。 28 | 29 | ![逻辑图](https://github.com/tangyibo/DataMigrationTool/blob/master/doc/main.png) 30 | 31 | 说明:假设数据库中的表test有A,B两个字段,其中A为主键,T为本模块补充的时间类型字段。 32 | 33 | 34 | 三、MySQL的binlog配置 35 | ----------------- 36 | 在程序启动前,需要配置MySQL的my.cnf 配置文件如下: 37 | ``` 38 | server-id=1 39 | log-bin=mysql-bin 40 | binlog-format=ROW 41 | binlog_row_image = full 42 | ``` 43 | 四、编译使用 44 | ----------- 45 | 46 | 1、依赖安装 47 | ``` 48 | pip install -r requirements.txt 49 | ``` 50 | 2、修改配置文件 51 | ``` 52 | [source] 53 | ;数据库类型,支持的类型包括:oracle,sqlserver,mysql 54 | type=sqlserver 55 | ;源端数据库ip地址 56 | host=172.16.13.63 57 | ;源端数据库端口 58 | port=1433 59 | ;源端数据库用户 60 | user=ei 61 | ;源端数据库用户密码 62 | passwd=fssaa 63 | ;源端数据库名称 64 | dbname=edu 65 | ;源端表名列表,表名间用英文逗号分隔 66 | tbname=tbClass,tbCourseDomain,tbRoom 67 | 68 | [destination] 69 | ;目标数据库类型必须为mysql,即type=mysql 70 | ;目的mysql 数据库ip地址 71 | host=127.0.0.1 72 | ;目的mysql 数据库端口 73 | port=3306 74 | ;目的mysql 数据库用户 75 | user=tangyibo 76 | ;目的mysql 数据库用户密码 77 | passwd=tangyibo 78 | ;目的mysql 数据库名称 79 | dbname=tangyb 80 | ;目的mysql 表名列表,表名间用英文逗号分隔 81 | tbname=tb_class,tb_course_domain,tb_room 82 | ``` 83 | 3、启动运行 84 | 85 | 说明:以下两个模块同时启动 86 | 87 | (1)启动数据迁移同步: 88 | ```buildoutcfg 89 | python ./data_migration.py 90 | ``` 91 | (2)启动数据增量处理: 92 | ```buildoutcfg 93 | python ./data_increament.py 94 | ``` 95 | 96 | 97 | -------------------------------------------------------------------------------- /dbreader/mysql_reader.py: -------------------------------------------------------------------------------- 1 | # -*- coding: UTF-8 -*- 2 | from base_reader import ReaderBase 3 | import pymysql 4 | from warnings import filterwarnings 5 | filterwarnings("error",category=pymysql.Warning) 6 | 7 | class ReaderMysql(ReaderBase): 8 | 9 | # 构造函数 10 | def __init__(self, host, port, dbname, username, password,magic_field_name): 11 | ReaderBase.__init__(self, host, port, dbname, username, password,magic_field_name) 12 | 13 | # 建立与mysql数据库的连接 14 | def connect(self): 15 | self._connection = pymysql.connect( 16 | host=self.host, 17 | port=self.port, 18 | db=self.dbname, 19 | user=self.username, 20 | passwd=self.password, 21 | charset='utf8') 22 | 23 | # 关闭与MySQL的连接 24 | def close(self): 25 | self._connection.close() 26 | 27 | # 查询表内所有的数据 28 | def find_all(self, cursor, sql): 29 | 30 | try: 31 | cursor.execute(sql) 32 | except pymysql.OperationalError, e: 33 | self.connect() 34 | cursor = self._connection.cursor() 35 | return self.find_all(cursor,sql) 36 | except pymysql.Warning as e: 37 | pass 38 | except Exception, e: 39 | return False, e.message 40 | 41 | return True, cursor 42 | 43 | # 获取mysql的建表语句, 原理:利用MySQL的 show create table 语句获取 44 | def get_mysql_create_table_sql(self, curr_table_name, new_table_name=None, create_if_not_exist=False): 45 | mysql_cursor = self._connection.cursor() 46 | show_create_table_sql = "show create table %s " % curr_table_name 47 | try: 48 | mysql_cursor.execute(show_create_table_sql) 49 | except pymysql.OperationalError, e: 50 | self.connect() 51 | mysql_cursor = self._connection.cursor() 52 | mysql_cursor.execute(show_create_table_sql) 53 | except Exception, e: 54 | return False, e.message, [] 55 | 56 | results = mysql_cursor.fetchone() 57 | if new_table_name is None: 58 | create_table_sql = results[1] 59 | else: 60 | create_table_sql = results[1].replace(curr_table_name, new_table_name) 61 | 62 | mysql_cursor.close() 63 | 64 | if create_if_not_exist is True: 65 | create_table_sql = create_table_sql.replace('CREATE TABLE', 'CREATE TABLE IF NOT EXISTS ') 66 | 67 | # remove the current time field 68 | create_table_sql = create_table_sql.replace('ON UPDATE CURRENT_TIMESTAMP', ' ') 69 | 70 | pos = create_table_sql.find('(', 0) 71 | first = create_table_sql[0:pos + 1] 72 | last = create_table_sql[pos + 1:] 73 | create_table_sql = "%s\n %s timestamp NOT NULL DEFAULT '0000-00-00 00:00:00', %s" % ( 74 | first, self.magic_field_name, last) 75 | 76 | column_names = [] 77 | columns = self.__query_table_columns(curr_table_name) 78 | for col in columns: 79 | column_names.append(col[0]) 80 | 81 | return True, create_table_sql, column_names 82 | 83 | # 获取表的列信息 84 | def __query_table_columns(self, table_name): 85 | cursor = self._connection.cursor() 86 | sql = "select column_name,data_type from information_schema.COLUMNS where TABLE_NAME='%s'" % table_name 87 | 88 | try: 89 | cursor.execute(sql) 90 | except pymysql.OperationalError, e: 91 | self.connect() 92 | cursor = self._connection.cursor() 93 | cursor.execute(sql) 94 | except Exception, e: 95 | raise Exception(e.message) 96 | 97 | columns = list(cursor.fetchall()) 98 | cursor.close() 99 | return columns 100 | -------------------------------------------------------------------------------- /data_increment.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python 2 | # -*- coding: UTF-8 -*- 3 | # Date: 2019-01-15 4 | # Author: tang 5 | # Document URL: 6 | # https://python-mysql-replication.readthedocs.io/en/latest/_modules/pymysqlreplication/binlogstream.html 7 | # 8 | ################################################## 9 | # 基于MySQL的binlog计算数据迁移同步过程中的增量数据 10 | # 即:insert/update/delete操作 11 | # ------------------------------------------------ 12 | # 说明:在程序启动前,需要配置MySQL的my.cnf 配置文件如下: 13 | ################################################## 14 | # server-id=1 15 | # log-bin=mysql-bin 16 | # binlog-format=ROW 17 | # binlog_row_image = full 18 | ################################################## 19 | 20 | import sys, os 21 | import json 22 | from datetime import datetime, date 23 | from pymysqlreplication import BinLogStreamReader 24 | from pymysqlreplication.row_event import ( 25 | DeleteRowsEvent, 26 | UpdateRowsEvent, 27 | WriteRowsEvent, 28 | ) 29 | 30 | from logger_file import * 31 | from config_file import ConfigFile 32 | 33 | 34 | ################ 35 | # 数据增量工具类 # 36 | ################ 37 | class BinlogIncrement(object): 38 | magic_field_name = 'magic_time' 39 | 40 | # 构造函数 41 | def __init__(self, filename): 42 | self.config = ConfigFile(filename) 43 | self.mysql_settings = { 44 | 'host': self.config.destination_mysql_host, 45 | 'port': self.config.destination_mysql_port, 46 | 'user': self.config.destination_mysql_user, 47 | 'passwd': self.config.destination_mysql_passwd 48 | } 49 | 50 | # 利用binlog计算增量 51 | def run(self, callback): 52 | stream = BinLogStreamReader( 53 | connection_settings=self.mysql_settings, 54 | blocking=True, 55 | server_id=100, 56 | resume_stream=True, 57 | only_schemas=self.config.destination_mysql_dbname, 58 | only_events=[DeleteRowsEvent, WriteRowsEvent, UpdateRowsEvent] 59 | ) 60 | 61 | logger.info("running data increment ...") 62 | 63 | for binlogevent in stream: 64 | for row in binlogevent.rows: 65 | event = {"schema": binlogevent.schema, "table": binlogevent.table} 66 | 67 | if isinstance(binlogevent, DeleteRowsEvent): 68 | event["action"] = "delete" 69 | event["data"] = row["values"] 70 | elif isinstance(binlogevent, UpdateRowsEvent): 71 | event["action"] = "update" 72 | 73 | new_data = row["after_values"] 74 | old_data = row["before_values"] 75 | 76 | old = {} 77 | for key, val in new_data.items(): 78 | if val != old_data.get(key): 79 | old[key] = old_data[key] 80 | 81 | # 对于binlog中的update操作,如果只有magic_field_name字段被修改,那说明 82 | # 这条记录没有变化,所以忽略这个binlog数据;否则才为实际的update操作。 83 | if BinlogIncrement.magic_field_name in old and len(old) == 1: 84 | continue 85 | 86 | event['data'] = new_data 87 | event['old'] = old 88 | event = dict(event.items()) 89 | elif isinstance(binlogevent, WriteRowsEvent): 90 | event["action"] = "insert" 91 | event["data"] = row["values"] 92 | event = dict(event.items()) 93 | 94 | callback(event) 95 | 96 | stream.close() 97 | 98 | 99 | ########################################################## 100 | 101 | # 对于dict中的日期字段进行处理,以便能够将dict转换成JSON格式 102 | class ComplexEncoder(json.JSONEncoder): 103 | def default(self, obj): 104 | if isinstance(obj, datetime): 105 | return obj.strftime('%Y-%m-%d %H:%M:%S') 106 | elif isinstance(obj, date): 107 | return obj.strftime('%Y-%m-%d') 108 | else: 109 | return json.JSONEncoder.default(self, obj) 110 | 111 | 112 | # 增量的处理,这里只是简单的打印输出 113 | def handle_data_increment(data): 114 | print json.dumps(data, cls=ComplexEncoder) 115 | sys.stdout.flush() 116 | 117 | 118 | if __name__ == '__main__': 119 | reload(sys) 120 | sys.setdefaultencoding('utf-8') 121 | 122 | logger.info("server start ...") 123 | file_name = '%s/config.ini' % os.path.dirname(os.path.realpath(__file__)) 124 | binlog = BinlogIncrement(file_name) 125 | binlog.run(handle_data_increment) 126 | logger.info("server run stop ...") 127 | -------------------------------------------------------------------------------- /dbreader/oracle_reader.py: -------------------------------------------------------------------------------- 1 | # -*- coding: UTF-8 -*- 2 | from base_reader import ReaderBase 3 | import cx_Oracle 4 | import os 5 | 6 | os.environ['NLS_LANG'] = 'SIMPLIFIED CHINESE_CHINA.UTF8' 7 | 8 | 9 | class ReaderOracle(ReaderBase): 10 | 11 | # 构造函数 12 | def __init__(self, host, port, dbname, username, password,magic_field_name): 13 | ReaderBase.__init__(self, host, port, dbname, username, password,magic_field_name) 14 | 15 | # 建立与oracle数据库的连接 16 | def connect(self): 17 | tns = cx_Oracle.makedsn(self.host, self.port, sid=self.dbname, service_name=self.dbname) 18 | self._connection = cx_Oracle.connect(self.username, self.password, tns,encoding = "UTF-8", nencoding = "UTF-8") 19 | 20 | # 关闭与Oracle的连接 21 | def close(self): 22 | self._connection.close() 23 | 24 | # 查询表内所有的数据 25 | def find_all(self, cursor, sql): 26 | 27 | try: 28 | cursor.execute(sql) 29 | except cx_Oracle.OperationalError, e: 30 | self.connect() 31 | cursor = self._connection.cursor() 32 | cursor.execute(sql) 33 | except Exception, e: 34 | return False, e.message 35 | 36 | return True, cursor 37 | 38 | # 获取oracle的建表语句,原理:利用Oracle的 SELECT * FROM table where rownum<1 语句获取列名信息 39 | def get_mysql_create_table_sql(self, curr_table_name, new_table_name=None, create_if_not_exist=False): 40 | oracle_cursor = self._connection.cursor() 41 | 42 | sql = "SELECT * FROM %s where rownum<1" % curr_table_name 43 | try: 44 | oracle_cursor.execute(sql) 45 | except cx_Oracle.OperationalError, e: 46 | self.connect() 47 | oracle_cursor = self._connection.cursor() 48 | oracle_cursor.execute(sql) 49 | except Exception, e: 50 | return False, e.message, [] 51 | 52 | table_metadata = [] 53 | columns_names = [] 54 | # "The description is a list of 7-item tuples where each tuple 55 | # consists of a column name, column type, display size, internal size, 56 | # precision, scale and whether null is possible." 57 | for column in oracle_cursor.description: 58 | columns_names.append(column[0]) 59 | table_metadata.append({ 60 | 'name': column[0], 61 | 'type': column[1], 62 | 'display_size': column[2], 63 | 'internal_size': column[3], 64 | 'precision': column[4], 65 | 'scale': column[5], 66 | 'nullable': column[6], 67 | }) 68 | 69 | oracle_cursor.close() 70 | 71 | try: 72 | # 获取主键列信息 73 | primary_key_column = self.__query_table_primary_key(curr_table_name) 74 | except Exception, e: 75 | return False, e.message, [] 76 | 77 | table_name = curr_table_name 78 | if new_table_name is not None: 79 | table_name = new_table_name 80 | 81 | if create_if_not_exist: 82 | create_table_sql = "CREATE TABLE IF NOT EXISTS `%s` (\n" % (table_name,) 83 | else: 84 | create_table_sql = "CREATE TABLE `%s` (\n" % (table_name,) 85 | 86 | column_definitions = [] 87 | 88 | for column in table_metadata: 89 | # 'LINES' is a MySQL reserved keyword 90 | column_name = column['name'] 91 | if column_name == "LINES": 92 | column_name = "NUM_LINES" 93 | 94 | if column['type'] == cx_Oracle.NUMBER: 95 | # column_type = "DECIMAL(%s, %s)" % (column['precision'], column['scale']) 96 | column_type = "BIGINT" 97 | elif column['type'] == cx_Oracle.STRING: 98 | if column['internal_size'] > 256: 99 | column_type = "TEXT" 100 | else: 101 | column_type = "VARCHAR(%s)" % (column['internal_size'],) 102 | elif column['type'] == cx_Oracle.DATETIME: 103 | column_type = "DATETIME" 104 | elif column['type'] == cx_Oracle.TIMESTAMP: 105 | column_type = "TIMESTAMP" 106 | elif column['type'] == cx_Oracle.FIXED_CHAR: 107 | column_type = "CHAR(%s)" % (column['internal_size'],) 108 | else: # cx_Oracle.CLOB or cx_Oracle.BLOB 109 | column_type = "TEXT" 110 | 111 | if column['nullable'] == 1: 112 | nullable = "null" 113 | else: 114 | nullable = "not null" 115 | 116 | column_definitions.append("%s %s %s" % (column_name, column_type, nullable)) 117 | 118 | column_definitions.append("%s timestamp NOT NULL DEFAULT '0000-00-00 00:00:00' " % self.magic_field_name) 119 | 120 | create_table_sql += ",\n".join(column_definitions) 121 | 122 | if len(primary_key_column) > 0: 123 | primary_key_column_fields = ",".join(["`%s`" % i for i in primary_key_column]) 124 | create_table_sql += ',\nPRIMARY KEY (%s)' % primary_key_column_fields 125 | create_table_sql += "\n)ENGINE=InnoDB DEFAULT CHARACTER SET = utf8;" 126 | 127 | return True, create_table_sql, columns_names 128 | 129 | # 获取表的主键列信息 130 | def __query_table_primary_key(self, table_name): 131 | oracle_cursor = self._connection.cursor() 132 | 133 | sql = "SELECT COLUMN_NAME FROM user_cons_columns WHERE constraint_name = \ 134 | (SELECT constraint_name FROM user_constraints WHERE table_name = '%s' AND \ 135 | constraint_type = 'P') " % table_name.upper() 136 | try: 137 | oracle_cursor.execute(sql) 138 | except cx_Oracle.OperationalError, e: 139 | self.connect() 140 | oracle_cursor = self._connection.cursor() 141 | oracle_cursor.execute(sql) 142 | except Exception, e: 143 | raise Exception(e.message) 144 | 145 | r = oracle_cursor.fetchall() 146 | oracle_cursor.close() 147 | 148 | ret = [] 149 | if r: 150 | for item in r: 151 | ret.append(item[0]) 152 | 153 | return ret 154 | -------------------------------------------------------------------------------- /INCREASE-ETL-METHOD.md: -------------------------------------------------------------------------------- 1 | 《ETL中的数据增量抽取机制》 2 | ============================= 3 | 4 | 增量抽取是数据仓库ETL(extraction,transformation,loading,数据的抽取、转换和装载)实施过程中需要重点考虑的问题。在ETL过程中,增量更新的效率和可行性是决定ETL实施成败的关键问题之一,ETL中的增量更新机制比较复杂,采用何种机制往往取决于源数据系统的类型以及对增量更新性能的要求。 5 | 6 | 1 ETL概述 7 | ------------- 8 | 9 | ETL包括数据的抽取、转换、加载。①数据抽取:从源数据源系统抽取目的数据源系统需要的数据:②数据转换:将从源数据源获取的数据按照业务需求,转换成目的数据源要求的形式,并对错误、不一致的数据进行清洗和加工;③数据加载:将转换后的数据装载到目的数据源。 10 | ETL作为构建数据仓库的一个环节,负责将分布的、异构数据源中的数据如关系数据、平面数据文件等抽取到临时中间层后进行清洗、转换、集成,最后加载到数据仓库或数据集市中,成为联机分析处理、数据挖掘的基础。ETL原来主要用户构建数据仓库和商业智能项目,现在也越来越多地应用于一般信息系统数据的迁 移、交换和同步。 11 | 在ETL的3个环节中,数据抽取直接面对各种分散、异构的数据源,如何保证稳定高效的从这些数据源中提取正确的数据,是ETL设计和实施过程中需要考虑的关键问题之一。 12 | 在集成端进行数据的初始化时,一般需要将数据源端的全部数据装载进来,这时需要进行全量抽取。全量抽取类似于数据迁移或数据复制,它将数据源中的表或视图的数据全部从数据库中抽取出来,再进行后续的转换和加载操作。全量抽取可以使用数据复制、导入或者备份的方式完成,实现机制比较简单。全量抽取完成后,后 续的抽取操作只需抽取自上次抽取以来表中新增或修改的数据,这就是增量抽取。 13 | 在数据库仓库中,无论是全量抽取还是增量抽取,抽取工作一般由数据仓库工具来完成,如oracle的OWB,Sql Server的Integration Services以及专业的ETL商业产品Informatica PowvrCenter等。如果企业的预算有限,也可以考虑使用开源项目Pentaho。这些工具都有一个特点,就是本身并没有实现特定的增量抽取机制,它们完成全量抽取后,用户可以通过定制计划任务的方式,实现按一定的周期从源系统中抽取当前周期内产生的增量数据,但至于这些增量数据如何产生,工具并没 有提供自动生成增量数据的功能。所以,ETL过程中增量数据的产生机制是一个需要用户重点研究和选择的问题。 14 | 15 | 2 增量抽取机制 16 | --------------- 17 | 18 | 要实现增量抽取,关键是如何准确快速的捕获变化的数据。优秀的增量抽取机制要求ETL能够将业务系统中的变化数据按一定的频率准确地捕获到,同时不能对业务系统造成太大的压力,影响现有业务。相对全量抽取而言,增量抽取的设计更复杂,有一种将全量抽取过程自动转换为增量抽取过程的ETL设计思路,前提是必 须捕获变化的数据,增量数据抽取中常用的捕获变化数据的方法有以下几种: 19 | 20 | 2.1 触发器方式 21 | 触发器方式是普遍采取的一种增量抽取机制。该方式是根据抽取要求,在要被抽取的源表上建立插入、修改、删除3个触发器,每当源表中的数据发生变化,就被相应的触发器将变化的数据写入一个增量日志表,ETL的增量抽取则是从增量日志表中而不是直接在源表中抽取数据,同时增量日志表中抽取过的数据要及时被标记或删除。为了简单起见,增量日志表一般不存储增量数据的所有字段信息,而只是存储源表名称、更新的关键字值和更新操作类型(insert、update或delete),ETL增量抽取进程首先根据源表名称和更新的关键字值,从源表中提取对应的完整记录,再根据更新操作类型,对目标表进行相应的处理。 22 | 例如,对于源表为Oracle类型的数据库,采用触发器方式进行增量数据捕获的过程如下: 23 | (1)创建增量日志表DML LOG: 24 | ··· 25 | create table DML_LOG( 26 | ID NUMBER primary key,--自增主键 27 | TABLE_NAME VARCHAR2(200),--源表名称 28 | RECORD_ID NUMBER,--源表增量记录的主键值 29 | DML_TYPE CHAR(1),--增量类型,I表示新增;U表示更新;D表示删除 30 | EXECUTE DATE DATE --发生时间 31 | ); 32 | ··· 33 | (2)为DML_LOG创建一个序列SEQ_DML_LOG,以便触发器写增量日志表时生成ID值。 34 | (3)针对要监听的每一张表,创建一个触发器,例如对表Test创建触发器如下: 35 | ··· 36 | Create or replace trigger T BEFORE INSERT OR UPDATE OR DELETE ON T for each row 37 | declare I_dml_type varchar2(1); 38 | begin 39 | if INSERTING then l_dml type:=’I’; 40 | elsif UPDATING then I_dml_type:=’U’; 41 | elsif DELETING then l_dml_type:=’D’; 42 | end if; 43 | if DELETING then 44 | insert into DML_LOG(ID,TABLE_NAME,RECORD_ID,EXECUTE_DATE,DML_TYPE) 45 | values(seq_dml_log.nextval,’Test’,:old.ID,sysdate,l_dml_type); 46 | else 47 | insert into DML_LOG(ID,TABLE_NAME,RECORD_ID,EXECUTE_DATE,DML_TYPE) 48 | values(seq_dml_log.nextval,’Test’,:new.ID,sysdate,l_dml_type); 49 | end if; 50 | end; 51 | ··· 52 | 这样,对表T的所有DML操作就记录在增量日志表DML_LOG中,注意增量日志表中并没有完全记录增量数据本身,只是记录了增量数据的来源。进行增量ETL时,只需要根据增量日志表中的记录情况,反查源表得到真正的增量数据。 53 | 54 | 2.2 时间戳方式 55 | 时间戳方式是指增量抽取时,抽取进程通过比较系统时间与抽取源表的时间戳字段的值来决定抽取哪些数据。这种方式需要在源表上增加一个时间戳字段,系统中更新修改表数据的时候,同时修改时间戳字段的值。 56 | 有的数据库(例如Sql Server)的时间戳支持自动更新,即表的其它字段的数据发生改变时,时间戳字段的值会被自动更新为记录改变的时刻。在这种情况下,进行ETL实施时就 只需要在源表加上时间戳字段就可以了。对于不支持时间戳自动更新的数据库,这就要求业务系统在更新业务数据时,通过编程的方式手工更新时间戳字段。 57 | 使用时间戳方式可以正常捕获源表的插入和更新操作,但对于删除操作则无能为力,需要结合其它机制才能完成。 58 | 59 | 2.3 全表删除插入方式 60 | 全表删除插入方式是指每次抽取前先删除目标表数据,抽取时全新加载数据。该方式实际上将增量抽取等同于全量抽取。对于数据量不大,全量抽取的时间代价小于执行增量抽取的算法和条件代价时,可以采用该方式。 61 | 62 | 2.4 全表比对方式 63 | 全表比对即在增量抽取时,ETL进程逐条比较源表和目标表的记录,将新增和修改的记录读取出来。 64 | 优化之后的全部比对方式是采用MD5校验码,需要事先为要抽取的表建立一个结构类似的MD5临时表,该临时表记录源表的主键值以及根据源表所有字段的数据 计算出来的MD5校验码,每次进行数据抽取时,对源表和MD5临时表进行MD5校验码的比对,如有不同,进行update操作:如目标表没有存在该主键 值,表示该记录还没有,则进行insert操作。然后,还需要对在源表中已不存在而目标表仍保留的主键值,执行delete操作。 65 | 66 | 2.5 日志表方式 67 | 对于建立了业务系统的生产数据库,可以在数据库中创建业务日志表,当特定需要监控的业务数据发生变化时,由相应的业务系统程序模块来更新维护日志表内容。增量抽取时,通过读日志表数据决定加载哪些数据及如何加载。日志表的维护需要由业务系统程序用代码来完成。 68 | 69 | 2.6 系统日志分析方式 70 | 该方式通过分析数据库自身的日志来判断变化的数据。关系犁数据库系统都会将所有的DML操作存储在日志文件中,以实现数据库的备份和还原功能。ETL增量 抽取进程通过对数据库的日志进行分析,提取对相关源表在特定时间后发生的DML操作信息,就可以得知自上次抽取时刻以来该表的数据变化情况,从而指导增量 抽取动作。 71 | 有些数据库系统提供了访问日志的专用的程序包(例如Oracle的LogMiner),使数据库日志的分析工作得到大大简化。 72 | 73 | 2.7 特定数据库的方式 74 | 针对特有数据库系统的增量抽取方式: 75 | 76 | 2.7.1 Oracle改变数据捕获(changed data capture,CDC)方式 77 | OracleCDC特性是在Oraele9i数据库中引入的。CDC能够帮助识别从上次抽取之后发生变化的数据。利用CDC,在对源表进行 insert、update或delete等操作的同时就可以提取数据,并且变化的数据被保存在数据库的变化表中。这样就可以捕获发生变化的数据,然后利 用数据库视图以一种可控的方式提供给ETL抽取进程,作为增量抽取的依据。 78 | CDC方式对源表数据变化情况的捕获有两种方式:同步CDC和异步CDC。同步CDC使用源数据库触发器来捕获变更的数据。这种方式是实时的,没有任何延 迟。当DML操作提交后,变更表中就产生了变更数据。异步CDC使用数据库重做日志(redolog)文件,在源数据库发生变更以后,才进行数据捕获。 79 | 80 | 2.7.2 Oracle闪回查询方式 81 | Oracle9i以上版本的数据库系统提供了闪回查询机制,允许用户查询过去某个时刻的数据库状态。这样,抽取进程可以将源数据库的当前状态和上次抽取时刻的状态进行对比,快速得出源表数据记录的变化情况。 82 | 83 | 3 比较和分析 84 | ------------- 85 | 86 | 可见,ETL在进行增量抽取操作时,有以上各种机制可以选择。现从兼容性、完备性、性能和侵入性4个方面对这些机制的优劣进行比较分析。 87 | 兼容性 88 | 数据抽取需要面对的源系统,并不一定都是关系型数据库系统。某个ETL过程需要从若干年前的遗留系统中抽取Excel或者CSV文本数据的情形是经常发生的。这时,所有基于关系型数据库产品的增量机制都无法工作,时间戳方式和全表比对方式可能有一定的利用价值,在最坏的情况下,只有放弃增量抽取的思路,转 而采用全表删除插入方式。 89 | 完备性 90 | 完备性方面,时间戳方式不能捕获delete操作,需要结合其它方式一起使用。 91 | 性能 92 | 增量抽取的性能因素表现在两个方面,一是抽取进程本身的性能,二是对源系统性能的负面影响。触发器方式、日志表方式以及系统日志分析方式由于不需要在抽取过程中执行比对步骤,所以增量抽取的性能较佳。全表比对方式需要经过复杂的比对过程才能识别出更改的记录,抽取性能最差。在对源系统的性能影响方面,触发器方式由于是直接在源系统业务表上建立触发器,同时写临时表,对于频繁操作的业务系统可能会有一定的性能损失,尤其是当业务表上执行批量操作时,行级触发器将会对性能产生严重的影响;同步CDC方式内部采用触发器的方式实现,也同样存在性能影响的问题;全表比对方式和日志表方式对数据源系统数据库的性能没 有任何影响,只是它们需要业务系统进行额外的运算和数据库操作,会有少许的时间损耗;时间戳方式、系统日志分析方式以及基于系统日志分析的方式(异步 CDC和闪回查询)对数据库性能的影响也是非常小的。 93 | 侵入性 94 | 对数据源系统的侵入性是指业务系统是否要为实现增量抽取机制做功能修改和额外操作,在这一点上,时间戳方式值得特别关注。该方式除了要修改数据源系统表结构外,对于不支持时间戳字段自动更新的关系型数据库产品,还必须要修改业务系统的功能,让它在源表t执行每次操作时都要显式的更新表的时间戳字段,这在ETL实施过程中必须得到数据源系统高度的配合才能达到,并且在多数情况下这种要求在数据源系统看来是比较“过分”的,这也是时间戳方式无法得到广泛运用的主要原因。另外,触发器方式需要在源表上建立触发器,这种在某些场合中也遭到拒绝。还有一些需要建立临时表的方式,例如全表比对和日志表方式。可能因为 开放给ETL进程的数据库权限的限制而无法实施。同样的情况也可能发生在基于系统日志分析的方式上,因为大多数的数据库产品只允许特定组的用户甚至只有 DBA才能执行日志分析。闪回杏询在侵入性方面的影响是最小的。 95 | 综述: 96 | 通过对各种增量抽取机制的对比分析,我们发现,没有一种机制具有绝对的优势,不同机制在各种因素的表现大体上都是相对平衡的。兼容性较差的机制,像CDC和闪回查询机制,由于充分利用了数据源系统DBMS的特性,相对来说具有较好的整体优势;最容易实现以及兼容性最佳的全表删除插入机制,则是以牺牲抽取性能为代价的;系统日志分析方式对源业务系统的功能无需作任何改变,对源系统表也无需建立触发器,而抽取性能也不错,但有可能需要源系统开放DBA权限给ETL抽取进程,并且自行分析日志系统难度较高,不同数据库系统的日志格式不一致,这就在一定程度上限制了它的使用范围。所以,ETL实施过程中究竞选择哪种增量抽取机制,要根据实际的数据源系统环境进行决策,需要综合考虑源系统数据库的类型、抽取的数据量(决定对性能要求的苛刻程度)、对源业务系统和数据库的控制能力以及实现难度等各种因素,甚至结合各种不同的增量机制以针对环境不同的数据源系统进行ETL实施。 97 | 98 | 4 结束语 99 | ----------- 100 | 101 | 为了实现数据仓库数据的高效更新,增量抽取是ETL数据抽取过程中非常重要的一环,其实现机制直接决定了ETL的整体实施效果。我们通过对几种常见的增量抽取机制进行了对比,总结了各种机制的特性并分析了它们的优劣。各种增量抽取机制都有它有存在的价值和固有的限制条件,在ETL的设计和实施工作过程中, 只能依据项目的实际环境进行综合考虑,甚至需要对可采用的多种机制进行实际的测试,才能确定一个最优的增量抽取方法。 -------------------------------------------------------------------------------- /doc/INCREASE-ETL-METHOD.md: -------------------------------------------------------------------------------- 1 | 《ETL中的数据增量抽取机制》 2 | ============================= 3 | 4 | 增量抽取是数据仓库ETL(extraction,transformation,loading,数据的抽取、转换和装载)实施过程中需要重点考虑的问题。在ETL过程中,增量更新的效率和可行性是决定ETL实施成败的关键问题之一,ETL中的增量更新机制比较复杂,采用何种机制往往取决于源数据系统的类型以及对增量更新性能的要求。 5 | 6 | 1 ETL概述 7 | ------------- 8 | 9 | ETL包括数据的抽取、转换、加载。①数据抽取:从源数据源系统抽取目的数据源系统需要的数据:②数据转换:将从源数据源获取的数据按照业务需求,转换成目的数据源要求的形式,并对错误、不一致的数据进行清洗和加工;③数据加载:将转换后的数据装载到目的数据源。 10 | ETL作为构建数据仓库的一个环节,负责将分布的、异构数据源中的数据如关系数据、平面数据文件等抽取到临时中间层后进行清洗、转换、集成,最后加载到数据仓库或数据集市中,成为联机分析处理、数据挖掘的基础。ETL原来主要用户构建数据仓库和商业智能项目,现在也越来越多地应用于一般信息系统数据的迁 移、交换和同步。 11 | 在ETL的3个环节中,数据抽取直接面对各种分散、异构的数据源,如何保证稳定高效的从这些数据源中提取正确的数据,是ETL设计和实施过程中需要考虑的关键问题之一。 12 | 在集成端进行数据的初始化时,一般需要将数据源端的全部数据装载进来,这时需要进行全量抽取。全量抽取类似于数据迁移或数据复制,它将数据源中的表或视图的数据全部从数据库中抽取出来,再进行后续的转换和加载操作。全量抽取可以使用数据复制、导入或者备份的方式完成,实现机制比较简单。全量抽取完成后,后 续的抽取操作只需抽取自上次抽取以来表中新增或修改的数据,这就是增量抽取。 13 | 在数据库仓库中,无论是全量抽取还是增量抽取,抽取工作一般由数据仓库工具来完成,如oracle的OWB,Sql Server的Integration Services以及专业的ETL商业产品Informatica PowvrCenter等。如果企业的预算有限,也可以考虑使用开源项目Pentaho。这些工具都有一个特点,就是本身并没有实现特定的增量抽取机制,它们完成全量抽取后,用户可以通过定制计划任务的方式,实现按一定的周期从源系统中抽取当前周期内产生的增量数据,但至于这些增量数据如何产生,工具并没 有提供自动生成增量数据的功能。所以,ETL过程中增量数据的产生机制是一个需要用户重点研究和选择的问题。 14 | 15 | 2 增量抽取机制 16 | --------------- 17 | 18 | 要实现增量抽取,关键是如何准确快速的捕获变化的数据。优秀的增量抽取机制要求ETL能够将业务系统中的变化数据按一定的频率准确地捕获到,同时不能对业务系统造成太大的压力,影响现有业务。相对全量抽取而言,增量抽取的设计更复杂,有一种将全量抽取过程自动转换为增量抽取过程的ETL设计思路,前提是必 须捕获变化的数据,增量数据抽取中常用的捕获变化数据的方法有以下几种: 19 | 20 | 2.1 触发器方式 21 | 触发器方式是普遍采取的一种增量抽取机制。该方式是根据抽取要求,在要被抽取的源表上建立插入、修改、删除3个触发器,每当源表中的数据发生变化,就被相应的触发器将变化的数据写入一个增量日志表,ETL的增量抽取则是从增量日志表中而不是直接在源表中抽取数据,同时增量日志表中抽取过的数据要及时被标记或删除。为了简单起见,增量日志表一般不存储增量数据的所有字段信息,而只是存储源表名称、更新的关键字值和更新操作类型(insert、update或delete),ETL增量抽取进程首先根据源表名称和更新的关键字值,从源表中提取对应的完整记录,再根据更新操作类型,对目标表进行相应的处理。 22 | 例如,对于源表为Oracle类型的数据库,采用触发器方式进行增量数据捕获的过程如下: 23 | (1)创建增量日志表DML LOG: 24 | ··· 25 | create table DML_LOG( 26 | ID NUMBER primary key,--自增主键 27 | TABLE_NAME VARCHAR2(200),--源表名称 28 | RECORD_ID NUMBER,--源表增量记录的主键值 29 | DML_TYPE CHAR(1),--增量类型,I表示新增;U表示更新;D表示删除 30 | EXECUTE DATE DATE --发生时间 31 | ); 32 | ··· 33 | (2)为DML_LOG创建一个序列SEQ_DML_LOG,以便触发器写增量日志表时生成ID值。 34 | (3)针对要监听的每一张表,创建一个触发器,例如对表Test创建触发器如下: 35 | ··· 36 | Create or replace trigger T BEFORE INSERT OR UPDATE OR DELETE ON T for each row 37 | declare I_dml_type varchar2(1); 38 | begin 39 | if INSERTING then l_dml type:=’I’; 40 | elsif UPDATING then I_dml_type:=’U’; 41 | elsif DELETING then l_dml_type:=’D’; 42 | end if; 43 | if DELETING then 44 | insert into DML_LOG(ID,TABLE_NAME,RECORD_ID,EXECUTE_DATE,DML_TYPE) 45 | values(seq_dml_log.nextval,’Test’,:old.ID,sysdate,l_dml_type); 46 | else 47 | insert into DML_LOG(ID,TABLE_NAME,RECORD_ID,EXECUTE_DATE,DML_TYPE) 48 | values(seq_dml_log.nextval,’Test’,:new.ID,sysdate,l_dml_type); 49 | end if; 50 | end; 51 | ··· 52 | 这样,对表T的所有DML操作就记录在增量日志表DML_LOG中,注意增量日志表中并没有完全记录增量数据本身,只是记录了增量数据的来源。进行增量ETL时,只需要根据增量日志表中的记录情况,反查源表得到真正的增量数据。 53 | 54 | 2.2 时间戳方式 55 | 时间戳方式是指增量抽取时,抽取进程通过比较系统时间与抽取源表的时间戳字段的值来决定抽取哪些数据。这种方式需要在源表上增加一个时间戳字段,系统中更新修改表数据的时候,同时修改时间戳字段的值。 56 | 有的数据库(例如Sql Server)的时间戳支持自动更新,即表的其它字段的数据发生改变时,时间戳字段的值会被自动更新为记录改变的时刻。在这种情况下,进行ETL实施时就 只需要在源表加上时间戳字段就可以了。对于不支持时间戳自动更新的数据库,这就要求业务系统在更新业务数据时,通过编程的方式手工更新时间戳字段。 57 | 使用时间戳方式可以正常捕获源表的插入和更新操作,但对于删除操作则无能为力,需要结合其它机制才能完成。 58 | 59 | 2.3 全表删除插入方式 60 | 全表删除插入方式是指每次抽取前先删除目标表数据,抽取时全新加载数据。该方式实际上将增量抽取等同于全量抽取。对于数据量不大,全量抽取的时间代价小于执行增量抽取的算法和条件代价时,可以采用该方式。 61 | 62 | 2.4 全表比对方式 63 | 全表比对即在增量抽取时,ETL进程逐条比较源表和目标表的记录,将新增和修改的记录读取出来。 64 | 优化之后的全部比对方式是采用MD5校验码,需要事先为要抽取的表建立一个结构类似的MD5临时表,该临时表记录源表的主键值以及根据源表所有字段的数据 计算出来的MD5校验码,每次进行数据抽取时,对源表和MD5临时表进行MD5校验码的比对,如有不同,进行update操作:如目标表没有存在该主键 值,表示该记录还没有,则进行insert操作。然后,还需要对在源表中已不存在而目标表仍保留的主键值,执行delete操作。 65 | 66 | 2.5 日志表方式 67 | 对于建立了业务系统的生产数据库,可以在数据库中创建业务日志表,当特定需要监控的业务数据发生变化时,由相应的业务系统程序模块来更新维护日志表内容。增量抽取时,通过读日志表数据决定加载哪些数据及如何加载。日志表的维护需要由业务系统程序用代码来完成。 68 | 69 | 2.6 系统日志分析方式 70 | 该方式通过分析数据库自身的日志来判断变化的数据。关系犁数据库系统都会将所有的DML操作存储在日志文件中,以实现数据库的备份和还原功能。ETL增量 抽取进程通过对数据库的日志进行分析,提取对相关源表在特定时间后发生的DML操作信息,就可以得知自上次抽取时刻以来该表的数据变化情况,从而指导增量 抽取动作。 71 | 有些数据库系统提供了访问日志的专用的程序包(例如Oracle的LogMiner),使数据库日志的分析工作得到大大简化。 72 | 73 | 2.7 特定数据库的方式 74 | 针对特有数据库系统的增量抽取方式: 75 | 76 | 2.7.1 Oracle改变数据捕获(changed data capture,CDC)方式 77 | OracleCDC特性是在Oraele9i数据库中引入的。CDC能够帮助识别从上次抽取之后发生变化的数据。利用CDC,在对源表进行 insert、update或delete等操作的同时就可以提取数据,并且变化的数据被保存在数据库的变化表中。这样就可以捕获发生变化的数据,然后利 用数据库视图以一种可控的方式提供给ETL抽取进程,作为增量抽取的依据。 78 | CDC方式对源表数据变化情况的捕获有两种方式:同步CDC和异步CDC。同步CDC使用源数据库触发器来捕获变更的数据。这种方式是实时的,没有任何延 迟。当DML操作提交后,变更表中就产生了变更数据。异步CDC使用数据库重做日志(redolog)文件,在源数据库发生变更以后,才进行数据捕获。 79 | 80 | 2.7.2 Oracle闪回查询方式 81 | Oracle9i以上版本的数据库系统提供了闪回查询机制,允许用户查询过去某个时刻的数据库状态。这样,抽取进程可以将源数据库的当前状态和上次抽取时刻的状态进行对比,快速得出源表数据记录的变化情况。 82 | 83 | 3 比较和分析 84 | ------------- 85 | 86 | 可见,ETL在进行增量抽取操作时,有以上各种机制可以选择。现从兼容性、完备性、性能和侵入性4个方面对这些机制的优劣进行比较分析。 87 | 兼容性 88 | 数据抽取需要面对的源系统,并不一定都是关系型数据库系统。某个ETL过程需要从若干年前的遗留系统中抽取Excel或者CSV文本数据的情形是经常发生的。这时,所有基于关系型数据库产品的增量机制都无法工作,时间戳方式和全表比对方式可能有一定的利用价值,在最坏的情况下,只有放弃增量抽取的思路,转 而采用全表删除插入方式。 89 | 完备性 90 | 完备性方面,时间戳方式不能捕获delete操作,需要结合其它方式一起使用。 91 | 性能 92 | 增量抽取的性能因素表现在两个方面,一是抽取进程本身的性能,二是对源系统性能的负面影响。触发器方式、日志表方式以及系统日志分析方式由于不需要在抽取过程中执行比对步骤,所以增量抽取的性能较佳。全表比对方式需要经过复杂的比对过程才能识别出更改的记录,抽取性能最差。在对源系统的性能影响方面,触发器方式由于是直接在源系统业务表上建立触发器,同时写临时表,对于频繁操作的业务系统可能会有一定的性能损失,尤其是当业务表上执行批量操作时,行级触发器将会对性能产生严重的影响;同步CDC方式内部采用触发器的方式实现,也同样存在性能影响的问题;全表比对方式和日志表方式对数据源系统数据库的性能没 有任何影响,只是它们需要业务系统进行额外的运算和数据库操作,会有少许的时间损耗;时间戳方式、系统日志分析方式以及基于系统日志分析的方式(异步 CDC和闪回查询)对数据库性能的影响也是非常小的。 93 | 侵入性 94 | 对数据源系统的侵入性是指业务系统是否要为实现增量抽取机制做功能修改和额外操作,在这一点上,时间戳方式值得特别关注。该方式除了要修改数据源系统表结构外,对于不支持时间戳字段自动更新的关系型数据库产品,还必须要修改业务系统的功能,让它在源表t执行每次操作时都要显式的更新表的时间戳字段,这在ETL实施过程中必须得到数据源系统高度的配合才能达到,并且在多数情况下这种要求在数据源系统看来是比较“过分”的,这也是时间戳方式无法得到广泛运用的主要原因。另外,触发器方式需要在源表上建立触发器,这种在某些场合中也遭到拒绝。还有一些需要建立临时表的方式,例如全表比对和日志表方式。可能因为 开放给ETL进程的数据库权限的限制而无法实施。同样的情况也可能发生在基于系统日志分析的方式上,因为大多数的数据库产品只允许特定组的用户甚至只有 DBA才能执行日志分析。闪回杏询在侵入性方面的影响是最小的。 95 | 综述: 96 | 通过对各种增量抽取机制的对比分析,我们发现,没有一种机制具有绝对的优势,不同机制在各种因素的表现大体上都是相对平衡的。兼容性较差的机制,像CDC和闪回查询机制,由于充分利用了数据源系统DBMS的特性,相对来说具有较好的整体优势;最容易实现以及兼容性最佳的全表删除插入机制,则是以牺牲抽取性能为代价的;系统日志分析方式对源业务系统的功能无需作任何改变,对源系统表也无需建立触发器,而抽取性能也不错,但有可能需要源系统开放DBA权限给ETL抽取进程,并且自行分析日志系统难度较高,不同数据库系统的日志格式不一致,这就在一定程度上限制了它的使用范围。所以,ETL实施过程中究竞选择哪种增量抽取机制,要根据实际的数据源系统环境进行决策,需要综合考虑源系统数据库的类型、抽取的数据量(决定对性能要求的苛刻程度)、对源业务系统和数据库的控制能力以及实现难度等各种因素,甚至结合各种不同的增量机制以针对环境不同的数据源系统进行ETL实施。 97 | 98 | 4 结束语 99 | ----------- 100 | 101 | 为了实现数据仓库数据的高效更新,增量抽取是ETL数据抽取过程中非常重要的一环,其实现机制直接决定了ETL的整体实施效果。我们通过对几种常见的增量抽取机制进行了对比,总结了各种机制的特性并分析了它们的优劣。各种增量抽取机制都有它有存在的价值和固有的限制条件,在ETL的设计和实施工作过程中, 只能依据项目的实际环境进行综合考虑,甚至需要对可采用的多种机制进行实际的测试,才能确定一个最优的增量抽取方法。 -------------------------------------------------------------------------------- /data_migration.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python 2 | # -*- coding: UTF-8 -*- 3 | # Date: 2018-12-03 4 | # Author: tang 5 | # 6 | import sys, os 7 | import datetime,time 8 | from logger_file import * 9 | from config_file import ConfigFile 10 | from dbwriter import * 11 | from dbreader import * 12 | 13 | ################ 14 | # 数据迁移工具类 # 15 | ################ 16 | class DataMigration: 17 | 18 | magic_field_name = 'magic_time' 19 | 20 | # 构造函数 21 | def __init__(self, filename): 22 | self.config = ConfigFile(filename) 23 | 24 | dbmapper = { 25 | "mysql": ReaderMysql, 26 | "oracle": ReaderOracle, 27 | "sqlserver": ReaderSqlserver 28 | } 29 | if not dbmapper.has_key(self.config.source_db_type): 30 | raise Exception("Unsupport database type :%s" % self.config.source_db_type) 31 | 32 | logger.info("server param: source database type is %s" % self.config.source_db_type) 33 | 34 | dbclass = dbmapper.get(self.config.source_db_type) 35 | self.db_reader = dbclass( 36 | host=self.config.source_db_host, 37 | port=self.config.source_db_port, 38 | dbname=self.config.source_db_dbname, 39 | username=self.config.source_db_user, 40 | password=self.config.source_db_passwd, 41 | magic_field_name=DataMigration.magic_field_name 42 | ) 43 | self.db_writer = WriterMysql( 44 | host=self.config.destination_mysql_host, 45 | port=self.config.destination_mysql_port, 46 | dbname=self.config.destination_mysql_dbname, 47 | username=self.config.destination_mysql_user, 48 | password=self.config.destination_mysql_passwd, 49 | magic_field_name=DataMigration.magic_field_name 50 | ) 51 | 52 | self.db_reader.connect() 53 | self.db_writer.connect() 54 | 55 | # 启动运行 56 | def run(self): 57 | success = True 58 | logger.info("running data migration ...") 59 | for src_table in self.config.mysql_table_map: 60 | starttime = datetime.datetime.now() 61 | dest_table = self.config.mysql_table_map[src_table] 62 | ret = self.__handle_one_table(src_table, dest_table, True, False) 63 | endtime = datetime.datetime.now() 64 | logger.info("migration table [%s=>%s] elipse %d(s)" % (src_table, dest_table, (endtime - starttime).seconds)) 65 | success = success and ret 66 | 67 | return success 68 | 69 | # 运行完成 70 | def fini(self): 71 | self.db_reader.close() 72 | self.db_writer.close() 73 | 74 | # 处理一个表及其数据 75 | def __handle_one_table(self, src_table, dest_table=None, create_if_not_exist=True, drop_if_exists=False): 76 | logger.info("handle table:%s ..." % src_table) 77 | 78 | ret, create_table_sql, column_names = self.db_reader.get_mysql_create_table_sql(src_table, dest_table, 79 | create_if_not_exist) 80 | if ret is False: 81 | logger.info("get create sql from source database failed,table:%s, error:%s" % (src_table, create_table_sql)) 82 | return False 83 | 84 | logger.info("get create sql from source database is :\n%s " % create_table_sql) 85 | 86 | table_name = src_table 87 | if dest_table is not None: 88 | table_name = dest_table 89 | 90 | if drop_if_exists is True: 91 | self.db_writer.drop_table(table_name) 92 | 93 | ret, error = self.db_writer.create_table(create_table_sql) 94 | if ret is False: 95 | logger.error("error: %s" % error) 96 | return False 97 | 98 | reader_cursor = self.db_reader.connection.cursor() 99 | query_field_string = ",".join(["%s" % name for name in column_names]) 100 | query_all_data_sql = "select %s from %s " % (query_field_string, src_table) 101 | logger.info("query all sql: %s" % query_all_data_sql) 102 | ret, reader_cursor = self.db_reader.find_all(reader_cursor, query_all_data_sql) 103 | if ret is False: 104 | logger.error("query all sql faild: %s" % reader_cursor) 105 | return False 106 | 107 | table_operator, current_time = self.db_writer.prepare_table_operator(table_name, column_names, drop_if_exists) 108 | logger.info("insert sql: %s" % table_operator.statement) 109 | success_insert_count = 0 110 | table_row = reader_cursor.fetchone() 111 | while table_row is not None: 112 | ret, error = table_operator.append(table_row) 113 | if ret is False: 114 | logger.error("insert data sql faild,error %s" % error) 115 | else: 116 | success_insert_count = success_insert_count + 1 117 | if success_insert_count % 1000 is 0: 118 | logger.info("read table [%s] data count: %d,and insert data count: %d" % ( 119 | src_table,reader_cursor.rowcount, success_insert_count)) 120 | table_row = reader_cursor.fetchone() 121 | 122 | ret, error = table_operator.commit() 123 | if ret is False: 124 | logger.error("insert data sql failed,error %s" % error) 125 | 126 | logger.info("query table [%s] data total count : %d,success insert %d " % ( 127 | src_table, reader_cursor.rowcount, success_insert_count)) 128 | 129 | delete_sql = "delete from %s where %s<'%s' " % (table_name, DataMigration.magic_field_name, current_time) 130 | logger.info("delete old sql: %s" % delete_sql) 131 | ret, error = self.db_writer.delete_value(delete_sql) 132 | if ret is False: 133 | logger.error("delete data sql failed,error %s" % error) 134 | 135 | reader_cursor.close() 136 | 137 | return True 138 | 139 | 140 | if __name__ == '__main__': 141 | reload(sys) 142 | sys.setdefaultencoding('utf-8') 143 | 144 | logger.info("server start ...") 145 | 146 | file_name = '%s/config.ini' % os.path.dirname(os.path.realpath(__file__)) 147 | migration = DataMigration(file_name) 148 | start = datetime.datetime.now() 149 | ret = migration.run() 150 | stop = datetime.datetime.now() 151 | migration.fini() 152 | logger.info("migration data elipse total %d(s)" % (stop - start).seconds) 153 | 154 | if not ret: 155 | logger.error("server run failed!") 156 | sys.exit(0) 157 | else: 158 | logger.info("server run success!") 159 | sys.exit(1) 160 | 161 | # shell 调用方法: 162 | # 163 | # python data_migration.py 164 | # 165 | # if [ $?==0 ];then 166 | # echo 'success' 167 | # else 168 | # echo 'error' 169 | # fi 170 | -------------------------------------------------------------------------------- /dbwriter/mysql_writer.py: -------------------------------------------------------------------------------- 1 | # -*- coding: UTF-8 -*- 2 | from base_writer import WriterBase 3 | import pymysql 4 | import warnings 5 | 6 | warnings.filterwarnings("error", category=pymysql.Warning) 7 | warnings.filterwarnings("ignore") 8 | 9 | class Callback: 10 | def __init__(self, instance, function_name): 11 | self.__instance = instance 12 | self.__function_name = function_name 13 | 14 | def action(self, param, data): 15 | return self.__instance.__getattribute__(self.__function_name)(param, data) 16 | 17 | 18 | class TableOperator: 19 | def __init__(self, opt_sql, cb, max_cache_size=10000): 20 | self.buffer_list = [] 21 | self.opt_sql = opt_sql 22 | self.callback = cb 23 | self.max_cache_size = max_cache_size 24 | 25 | @property 26 | def statement(self): 27 | return self.opt_sql 28 | 29 | def append(self, row): 30 | column_values = [] 31 | for column_value in row: 32 | if column_value is None: 33 | column_values.append(None) 34 | elif column_value is True: 35 | column_values.append('1') 36 | elif column_value is False: 37 | column_values.append('0') 38 | else: 39 | column_values.append(pymysql.escape_string(str(column_value))) 40 | 41 | self.buffer_list.append(column_values) 42 | if len(self.buffer_list) >= int(self.max_cache_size): 43 | ret, error = self.callback.action(param=self.opt_sql, data=self.buffer_list) 44 | self.buffer_list = [] 45 | return ret, error 46 | 47 | return True, 'ok' 48 | 49 | def commit(self): 50 | if len(self.buffer_list) > 0: 51 | ret, error = self.callback.action(self.opt_sql, self.buffer_list) 52 | self.buffer_list = [] 53 | return ret, error 54 | 55 | 56 | class WriterMysql(WriterBase): 57 | 58 | def __init__(self, host, port, dbname, username, password,magic_field_name): 59 | WriterBase.__init__(self, host, port, dbname, username, password,magic_field_name) 60 | 61 | def connect(self): 62 | self._connection = pymysql.connect( 63 | host=self.host, 64 | port=self.port, 65 | db=self.dbname, 66 | user=self.username, 67 | passwd=self.password, 68 | charset='utf8') 69 | 70 | # 该选项影响列为自增长的插入。在默认设置下,插入0或者null代表生成下一个自 71 | # 增长值。如果用户希望插入的值为0,而该列又是自增长的 72 | mysql_cursor = self._connection.cursor() 73 | try: 74 | mysql_cursor.execute("SET sql_mode='NO_AUTO_VALUE_ON_ZERO'") 75 | mysql_cursor.execute("set names 'utf8'") 76 | except pymysql.Warning as e: 77 | pass 78 | 79 | mysql_cursor.close() 80 | 81 | def close(self): 82 | self._connection.close() 83 | 84 | def drop_table(self, table_name): 85 | cursor = self._connection.cursor() 86 | 87 | try: 88 | drop_table_sql = "DROP TABLE IF EXISTS `%s`;" % table_name 89 | cursor.execute(drop_table_sql) 90 | self._connection.commit() 91 | except pymysql.OperationalError, e: 92 | self.connect() 93 | cursor = self._connection.cursor() 94 | cursor.execute(drop_table_sql) 95 | self._connection.commit() 96 | except Exception, e: 97 | self._connection.rollback() 98 | return False, e.message 99 | finally: 100 | cursor.close() 101 | 102 | return True, 'ok' 103 | 104 | def create_table(self, create_table_sql): 105 | cursor = self._connection.cursor() 106 | 107 | try: 108 | cursor.execute(create_table_sql) 109 | self._connection.commit() 110 | except pymysql.err.Warning,e: 111 | return True,e.message 112 | except pymysql.OperationalError, e: 113 | self.connect() 114 | cursor = self._connection.cursor() 115 | cursor.execute(create_table_sql) 116 | self._connection.commit() 117 | except Exception, e: 118 | self._connection.rollback() 119 | return False, e.message 120 | finally: 121 | cursor.close() 122 | 123 | return True, 'ok' 124 | 125 | def prepare_table_operator(self, table_name, column_names, drop_if_exists): 126 | key_value_pair = [] 127 | for name in column_names: 128 | key_value_pair.append("%s=VALUES(%s)" % (name, name)) 129 | 130 | key_value_pair.append("%s=now()" % self.magic_field_name) 131 | on_duplicate_key_update = ",".join([" %s " % i for i in key_value_pair]) 132 | question_marks = ",".join(["%s" for i in range(len(column_names))]) 133 | if drop_if_exists is True: 134 | sql_insert = "INSERT INTO %s (%s) VALUES (%s,%s=now())" % ( 135 | table_name, ",".join(column_names), question_marks, self.magic_field_name) 136 | else: 137 | sql_insert = "INSERT INTO %s (%s,%s) VALUES (%s,now()) ON DUPLICATE KEY UPDATE %s " % \ 138 | (table_name, ",".join(column_names), self.magic_field_name, question_marks, 139 | on_duplicate_key_update) 140 | 141 | mysql_cursor = self._connection.cursor() 142 | current_time_sql = "SELECT now()" 143 | try: 144 | mysql_cursor.execute(current_time_sql) 145 | except pymysql.OperationalError, e: 146 | self.connect() 147 | return self.prepare_table_operator(table_name, column_names, drop_if_exists) 148 | except Exception, e: 149 | raise Exception(e.message) 150 | 151 | r = mysql_cursor.fetchall() 152 | mysql_cursor.close() 153 | 154 | cb = Callback(self, self.insert_value.__name__) 155 | return TableOperator(sql_insert, cb),r[0][0] if r else None 156 | 157 | def insert_value(self, insert_sql, rows): 158 | mysql_cursor = self._connection.cursor() 159 | 160 | try: 161 | mysql_cursor.executemany(insert_sql, rows) 162 | self._connection.commit() 163 | return True, 'ok' 164 | except pymysql.OperationalError, e: 165 | self.connect() 166 | return self.insert_value(insert_sql, rows) 167 | except Exception, e: 168 | return False, e.message 169 | 170 | return False, 'error' 171 | 172 | def delete_value(self,delete_sql): 173 | mysql_cursor = self._connection.cursor() 174 | 175 | try: 176 | mysql_cursor.execute(delete_sql) 177 | self._connection.commit() 178 | return True, 'ok' 179 | except pymysql.OperationalError, e: 180 | self.connect() 181 | return self.delete_value(delete_sql) 182 | except Exception, e: 183 | return False, e.message 184 | 185 | return False, 'error' -------------------------------------------------------------------------------- /dbreader/sqlserver_reader.py: -------------------------------------------------------------------------------- 1 | # -*- coding: UTF-8 -*- 2 | from base_reader import ReaderBase 3 | import pymssql 4 | import re 5 | 6 | 7 | class ColumnDesc: 8 | TABLE_CATALOG = 0 9 | TABLE_SCHEMA = 1 10 | TABLE_NAME = 2 11 | COLUMN_NAME = 3 12 | ORDINAL_POSITION = 4 13 | COLUMN_DEFAULT = 5 14 | IS_NULLABLE = 6 15 | DATA_TYPE = 7 16 | CHARACTER_MAXIMUM_LENGTH = 8 17 | CHARACTER_OCTET_LENGTH = 9 18 | NUMERIC_PRECISION = 10 19 | NUMERIC_PRECISION_RADIX = 11 20 | NUMERIC_SCALE = 12 21 | DATETIME_PRECISION = 13 22 | CHARACTER_SET_CATALOG = 14 23 | CHARACTER_SET_SCHEMA = 15 24 | CHARACTER_SET_NAME = 16 25 | COLLATION_CATALOG = 17 26 | COLLATION_SCHEMA = 18 27 | COLLATION_NAME = 19 28 | DOMAIN_CATALOG = 20 29 | DOMAIN_SCHEMA = 21 30 | DOMAIN_NAME = 22 31 | IS_IDENTITY = 23 32 | 33 | 34 | def handle_decimal_type(column_desc): 35 | precision = column_desc[ColumnDesc.NUMERIC_PRECISION] 36 | scale = column_desc[ColumnDesc.NUMERIC_SCALE] 37 | return "DECIMAL(%s, %s)" % (precision, scale) 38 | 39 | 40 | def handle_char_type(column_desc): 41 | length = column_desc[ColumnDesc.CHARACTER_MAXIMUM_LENGTH] 42 | if length <= 255: 43 | return "CHAR(%s)" % length 44 | else: 45 | return "LONGTEXT" 46 | 47 | 48 | def handle_text_type(column_desc): 49 | length = column_desc[ColumnDesc.CHARACTER_MAXIMUM_LENGTH] 50 | if 0 <= length <= 65535: 51 | return "VARCHAR(%s)" % length 52 | elif 0 <= length <= 166777215: 53 | return "MEDIUMTEXT" 54 | else: 55 | return "LONGTEXT" 56 | 57 | 58 | def handle_blob_type(column_desc): 59 | data_type = column_desc[ColumnDesc.DATA_TYPE].upper() 60 | length = column_desc[ColumnDesc.CHARACTER_MAXIMUM_LENGTH] 61 | if data_type == "BINARY" and length <= 255: 62 | return "BINARY(%s)" % length 63 | elif data_type == "VARBINARY" and 0 <= length <= 65535: 64 | return "VARBINARY(%s)" % length 65 | elif 0 <= length <= 65535: 66 | return "BLOB" 67 | elif 0 <= length <= 166777215: 68 | return "MEDIUMBLOB" 69 | else: 70 | return "LONGBLOB" 71 | 72 | 73 | TYPES_MAP = { 74 | 'INT': 'INT', 75 | 'TINYINT': 'TINYINT', 76 | 'SMALLINT': 'SMALLINT', 77 | 'BIGINT': 'BIGINT', 78 | 'BIT': 'TINYINT(1)', 79 | 'FLOAT': 'FLOAT', 80 | 'REAL': 'FLOAT', 81 | 'NUMERIC': handle_decimal_type, 82 | 'DECIMAL': handle_decimal_type, 83 | 'MONEY': handle_decimal_type, 84 | 'SMALLMONEY': handle_decimal_type, 85 | 'CHAR': handle_char_type, 86 | 'NCHAR': handle_char_type, 87 | 'VARCHAR': handle_text_type, 88 | 'NVARCHAR': handle_text_type, 89 | 'DATE': 'DATE', 90 | 'DATETIME': 'DATETIME', 91 | 'DATETIME2': 'DATETIME', 92 | 'SMALLDATETIME': 'DATETIME', 93 | 'DATETIMEOFFSET': 'DATETIME', 94 | 'TIME': 'TIME', 95 | 'TIMESTAMP': 'TIMESTAMP', 96 | 'ROWVERSION': 'TIMESTAMP', 97 | 'BINARY': handle_blob_type, 98 | 'VARBINARY': handle_blob_type, 99 | 'TEXT': handle_text_type, 100 | 'NTEXT': handle_text_type, 101 | 'IMAGE': handle_blob_type, 102 | 'SQL_VARIANT': handle_blob_type, 103 | 'TABLE': handle_blob_type, 104 | 'HIERARCHYID': handle_blob_type, 105 | 'UNIQUEIDENTIFIER': 'VARCHAR(64)', 106 | 'SYSNAME': 'VARCHAR(160)', 107 | 'XML': 'TEXT' 108 | } 109 | 110 | 111 | def get_column_type(column_desc): 112 | source_type = column_desc[ColumnDesc.DATA_TYPE].upper() 113 | target_type = TYPES_MAP.get(source_type) 114 | if target_type is None: 115 | return None 116 | elif isinstance(target_type, basestring): 117 | return target_type 118 | else: 119 | return target_type(column_desc) 120 | 121 | 122 | def convert_column_default(col): 123 | default_value = col[ColumnDesc.COLUMN_DEFAULT] 124 | if default_value is None: 125 | return '' 126 | if default_value.startswith('((') and default_value.endswith('))'): 127 | default_value = default_value[2:-2] 128 | elif default_value.startswith('(') and default_value.endswith(')'): 129 | default_value = default_value[1:-1] 130 | 131 | if '(' in default_value and ')' in default_value: 132 | default_value = None 133 | elif default_value.startswith('CREATE'): 134 | default_value = None 135 | return ' DEFAULT %s' % default_value if default_value else '' 136 | 137 | 138 | class ReaderSqlserver(ReaderBase): 139 | 140 | # 构造函数 141 | def __init__(self, host, port, dbname, username, password,magic_field_name): 142 | ReaderBase.__init__(self, host, port, dbname, username, password,magic_field_name) 143 | 144 | # 建立与SQLServer数据库的连接 145 | def connect(self): 146 | params = {'server': self.host, 'port': self.port, 'database': self.dbname, 147 | 'user': self.username, 'password': self.password, } 148 | self._connection = pymssql.connect(timeout=90, **params) 149 | 150 | # 关闭与SQLServer的连接 151 | def close(self): 152 | pass 153 | 154 | # 查询表内所有的数据 155 | def find_all(self, cursor, sql): 156 | 157 | try: 158 | cursor.execute(sql) 159 | except pymssql.OperationalError, e: 160 | self.connect() 161 | cursor = self._connection.cursor() 162 | cursor.execute(sql) 163 | except Exception, e: 164 | return False, e.message 165 | 166 | return True, cursor 167 | 168 | # 获取SQLServer的建表语句,原理:利用SQLServer的三个SQL获取表的列、主键、索引信息,然后生成MySQL的建表语句 169 | def get_mysql_create_table_sql(self, curr_table_name, new_table_name=None, create_if_not_exist=False): 170 | 171 | try: 172 | # 获取列信息 173 | columns = self.__query_table_columns(curr_table_name) 174 | 175 | # 获取主键列信息 176 | primary_key_column = self.__query_table_primary_key(curr_table_name) 177 | 178 | # 获取索引信息 179 | indexes = self.__query_table_indexes(curr_table_name) 180 | except Exception, e: 181 | return False, e.message, [] 182 | 183 | indexes = indexes or [] 184 | 185 | ###################### 186 | # 生成创建表的SQL语句 187 | ###################### 188 | 189 | columns = sorted(columns, key=lambda x: x[ColumnDesc.ORDINAL_POSITION]) 190 | 191 | table_name = columns[0][ColumnDesc.TABLE_NAME] 192 | if new_table_name is not None: 193 | table_name = new_table_name 194 | 195 | cols = [] 196 | columns_names = [] 197 | auto_increment_column = None 198 | for col in columns: 199 | columns_names.append(col[ColumnDesc.COLUMN_NAME]) 200 | cols.append("`%s` %s %s%s" % (col[ColumnDesc.COLUMN_NAME], 201 | get_column_type(col), 202 | convert_column_default(col), 203 | " NOT NULL" if col[ColumnDesc.IS_NULLABLE] == 'NO' else '')) 204 | auto_increment_column = col[ColumnDesc.COLUMN_NAME] if col[ 205 | ColumnDesc.IS_IDENTITY] else auto_increment_column 206 | 207 | cols.append("%s timestamp NOT NULL DEFAULT '0000-00-00 00:00:00'" % self.magic_field_name) 208 | 209 | if primary_key_column: 210 | primary_key_column_fields = ",".join(["`%s`" % i for i in primary_key_column]) 211 | cols.append('PRIMARY KEY (%s)' % primary_key_column_fields) 212 | 213 | for index in indexes: 214 | unique = 'UNIQUE' if 'unique' in index[1].lower() else '' 215 | cols.append('%s KEY `%s` (%s)' % (unique, index[0][:64], re.sub("\([+-]+\)", "", index[2]))) 216 | 217 | if create_if_not_exist: 218 | create_table_sql = 'CREATE TABLE IF NOT EXISTS `%s` (\n%s) ENGINE=InnoDB DEFAULT CHARSET=utf8' % ( 219 | table_name, ',\n'.join(cols)) 220 | else: 221 | create_table_sql = 'CREATE TABLE `%s` (\n%s) ENGINE=InnoDB DEFAULT CHARSET=utf8' % ( 222 | table_name, ',\n'.join(cols)) 223 | 224 | return True, create_table_sql, columns_names 225 | 226 | # 获取表的列信息 227 | def __query_table_columns(self, table_name): 228 | cursor = self._connection.cursor() 229 | sql = "select *, COLUMNPROPERTY(object_id(TABLE_NAME), COLUMN_NAME, 'IsIdentity') as IS_IDENTITY from information_schema.COLUMNS where TABLE_NAME='%s'" % table_name 230 | 231 | try: 232 | cursor.execute(sql) 233 | except pymssql.OperationalError, e: 234 | self.connect() 235 | cursor = self._connection.cursor() 236 | cursor.execute(sql) 237 | except Exception, e: 238 | raise Exception(e.message) 239 | 240 | columns = list(cursor.fetchall()) 241 | cursor.close() 242 | return columns 243 | 244 | # 获取表的主键列信息 245 | def __query_table_primary_key(self, table_name): 246 | cursor = self._connection.cursor() 247 | sql = "select CONSTRAINT_NAME from information_schema.TABLE_CONSTRAINTS where TABLE_NAME='%s' and CONSTRAINT_TYPE='PRIMARY KEY'" \ 248 | % table_name 249 | 250 | try: 251 | cursor.execute(sql) 252 | except pymssql.OperationalError, e: 253 | self.connect() 254 | cursor = self._connection.cursor() 255 | cursor.execute(sql) 256 | except Exception, e: 257 | raise Exception(e.message) 258 | 259 | r = cursor.fetchall() 260 | cursor.close() 261 | if not r: 262 | return None 263 | 264 | constraint_name = r[0][0] 265 | sql = "select COLUMN_NAME from information_schema.KEY_COLUMN_USAGE where TABLE_NAME='%s' and CONSTRAINT_NAME='%s'" \ 266 | % (table_name, constraint_name) 267 | 268 | cursor = self._connection.cursor() 269 | try: 270 | cursor.execute(sql) 271 | except pymssql.OperationalError, e: 272 | self.connect() 273 | cursor = self._connection.cursor() 274 | cursor.execute(sql) 275 | except Exception, e: 276 | raise Exception(e.message) 277 | 278 | r = cursor.fetchall() 279 | cursor.close() 280 | 281 | ret = [] 282 | if r: 283 | for item in r: 284 | ret.append(item[0]) 285 | 286 | return ret 287 | 288 | # 获取表的索引信息 289 | def __query_table_indexes(self, table_name): 290 | cursor = self._connection.cursor() 291 | sql = "sp_helpindex '%s'" % table_name 292 | 293 | try: 294 | cursor.execute(sql) 295 | except pymssql.OperationalError, e: 296 | self.connect() 297 | cursor = self._connection.cursor() 298 | cursor.execute(sql) 299 | except Exception, e: 300 | raise Exception(e.message) 301 | 302 | indexes = list(cursor.fetchall()) if cursor.description else [] 303 | cursor.close() 304 | return indexes 305 | --------------------------------------------------------------------------------