三、将分钟气象观测数据处理后导入 Oracle 数据库


  将数据文件从数据源采集回来了,要将数据导入数据库中。
在这里插入图片描述

一、分析需求设计参数及准备程序的前期工作

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();// 闭文件指针,并删除文件

四、释放数据库连接

  数据库连接也是一种资源,应该在处理了一批数据之后,如果没有数据要处理,或者要等待一段时间后才有数据。这时候应该要释放数据库的连接。

五、总结

  这次要注意的就是,防止数据主键冲突而导致程序的性能下降,如何防止冲突如果发生了,冲突如何处理。

  • 0
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值