一次业务场景需要,得把一个数据库中的数据导入另一个数据库,2个数据库类型不同,当时采用的方式为:
把数据库A的表中的数据导出成dat文件(这个数据库导出的文件就是dat文件),一行一条记录,字段顺序按照建表字段顺序,各个字段中间用欧元符号分隔,在数据库B中建表,表结构与数据库中的表结构完全一致,最后再增加一个ids自增字段。dat文件存放在文件服务器指定目录,程序通过ssh连接服务器,并获取dat文件流来获取文件,一行一行读取文件内容然后批量入B库。文件名称中的编码字符可以确定这个文件入哪个表。持久层框架用的是mybatis。
总共十几+个表,表的字段也都很多,既然文件格式有约定,那入B库时的sql不计划指定字段插入,太多太麻烦。
#指定字段插入数据
insert into tableB (field1,field2) values (#{i1},#{i2});
#不指定字段插入数据,需要按顺序给出全部字段的值
insert into tableB values (#{i1} ... );
最终处理流程是:
1.通过BufferedReader的readLine()方法一行一行读数据;
2.将读出的数据按照字段分隔符(此处为€)分隔成字符串数组,再将字符串数组转成List line,再用ArrayList 包装一下;
List lineList=new ArrayList(Arrays.asList(line.split(dataFileSeparator)));
那么这个lineList里的数据就为一行数据的各个字段值。
为什么要用ArrayList再包装下呢?因为Arrays.asList()返回的对象是 Arrays的内部类,继承了AbstractList,AbstractList中的set, add, remove 抽象方法中都是直接throw new UnsupportedOperationException(); 但内部类 ArrayList 并未重写父类的 add,remove方法,所以我们不能再对存放各个字段值的List不能增加删除,如果不涉及增加其他字段值或删除字段值的操作,可以直接使用,不用包装。对Arrays.asList 再包装一层,生成的List对象就是我们熟知的 ArrayList了。
3.在循环外层定义一个ArrayList datas,用来存放一行一行数据的List lineList;
4.将datas作为参数传给mapper中的入库方法,xml这么写的:
<insert id="insertModelData">
insert into ${tableName} values
<foreach collection="list" item="line" separator=",">
(
<foreach collection="line" item="item" separator=",">
#{item}
</foreach>
)
</foreach>
</insert>
表名根据文件名称映射,第一层foreach取出一行一行的所有数据,第二层foreach取出每行的各个字段。
就这样,不用写一大堆sql,一个方法就可以满足此任务的所有表的数据入库操作。但是要注意${}、#{}的区别,有需要要额外处理${}字段。
此任务中其他需要注意的问题:
1.BufferedReader的readLine()读出的字符串中不包含结尾的"\r","\n"
2.BufferedReader的readLine()相关源码如下:
public String readLine() throws IOException {
return readLine(false);
}
String readLine(boolean ignoreLF) throws IOException {
StringBuffer s = null;
int startChar;
synchronized (lock) {
ensureOpen();
boolean omitLF = ignoreLF || skipLF;
bufferLoop:
for (;;) {
if (nextChar >= nChars)
fill();
if (nextChar >= nChars) { /* EOF */
if (s != null && s.length() > 0)
return s.toString();
else
return null;
}
boolean eol = false;
char c = 0;
int i;
/* Skip a leftover '\n', if necessary */
if (omitLF && (cb[nextChar] == '\n'))
nextChar++;
skipLF = false;
omitLF = false;
charLoop:
for (i = nextChar; i < nChars; i++) {
c = cb[i];
if ((c == '\n') || (c == '\r')) {
eol = true;
break charLoop;
}
}
startChar = nextChar;
nextChar = i;
if (eol) {
String str;
if (s == null) {
str = new String(cb, startChar, i - startChar);
} else {
s.append(cb, startChar, i - startChar);
str = s.toString();
}
nextChar++;
if (c == '\r') {
skipLF = true;
}
return str;
}
if (s == null)
s = new StringBuffer(defaultExpectedLineLength);
s.append(cb, startChar, i - startChar);
}
}
}
可以看出该方法可以设置跳过LF,但是该方法访问权限为default,不支持继承该类重写方法,而且其他方法也不支持我们设置修改变量,所以我们没办法控制readLine()是读到\r 换行还是 \n换行还是\r\n换行,我们没法指定特定换行符来让它读一行,所以文件中有^M时需要留意,有可能它读了半行就返回了。
在windows平台下,换行符是\r\n,而在linux下是\n,这多出来的\r被vim解释成了^M。
windows linux MacBook 换行符 \r\n \n \r ASCII 0x0d0a 0x0a 0x0d 其中:
- "\r"在ASCII中表示“换行(LF)”
- "\n"在ASCII中表示“回车(CR)”
访问控制修饰符 private default protected public 同一类中的成员 √ √ √ √ 同一包中其他类 √ √ √ 不同包中的子类 √ √ 不同包中非子类 √
笔者当时就遇到^M这个问题,返回半行,入库时mysql报字段不匹配的错误,当然了,少字段嘛。当时时间紧而且是临时需求 ,过一段时间就不用了,所以没有通过程序解决,采用的方式是替换文件中的^M符号。通过程序的话貌似得用read方法,这个就有点复杂了,到指定的换行符才停,逻辑有点小复杂,没时间呀~这里mark一下,有空研究,也欢迎有经验的朋友分享心得。
以下内容来自:Linux下去掉^M的4种方法
第一种方法:
cat -A filename
就可以看到windows下的断元字符 ^M要去除他,最简单用下面的命令:
dos2unix filename
第二种方法:
1
2
sed
-i ‘s/^M
//g
' filename
#注意:^M的输入方式是 Ctrl + v ,然后Ctrl + M
第三种方法:
1
2
3
#vi filename
:1,$ s/^M
//g
^M 输入方法: ctrl+V ,ctrl+M
第四种方法:
1
2
#cat filename |tr -d ‘/r' > newfile
#^M 可用 /r 代替