将数据文件从数据源采集回来了,要将数据导入数据库中。
![在这里插入图片描述](https://i-blog.csdnimg.cn/blog_migrate/0a01b875cdf4f038a893177751fdf0ef.png)
一、分析需求设计参数及准备程序的前期工作
1.设计参数
这个程序的目的是将数据导入数据库中,所以要找到数据存放的路径,指定的数据库。如果加上程序本身的可执行文件和程序运行日志文件名,一共就是 4个参数。
(1)数据文件存放的目录
(2)连接目的数据库连接参数
(3)程序本身可执行程序
(4)程序运行日志文件名
2.准备写程序的前期工作
在开始设计程序或者说写程序之前,前期工作准好
(1)包含所需的头文件
(2)编写好使用程序的提示信息
(3)程序调试的一个重要的工具,贯穿程序始终——程序的运行日志 ,
所以把程序的日志文件准备好。也就是定义日志文件操作对象,打开日志文件。
(4)屏蔽程序的中的信号,并编写 处理程序退出的信号的函数
3.设计表
(1)分钟气象观测数据格式
要分钟气象观测数据导入数据库前,需要根据数据的格式设计表。数据的格式如下:
(2)存放数据的表的格式
设计的表如下图:
注意这个表的主键是由站点代码和数据的时间组成的,因为一个站点会生成多个数据,需要用到数据的生成时间和站点代码来共同标识一个气象观测数据。
二、设计程序的基本流程
程序的目的是将分钟气象观测数据处理后导入数据库中。所以程序的大致流程应该如下:
(1)打开分钟气象观测数据存放的目录,并匹配指定的文件格式
(2)读取目录中的分钟气象观测数据文件,处理数据
(3)连接数据库,将处理后的数据导入数据库,提交事务
为了主程序的简洁性,将这些步骤放到一个处理函数中。
三、具体实现
1.打开数据存放的目录
打开某个目录,用到的是 “_freecplus.h” 中 CDir 类(目录操作类)的 OpenDir 函数。
这个函数的作用是:打开目录,获取目录中的文件列表信息,存放于m_vFileName容器中。
bool OpenDir(const char *in_DirName,const char *in_MatchStr,const unsigned int in_MaxCount=10000,const bool bAndChild=false,bool bSort=false);
(1)in_DirName,待打开的目录名,采用绝对路径,如/tmp/root。
(2) in_MatchStr,待获取文件名的匹配规则,不匹配的文件被忽略,具体请参见freecplus框架的MatchStr函数。
(3)in_MaxCount,获取文件的最大数量,缺省值为10000个。
(4)bAndChild,是否打开各级子目录,缺省值为false-不打开子目录。
(5)bSort,是否对获取到的文件列表(即m_vFileName容器中的内容)进行排序,缺省值为false-不排序。
(6)返回值:true-成功,false-失败,如果in_DirName参数指定的目录不存在,OpenDir方法会创建该目录,如果创建失败,返回false,如果当前用户对in_DirName目录下的子目录没有读取权限也会返回false。
// 打开分钟气象观测数据文件所在的目录
if ( Dir.OpenDir(argv[1],"SURF_ZH_*.txt",1000,true,true) == false )
{
logfile.Write("Dir.OpenDir(%s)失败。\n",argv[1]);
return false;
}
2.读取 m_vFileName容器中的文件名
打开了目录,获取目录中的指定类型的文件,将匹配的文件名,放入m_vFileName 容器中。现在需要将容器的文件名读取出来,用到 CDir 类中的 ReadDir 函数
bool ReadDir();
该函数的作用为:从m_vFileName容器中获取一条记录(文件名),同时获取该文件的大小、修改时间等信息。
if( Dir.ReadDir() == false )
{
logfile.Write("Dir.ReadDir() 完毕。\n");
return false;
}
3.打开文件,读取数据,将数据处理后入库
从上面的步骤,获取到了匹配条件的文件名。打开文件,先读取数据,再将数据处理后导入数据库。
(1)打开文件用,CFile 类中的 Open 函数
bool Open(const char *filename,const char *openmode,bool bEnBuffer=true);
1) filename:待打开的文件名,建议采用绝对路径的文件名。
2) openmode:打开文件的模式,与fopen库函数的打开模式相同。
3) bEnBuffer:是否启用缓冲,true-启用;false-不启用,缺省是启用。
// 注意:如果待打开的文件的目录不存在,就会创建目录。
if ( File.Open(Dir.m_FullFileName,"r") == false )
{
logfile.Write("File.Open(%s) 失败。\n",Dir.m_FullFileName);
return false;
}
(2)while(true) 逐条读取文件中,用 CFile 类中的 Fgets 函数。
bool Fgets(char *buffer,const int readsize,bool bdelcrt=false)
// 从文件中读取以换行符"\n"结束的一行。
// buffer:用于存放读取的内容,buffer必须大于readsize+1,否则可能会造成内存的溢出。
// readsize:本次打算读取的字节数,如果已经读取到了结束标志"\n",函数返回。
// bdelcrt:是否删除行结束标志"\r"和"\n",true-删除;false-不删除,缺省值是false。
// 返回值:true-成功;false-失败,一般情况下,失败可以认为是文件已结束。
char strBuffer[301];
while (true)
{
memset(strBuffer,0,sizeof(strBuffer));
if ( File.Fgets(strBuffer,300,true) == false )
{
logfile.Write("File.Fgets(%s) 读取一个文件的数据记录完毕。\n",Dir.m_FullFileName);
break;
}
}
(3)处理读取出来的数据记录
读取出来的数据记录是一串字符串,如下图:
在入库之前,要将整体的一行字符串的每个对应值(分钟气象观测数据的格式)拆分出来。因为拆分出来的值比较多,可以考虑将它们放到分钟气象观测数据的结构体里面。
拆分字符串用到 CCmdStr 类(拆分字符串类)
// CCmdStr类用于拆分有分隔符的字符串。
// 字符串的格式为:字段内容1+分隔符+字段内容2+分隔符+字段内容3+分隔符+...+字段内容n。
// 例如:"messi,10,striker,30,1.72,68.5,Barcelona",这是足球运动员梅西的资料,包括姓名、
// 球衣号码、场上位置、年龄、身高、体重和效力的俱乐部,字段之间用半角的逗号分隔。
class CCmdStr
{
public:
vector<string> m_vCmdStr; // 存放拆分后的字段内容。
CCmdStr(); // 构造函数。
// 把字符串拆分到m_vCmdStr容器中。
// buffer:待拆分的字符串。
// sepstr:buffer中采用的分隔符,注意,sepstr参数的数据类型不是字符,是字符串,如","、" "、"|">、"~!~"。
// bdelspace:拆分后是否删除字段内容前后的空格,true-删除;false-不删除,缺省删除。
void SplitToCmd(const string buffer,const char *sepstr,const bool bdelspace=true);
// 获取拆分后字段的个数,即m_vCmdStr容器的大小。
int CmdCount();
// 从m_vCmdStr容器获取字段内容。
// inum:字段的顺序号,类似数组的下标,从0开始。
// value:传入变量的地址,用于存放字段内容。
// 返回值:true-成功;如果inum的取值超出了m_vCmdStr容器的大小,返回失败。
bool GetValue(const int inum,char *value,const int ilen=0); // 字符串,ilen缺省值为0。
bool GetValue(const int inum,int *value); // int整数。
bool GetValue(const int inum,unsigned int *value); // unsigned int整数。
bool GetValue(const int inum,long *value); // long整数。
bool GetValue(const int inum,unsigned long *value); // unsigned long整数。
bool GetValue(const int inum,double *value); // 双精度double。
bool GetValue(const int inum,bool *value); // bool型。
~CCmdStr(); // 析构函数。
};
1)根据分钟气象观测数据的格式,一条数据记录拆分出来的值要有 9 个,将数据记录拆分成九个值,用 CCmdStr 类中的:
void SplitToCmd(const string buffer,const char *sepstr,const bool bdelspace=true)
CCmdStr CmdStr;
CmdStr.SplitToCmd(strBuffer,",");
if ( CmdStr.CmdCount() != 9 )
{
logfile.Write("CmdStr.CmdCount(%s),拆分错误数量不对。\n",strBuffer);
continue;
}
2)拆分出来的值,暂时存放到对应的数据结构体的成员变量里面。
// 将拆分出来的值放到数据结构体的对应成员变量里面
CmdStr.GetValue(0,stsurfdata.obtid,5); // 站点代码
CmdStr.GetValue(1,stsurfdata.ddatetime,19); // 数据时间:格式yyyy-mm-dd hh:mi:ss。
// 注意有些变量是浮点型的,数据结构体里面的数是整型的,要转换
double dtmp=0;
CmdStr.GetValue(2,&dtmp); stsurfdata.t=(int)(dtmp*10); // 气温:单位,0.1摄氏度
CmdStr.GetValue(3,&dtmp); stsurfdata.p=(int)(dtmp*10); // 气压:0.1百帕
CmdStr.GetValue(4,&stsurfdata.u); // 相对湿度,0-100之间的值。
CmdStr.GetValue(5,&stsurfdata.wd); // 风向,0-360之间的值。
CmdStr.GetValue(6,&dtmp); stsurfdata.wf=(int)(dtmp*10); // 风速:单位0.1m/s
CmdStr.GetValue(7,&dtmp); stsurfdata.r=(int)(dtmp*10); // 降雨量:0.1mm
CmdStr.GetValue(8,&dtmp); stsurfdata.vis=(int)(dtmp*10); // 能见度:0.1米
(4)将数据导入数据库表中
将数据记录拆分放到数据结构体里后,把数据插入数据库中。这涉及数据库的操作,数据库的操作步骤之前的文章已经介绍过。
数据库操作步骤文章链接:https://blog.csdn.net/qq_43403759/article/details/116716792
1)定义数据库连接对象,并连接数据库
connection conn; // 数据库连接池对象
// 数据处理后,导入数据库,连接数据库
if ( conn.m_state == 0 )
{
if ( conn.conecttodb(arg[2],"Simplified Chinese_China.ZHS16GBK") == false )
{
logfile.Write("connect database(%s) failed.\n%s\n",strConnectDb,conn.m_cda.message);
return false;
}
logfile.Write("连接数据库成功。\n");
}
2)定义SQL语句执行对象
将数据插入数据库还有一个要考虑的问题:插入的数据是否会经常发生主键冲突,如果数据量庞大且发生冲突,将会导致程序性能下降。
主键冲突的影响及解决方法之前的文章有介绍:
https://blog.csdn.net/qq_43403759/article/details/116484438
为避免程序的性能下降严重,要在插入数据之前,查询数据库中是否已有该数据的记录,如果没有就插入。
进行查询(select)操作,定义执行查询语句对象,并绑定数据库连接池对象地址。
// 定义sql语句执行对象
sqlstatement selstmt;
slestmt.connect(&conn);
进行插入(insert)操作,定义执行插入语句对象,并绑定数据库连接池对象地址。
sqlstatement insstmt;
insstmt.connect(&conn);
3)准备SQL语句
查询语句
selstmt.prepare("select count(*) from t_surfdata where obtid=:1 and ddatetime=to_date(:2,'yyyy-mm-dd hh24:mi:ss')");
// 绑定输入输出变量
int icount=0;
selstmt.bindout(1,&iccount);
selstmt.bindin(1,stsurfdata.obtid,5);
selstmt.bindin(2,stsurfdata.ddatetime,19);
插入语句
// 准备插入SQL语句
insstmt.prepare("insert into t_surfdata (obtid,ddatetime,t,p,u,wd,wf,r,vis,crttime,keyid) values(:1,to_date(:2,'yyyy-mm-dd hh24:mi:ss'),:3,:4,:5,:6,:7,:8,:9,sysdate,SEQ_SURFDATA.nextval)");
// 绑定输入变量
insstmt.bindin(1, stsurfdata.obtid,5);
insstmt.bindin(2, stsurfdata.ddatetime,19);
insstmt.bindin(3,&stsurfdata.t);
insstmt.bindin(4,&stsurfdata.p);
insstmt.bindin(5,&stsurfdata.u);
insstmt.bindin(6,&stsurfdata.wd);
insstmt.bindin(7,&stsurfdata.wf);
insstmt.bindin(8,&stsurfdata.r);
insstmt.bindin(9,&stsurfdata.vis);
4) 执行查询语句
执行SQL语句成功的返回值为 0。如果执行失败,将执行的SQL语句,错误信息写到程序运行日志中。执行失败,还要检查是否是因为数据库连接断开,如果是,就退出程序,防止数据丢失(之前的文章有介绍)。
if ( selstmt.execute() != 0 )
{
logfile.Write("selstmt.execute() failed.执行查询语句失败。\n%s\n%s\n",selstmt.m_sql,selstmt.m_cda.message);
if ( (stmtsel.m_cda.rc >= 3113 )&&(selstmt.m_cda.rc <= 3115 ) )
{ return false; }
continue;
}
现在来补充一些变量的含义:
int m_state; // 与数据库的连接状态,0-未连接,1-已连接。
char m_sql[10241]; // SQL语句的文本,最长不能超过10240字节。
CDA_DEF m_cda; // 数据库操作的结果或最后一次执行SQL语句的结果。
失败的代码在m_cda.rc中,失败的描述在m_cda.message中
解释执行查询语句这段代码:
stmtsel.m_cda.rc >= 3113 )&&(selstmt.m_cda.rc <= 3115 ) 信号值 3113-3115 是数据库断开连接的值。 数据库断开连接了,要退出程序,防止数据丢失。
而其他的错误要继续执行(continue)程序,这是为什呢?当其他的错误发生时,有可能是这一次执行查询的数据的格式不正确,但是它的后面还有很多数据,不能够因为一条数据出错就将整个程序停下来,拣了芝麻丢了西瓜,因小失大。
5)主键不冲突,执行插入语句,插入数据
查询语句是查询 t_surfdata 表中,有多少条记录与现在插入的这一条是相同的(主键),把数目放在 icount 变量中。
if ( icount > 0 ) continue;
如果,icount 大于 0 ,那么表中已经有这条记录,不执行插入语句了。
if ( icount > 0 ) continue;
if ( insstmt.execute() != 0 )
{
logfile.Write("insstmt.execute() failed.执行插入语句失败。\n%s\n%s\n",insstmt.m_sql,insstmt.m_cda.message);
if ( ( insstmt.m_cda.rc >= 3113 ) && ( insstmt.m_cda.rc <= 3115 ))
{ return false; }
continue;
}
6)提交事务
conn.commit(); // 提交事务
logfile.Write("%s 文件导入数据库成功。\n",Dir.m_FullFileName);
File.CloseAndRemove();// 闭文件指针,并删除文件
四、释放数据库连接
数据库连接也是一种资源,应该在处理了一批数据之后,如果没有数据要处理,或者要等待一段时间后才有数据。这时候应该要释放数据库的连接。
五、总结
这次要注意的就是,防止数据主键冲突而导致程序的性能下降,如何防止冲突如果发生了,冲突如何处理。