2.진보적인 ODBC 프로그래밍

본장에서는 입력,검색,수정,삭제 외에도 한데이터베이스에서 여러 테이블이 있을 때 이 여러 테이블을 공유하거나 또는 MFC AppWizard를 이용하지 않고 그냥 프로그램으로 ODBC를 로드하는 방법,  현재 ODBC안의 구조를 알지 못해도 자동으로 레코드를 읽어서 출력하는 방법등 다양하게 방법에 대해서 설명합니다. 본장에서는 ODBC를 실제로 컨트롤 하는 CDatabase 클래스와 이것과 연결되는 CRecordset클래스의 내부를 설명하겠습니다. 이 두 개의 클래스와 관계와 내부적으로 구동되는 것을 알면 여러 문제가 쉽게 풀릴것으로 봅니다.

대상자: 고급

Chapter 등급: 선택

공부방법:

간단한 데이터베이스를 구동한다고 할경우 본장의 내용은 크게 필요하지 않습니다. 선택적 DB구성과 CDatabase클래스와 CRecordSet클래스의 기능을 꼭알아야 할경우 보시기 바랍니다.

<1>OdbcView 예제 프로그램

OdbcView예제 프로그램은 AppWizard에서 ODBC항목을 설정하지 않고 FormView를 가지는 단순한 SDI형에서 출발하는 프로그램입니다. 그림 3-2-1은 OdbcView의 출력 결과 입니다.

(그림 3-2-1)OdbcView 실행 결과

ODBC설정 버튼을 클릭하면 그림 3-2-2와 같이 현재 설정된 ODBC드라이버 리스트가 나옵니다.

(그림 3-2-2)데이터 베이스 소스 설정하기

카드철 메뉴에서 Machine Data Source를 선택하면 현재 컴퓨터에 등록된 ODBC리스트가 나옵니다. 그림 3-2-3이 이 리스트화면입니다.

(그림 3-2-3) 컴퓨터에 등록된 ODBC파일 설정

전장에서 사용한 namecard가 현재 등록되어 있습니다. 이등록된 DB를 클릭하면 자동으로 현재 프로그램에서 사용할수 있도록 내장됩니다.

OdbcView의 메인 화면에서 TABLE를 선택하고 DB보기 버튼을 클릭하면 그림1과 같이 현재 등록된 데이터가 리스트 컨트롤안에 각 칼럼에 필드명이 설정되면서 출력됩니다.

OdbcView프로그램은 간단히 말해서 특정 ODBC파일을 읽고 그안에 있는 레코드명과 데이터를 읽어내는 프로그램입니다. 기존에 사용하였던 방법은 이미구조를 알고 있는 특정 데이터베이스를 m_pSet이라는 CRecordset 클래스를 상속받은 변수를 설정하고 이 클래스안에 DoFieldExchange 해당하는 레코드를 형에 맞게 연결시켜 사용하였습니다. 그러나 OdbcView는 설정된 데이터베이스의 구조를 전혀 모르는 상태에서부터 시작합니다. 이상태에 데이터베이스의 구조를 알고 또한 자신의 프로그램에 사용하기 쉽도록 등록하게 됩니다.

OdbcView 프로그램의 전반적인 프로그래밍 방법만 이해를 하신다면 지금까지 필자의 홈페이지에 질문한 많은 내용의 해답이 될것으로 봅니다.

<2>데이터베이스 오픈

그림3-2-2와 같은 Select Data Source 대화상자를 출력하여 선택된 ODBC를 프로그램에 내장할 때 사용하는 클래스는 CDatabase 입니다. 지금까지 공부한 것으로 데이터베이스를 핸들링하는 클래스는 단지 CRecordset라고 생각을 할지 몰라도 CRecordset는 CDatabase를 컨트롤 하면서 프로그래머가 쉽게 레코드를 컨트롤 하게 도와주는 하나의 중간 연결자 형태를 가지고 있습니다. CRecordset의 멤버의 일부분을 보겠습니다.

m_hstmt     Contains the ODBC statement handle for the recordset. Type	HSTMT.

m_nFields    Contains the number of field data members in the recordset.	Type UINT.

m_nParams  Contains the number of parameter data members in the	recordset. Type UINT.

m_pDatabase  Contains a pointer to the CDatabase object through which the	recordset is connected to a data source.

데이터베이스를 핸들링할 경우 필요한 멤버가 위에 보일것입니다. 그중 중요한 맴버가 m_pDatabase인데 이것이 바로 CDatabase클래스입니다. 이 클래스를 핸들링하여 해당하는 레코드를 얻어서 RecordView에 넘겨주고 또는 자체적으로 데이터를 저장입력,수정,검색을 할수 있도록 하는 것입니다. CRecordset클래스의 생성자를 보시겠습니다.

CRecordset::CRecordset( CDatabase* pDatabase = NULL);

바로 생정자에 이 데이터베이스 핸들을 넘겨주는 것이지요. 이제 데이터베이스 파일을 설정하는 정체를 알아 냈습니다.

이 데이터베이스 핸들을 이용하여 컴퓨터에 등록된 데이터베이스를 설정하겠습니다.

제일 먼저 CDatabase클래스의 변수를 하나 설정해야 합니다.

CDatabase m_pOdb;

위와 같이 설정한후에 데이터베이스 설정 대화상자를 구동하고 이 대화상자에서 설정한 파일을 m_pOdb가 핸들링하기 위해서는 Open함수의 인자를 NULL로 설정하면 됩니다.

m_pOdb.Open(NULL);

위와 같이 실행하면 그림 2와 같은 데이터베이스 설정 대화상자가 구동되고  이대화상자에서 설정한 파일 핸들러가 m_pOdb에 설정됩니다.

<3>데이터베이스 안의 테이블 리스트 보기

CDatabase클래스에 데이터베이스를 설정하는 것까지는 그렇게 큰 문제는 없었습니다. 필자의 홈페이지에 질문이 오는 내용도 바로 이다음을 물어보는 것이 많았습니다. 데이터베이스 안에는 여러개의 테이블이 있습니다. 이여러개의 테이블에서 특정 테이블을 선택하여야 합니다. 이렇게 특정 테이블을 선택하면서 바로 CRecordset클래스와 연결이 되기 때문입니다. 원초적인 문제로서 데이터베이스 안에는 몇 개의 테이블이 있으며 이테이블 이름은 무엇인가를 알아낸다면 특정 데이터베이스 테이블을 설정하는데는 큰문제점이 없을것으로 봅니다. 데이터베이스 안에 테이블도 하나의 레코드입니다. 즉 테이블이라는 큰레코드가 있고 이 테이블 안에 필드들이 있는것입니다. 그렇기 때문에 테이블을 컨트롤 하는것도 CRecordset로 가능합니다.

본장의 예제에서는 테이블 리스트를 얻기위한 클래스를 CTables 로 만들었습니다. 이 CTables는 바로 CRecordset를 상속받은 것입니다. 헤더를 먼저 보겠습니다.

//테이블 리스트를 얻는 클래스

class CTables : public CRecordset

{

public:

CTables(CDatabase* pDatabase = NULL);

DECLARE_DYNAMIC(CTables)

// Field/Param Data

//{{AFX_FIELD(CTables, CRecordset)

//}}AFX_FIELD

//새로 만든 Open함수

BOOL Open(LPCSTR pszTableQualifier = NULL,

LPCSTR pszTableOwner = NULL,

LPCSTR pszTableName = NULL,

LPCSTR pszTableType = NULL,

UINT nOpenType = forwardOnly);

CString m_strTableQualifier;//허자자

CString m_strTableOwner;//소유자

CString m_strTableName;//테이블 이름

CString m_strTableType;//테이블 형태

CString m_strRemarks;//설명문

// Overrides

// ClassWizard generated virtual function overrides

//{{AFX_VIRTUAL(CTables)

public:

virtual CString GetDefaultConnect();    // Default connection string

virtual CString GetDefaultSQL();    // Default SQL for Recordset

virtual void DoFieldExchange(CFieldExchange* pFX);  // RFX support

//}}AFX_VIRTUAL

// Implementation

#ifdef _DEBUG

virtual void AssertValid() const;

virtual void Dump(CDumpContext& dc) const;

#endif

};

CTables클래스에 새로운 맴버 5개가 있을것입니다. 테이블이 있다면 그테이블의 수퍼 소유자 또한 그테이블의 데이터를 수정할수 있는 사람 테이블 이름 테이블 형태 등 등의 정보가 있습니다. 이정보를 저장할 변수를 만든것입니다. CRecordset는 DoFieldExchange 에 의해서 각 필드와 연결되는 변수를 설정한다고 지금까지 알고 있었습니다. 또한 이렇기 때문에 언제나 CRecordset를 상속받은 클래스는 해당 데이터베이스 테이블안의 필드 형태와 연동할수 있는 형태의 변수들이 등록되었습니다.

ExOdb2에서 보면 헤더에 다음과 같이 설정되어있고

CString	m_name; //데이터 베이스의 name과 연결될 변수

이것이 DoFieldExchange에는 다음과 같이 연결된 것을 보았을것입니다.

RFX_Text(pFX, _T("[name]"), m_name);

CTables클래스는 테이블안에 있는 데이터를 얻고자 하는 것이아니라 테이블 리스트를 얻고자 하는 것입니다. 그렇기 때문에 필드명,권한자,소유자,마크 등의 정보를 저장할 변수를 만들고 이것을 DoFieldExchange에 연결하는 것입니다.

위의 클래스 헤더에서 본 5개의 변수를 CTabes소스의 DoFieldExchange에서는 다음과 같이 연결하였습니다.

void CTables::DoFieldExchange(CFieldExchange* pFX)

{

//{{AFX_FIELD_MAP(CTables)

pFX->SetFieldType(CFieldExchange::outputColumn);

RFX_Text(pFX,_T("TABLE_QUALIFIER"),m_strTableQualifier);

RFX_Text(pFX,_T("TABLE_OWNER"),m_strTableOwner);

RFX_Text(pFX,_T("TABLE_NAME"),m_strTableName);

RFX_Text(pFX,_T("TABLE_TYPE"),m_strTableType);

RFX_Text(pFX,_T("REMARKS"),m_strRemarks);

//}}AFX_FIELD_MAP

}

위와 같이 연결함으로써 데이터베이스를 오픈하면 테이블의 정보를 얻어서 해당되는 변수에 등록되는 것입니다.

CRecordset의 맴버 함수중에 Open함수가 있습니다. 이것은 특정 테이블을 선택하면 해당 테이블을 개방하는 함수입니다. 그러나 우리가 만든 CTables클래스는 테이블 안에 있는 필드 정보를 얻고자 하는 것이 아니라 데이터베이스 안에 있는 모든 테이블 리스트 정보를 얻고자 하기 때문에 Open함수를 바꾸어야 합니다.

이때 사용하는 방법은 ODBC API함수를 사용하면 됩니다. 테이블을 알고자 하는 ODBC API함수는 SQLTables입니다. 이함수를 사용하여 위의 5개의 변수에 값을 얻고자 하는 방법은 다음과 같습니다.

::SQLTables(m_hstmt,

(UCHAR FAR*)pszTableQualifier,SQL_NTS,

(UCHAR FAR*)pszTableOwner,SQL_NTS,

(UCHAR FAR*)pszTableName,SQL_NTS,

(UCHAR FAR*)pszTableType,SQL_NTS)

m_hstmt란 파일 핸들러같은 개념의 데이터베이스 핸들러 입니다. 그다음 인자가

허자가,허가자이름길이,소유자,소유자이름길이,테이블이름,테이블이름길이,테이블 형태,테이블형태 길이 등을 넘겨주는 것입니다. 이렇게 넘겨주면 각 변수에 해당하는 값들이 등록됩니다.

이 등록된 값들을 다시 CTables의 클래스에 연결하고자 할 경우 이때 사용하는 함수가 바로 AFX_SQL_ASYNC(prc,SqlFunc); 입니다.

여기서 prc란 데이터베이스를 핸들링하는 클래스 구조체이며 SqlFunc란 바로 ODBC용 API 함수이지요. 테이블 이름과 CTable안에 있는 변수와 연결하고자 한다면 AFX_SQL_ASYNC함수와 위의 SQLTables함수를 같이 연결하는 것입니다.

//ODBC함수 호출

AFX_SQL_ASYNC(this,::SQLTables(m_hstmt,

(UCHAR FAR*)pszTableQualifier,SQL_NTS,

(UCHAR FAR*)pszTableOwner,SQL_NTS,

(UCHAR FAR*)pszTableName,SQL_NTS,

(UCHAR FAR*)pszTableType,SQL_NTS));

위와 같이 함으로써 현재 프로그램은 레코드가 아닌 테이블을 연결한다는 의미입니다.

이제 Open함수를 완성해 보겠습니다.

BOOL CTables::Open(LPCSTR pszTableQualifier,

LPCSTR pszTableOwner,LPCSTR pszTableName,LPCSTR pszTableType,

UINT nOpenType)

{

//ODBC함수 호출

AFX_SQL_ASYNC(this,::SQLTables(m_hstmt,

(UCHAR FAR*)pszTableQualifier,SQL_NTS,

(UCHAR FAR*)pszTableOwner,SQL_NTS,

(UCHAR FAR*)pszTableName,SQL_NTS,

(UCHAR FAR*)pszTableType,SQL_NTS));

//메로리와 케쉬 정보를  할당한다.

AllocAndCacheFieldInfo();

AllocRowset();

//필드 정보와 상태 를 할당한다.

AllocStatusArrays();

MoveNext();

return TRUE;

}

필드를 개방하는 것이 아니라 필드 리스트를 개방할경우에는 기존의Open 함수와는 다르게 만들어 지지요. 필드 리스트를 만들고 필요한 정보를 저장할 공간을 확보하는 3개의 Alloc계열 함수가 있을것입니다. 이함수는 도움말을 참조하세요. 이와 같이 Open과 함께 첫 번째 테이블 데이터가 CTables의 내부로 연결됩니다. 그렇다면 다음 데이터로 넘어가기 위해서는 현재 위치를 첫 번째 테이블 정보 다음으로 이동해야 합니다. 따라서 MoveNext함수를 호출한것입니다.

<4>테이블 리스트 얻기

처음에는 먼저 CDatabase의 클래스 변수인 m_pOdb를 Open함수를 사용하여 특정 데이터베이스를 선택하였습니다. 이제 이 데이터베이스 핸들러를 다시 CTables라는 클래스에 등록해줍니다.

//테이블 클래스로 m_pOdb를 등록

CTables rs(&m_pOdb);

다은 rs라는 CTables의 Open함수를 사용하여 첫 번째 테이블 정보를 CTables안에 기록합니다.

rs.Open(NULL, NULL, NULL, "TABLE");

소유자, 권한자 , 테이블 명 모두 우리는 알지 못합니다. 다만 우리가 원하는 것은 테이블 형태를 얻고자 하는 것입니다. 그렇기 때문에 Open의 3개의 인자는 NULL이고 마지막 인자 득 테이블 형태를 설정하는 값에 "TABLE"라고 하여 테이블 리스트를 얻고자 하는 형태로 파일을 개방합니다.

이와 같이하면 CTables클래스의 m_strTable에 첫 번째 테이블 명이 등록됩니다.

이 다음 테이블 명을 얻고자 한다면 MoveNext함수를 호출하면 됩니다.

본장에서 사용하는 예제 COdbcView에서 □ODBC설정□ 이라는 버튼을 클릭하면 바로 지금까지 배운 방법으로 데이터베이스를 개방하고 테이블 리스틀 얻어서 그림 1 화면의 중간에 있는 콤보 박스에 테이블 리스트를 기록합니다.

□ODBC 설정□ 버튼의 ID는 IDC_SETDB이며 이 버튼이 클릭했을 때 구동되는 함수는 OnSetDb로 설정되었습니다. 콤보 박스와 연결된 변수는 m_listTable입니다.

OnSetDb의 소스를 보시겠습니다.

//ODBC를 설정한다.

void COdbcViewView::OnSetdb()

{

//테이블 리스트에 데이터를 모두지우고

m_listTable.ResetContent();

//만약 m_pSet이 설정되어 있으면

if(m_pSet)

{

//Close하고

m_pSet->Close();

//설정을 해제 한다음

delete m_pSet;

//널로 초기화

m_pSet=NULL;

}

//m_pOdb가 개방되어 있으면 닫는다.

if (m_pOdb.IsOpen())

m_pOdb.Close();

//NULL로 설정하면 데이터베이스 설정 대화상자가

//구동된다.

if (!m_pOdb.OpenEx(NULL))

{

GetDlgItem(IDC_TABLELIST)->EnableWindow(FALSE);

GetDlgItem(IDC_DBVIEW)->EnableWindow(FALSE);

return;

}

//테이블 클래스로 m_pOdb를 등록

CTables rs(&m_pOdb);

//테이블 정보를 얻고자 하는형태로 개방

rs.Open(NULL, NULL, NULL, "TABLE");

CString szTable;

//테이블 리스트를 콤보 박스에 설정

while (!rs.IsEOF())

{

szTable = "[";

szTable += rs.m_strTableName + "]";

m_listTable.AddString(szTable);

rs.MoveNext();

}

rs.Close();

//테이블 리스트 콤보박스 활성화

GetDlgItem(IDC_TABLELIST)->EnableWindow(TRUE);

//DB보기 버튼 활성화

GetDlgItem(IDC_DBVIEW)->EnableWindow(TRUE);

}

<5>선택된 테이블 데이터 와 연결

□테이블만 선택되면 그테이블을 CRecoreset를 이용하여 보면 되기 때문에 그렇게 큰문제는 없다□ 라고 생각할수 있습니다. 이것은 선택된 테이블의 필드 구조를 이미 알고 있는 상태입니다. 필드구조를 알고 있는 방법은 전장을 참조하면 되니까 그렇게 큰 어려움은 없을 것입니다. 문제는 필드 구조를 알수 없을때가 바로 문제입니다.

DoFieldExchange 함수에 어떻게 필드명과 변수를 연결시킬수가 있나요?

ExOdb2의 DoFieldExchange를 한 번 보겠습니다.

void CExOdb2Set::DoFieldExchange(CFieldExchange* pFX)

{

//{{AFX_FIELD_MAP(CExOdb2Set)

pFX->SetFieldType(CFieldExchange::outputColumn);

RFX_Long(pFX, _T("[ID]"), m_ID);

RFX_Text(pFX, _T("[name]"), m_name);

RFX_Text(pFX, _T("[tel]"), m_tel);

RFX_Text(pFX, _T("[postnumber]"), m_postnumber);

RFX_Text(pFX, _T("[address]"), m_address);

RFX_Text(pFX, _T("[office]"), m_office);

RFX_Text(pFX, _T("[officetel]"), m_officetel);

RFX_Text(pFX, _T("[officepostnumber]"), m_officepostnumber);

RFX_Text(pFX, _T("[officeaddress]"), m_officeaddress);

RFX_Text(pFX, _T("[beep]"), m_beep);

RFX_Text(pFX, _T("[handpone]"), m_handpone);

RFX_Text(pFX, _T("[description]"), m_description);

//}}AFX_FIELD_MAP

}

위에서 보듯이 각각의 필드명과 변수를 연결하였습니다. 또 각각의 변수는 CRecordset 를 상속받은 CExOdb2Set에 미리 정의 하였습니다. 그러나 만약 필드 명을 알지 못하는 상태에서는 이것이 불가능 합니다. 따라서 DoFieldExchange 새로운 방법으로 설정을 해주어야 합니다.

OdbcView 예제에서는 필드의 데이터를 읽는 CRecordset 클래스를 상속받은 클래스명을 CDataBulkSet 로 만들었습니다. DoFieldExchange 는 CDataBulkSet에서는 사용할 수가 없습니다. 필드명을 모르기 때문입니다. 이것과 대응되는 DoBulkFieldExchange 는 필드명을 모를 때 사용하는 방법입니다. DoBulkFieldExchange 는 다음과 같습니다.

void CDataBulkSet::DoBulkFieldExchange(CFieldExchange* pFX)

{

// 데이터를 하나의 버퍼에 읽이 위해 버퍼를 메모리에 할당한다.

if (pFX->m_nOperation == CFieldExchange::AllocMultiRowBuffer &&

m_nAllocatedFields == 0)

{

// 필드의 수만큼 m_ppvData와 m_ppvLentths를 할당한다.

m_ppvData = new void*[GetODBCFieldCount()];

memset(m_ppvData, 0, sizeof(void*) * GetODBCFieldCount());

m_ppvLengths = new void*[GetODBCFieldCount()];

memset(m_ppvLengths, 0, sizeof(void*) * GetODBCFieldCount());

//필드의 수를 얻어서 m_nAllocatedFields에 설정한다.

m_nAllocatedFields = GetODBCFieldCount();

m_nFields = m_nAllocatedFields;

}

ASSERT(m_nAllocatedFields != 0);

//필드 형태 설정

pFX->SetFieldType(CFieldExchange::outputColumn);

for (int nNum = 0; nNum < GetODBCFieldCount(); nNum++)

{

//데이터베이스에 잇는 필드의 정보를 m_ppvData에 저장한다.

RFX_Text_Bulk(pFX, _T("Dummy"), (LPSTR*)&m_ppvData[nNum],

(long**)&m_ppvLengths[nNum], MAX_TEXT_LEN);

}

}

여기세어 m_ppvLengths와 m_ppvData 는 바로 필드 데이터의 정보와 해당하는 정보의 길이가 설정된것입니다. 또한 MAX_TEXT_LEN는 고정적으로 현재 정보에서 몇 개의 최대 길이만 읽고 나머지는 짜르겠다는 것입니다. 이값은 512로 설정되어있습니다.

(주의!)

만일 MAX_TEXT_LEN보다 큰 데이터가 있으면 에러가 나옵니다. 이때 무시를 하면 데이터를 읽을수 있으나 문제가 있겠지요. MAX_TEXT_LEN 보다 큰 데이터가 나왔을 때 에러가 발생하여도 이것을 무시하라는 방법으로 다음과 같은 함수를  만들고 빈공백으로 만들면 큰 문제는 없습니다.

void CDataBulkSet::CheckRowsetError(RETCODE nRetCode)

{

}

(주의 끝)

OdbcView의 전체 소스는 본장에서 생략합니다. 제공되는 CD를 참조하시기 바랍니다.