65.9K
CodeProject 正在变化。 阅读更多。
Home

终极网格的 Oracle OCI 数据源类,第二部分 - 构建 OCI 数据源类

starIconstarIconstarIconstarIconstarIcon

5.00/5 (7投票s)

2014 年 5 月 22 日

CPOL

31分钟阅读

viewsIcon

25257

downloadIcon

577

一个三部分系列, 演示了如何为 Ultimate Grid 开发 Oracle Call Interface (OCI) 自定义数据源。

引言

本文是三部分系列文章的第二部分,旨在展示如何为 Ultimate Grid 控件构建自定义数据源类。自定义数据源将使用 Oracle Call Interface (OCI) 从 Oracle 数据库为网格提供数据。

第一部分中,我们构建了 Ultimate Grid 控件作为外部 DLL,以便可以将其包含在数据源类中。

在第二部分中,将构建数据源类。我们将设置 OCI 环境,从数据库获取元数据,检索结果列表,并为 Ultimate Grid 控件提供要显示的数据。

第三部分中,我们将把所有内容整合到一个示例应用程序中,该应用程序将使用第一部分构建的网格控件 DLL 和第二部分构建的数据源类,开发一个示例应用程序,在 Ultimate Grid Control 中显示来自 scott 示例模式的 EMP 表的数据。

背景

如果您像我一样面临着从 Windows 客户端浏览存储在 Oracle 数据库中的大量数据的任务,那么您就知道这可能多么具有挑战性。并非不能完成,因为有许多不同的方法可以做到。Oracle 提供了几种集成产品,如 Oracle Objects for OLE (OO4O)、.NET 数据提供程序、Pro*C 预编译器和 OCI。我决定为这个应用程序选择 OCI,因为它在控制和易用性之间提供了良好的权衡。

关注点

过去,在从 Oracle 数据库提取数据时,一直让我感到困扰的一件事就是处理 Oracle DATE 字段。通常,默认格式设置为显示日期,例如 20-APR-2014 。我更喜欢 04/20/2014。过去,我会在 SELECT 语句中使用 TO_CHAR 函数来重新格式化日期。在处理此项目时,我偶然发现了一些 Oracle 文档,其中提到对于 Windows 客户端,格式由 NLS_DATE_FORMAT 环境变量控制。因此,我在开发机器上将其设置为“MM/DD/YYYY”,现在我每次都能获得想要的格式,而无需编写 TO_CHAR 函数。

我在编写数据源类时使用了这种方法。我没有告诉网格期望 DATE 字段,而是将列类型指定为 SQLT_STR。这样,数据库就会使用我在 NLS_DATE_FORMAT 环境变量中指定的格式为我转换 DATE 字段。网格无需担心如何处理 Oracle DATE 字段,我总是能以我想要的方式格式化日期。

构建数据源类

为了开发数据源类,我首先从 Ultimate Grid 产品提供的 CUGDataSource 类派生了一个类。这个类定义了许多虚拟函数,提供了对需要实现的内容的很好认识。还有几个示例程序显示了其他数据源的实现。我以 SQL Server 的示例作为基本指南。

类的定义被放入了一个名为 ocicalls.h 的头文件中,实现了来自基类的以下虚拟函数:

public:
    COci(void);
    ~COci(void);

    //opening and closing
    virtual int Open(LPCTSTR name,LPCTSTR option);
    virtual BOOL IsOpen();
    virtual int SetPassword(LPCTSTR user,LPCTSTR pass);
    virtual int Close();

    //cell info
    int    GetCell(int col,long row,CUGCell *cell);
    int    SetCell(int col,long row,CUGCell *cell);

    //row and col info
    virtual long GetNumRows();
    virtual int GetNumCols();
    virtual int GetColName(int col,CString * string);
    virtual int GetColType(int col,int *type);

    // Set the value of the SQL statement used to build the record set.
    int SetStatement(CString SqlStatement);

此外,还需要一些内部函数来支持 OCI。

private:
    // OCI Internal functions
    int initialize();
    int logon();
    int  describe_column(int numcols);
    int    get_row_count();
    int get_result_list();
    CString GetOciError();

    // So view class can display in title bar
    CString GetTableName(void);

Oracle Call Interface 在其实现中使用指针进行处理。起点是 OCI 环境句柄,它由调用 OCIEnvCreate 返回。此调用初始化 OCI 库并设置环境。

其他将要使用的句柄是服务器和服务器上下文句柄、错误句柄、语句句柄、描述句柄和会话句柄。指针变量定义在 ocicall.h 中。

    OCIEnv      *m_envhp;
    OCIServer   *m_srvhp;
    OCIError    *m_errhp;
    OCISvcCtx   *m_svchp;
    OCIStmt     *m_stmhp;
    OCIDescribe *m_dschp;
    OCISession  *m_authp;
    desc_parms  *m_Parms;

我们将随着代码的查看来解释每个句柄的用法。

为了存储列属性(如列名、列类型等)以及存储 SQL SELECT 语句返回的数据值,我定义了一个名为 desc_parms 的结构,因为这些属性由 OCI 隐式描述功能提供。结构定义如下:

typedef struct
{
    char      *ColName;
    ub2          collen;
    ub2          coltyp;
    ub1          precision;
    sb1          scale;
    void      *pValue;
    sb4          value_size;
    ub2          rlenp;
    sb2          indp;
    OCIDefine *defnpp;
} desc_parms;

将使用一个结构数组,每个列出现一次。首先确定列的数量,然后动态分配适当的内存。

关于 ocicalls.h 文件,最后一点是,请记住我在第一部分中提到过我将使用一个 define ,这与 Ultimate Grid 的开发人员使用的类似。这将使这个头文件既可以被构建 DLL 的项目使用,也可以被使用它的任何项目使用。这个 define 如下所示:

#ifndef OCI_CLASS_DECL
    #ifdef _BUILD_OCI_INTO_EXTDLL
        #define OCI_CLASS_DECL AFX_CLASS_EXPORT
    #elif defined _LINK_TO_OCI_IN_EXTDLL
        #define OCI_CLASS_DECL AFX_CLASS_IMPORT
    #else
        #define OCI_CLASS_DECL
    #endif
#endif

然后,类的定义如下:class OCI_CLASS_DECL COci : public CUGDataSource,预处理器定义控制 OCI_CLASS_DECL 被定义为 AFX_CLASS_EXPORT 还是 AFX_CLASS_IMPORT

构建 DLL 时,我们添加一个预处理器定义 _BUILD_OCI_INTO_EXTDLL 来导出函数。在我们即将使用 DLL 的任何项目中,我们添加一个预处理器定义 _LINK_TO_OCI_IN_EXTDLL

类的实现完成于 ocicalls.cpp 文件。这里有很多内容,所以我将首先介绍 OCI 相关函数,然后介绍 Ultimate Grid 相关函数。

使用 Oracle Call Interface 例程

使用 OCI 涉及几个步骤。首先,如上所述,必须初始化 OCI 库和环境。接下来,必须分配我们将使用的函数所需的所有句柄。然后必须初始化应用程序,建立与数据库的连接,并创建一个会话。我在我的数据源类中实现的虚拟函数之一是 Open() 函数。在此函数中,我负责处理所有这些任务,以及获取网格数据所需的任务。

OCIEnvCreate 的调用在 initialize 函数中完成。这会设置 OCI 环境,在此之前不能进行其他 OCI 调用。此函数成功后,将为我们提供环境句柄的指针,存储在指针变量 m_envhp 中。一旦我们获得了指向此句柄的指针,我们就可以分配服务器和服务器上下文句柄以及错误句柄。

服务器句柄用于调用 OCIServerAttach。这时就建立了与 Oracle 数据库的实际连接。除了服务器句柄,我们还传递错误句柄、包含我们要建立连接的 Oracle 服务名的文本 string 、服务名 string 的长度,以及模式值 OCI_DEFAULT 。唯一的其他模式选项是 OCI_CPOOL ,用于连接池。但由于我只使用一个连接,所以我选择了默认模式。

成员变量 m_Service 存储 Oracle 服务名。这必须从调用进程中获取,并且在 Open() 函数的顶部会检查此参数是否 supplied 了值。该值是在 TNSNAMES.ORA 文件中定义的地址名称。

在我的例子中,TNSNAMES.ORA 中的条目如下所示:
ORCL =
  (DESCRIPTION =
    (ADDRESS = (PROTOCOL = TCP)(HOST = linux2)(PORT = 1521))
    (CONNECT_DATA =
      (SERVICE_NAME = orcl.home)
    )
  )

调用进程将 ORCL 作为服务名 supplied 。此参数不区分大小写,也可以输入为 orcl

成功时,此调用会初始化服务器句柄,然后通过调用 OCIAttrSet 将其关联到服务器上下文句柄。一旦完成这两个步骤,就可以向 Oracle 数据库发出 OCI 调用了。

这是 Initialize() 函数的完整代码:

int COci::initialize()
{
    int retval = 0;

    ub4 service_len = (ub4)strlen(m_Service);

    retval = OCIEnvCreate((OCIEnv **)&m_envhp,
            (ub4)OCI_DEFAULT,
            (void *)0, (void * (*)(void *, size_t))0,
            (void * (*)(void *, void *, size_t))0,
            (void (*)(void *, void *))0,
            (size_t)0, (void **)0);

    if (retval != OCI_SUCCESS)
        return -1;

  /* server contexts */
  retval = OCIHandleAlloc ((dvoid *) m_envhp, (dvoid **) &m_svchp, OCI_HTYPE_SVCCTX,
                          (size_t) 0, (dvoid **) 0);

  retval = OCIHandleAlloc ((dvoid *) m_envhp, (dvoid **) &m_srvhp, OCI_HTYPE_SERVER,
                         (size_t) 0, (dvoid **) 0);

  /* error handle */
  retval = OCIHandleAlloc ((dvoid *) m_envhp, (dvoid **) &m_errhp, OCI_HTYPE_ERROR,
                         (size_t) 0, (dvoid **) 0);

  retval = OCIServerAttach(m_srvhp, m_errhp, (text *)m_Service, service_len, OCI_DEFAULT);

  if (retval == OCI_SUCCESS)
  {
    /* set attribute server context in the service context */
    (void) OCIAttrSet ((dvoid *) m_svchp, OCI_HTYPE_SVCCTX, (dvoid *)m_srvhp,
                     (ub4) 0, OCI_ATTR_SERVER, (OCIError *) m_errhp);
  }
  else
    retval = OCI_ERROR;

  return retval;
}

此时,我觉得我需要谈谈 OCI 程序中的错误处理。我们必须分配的句柄之一是错误句柄。然后,该句柄在调用 OCIServerAttach() 时被用作参数。OCI 在发生 OCI 错误时使用错误句柄存储信息。可用的错误信息包括熟悉的 Oracle 错误代码。在发生错误条件时,对 OCI 函数的调用将返回 OCI_ERROR 。然后可以检查此返回码并调用错误处理程序。

Open() 函数可能成功、出现缺失或无效参数,或者在调用 OCI 函数时发生 OCI 错误。将使用返回码来指示发生哪种情况。我使用返回码 9 来指示发生了 OCI 错误。然后调用例程可以调用我的错误处理程序 GetOciError() 来获取错误码和描述。

这是错误处理程序的代码:

CString COci::GetOciError()
{
    text errbuf[512];
    sb4 errcode = 0;
    CString OciErrorMsg;
    int x;

    (void) OCIErrorGet ((dvoid *)m_errhp, (ub4) 1, (text *) NULL, &errcode,
                    errbuf, (ub4) sizeof(errbuf), OCI_HTYPE_ERROR);

    if (errcode == 1406)    // Data Truncation
    {
        for (x=0; x<m_cols; x++)
        {
            if (m_Parms[x].indp == -2)
                OciErrorMsg.Format("Data Truncation on column %d, exceeds max size", x);
            else
                if (m_Parms[x].indp > 0)
                    OciErrorMsg.Format("Data Truncation on column %d, actual size %d",
                            x, m_Parms[x].indp);
                else
                    OciErrorMsg.Format("Data truncation on column %d reported, but no size indicator", x);
        }
    }
    else
        OciErrorMsg.Format("%s", (char *)errbuf);

    return OciErrorMsg;
}

在讨论如何获取网格数据时,我们再推迟对为什么需要特殊处理 ORA-1406 错误的讨论。

回到 Open() 函数,下一个调用的例程是 logon() 函数。我将这些调用从初始化例程中分离出来,因为初始化应用程序的 OCI 调用只需要执行一次,但我可以预见到需要多个登录会话。

在此函数中,我们分配身份验证句柄,通过调用 OCIAttrSet 将 Oracle 用户 ID 和密码与此句柄关联,然后使用 OCISessionBegin 调用建立会话。如果到目前为止一切顺利,我们现在已经以调用例程 supplied 的 ID 登录到 Oracle 数据库了。

int COci::logon()
{
    int retval = 0;

  (void) OCIHandleAlloc ((dvoid *) m_envhp, (dvoid **)&m_authp,
                         (ub4) OCI_HTYPE_SESSION, (size_t) 0, (dvoid **) 0);

  (void) OCIAttrSet ((dvoid *)m_authp, (ub4)OCI_HTYPE_SESSION,
                     (dvoid *)m_UserID, (ub4)strlen(m_UserID),
                     OCI_ATTR_USERNAME, m_errhp);

  (void) OCIAttrSet ((dvoid *)m_authp, (ub4)OCI_HTYPE_SESSION,
                     (dvoid *)m_Password, (ub4)strlen(m_Password),
                     OCI_ATTR_PASSWORD, m_errhp);

  retval = OCISessionBegin (m_svchp,  m_errhp, m_authp, OCI_CRED_RDBMS, (ub4) OCI_DEFAULT);
  if (retval == OCI_SUCCESS)
  {
    (void) OCIAttrSet ((dvoid *) m_svchp, (ub4) OCI_HTYPE_SVCCTX,
                     (dvoid *) m_authp, (ub4) 0,
                     (ub4) OCI_ATTR_SESSION, m_errhp);
  }
  else
      retval = OCI_ERROR;

  return retval;
}

现在我们开始处理重要的 OCI 工作。Ultimate Grid 想要了解的关于要呈现的数据的一个方面是行数。有几种方法可以获取此值,但它们都涉及执行 SQL 查询以获取结果集,然后使用其他函数从结果集获取行计数。我不太喜欢这种方法,所以我决定执行一个 SQL 查询来获取行数。使用 select count(*) from table 的 SQL 语句来完成此操作。在实际应用中,您还必须复制任何将用于获取结果集的 where 子句。但就本文而言,我只执行 select * from table。通过这种方法,我可以获取并存储实际行数以供将来参考。

如果您还记得我们对 ocicalls.h 的讨论,我决定实现的 CUGDataSource 的一个虚拟函数是 GetNumRow() 函数。我使用成员变量 m_rows 来存储此值,以便网格可以访问它。get_row_count() 函数用于获取此值。这是通过一系列 OCI 调用完成的。这些调用将非常类似于我们如何获取网格数据,因此现在讨论所有内容似乎都很合适。

获取行计数需要三个 OCI 调用:OCIStmtPrepare()OCIDefineByPos()OCIStmtExecute

OCIStmtPrepare 用于初始化将数据库返回的值绑定到程序变量时所需的设置。简单来说,我们将 SQL 语句文本传递给 OCI,然后它返回一个供后续调用使用的语句句柄。

OCIDefineByPos 将 SQL 语句的 select 列表中的一个项与一个数据缓冲区关联,该缓冲区用于存储语句执行时检索到的值。成员变量定义 int m_rows 将用作存储 SQL 语句返回的值的数据缓冲区。第一个参数是 OCIStmtPrepare 调用返回的语句句柄。第二个参数是指向定义句柄的指针,用于存储被描述列的定义信息。在这种情况下,我不需要此信息,因此将其设置为 NULL。第三个参数是我们的错误句柄指针变量。第四个参数是正在描述的列在 select 列表中的位置。在本例中,只有一列,即 count(*) 字段,因此设置为 1。位置参数是 1 基的,不是 0 基的。第五个参数是指向我们的数据缓冲区的指针,第六个参数给出缓冲区的大小,第七个参数给出返回值的 类型。第八个参数是指向指示器变量的指针。我将其设置为 null 值,因为在这种情况下我不需要它,但当我们描述要检索的实际数据列以供网格使用时,我们将有更多关于指示器变量的内容要讲。其余参数也一样。由于在此情况下不需要它们,因此我将它们设置为 null 或默认值。

简单来说,OCIDefineByPos 告诉 OCI 我们希望将 OCIStmtPrepare 调用中指定的 SQL 语句返回的值存储在哪里。

在调用 OCIStmtExecute 时,我们将所有先前的步骤整合起来,并实际从 Oracle 数据库检索数据。

定义我们与数据库会话的服务上下文句柄是此调用中使用的第一个参数。然后我们有语句句柄,由 OCIStmtPrepare 初始化,并由 OCIDefineByPos 使用,作为第二个参数。然后传递我们的错误句柄。第四个参数告诉 OCI 在每次执行语句时要获取多少行。当此参数对于 SQL Select 语句非零时,需要为语句句柄定义,我们在 OCIDefineByPos 调用中已完成此操作。接下来的两个参数涉及快照,以在多个服务器上下文使用时提供数据库的一致视图。它们不适用于此情况,因此设置为 null 。最后一个参数是模式参数,将其设置为 OCI_DEFAULT ,我们告诉 OCI 我们希望实际执行语句并为 select 列表存储隐式描述信息。这对该语句影响不大,但同样,当我们处理用于检索实际表数据的语句时,我们将对该参数进行更详细的讨论。

成功执行此调用后,m_rows 将包含我们期望网格包含的行数。这是此函数的完整代码:

int COci::get_row_count()
{
    sword rc = OCI_SUCCESS;
    OCIDefine *defnp = NULL;
    char sql_count[100];

    strcpy(sql_count, "select count(*) from ");
    strcat(sql_count, m_Table);

    rc = OCIStmtPrepare(m_stmhp, m_errhp, (text *)sql_count, (ub4)strlen(sql_count)+1,
                        OCI_NTV_SYNTAX, OCI_DEFAULT);
    if (rc != OCI_SUCCESS)
        return OCI_ERROR;

    rc = OCIDefineByPos(m_stmhp, &defnp, m_errhp, (ub4) 1,
                        (void *) &m_rows, (sb4) sizeof(m_rows), SQLT_INT,
                        (void *) 0, (ub2 *) 0, (ub2 *) 0, OCI_DEFAULT);
    if (rc != OCI_SUCCESS)
        return OCI_ERROR;

    rc = OCIStmtExecute(m_svchp, m_stmhp, m_errhp, (ub4) 1, (ub4) 0,
                        (OCISnapshot *) 0, (OCISnapshot *) 0, OCI_EXACT_FETCH);
    if (rc != OCI_SUCCESS)
        return OCI_ERROR;

    return rc;
}

成功执行此函数后,成员变量 m_rowsOCIDefineByPos 调用的第五个参数)将包含我们 SELECT 语句返回的计数。

隐式描述

如果您在此仍然跟着我,我非常钦佩。请再忍受我一下,接下来的部分将非常详细,包含一些重要的 OCI 考虑。但一旦我们完成,我相信您会觉得这是值得的。尤其是在我们最终到达第三部分时,您将看到开发一个 MFC 应用程序来从 Oracle 数据库检索数据是多么容易。

Open 函数内部调用的最后一个函数是 get_result_list()。在这里,我们设置所有内容,以便可以从 Oracle 数据库检索数据,并使其可用于网格控件。在此过程中,我们为网格提供了有关每列的所有必要信息,以便能够成功地将其加载到网格的每个单元格中。

我必须提到的第一件事是此语句:AFX_MANAGE_STATE(AfxGetStaticModuleState()); 您将在 App Wizard 生成的代码开头看到它。App Wizard 生成的代码中有一个警告注释,解释了此语句的必要性。任何时候在 DLL 中使用 MFC 元素时,都需要此语句。在此函数中使用 CWaitCursor,因为执行可能需要很长时间,具体取决于检索到的结果集的大小,这将在此过程中显示系统等待光标。任何时候我使用 MFC 组件,甚至认为我可能会使用一个,我都会包含此语句。我没有进行任何实验来查看如果我省略它会发生什么。

再次,我们调用 OCIStmtPrepare 来设置语句句柄。这次我使用 SQL 语句 select * from table,其中 table 由调用进程 supplied 。为了本文的目的,我只这样做以保持简单。但我实现的一个虚拟函数是 SetStatement() 函数,它可以用来从用户输入构建更有意义的语句。隐式描述的优点是,无论基表中实际存在什么,都可以获取 select 列表中的任何列的列属性。

第一次调用 OCIStmtExecute 实际上并不会返回数据。相反,我将模式设置为 OCI_DESCRIBE_ONLY,它不会执行语句,但会返回 select 列表的描述。然后可以通过调用 OCIAttrGet 来获取列计数,并将属性类型参数设置为 OCI_ATTR_PARAM_COUNT。当使用语句句柄作为输入句柄执行此操作时,就像我现在做的那样,Param Count 就是 select 列表中的列数。

列属性数组的内存分配

我开始此项目时的一个设计考虑是能够从数据库中的任何表中检索数据。问题是必须有一个列属性数组,每个 select 列表中的列都出现一次。我为此目的使用了 desc_parms 结构。此结构包含用于存储列名、数据库中定义的列长度、列类型以及网格所需的其他属性的元素,以便它知道如何显示数据。

因此,我需要一个 desc_parms 结构数组来存储列属性。ocicall.h 中定义了一个成员变量,如下所示:desc_parms *m_Parms。所以我有一个指向 desc_parms 结构的指针成员变量。为了能够动态设置结构数组,我使用了 C calloc() 语句。

由于编译器知道 desc_parms 是什么,它可以根据我请求的出现次数分配内存,任何指针算术都以结构出现次数为单位,而不是字节。因此 *m_Parms+1 引用我的 desc_parm 结构数组的第二个出现。但这种方法的真正优点是还可以使用数组表示法。我可以将数组中的一个出现称为 m_Parms[1] 并获得相同的结果。这使我能够根据 select 列表中的列数动态分配内存。

m_Parms = (desc_parms *)calloc(m_cols, sizeof(desc_parms)); 语句完成了这个任务。

我在列名上也遇到了同样的困境。我首先尝试使用 CString,但我无法让 OCI 数据类型与 MFC 类良好配合。因此,我再次求助于熟悉的做法,将它们作为字符指针处理,并使用 malloc 为每个列名属性分配 31 个字节。下面的 for 循环完成了这个任务。由于 malloc() 不像 calloc() 那样初始化内存,所以我自己完成。为了安全起见,我还在同一个循环中将 OCIDefine 指针初始化为 null

        // Allocate memory for column names
        for (x=0; x<m_cols; x++)
        {
            m_Parms[x].ColName = (char *)malloc(31);
            memset(m_Parms[x].ColName, 0, 31);
            m_Parms[x].defnpp = (OCIDefine *)0;
        }

我相信 Oracle 对列名的限制是 30 个字符,但我不确定这在更新的发行版中是否已被消除,但我仍然强制执行此限制以防止缓冲区溢出。

获取列属性

完成内存分配后,现在可以获取列属性。这通过调用 describe_column() 函数来完成。

describe_column() 函数执行一个 for 循环来获取 select 列表中每个列的列属性。通过传递指向语句句柄的指针 (void *)m_stmhp,我们返回指向 OCIParam 指针的指针,(void **)&parmdp。然后使用 OCIParam 指针通过调用 OCIAttrGet() 函数来获取我们所需的所有列属性。因此获得的属性存储在 desc_parms 结构的成员变量中。

describe_column() 中,我们获取数据库中定义的 元素的最大大小、列名、类型以及精度和标度属性。

在获取列名时,我首先检查长度,如果超过 30 个字符,则截断它,以防止缓冲区溢出。精度和标度对于 Oracle NUMBER 数据类型很重要。它将允许我们区分整数和浮点数据类型,并相应地设置网格属性。

最后,在循环的底部,我们调用 OCI 函数 OCIDescriptorFree() 来释放为 OCIParam 句柄引用的结构所使用的资源。OCI Programmer's Guide 警告要这样做,否则每次调用 OCIParamGet() 获取每个列时都会出现内存泄漏。

这是 describe_column 例程的代码:

int COci::describe_column(int numcols)
{
    sword     retval;
    text     *namep;
    ub4       sizep;
    OCIParam *parmdp;
    ub4       pos;
    ub4    parmcnt = numcols;

    OCIDefine *defnp = NULL;

    for (pos = 1; pos <= parmcnt; pos++)
    {
        /* get the parameter descriptor for each column */
        if ((retval = OCIParamGet((void *)m_stmhp, (ub4)OCI_HTYPE_STMT, m_errhp,
                      (void **)&parmdp, (ub4) pos)) != OCI_SUCCESS)
            return OCI_ERROR;

        /* column length */
        if ((retval = OCIAttrGet((dvoid*) parmdp, (ub4) OCI_DTYPE_PARAM,
                      (dvoid*) &m_Parms[pos-1].collen, (ub4 *) 0,
                      (ub4) OCI_ATTR_DATA_SIZE, (OCIError *)m_errhp)) != OCI_SUCCESS)
            return OCI_ERROR;

        /* column name */
        if ((retval = OCIAttrGet((dvoid*) parmdp, (ub4) OCI_DTYPE_PARAM,
                      (dvoid*) &namep, (ub4 *) &sizep,
                      (ub4) OCI_ATTR_NAME, (OCIError *)m_errhp)) != OCI_SUCCESS)
            return OCI_ERROR;

        if (sizep > 30)
            sizep = 30;

        if (sizep)
        {
            strncpy((char *)m_Parms[pos-1].ColName, (char *)namep, (size_t) sizep);
            m_Parms[pos-1].ColName[sizep] = '\0';
        }

        /* data type */
        if ((retval = OCIAttrGet((dvoid*) parmdp, (ub4) OCI_DTYPE_PARAM,
                      (dvoid*) &m_Parms[pos-1].coltyp, (ub4 *) 0,
                      (ub4) OCI_ATTR_DATA_TYPE, (OCIError *)m_errhp)) != OCI_SUCCESS)
            return OCI_ERROR;

        /* precision */
        if ((retval = OCIAttrGet ((dvoid*) parmdp, (ub4) OCI_DTYPE_PARAM,
                      (dvoid*) &m_Parms[pos-1].precision, (ub4 *) 0,
                      (ub4) OCI_ATTR_PRECISION, (OCIError *)m_errhp)) != OCI_SUCCESS)
            return OCI_ERROR;

        /* scale */
        if ((retval = OCIAttrGet ((dvoid*) parmdp, (ub4) OCI_DTYPE_PARAM,
                      (dvoid*) &m_Parms[pos-1].scale, (ub4 *) 0,
                      (ub4) OCI_ATTR_SCALE, (OCIError *)m_errhp)) != OCI_SUCCESS)
            return OCI_ERROR;

        // Release memory associated with handle, or we leak memory on each column.
        OCIDescriptorFree((void *)parmdp, OCI_DTYPE_PARAM);
        
    }

    return retval;
}

绑定变量的内存分配

现在我们已经获取了所有列属性,我们可以分配缓冲区来保存数据库返回的实际数据值。

再次,我们遍历 select 列表中的每个列。再次是 desc_parms 结构:

typedef struct
{
    char    *ColName;
    ub2    collen;
    ub2    coltyp;
    ub1    precision;
    sb1    scale;
    void    *pValue;
    sb4    value_size;
    ub2    rlenp;
    sb2    indp;
    OCIDefine *defnpp;
} desc_parms;

void 指针 pValue 用于存储指向 SQL select 语句返回的实际数据的指针。在此之前,我们需要分配内存。我们为此目的使用调用 describe_column 获取的信息。根据列的数据类型,为每列分配适当的内存。

switch 语句检查每列的列类型,并在每种情况下进行适当的内存分配。

如果您查看 EMP 表,它在 Oracle 数据库中的定义如下:

Name                Null?        Type
 ----------------------------------------- -------- -------------

 EMPNO            NOT NULL    NUMBER(4)
 ENAME                    VARCHAR2(10)
 JOB                        VARCHAR2(9)
 MGR                        NUMBER(4)
 HIREDATE                    DATE
 SAL                        NUMBER(7,2)
 COMM                        NUMBER(7,2)
 DEPTNO                    NUMBER(2)

在我测试中,不仅是 EMP 表,还有我自己的表,我从未见过 SQL_INT 被返回,但它在 OCI Programmer's Guide 中被列为可能的日期类型,所以我为它留出了空间。

我之前提到过如何使用环境变量 NLS_DATE_FORMAT NLS_TIMESTAMP_FORMAT 来控制 Oracle DATE TIMESTAMP 数据类型的表示。对于 DATE,我分配 11 个字节的内存并将数据类型设置为 SQL_STR,这是 C 风格的、null 终止的 string 代号。当执行 select 语句时,OCI 将使用此信息将 DATE 字段从 Oracle 内部日期格式转换为易于管理的字符串。对于 Oracle TIMESTAMP 数据类型也是如此。

因此,如果您使用我的数据源类,并且没有设置 NLS_DATE_FORMAT 环境变量,您将获得 Oracle DATE 字段的数据库默认值。这会导致访问冲突,如果日期格式超过 10 个字符。

这也适用于 Oracle TIMESTAMP 字段。它们的格式由 NLS_TIMESTAMP_FORMAT 环境变量决定。我的设置为 ‘MM/DD/YYYY HH24:MI:SS.FF’。如果您的时间戳格式超过 30 个字符,您将遇到访问冲突。可以通过调整 malloc() 调用中请求的字节数来修复此问题。

处理 Oracle NUMBER 数据类型

从上面 EMP 表的描述中可以看出,Oracle NUMBER 数据类型可以定义为带或不带小数点右边的数字。我们使用之前获取的精度和标度属性来区分我们遇到的情况。如果 OCI 将列的数据类型报告为 SQLT_NUM,我们需要确定我们是整数(即小数点右边没有数字)还是浮点数。

如果列在 Oracle 表中定义为 NUMBER,而没有提供精度或标度,则精度属性设置为零,标度设置为 -127。这表示该值在 Oracle 数据库中存储为浮点数。这给我带来了一个问题,因为网格希望知道列需要多少位小数,但 NUMBER 定义并未提供任何指示。在我测试 EMP 表和其他我能够访问的表时,我只有一个数据库列被这样定义,并且包含的数据没有小数点后的数字。我任意设置了标度,稍后将在 GetCell 函数中使用,为 5。列类型成员变量设置为 SQLT_BDOUBLE,表示二进制 double 值,并相应地分配内存。

对于像 EMPNO 这样的列定义,没有提供精度,OCI 将标度属性设置为零,我们就知道我们有一个整数。列类型设置为 SQLT_INT,并相应地分配内存。如果标度不等于零,我们有一个浮点数,其小数点右边的位数由指定,因此我们保留标度,将列类型设置为 SQLT_BDOUBLE 并相应地分配内存。

对于所有其他情况,数据被视为字符数据,列类型设置为 SQLT_STR,这是 OCI 对 C 风格的、null 终止的 string 代号。

这是缓冲区分配例程的完整代码:

    // Allocate memory to store cell values
    for (x=0; x<m_cols; x++)
    {
        switch(m_Parms[x].coltyp)
        {
            case SQLT_INT: 
                m_Parms[x].pValue = (int *)malloc(sizeof(int));
                m_Parms[x].value_size = sizeof(int);
                break;
            case SQLT_DAT:
                // Environment variable NLS_DATE_FORMAT has been set to MM/DD/YYYY
                // Coerce to character format
                m_Parms[x].coltyp = SQLT_STR;
                m_Parms[x].pValue = (char *)malloc(11);
                m_Parms[x].value_size = 11;
            break;
            case SQLT_TIMESTAMP:
                // Environment variable NLS_TIMESTAMP_FORMAT has been set to
                // MM/DD/YYYY HH24:MI:SS.FF
                // Coerce to character format
                m_Parms[x].coltyp = SQLT_STR;
                m_Parms[x].pValue = (char *)malloc(30);
                m_Parms[x].value_size = 30;
            break;
            case SQLT_NUM:
                if ((m_Parms[x].precision == 0) && (m_Parms[x].scale == -127))
                {
                    m_Parms[x].coltyp = SQLT_BDOUBLE;
                    m_Parms[x].pValue = (double *)malloc(sizeof(double));
                    m_Parms[x].value_size = sizeof(double);
                    m_Parms[x].scale = 5;
                }
                else
                {
                    if (m_Parms[x].scale == 0)
                    {
                        m_Parms[x].coltyp = SQLT_INT;
                        m_Parms[x].pValue = (int *)malloc(sizeof(int));
                        m_Parms[x].value_size = sizeof(int);
                    }
                    else
                    {
                        m_Parms[x].coltyp = SQLT_BDOUBLE;
                        m_Parms[x].pValue = (double *)malloc(sizeof(double));
                        m_Parms[x].value_size = sizeof(double);
                    }
                }
            break;
            default:
                    // Must be a character format, lets force to null terminated string
                    m_Parms[x].coltyp = SQLT_STR;
                    m_Parms[x].pValue = (char *)malloc(m_Parms[x].collen + 1);
                    m_Parms[x].value_size = m_Parms[x].collen + 1;
            break;
        }
    }

主机变量绑定

现在我们准备指示 OCI 在哪里存储列值。这是通过 OCI OCIDefineByPos 函数完成的。这是 OCI Programmer's Guide 中的函数原型:

sword  OCIDefineByPos(OCIStmt    *stmtp,
            OCIDefine    **defnpp,
            OCIError    *errhp,
            ub4        position,
            void        *valuep,
            sb4        value_sz,
            ub2        dty,
            void        *indp,
            ub2        *rlenp,
            ub2        *rcodep,
            ub4        mode);

第一个参数是我们的语句句柄,之前使用 OCIStmtPrepare 调用获取。

第二个参数是指向定义句柄指针的指针。如果您还记得,我将每个成员变量都设置为 null,因为我们不会在后续调用中使用返回的句柄。

第三个参数是我们的错误句柄。

第四个参数是列在 select 列表中的位置。这些位置是 1 基的,因此 select 列表中的第一个列是位置 1,而不是零,这使得 C 语句有些有趣,因为 C 数组元素是零基的。在我的调用中,我为此参数使用 x+1,其中 x 是我的索引变量。

第五个参数是指向分配的缓冲区以存储此列数据的 void 指针。我们在刚刚讨论的 for 循环中分配了这些。

第六个参数是每个缓冲区的大小。我们在分配每个列缓冲区的内存时设置了这个值。

第七个参数是列的数据类型,我们也根据之前获取的数据类型属性设置了这个值。

第八个参数非常重要,我们需要花一些时间讨论。

Oracle 将其称为指示器变量,并使用它来在发生 OCI 不喜欢的列数据情况时通知调用程序,最值得注意的是如果列值是 null

就我们对 Ultimate Grid 的数据源而言,Oracle 指示器变量用于指示两种情况:null 列值,或列数据超出为其设置的缓冲区大小。如果不使用指示器变量,如果任何列值包含 null,则会发生 Oracle ORA-1405 错误。如果列中的数据超出为其分配的缓冲区大小,则发生 ORA-1406 错误。由于我不想让我的例程在发生这些错误时失败,因此我在 desc_parms 结构中设置了指示器变量。目前,我们必须将每个相应列的指示器变量的地址传递给 OCIDefineByPos,因此当执行我们的 SELECT 语句时,OCI 将知道在发生这些情况之一时设置哪个指示器变量。当我们查看 GetCell() 函数时,我们将看到如何使用指示器变量,该函数用于向网格提供我们所有努力获取的数据。

第九个参数是指向用于存储提取数据长度的字段的指针。上面用于分配 desc_parm 结构数组内存的 calloc() 语句会将这些变量初始化为零。我从未发现需要使用这些值。

第十个参数是指向列级别返回码数组的指针。我也没有使用这些值,因此将此参数设置为 null 指针。

最后一个参数是 OCIDefineByPos 应使用的模式。我不需要其他模式提供的任何功能,因此将其设置为 OCI_DEFAULT

再次审视 OCIStmtExecute

OCIDefineByPos 成功返回时,我们已将 OCI 所需的信息提供给将列值存储在列值缓冲区中。现在我们准备再次执行 SELECT 语句,但这次我们将实际从 Oracle 数据库检索数据。

第二次调用 OCIStmtExecute 与第一次非常相似,有一些值得注意的例外。第四个参数设置为 1,而不是零。我们设置了缓冲区来存储一行数据,并且模式参数从 OCI_DESCRIBE_ONLY 更改为 OCI_STMT_SCROLLABLE_READONLY

我使用 OCI_STMT_SCROLLABLE_READONLY,因为我想能够前后滚动结果列表,而 Ultimate Grid 使这变得容易。GetCell 函数(我们接下来将要查看的)按行和列请求数据。在 EMP 表中只有 14 行,此行为在此示例中不会显现,但我在处理大得多的表时使用了它,其中一个表有近三百万行,我在 16 GB 的 Windows 8.1 PC 上的性能相当令人满意。

OCI 确实有一个预取功能,可以一次检索多条记录,但这需要一个缓冲区数组。对我来说,这意味着一个二维的 desc_parm 结构数组。我设置的数组存储了一行所有列的数据。我必须设置第二个维度来存储每一行的列值数组。

使用此方法,预取属性在调用 OCIStmtPrepare(以获取语句句柄)之后,但在调用 OCIStmtExecute 之前设置。然后将 OCIStmtExecute 调用中的第四个参数设置为预取计数。我研究了这种方法,但由于性能对我来说已经足够满意,所以我不想过度复杂化我正在做的事情。

成功完成 OCIStmtExecute 后,我们的结果列表已从数据库检索,我们已准备好将数据加载到网格中。

GetCell 虚拟函数

我们需要在数据源类中实现 GetCell 虚拟函数,以向网格控件提供要显示的数据。网格调用此函数,传递一个列号、一个行号和一个指向要填充的单元格对象的指针。

设置行和列标题

行和列是基于零的,因此要填充的第一个单元格是第 0 行,第 0 列。-1 行指的是列标题,-1 列指的是行标题。单元格 (-1, -1) 是角按钮。查看 GetCell 的代码,我检查了角按钮并直接返回,因为我不想对其进行任何操作。但是,我想使用行号作为行标题,但此行计数将是基于 1 的,因为大多数人都不期望他们的第一行是 0。因此,如果 col 小于零,但 row 不小于零,我将文本设置为行号 + 1。

如果 row 参数小于零,这是列标题,因此从数据源中检索列名。GetColName() 是数据源中实现的虚拟函数之一。它返回我们在执行隐式描述时努力获取的列名。

为了安全起见,会检查 col 参数,以确保它不在我们拥有数据的列范围内。如果它是 UG_NA,则返回。

获取数据

现在我们准备从上面获得的结果列表中实际检索一些数据。使用 OCIStmtFetch2 来完成此操作。我们作为参数传递语句句柄、错误句柄和值 1,以表示我们希望返回一行。Oracle 称下一个参数为方向,我们将其设置为 OCI_FETCH_ABSOLUTE,以获取下一个参数(fetchOffset)指定的行。fetchOffset 参数设置为 row + 1,因为 OCI 行是 1 基的。目前,模式属性只有一个可接受的值,即 OCI_DEFALUT

根据列的数据类型,设置小数位数、对齐方式和单元格值。我们还检查每个列的指示器变量,以查看是否存在 null 值,值为 -1 表示此情况。这意味着从数据库接收到的值是 Oracle null,这与 C 中的 null 值不同。这也意味着从上一个 fetch 开始,缓冲区中的任何内容都保持不变。对于数值,我将单元格值设置为零。对于 string 值,我将其设置为空 string

这是 GetCell 函数的代码:

/***************************************************
GetCell
    A virtual function that provides standard way
    for the grid to populate a cell object.  This
    function is called as a result of the 
    CUGCtrl::GetCell being called.
Params:
    col, row    - coordinates of the cell to retrieve
                  information on.
    cell        - pointer to CUGCell object to populate
                  with the information found.
Return:
    UG_NA        not available
    UG_SUCCESS    success
    1...        An OCI error has occurred.
****************************************************/
int COci::GetCell(int col, long row, CUGCell *cell)
{
    AFX_MANAGE_STATE(AfxGetStaticModuleState());

    int retval;
    CString celltext;
    sb4 fetchOffset = row + 1;

    if (col < 0)
    {
        if (row < 0)
            return UG_SUCCESS;  // disregard corner button

        celltext.Format("%d", row+1);
        cell->SetText(celltext);

        return UG_SUCCESS;
    }

    if (row < 0)
    {
        CString string;
        GetColName(col, &string);
        cell->SetText(string);
        return UG_SUCCESS;
    }

    if (col >= m_cols)
        return UG_NA;

    // Use OCIStmtFetch2 to obtain cell value
    retval = OCIStmtFetch2(m_stmhp, m_errhp, (ub4) 1,
                OCI_FETCH_ABSOLUTE, fetchOffset, OCI_DEFAULT);
    if (retval != OCI_SUCCESS)
        return 1;

    // Not doing anything fancy
    cell->SetCellType(UGCT_NORMAL);

    if (m_Parms[col].coltyp == SQLT_INT)
    {
        cell->SetNumberDecimals(0);
        cell->SetAlignment(UG_ALIGNRIGHT);
        if (m_Parms[col].indp < 0)
            cell->SetNumber(0);
        else
            cell->SetNumber(*(int *)m_Parms[col].pValue);
    }
    else
    {
        if (m_Parms[col].coltyp == SQLT_BDOUBLE)
        {
            cell->SetNumberDecimals(m_Parms[col].scale);
            cell->SetAlignment(UG_ALIGNRIGHT);
            if (m_Parms[col].indp < 0)
                cell->SetNumber(0);
            else
                cell->SetNumber(*(double *)m_Parms[col].pValue);
        }
        else
        {
            if (m_Parms[col].indp < 0)
                cell->SetText("");
            else
            {
                celltext.Format("%s", (char *)m_Parms[col].pValue);
                cell->SetText(celltext);
            }

        }
    }

    return UG_SUCCESS;
} 

由于 OCI 描述工作做得很好,我还可以相对轻松地实现其他虚拟函数,如 GetNumRows()GetNumCols()GetColName()GetColType()

Using the Code

本文包含两个 zip 文件。一个包含数据源 DLL 的 Visual Studio 2013 版本的项目文件。另一个包含 Visual Studio 2010 版本的项目文件。VS2010 项目具有 32 位和 64 位版本、调试和发布的配置设置。预先构建的 .lib.dll 文件包含在 zip 文件中,因此除非您想自己进行修改,否则无需构建。

我需要提到一件事,VS2010 项目的 64 位调试版本有一个自定义构建步骤,我将 DLL 复制到 c:\apps\Ultimate Grid\DLLs。我在开发过程中这样做,因为我在 PATH 环境变量中包含了此目录。这样,我开发的任何应用程序都会在此目录中查找数据源 DLL。同样,VS2013 项目的调试版本有一个自定义构建步骤,将 DLL 复制到 D:\apps\Ultimate Grid\DLLs。如果这不适合您,您将需要编辑自定义构建步骤的命令行,或者完全删除它。

为了将数据源用作 Ultimate Grid 控件的输入,首先将您选择的 OciDtSrc.dll 文件复制到包含在您的 PATH 环境变量中的位置。接下来,您需要声明数据源类的一个实例:

// Attributes
public:
    // Declare instance of data source class
    COci DtSrc;

根据您开发的应用程序类型,这将在不同的文件中。对于对话框应用程序,它可以放在 AppDlg.h 文件中。在第三部分中,我做了一个 SDI 应用程序,在这种情况下,我将此声明放在 Document.h 文件中。

要从 Oracle 数据库获取数据,代码如下:

    DtSrc.SetPassword("scott", "tiger");
    rc = DtSrc.Open("orcl", "emp");
    if (rc != OCI_SUCCESS)
    {
        if (rc == OCI_ERROR)
            msg = DtSrc.GetOciError();
        else
            if (rc < 9)
                msg.Format("Missing Argument: %d", rc);
            else
                if (rc == 16)
                    msg.Format("Couldn't allocate memory for value buffers!");
                else
                    msg.Format("Unknown Error!");

        AfxMessageBox(msg);

        return FALSE;
    }

在第三部分的示例程序中,我将此代码放在 Document.cpp 文件中的 OnNewDocument() 例程中。您可以更改此代码以访问任何模式下的任何表。将 scott/tiger 替换为相应的用户 ID 和密码。将服务名从“orcl”更改为您的服务名,并将“emp”替换为您所需的表。无论有多少列或数据类型,数据源都应该能够处理。

最后,您必须将数据源附加到网格。在第三部分的示例程序中,这在 View.cpp 文件中的 OnInitialUpdate() 例程中完成。代码如下:

    if (m_pDocument->DtSrc.IsOpen())
    {
        int index = m_grid.AddDataSource(&m_pDocument->DtSrc);
        m_grid.SetDefDataSource(index);
        m_grid.SetGridUsingDataSource(index);
        m_grid.SetNumberRows(GetDocument()->DtSrc.GetNumRows());
        m_grid.SetNumberCols(GetDocument()->DtSrc.GetNumCols());
        m_grid.OnSetup();
    }

就是这样。这应该为您构建使用 Ultimate Grid 控件的自己的应用程序提供了所有必需的东西。但是,如果您想看到所有这些内容整合在一起,请查看第三部分,在那里我开发了一个示例应用程序,将我们在第一部分构建的 Ultimate Grid DLL 与我们在第二部分构建的数据源类结合起来。

摘要

在第二部分中,我们使用了 Oracle OCI 库为 Ultimate Grid 控件开发了自己的自定义数据源。我们确实只是触及了网格控件和数据源功能的表面。我没有深入探讨数据源用于更新数据库的使用。这并不难做到,但就我而言,我只需要浏览表的能力,而不是更新它们。Ultimate Grid 文档很好地概述了所需的步骤,并且有大量的示例代码,因此我鼓励任何一直在寻找功能强大、用途广泛的数据网格控件的人进一步探索。

历史

  • 2014/5/18:初版发布
© . All rights reserved.