码工考古之DataSync
^^^^^^^^^^^^^^^^^^
 - 作者:臭豆腐[trydofor.com]
 - 日期:2010-06-15
 - 授权:署名-非商业-保持一致 1.0 协议
 - 声明:拷贝、分发、呈现和表演本作品,请保留以上全部信息。

0. 文档目录
^^^^^^^^^^^
[[<=$INDEX]]

1. DataSync是啥玩意
^^^^^^^^^^^^^^^^^^^
DataSync 是一个小布丁点的工程,用来在两个数据库间倒扯数据。

 * 纯JDBC,以PreparedStatement插数据,以SQL记log。
 * 纯SQL同步,支持变量。
 * 自动解决表间依赖关系。
 * 可自定义字段生成规则。
 * 不支持目标数据库的Update,只能Insert。
 * 不能一次倒扯海量数据。
 * java文件27个。
 * 代码1827行,注释196行,空行358,共计2362行。
 * 累积人力约1*8人时。

原始需求是这样的:
我们的高利贷系统,有近20*5个测试DB,经常要在两个DB间倒扯数据玩。
但不幸的是DB的主键有自增的,有业务逻辑产生的;表关系复杂;表多。

2. 蛇打七寸擒贼擒王
^^^^^^^^^^^^^^^^^^^
初步的需求分析和程序设计,集中在配置文件的定义上。
工具是文本编辑器,两张打印纸加签字笔。电脑方便撤销与修改,笔纸则便于涂鸦。
写了改,改了写的过程贯穿项目始终,美其名曰:持续重构 O(∩_∩)O。

[![测试数据的ER图:]]
[[<=./data/erd.png]]

一个自然人(PERSON,自增主键)对应一个顾客(CUSTOMER,业务主键),
一个顾客对应多个债权(LOAN,业务主键),
一个债权对应多个契约(CONTRACT,双主键,MAX+1)。
各表有特殊用途的字段,如:LOGNO,CREATEDBY,CREATEDDT。

[![程序配置文件:]]
=========================== xml : 配置文件 ===========================
<?xml version="1.0" encoding="UTF-8"?>
<root>
  <!-- 数据库配置 -->
  <database>
    <!-- 源数据库 -->
    <source>
      <driver>com.ibm.db2.jcc.DB2Driver</driver>
      <url>jdbc:db2://10.4.9.52:50000/SEC0601</url>
      <user>db2inst1</user>
      <pass>LNjT4DRw</pass>
    </source>
    <!-- 目标数据库 -->
    <target>
      <driver>com.ibm.db2.jcc.DB2Driver</driver>
      <url>jdbc:db2://10.4.5.242:50000/PLX</url>
      <user>db2inst1</user>
      <pass>s1Iup3Dz</pass>
    </target>
  </database>
  <!-- 全局变量 以 ${name}格式调用 -->
  <parameters>
    <!--
        | type=text时,以字符串保持value值或value指向文件的全部内容。
        | type=list时,以字符串列表保持value值或value指向文件的每一行。
    -->
    <para name="system.commit.debug" type="text" value="true"/>
    <para name="system.target.delete" type="text" value="false"/>
    <para name="system.target.existed" type="text" value="delete"/>
    <para name="system.source.delete" type="text" value="false"/>
    <!-- ./data/loanid.txt 文件一行一个债权,存到 loanids的List中 -->
    <para name="loanids" type="list" value="./data/loanid.txt"/>
    <para name="logno" type="text" value="9999"/>
  </parameters>
  <!-- 操作对象(表) -->
  <candidate>
    <!-- 表的共通字段 -->
    <common>
      <!-- 使用变量 logno -->
      <column name="LOGNO"         value="${logno}" />
      <!-- 使用固定值 9999 -->
      <column name="CREATEDBY" value="9999" />
      <!-- 
          | 可以是固定值 2010-05-05 05:05:05 也可是DB变量 CURRENT TIMESTAMP
          | mutable 为true,该字段可变,不能作为where条件。默认为false。
      -->
      <column name="CREATEDDT" mutable="true" value="CURRENT TIMESTAMP" />
    </common>
    <tables> <!-- 表的集合 -->
      <table name="LOAN"> <!-- 表名 LOAN -->
        <!-- 该表在源数据库上抽取数据的SQL,使用全局变量 loanids -->
        <source>SELECT * FROM LOAN WHERE
                LOANID IN (${loanids})
        </source>
      </table>
      <table name="CUSTOMER">
        <!-- 以源数据中LOAN表的CUSTID集合作为参数 -->
        <source>SELECT * FROM CUSTOMER WHERE 
                CUSTID IN (${LOAN:CUSTID})
        </source>
        <target>
          <!--
              | 在目标数据库中,该表的指定字段如何设值
              | 这里按源数据对应关系,设置成目标数据库中PERSON表的PERSONID值。
          -->
          <column name="PERSONID"  value="${PERSON:PERSONID}" />
        </target>
      </table>
      <table name="PERSON">
        <source>SELECT * FROM PERSON WHERE 
                PERSONID IN (${CUSTOMER:PERSONID})
        </source>
      </table>
      <table name="CONTRACT">
        <source>SELECT * FROM CONTRACT WHERE 
                LOANID IN (${LOAN:LOANID})
        </source>
        <target>
          <!-- 在目标数据库中执行SQL,以返回值设置字段值 -->
          <column name="CONTRACTSEQ"   clazz="SqlQuery" 
              value="SELECT CASE WHEN  MAX(CONTRACTSEQ) IS NULL THEN 1 
                     ELSE MAX(CONTRACTSEQ)+1 END FROM CONTRACT
                     WHERE LOANID=${CONTRACT:LOANID}"
          />
        </target>
      </table>
    </tables>
  </candidate>
</root>
========================================================================

3. 行而知之知而又行
^^^^^^^^^^^^^^^^^^^
编码中有几个兴趣点,稍稍驻足了一些时间,调研和拓展了不少知识。

3.1. XML到类和类关系
^^^^^^^^^^^^^^^^^^^^
类和类关系,在编码,编译和测试时十分方便。
xstream.alias("root", DataSyncConf.class);
xstream.aliasField("parameters",DataSyncConf.class,"parameters");
... ...
xstream.alias("para", ParaConf.class);
xstream.useAttributeFor(ParaConf.class,"name");
... ...
xstream.alias("candidate", CandidateConf.class);
xstream.aliasField("common",CandidateConf.class, "common");

3.2. 数据的依赖关系
^^^^^^^^^^^^^^^^^^^
数据依赖分两中情况:关系依赖和赋值依赖。

关系依赖和DB表的逻辑关系一致,如,
SELECT时是这样的:
一条线是,${loanids}到LOAN到CUSTOMER到PERSON;
另条线是,LOAN到CONTRACT。
而INSERT时,一般是SELECT的反过程。

赋值依赖,即一个字段的赋值方法,可能需要业务逻辑或变量生成。如,
column name="LOGNO" value="${logno} 是变量。
column name="LOANID" clazz="LoanIdMaker" 是业务逻辑。
通常两者是结合使用的,如,
column name="CONTRACTSEQ" clazz="SqlQuery" value="SELECT...${CONTRACT:LOANID}"

3.3. 变量作用及性质
^^^^^^^^^^^^^^^^^^^
变量主要是用来表示和解决依赖的。运行时涉及四类变量。
  1) 系统设置,定义程序行为。如:system.commit.debug
  2) 程序参数,作为生产原料。如:loanids
  3) 数据库结构(Schema)。如,表结构,表关系,字段类型。
  4) 实时数据,是Schema的具体表现。包括源数据和目标数据。

程序设计中,
(1)和(2)放到全局变量中,以 ${name}表示,运行时为 ParaContext。
(3)源DB结构,目标DB须与其一致。不可直接被变量引用。运行时为 DbContext。
(4)实时数据,以${TABLE:COLUMN}表示,运行时为 DataContext。

下面是变量的一个具体应用。
"CONTRACT WHERE LOANID IN (${LOAN:LOANID})"
这个SQL中有一个变量。解析时,重要信息有,
 * 所属DB,是source还是target,视SQL所在位置而定。
 * DB结构,是CONTRACT表LOANID字段。
 * 取值范围,是LOAN表
 * 取值标识,是LOANID,即LOAN表的LOANID字段。

3.4. 字符串分析处理
^^^^^^^^^^^^^^^^^^^
简单的解析SQL。如,
SELECT * FROM LOAN WHERE LOANID IN (${loanids})
必须知道${loanids} 对应的类型是 LOAN表的LOANID字段。
如果loanids是一组值,输出SQL时,是 '***','***'格式。

识别变量格式。如,
 1) name="CREATEDDT" value="2010-05-05 05:05:05"
 2) name="CREATEDDT" mutable="true" value="CURRENT TIMESTAMP"
在SQL表示中,1)输出为 '2010-05-05 05:05:05',2)则是 CURRENT TIMESTAMP

数据构造SQL。如,
单引号转义,数字型直接输出,字符日期加单引号等。

4. 走两步拉出来溜溜
^^^^^^^^^^^^^^^^^^^
程序设计为单线程,并且源数据全部载入后,才操作目标数据库。
优点是,程序结构简单易懂,数据依赖处理方便灵活。缺点是吃内存。

VMware上 java -server -Xmx512M -jar 执行测试数据,
LOAN, columns:212, records:3000
CUSTOMER, columns:109, records:2794
RPERSON, columns:312, records:2794
CONTRACT, columns:182, records:7089

初始化DbContext耗时1.5秒,
Select 15677 条源数据8秒,
插入目标数据(DB2)用了30分钟,平均每秒8.7条。
插入目标数据(MySql)用了8分钟,平均每秒32.7条。
速度真慢,难道是没批量提交?

程序是这样工作地,描述如下,(此图半天功,O(∩_∩)O)

[[<=./data/prc.png]]

下面是应用中较为重要的几个类。
TargetColumnSetter,用来实现如何对目标数据库字段赋值。
SimplePara,默认setter。
SqlQuery,执行SQL,使用结果赋值。

SqlFormat,根据sqlType类型和Object类型构造SQL语句。
VarParser,用来把字符串(主要是SQL)中的变量解析出来。
SqlBuilder,是根据参数,构造 JDBC Statement用的。

DataContext,按顺序存放源数据和目标数据。
ParaContext,存放全局变量。
DbContext,存放DB的表结构和关系;源DB和目标DB连接。

[[<=./data/clz.png]]

5. 后续的反馈和补充
^^^^^^^^^^^^^^^^^^^
-- 2010-06-22 --
阿福在哲思上回复,提到了 http://scriptella.javaforge.com/ 
这个东西是我一直要找但没找到的东西。于是造了个轮子,享受了下过程。
总体来讲,造轮子算重复劳动,尽管对当前需求讲,自己的东西用着舒坦。