PART 3 ODBC와 DAO 데이터베이스

ODBC란  Open Database Connectivity의 약자입니다. 우리가 아는 유명한 데이터베이스에는 Oracle, Sybase, Access 등이 있습니다.

데이터베이스 안의 내부에는 매우 복잡한 알고리듬들이 들어가 있죠. B트리 같은 searching 알고리듬, 데이터를 기록하는 상황을 순차적으로 정의해 두는 트랜잭션 알고리듬, 집합 관계형 DB를 구축하기 위한 알고리듬, 또한 레코드를의 관계를 하나의 연산으로 보는 알고리듬 등등 수도 없이 많은 알고리듬들이 있습니다.

하나의 DBMS를 만든다고 한다면 엄청난 시간과 노력을 투자해야 합니다. 그만큼 데이터베이스 엔진을 구축하는 것은 어렵다고 볼 수 있습니다. 컴퓨터를 이용하는 것 중 데이터 저장이 큰 목적 중에 하나입니다. 만일 프로그램이 좋지 않아 실수가 잦아서 데이터 하나라도 유실된다면 그에 따른 문제점은 매우 큽니다. 그렇기 때문에 위에서 말한 유명한 데이터베이스 엔진들 중 많은 것들이 고가입니다. 그 이유는 그만큼 많은 인력이 많은 시간을 두고 만들었기 때문이죠.

데이터베이스 엔진은 그렇게 복잡하다고 하더라도 사용자 입장에서 데이터베이스를 다루는 과정은 어떻습니까? 사실 매우 간단합니다. 몇 개 안 된다는 이야기죠. 데이터를 넣고(입력), 데이터를 찾고(검색), 데이터를 수정하고(수정), 데이터를 삭제하고(삭제) 이게 전부 아닙니까?

결국 엔진은 복잡하더라도 사용자 입장에서는 입력, 검색, 수정, 삭제 4가지만 잘하면 됩니다. 물론 위의 4가지를 잘하기 위해서 내부적인 데이터베이스 엔진은 매우 복잡하게 돌아가지만요.

어느 큰 시스템에서 수천만 원을 들여서 오라클 엔진을 탑재하여 데이터베이스를 구축했습니다. 새로운 데이터베이스가 필요하여 현재에서 업그레이드한다고 했을 경우 오라클 사에 다시 의뢰를 해야겠지요.

그런데 경쟁 데이터베이스 제작업체가 우리의 제품을 써 달라고 요청했을 때 오라클 사의 데이터베이스와 호환이 없다면 타사의 프로그램을 구입하기는 어렵게 되지요.

사용자가 엔진을 컨택하는 명령은 극히 일부분이고 공통적인 데 반하여 내부적으로 돌아가는 데이터베이스 엔진 부분은 서로 상이한 기법을 많이 씁니다. 그렇다면 사용자에게는 공통적인 언어를 사용하게 하고 중간에 그 언어를 자신의 데이터베이스에 적절하게 조절하기 위한 번역 프로그램을 설치하여 데이터베이스에 접근한다면 오라클을 사용하는 시스템 옆에 사이베이스 프로그램을 설치하고 사용해도 무방하게 되지 않겠습니까? 이렇게 해서 나온 것이 4GL입니다. 결국 사용자는 4GL만 사용하면 되고 모든 데이터베이스 엔진이 4GL을 지원하면 되므로 어떤 엔진이든 상관없이 사용할 수 있게 만든 것이죠. 이런 의미가 바로 ODBC입니다.

윈도 95에서도 이런 ODBC를 지원합니다. 따라서, 윈도 프로그래머는 윈도 상황에서 SQL문이나 Visual C++ 안에서 제공하는 ODBC 클래스나 또는 Win32용 함수를 이용하여 데이터베이스 프로그램을 구현하고 이 프로그램이 오라클을 엔진에서 돌아가게 만들려면 오라클 드라이버만 설치하면 됩니다. 이 프로그램이 액세스에서 돌아가게 하려면 액세스 드라이버만 설치하면 바로 데이터베이스를 컨트롤할 수 있습니다.

ODBC는 네트워킹 안에서 돌아갈 수 있게끔 설계되어 있어서 한 컴퓨터 안에 단순히 한 사람이 작업하는 데이터베이스 엔진으로는 너무 큽니다. 한 사람이 출퇴근용으로 버스 한 대를 자가용으로 사용한다면 웃기는 일이지요.

STAND ALONE(한 컴퓨터에서 작업이 이루어짐) 컴퓨터에서 용량이 큰 ODBC 엔진을 사용하면 사실 기존의 단순한 B트리 구조의 데이터베이스에 비해서 성능이 떨어집니다. STAND ALONE 컴퓨터에서는 그 환경에 맞게 작은 데이터베이스 엔진이 필요합니다. 이런 것을 고려하여 만든 것이 DAO입니다.

DAO란 MS 사에서 만든 Data Access Object의 약자입니다. STAND ALONE 컴퓨터에서 데이터베이스를 가지고 프로그램할 경우 사용하라고 만든 엔진이지요.

본편에서는 ODB와 DAO 두 개의 데이터베이스를 이용하는 방법을 설명하고 여기서는 ODBC를 중점으로 하여 프로그램을 제작합니다.

1.ODBC프로그래밍,DAO프로그래밍

본장은 데이터베이스 프로그램을 작성할때 필수적으로 필요한 ODBC와 DAO에 기본프로그래밍에 대해 설명을 합니다. ODBC는 윈도우에 설치된 여러 데이터베이스 엔진을 쉽게 접근하며 SQL를 이용한 프로그래밍을 할때 자주사용되는 프로그래밍 입니다. DAO은 Microsoft 사에서 제공하는 PC용 데이터 베이스 엔진입니다. 이 두개의 사용법및 프로그래밍 방법은 매우 유사한 면이 많습니다. 본장에서는 ODBC를 기반으로 프로그래밍 하는 방법을 설명하고 이개념을 이용하여 DAO에 접근할것입니다.

대상자: 중급,고급

Chapter 등급: 필수

공부방법:

데이터베이스를 구동하는 방법은 프로그래밍에서 꼭 해야 합니다. 윈도우 프로그래밍에서는 ODBC가 매우 많이 사용되며 데이터베이스 엔진을 만들수 없다 하더라도 DAO만 알면 쉽게 데이터베이스 프로그램을 할수 있습니다. 본장의 예제는 꼭 알고 넘어가시기 바랍니다.

1.ODBC

<1>ODBC의 개념

ODBC는 Open DataBase Connectivity의 약자입니다. 데이터베이스 엔진을 이용하여 유저 인터페이스 프로그램을 작성할때 사용하는 어플리케이션 인터페이스 입니다. 조금 말이 어렵군요. 쉽게 이야기 한다면 여러 DBMS를 Visual C++프로그램에서 컨트롤 하거나 또는 Visual Basic등의 프로그래밍 언어나 툴에서 편리하게 컨트롤 하기 위한 인터페이스라는 것입니다.

DBMS란 데이터베이스 관리 시스템이라는 의미입니다. 오라클같은 프로그램을 DBMS라고 하는데 이것은 거대한 데이터베이스를 관리하는 데이터베이스 시스템입니다. 이런 엔진들과 연결하여 데이터베이스 프로그램을 작성할때 사용하는 인터페이스가 ODBC입니다.

데이터베이스 엔진을 만드는 회사에서는 윈도우용 데이터베이스 엔진 드라이버를 제공합니다. 이것을 ODBC드라이버라고 하는데 어플리케이션 프로그램에서는 이 ODBC드라이버에 연결하고 몇가지 명령을 드라이버에서 명령에 따라서 데이터베이스 파일들을 컨트롤 합니다.

ODBC를 모르시는 분들이 있을것 같아서 쉽게 예를 들어 개념을 설명하겠습니다.

자동차를 운전하고자 할때 운전면허증을 땁니다. 예를 들어 2종 운전 면허증을 교부받으면  어떤 승용차든 운전할수 있습니다.  사실 운전하는것은 어느차를 운전하든 방법은 같습니다. 다만 조작대의 위치나 레버 버튼등이 차따라 다릅니다. 운전면허를 교부받을때 □본운전 면허는 현모사에서 나온 알랑트라만 운전할수 있음□ 이라고 적혀있지는 않습니다. 운전을 잘하고 못하고는 상관없이 비싼차는 그만큼 좋은것이고 싼차는 그만큼 운행하는데 불편을 격습니다. 다만 운전하는것은 똑같다는 의미입니다. ODBC도 이런개념에서 생각하시면 됩니다. 수억또는 수천만원이 가는 비싼 데이터베이스 엔진을 구입하든 또는 싼 데이터베이스 엔진을 구입하든 파일에 데이터를 저장하고 검색하고 수정하고 삭제하는 기능은 같습니다.

자동차를 움직일때 핸들과 기아만 작동하지만 이 작동에 의해서 자동차가 움직일때는 내부에 아주복잡한 기계들이 복잡하게 조직화 되어서 움직이듯이 데이터베이스 엔진도 내부적으로는 아주 복잡하게 움직이지만 외부에서 사용자가 이용하는 기능은 단순하게 데이터 입력,검색,수정,삭제 뿐이라는 것입니다. 그렇기 때문에 이 기능을 드라이버로 제공하고 어플리케이션 프로그래머는 이드라이버와 연결하여 입력,검색,수정,삭제의 간단한 명령을 드라이버에 제공하는 것입니다.

여기에서 생각하실 점은 바로 드라이버가 있어야만 그 데이터베이스 파일을 구동할수 있는것입니다.

ODBC는 단순하게 면허증에 불과합니다. 컴퓨터안에 오라클 ODBC드라이버가 없다면 오라클로 작성한 데이터베이스에는 접근할수 없는것입니다. 또한 MDB드라이버가 없으면 MDB(마이크로 소프트사에서 만든 데이터베이스 파일)를 ODBC로 접근할수 없는것입니다. 운전면허증만 있다고 자동차도 없는데 운전할수 없는것과 마찬가지입니다. 즉 ODBC자체로는 드라이버가 설치되지 않는 상태에서는 어떤 데이터베이스 엔진도 접근할수가 없습니다. 현재 자신의 컴퓨터에 어떤 데이터베이스 드라이버가 있는가를 확인할려고 한다면 제어판에서 ODBC항목을 클릭하시면 됩니다. 그림 3-1-1은 ODBC항목을 클릭했을때 출력되는 화면입니다.

(그림 파일)

(그림 3-1-1)ODCB항목을 클릭했을때 출력되는 화면

그림3-1-1에서는 Microsoft 사의 Access, DBase,Excel,FoxPro,Oracle73 SQL 서버등이 설정되어 있습니다.  그림3-1-1과 같이 설치된 컴퓨터에서는 Access데이터베이스 파일이나 또는 DBase,Excel,FoxPro,오라클등은 ODBC를 사용하여 데이터베이스에 접근을할수 있습니다. 그3-1-11에 등록되어 있지 않는 데이터베이스는 ODBC로는 접근을 할수 없다는 의미입니다.

지금까지의 내용으로 본다면 결국 ODBC란 드라이버와 연결되는 명령통로라고 생각하시면 됩니다. ODBC를 배우시기 전에 자신의 컴퓨터에는 어떤 드라이버들이 있는지 확인을 해주시기 바랍니다.

<2>ODBC의 등록

오라클과 같은 ODBC는 파일을 등록할 필요가 없으나 일반적으로 많이 사용하는 Access의 파일 (*.mdb)는 컴퓨터에 등록되어 있어야만 합니다. 제어판에서 ODBC항목을 선택하면 그림 3-1-2와 같은 화면이 출력될것입니다.

(그림 파일)

(그림 3-1-2)제어판에서 ODBC 항목을 클릭했을때 출력화면

각컴퓨터 마다 그림 3-1-2와 같이 똑같은 형태로 출력되지 않습니다. 해당 데이터베이스 엔진이 설치된 유무에 따라서 다르게 나타납니다. 중요한것인 사용자 DSN에서 추가 항목이 있을것이고 이버튼을 클릭하여 원하는 데이터베이스 파일을 선택한다는 것입니다.

추가 버튼을 클릭하면 그림 3-1-3과 같은 화면이 나옵니다.

(그림 파일)

(그림 3-1-3) 데이터베이스 파일 설정

추가버튼을 클릭한다는 것은 ODBC에 등록할 새로운 데이터베이스 파일을 설정한다는 것입니다. 일반적으로 ODBC에 등록할 파일들이나 테이블들은 데이터베이스 엔진에서 제공하여 주는 프로그램으로 먼저 만들어 놓아야 합니다. 예를 들어서 Micorsoft Access Driver(*.mdb) 파일을 ODBC에 등록하고자 한다면 Access프로그램을 이용하여 등록할 파일을 만드셔야 합니다. Access프로그램을 사용하는 방법과 각 엔진에 의해서 파일을 만드는 방법은 본책에 내용과는 거리가 있기 때문에 기록하지 않습니다.

다시 말씀드린다면 ODBC는 데이터베이스관리 프로그램에서 제작한 파일을 연결하여 이파일에 데이터를 쓰고,읽고,수정하는 기능을 주로 합니다. 본장에서는 일반적으로 사용하는 Access 드라이버를 이용하여 ODBC를 등록하는 방법에 대해서 설명하겠습니다.  그림 3-1-3과 같은 하면이 나온상태에서 완료 버튼을 클릭하면 그림 3-1-4와 같은 화면이 나타납니다.

(그림 파일)

(그림 3-1-4)Access를 이용하여 ODBC를 설정

그림 3-1-4와 같은 화면이 나올때에 □선택□ 이라는 버튼을 클릭하면 파일 대화상자가 출력됩니다. 이파일 대화상자에서 자신이 만든 mdb파일을 선택하면 됩니다. 이렇게 선택하면 □데이터베이스□ 라는 란에 현재 설정된 파일의 전체 경로가 나타납니다. 그림 3-1-4에서 보면 □D:\public\winpro\namecard.mdb"를 설정한것이 보일것입니다. 즉 현재 ODBC에 D:\public\winpro\namecard.mdb가 설정되었다는 것입니다. 이렇게 파일을 선택하고 이파일의 어떤 파일인지 이름을 설정합니다. 이름을 설정하는 항목이 □데이터 원본 이름(N)" 입니다. 이곳에 □namecard"라고 설정하였습니다. □namecard"라고 등록하면 ODBC를 이용할때 데이터베이스의 파일명인 □D:\public\winpro\namecard.mdb" 을 이용하지 않고 "namecard" 만이용합니다.

예를 들어

LoadDatabase("namecard");

같은 방식이라는 의미입니다.(MFC에서 실제 위와 같이 쓰지는 않습니다.) 즉 데이터베이스를 로드할경우 "namecard"라는 이름으로 로드하면 드라이버는 이데이터베이스 파일이 어느곳에 위치해 있다는 것을 알아내고 그파일을 핸들링하는 것입니다. 설명은 본파일에 대한 설명합니다. 본장에서 만든 namecard 는 간단하게 개인정보를 기억하는 파일입니다. 본파일의 Table1 대한 정보는 표1과 같습니다.

필드 명	데이터 형식	내용
ID	일련 번호	데이터가 기록될때의 번호
name	문자열	이름 (색인됨)
tel	문자열	전화번호
address	문자열	주소
bigo	문자열	기타정보

(표1) namecard파일의 Table1 필드정보

MDB 에서는 한개의 파일에 여러개의 테이블을 넣을수 있습니다. namecard.mdb파일에 첫번째 테이블에 설정되어 있는 필드정보는 표1과 같다는 의미입니다.

Microsoft Access를 이용하여 namecard를 직접 제작하든지 또는 제공하는 CD-ROM에 있는 namecard.mdb를 특정 디렉토리에 복사하든지 하여 이것을 ODBC에 등록하여야 합니다. 또한 필드 정보는 표1과 같아야 합니다. 이렇게 하나를 만들어 놓아야 이것을 이용하여 예제 프로그램을 작성할수 있기 때문입니다.

그림 3-1-4와 같은 화면에서 □확인□ 버튼을 클릭한면 ODBC에 namecard가 존재하게 됩니다.

<3>ODBC 에제 MExOdbc 프로젝트 만들기

이제 namecard를 이용하는 예제를 작성해 보겠습니다. 프로젝트명을 MExOdbc로 선택하고 SDI형으로 선택합니다. AppWizard Step2에서 그림 3-1-5와 같은 화면이 나올때 Database view without file support를 선택합니다.

(그림 파일)

(그림 3-1-5)AppWizard Step2

ODBC를 구종하는 방법에 대한 내용을 설정한것이 AppWizard의 Step2입니다. 이 스텝에서는 ODBC를 사용할것인가 아닌가를 물어봅니다. None를 선택하시면 ODBC를 이용하지 않는다는 의미입니다.(지금까지는 None를 사용하였습니다) Header files only는 헤더만 사용하겠다는 의미이며 Database view whithout file support는 데이터베이스를 이용하여 □파일 열기□나 □파일저장□같은 일반적인 파일 입출력은 사용하지 않겠다는 의미이며 Database view with file support는 □파일 입출력□ 기능을 제공하겠다는 것입니다. 우리가 만드는 예제는 단순하게 데이터베이스만 이용하기 때문에 3번째 항목 Database view whthout file supprot항목을 선택한것입니다. 이렇게 선택하면 어떤 데이터베이스를 이용할것인가를 설정해 주어야 합니다. 그림 3-1-5에서 Data Source를 클릭하여 이용할 데이터베이스를 설정해 줍니다. Data Source버튼을 클릭하면 그림 3-1-6과 같은 화면이 출력됩니다.

(그림파일)

(그림 3-1-6)Database Otptions 화면

Datasource항목에서 ODBC를 선택하고 콤보박스를 클릭하면 전에 우리가 설정하였던 namecard 가 나타납니다. namecard라고 선택을 하면 본 프로젝트에서는 namecard라는 데이터베이스를 ODBC로 이용하겠다는 의미입니다. DAO를 선택하면 ODBC가 아닌 DAO를 이용하여 데이터베이스를 사용하겠다는 의미인데 DAO에 대해서는 뒤에 설명을 하겠습니다. Recordset type는 레코드에 데이터가 갱신 되었을때 바로 데이터베이스에 기록할것인가? 아니면 데이터베이스 이용을 끝낸다음 갱신할것인가를 설정해주는 것입니다. Snapshot란 현재 데이터베이스의 이용이 끝난다음 갱신된 내용을 저장하는 것이고 Dynaset는 갱신이 되는 순간 바로 수정을 하는 것입니다. 보통 ODBC는 Snapshot를 이용하며 DAO는 Dynaset를 이용합니다. 그이유는 ODBC는 한곳에서만 데이터베이스를 이용하는 것이 아니라 여러 곳에서 한개의데이터베이스를 읽고 수정하기 때문에 자신이 수정한 내용이 완벽할경우에만 기록을 해야지 데이터를 잘목기록해놓고 □아이고 이게 아닌가 부다!□ 라고 해서 다시 수정을 할때 이순간 잘목기록해 놓은 내용을 다른 곳에서 검색을 하고 오류를 범할수 있기 때문입니다. 반면에 DAO는 개인 PC에서 단일적으로 사용하기 때문에 이런 오류가 없고 그렇기 때문에 바로바로 수정을 하는 방식을 택하게 됩니다.

위와 같이 한후에 OK버튼을 클릭하게 되면 그림 3-1-7과 같은 화면이 출력됩니다.

(그림 파일)

(그림 3-1-7) Select Database Tables

그림 3-1-7의 의미는 현재 선택된 데이터베이스안에 있는 테이블중 특정 테이블을 선택하라는 의미입니다.

그림 3-1-7과같이 namecard에 선택되어 있는 Tabel 1을 선택합니다.

위와 같이 한후 OK버튼을 클릭하고 Next키를 눌러서 Step6에 가면 그림 3-1-8과 같은 화면이 나타납니다.

(그림 파일)

(그림 3-1-8) MFC AppWizard Step6

그림 3-1-8에서 보듯이 View은 Base class  가 CRecordView입니다. 지금까지 사용한 View는 FormView나 또는 CView또는 ScrollView이었으나 이번장에서는 새롭게 CRecordView를 보시게 될것입니다. 또한 CMExOdbSet라는 새로운 클래스가 하나 더 생성되었습니다. 계속 예제를 만들어 가면서 설명을 하겠지만 여기에서 ODBC를 이용할때 두개의 새로운 클래스가 도입된다는 것을 기억해 주시기 바랍니다. CMExObjecSet 는 CRecordset을 상속받은 클래스이며 View는 CRecordView를 상속받았다는 내용입니다.

Finish버튼을 클릭하면 프로젝트가 만들어 집니다. 이제 각 클래스에 대해서 조사를 해보겠습니다.

<4>CRecordView

CMExOdbcView는 CRecordView를 상속받았습니다. 그렇다면 이제 우리는 CRecordView가 도대체 무엇인가 그정체를 알아야 합니다. 도움말에서 CRecordView를 찾아 보면 그림 3-1-9와 같은 계층도표가 나올것입니다.

(그림 파일)

(그림 3-1-9)CRecordView의 계층구조

CRecordView는 CFormView를 상속받았습니다. 이렇게 상속받았다는것은 대화상자 자원을 이용한다는 것입니다. 대화상자 자원을 이용하는 것은 CFormView와 같은데 다음으로 궁금한것은 어떤 특징이 추가되었는가입니다. 이에 대한 답은 CRecordView를 상속받은 CMExOdbcView의 헤더에 나와있습니다. CMExOdbcView의 헤더인 MExOdbcView.h에 보시면 다음과 같은 내용이 있을것입니다.(직접 확인을 해보시기 바랍니다.)

CMExOdbcSet* m_pSet;

CRecordView를 상속받은 CMExOdbcView에서는 CMExOdbcSet형인 m_pSet가 있습니다. 프로젝트를 만들때 우리는 CMExOdbcSet클래스는 CRecordset을 상속받은 클래스라는것을 어렴풋이 알고 넘어갔습니다.

중요한것은 CMExOdbcView에서는 CRecordset를 상속받은 CMExOdbcSet을 클래스 맴버 변수로 가지고 있다는 것입니다. 다음에 CRecordSet에 대한 기능을 자세히 서명하기로 하고 우선 CRecordView의 정체에 대해서 알려 드리겠습니다.

CRecordView는 FormView형태이면서 맴버 변수로 CRecordSet를 가지고 있는 View입니다. CRecordSet는 우리가 설정한 ODBC를 핸들링하는 클래스이며 이맴버를 가지고 있는 View는 ODBC에설정된 필드의 정보들을 화면에 출력할수 있겠금 CRecordSet를 컨트롤 하는 맴버 함수를 내장한 View라는 것입니다.

즉 다시 말하면 RecordView는 FormView처럼 대화상자자원을 가지고 있고 내부 맴버로 데이터베이스 컨트롤하는 클래스 CRecordSet를 맴버 변수로 가지고 있다는 것입니다. 그래서 이변수를 이용하여 데이터를 얻고 얻은 데이터를 화면에 보여주는 기능을 하는 것입니다.

namecard는 Table1 □이름□,□전화번호□,□주소□,□비고□ 필드를 가지고 있는 데이터베이스 파일입니다. 프로그램을 작성한다면 □이름□,□전화번호□,□주소□,□비고□ 가 출력되는 에디터박스가 있고 □다음□,□이전□ 등의 버튼을 이동하면 해당 데이터가 바뀌게 하면 좋지 않겠습니까? 그리고 해당 데이터를 수정하고 □수정□버튼을 클릭하면 해당 데이터가 바뀌게 되면 더욱 좋고요. 즉 정보가 출력되게 하기 위해서 가장 손쉽게 하는 방법은 FormView를 이용하여 각 필드에 해당하는 에디터 박스를 넣어두고 또한 □수정□ 이나 □삭제□ 같은 버튼을 두어서 해당 데이터를 수정하면 더욱좋습니다. 버튼이나 에디터 박스등을 도시하는것은 FormView가 적당하고 여기에 현재 설정된 데이터베이스를 핸들링하기 위해서 맴버로 데이터베이스 핸들링 클래스 CRecordSet를 설정한것입니다.

<5>CRecordset

CRecordset는 데이터베이스를 로드하고 로드된 데이터베이스의 레코드를 관리하는 클래스입니다. CRecordset를 상속받은 CMExOdbcSet클래스를 보면 CRecordset속성을 알수 있습니다. CMexOdbcSet의 헤더에 보면다음과 같은 맴버들이 있을것입니다.

// Field/Param Data

//{{AFX_FIELD(CMExOdbcSet, CRecordset)

long	m_ID;

CString	m_name;

CString	m_tel;

CString	m_address;

CString	m_bigo;

//}}AFX_FIELD

프로젝트를 만들때 설정한 namecard 데이터베이스의 Table1에는 이름,전화번호,주소,비고 등이 있었습니다. 이 각 필드에 있는 정보들을 헤더에 설정한 각 변수에 로드하는 것입니다. 자원에 있는 항목들을 클래스이 맴버 변수로 연결시킬때 DoDataExchange를 사용하였습니다. CRecordset에서는 위에 설정한 m_name,m_tel 등등과 연결될 실제 데이터베이스 필드를 다음과 같은 함수를 사용하였습니다.

void CMExOdbcSet::DoFieldExchange(CFieldExchange* pFX)

{

//{{AFX_FIELD_MAP(CMExOdbcSet)

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("[address]"), m_address);

RFX_Text(pFX, _T("[bigo]"), m_bigo);

//}}AFX_FIELD_MAP

}

DoFieldExchange 함수는 필드와 맴버의 변수와 연결시키주는 기능을 합니다. RFX_Long는 필드의 long를 현재 맴버변수 m_ID와 연결을 하면 RFX_Text는 필드의 텍스트를 현재 설정되어있는 CSTring와 연결을 합니다. 대화상자와 연결하는 DoDataExchange와 비슷한 개념으로 보시면 될것입니다.

프로젝트를 만들때 우리는 namecard라는 데이터베이스를 선택하였고 이데이터베이스의 첫번째 테이블 "Table1"를 테이블로 선택하였습니다. AppWizard는 프로그래머가 편리하도록 소스를 대신 코딩하는 작업을 한다고 하였습니다. 그렇다면 namecard라고 설정한 데이터베이스 명과 "Table1"를 설정한 항목이 분명 어느곳에 기록되어 있다는 추측을 할수 있습니다. 이추측과 걸맞게 바로 CMExOdbcSet의 소스에 다음과 같은 내용이 있을것입니다.

CString CMExOdbcSet::GetDefaultConnect()

{

return _T("ODBC;DSN=namecard");

}

CString CMExOdbcSet::GetDefaultSQL()

{

return _T("[Table1]");

}

GetDefaultConnect함수는 현재 설정되어 있는 데이터베이스명을 리턴하며 GetDefaultSQL은 현재 설정되어 있는 테이블을 리턴합니다.(Access에서만 그렇다는 이야기 입니다.) 이두함수에 데이터베이스명과 테이블 명만 바꾸면 다른 데이터베이스와 다른 테이블을 로드할수 있는것입니다.

현재는 ODBC의 namecard와 Table1으로 설정되어 있습니다. 한가지 더있다면 Snapshot, Dynaset 은 어느곳에 설정되어 있는가입니다. CMExOdbcSet의 생성자를 보시면 다음과 같은 코드가 있을것입니다.

m_nDefaultType = snapshot;

m_nDefaultType는 CRecordSet의 맴버 함수로써 현재 레코드 컨트롤 클래스가 어떤 형태인가 스타일을 설정해주는 플래그입니다. 이플래그에 snapshor값을 기록하여 snapshot로 설정한것입니다.

여기까지 보면 AppWizard에서 잘못설정했다 하더라도 우리가 수정을 할수 있다는 것을 느끼실것입니다. □아차 namecard가 아니라 name라는 데이터베이스인데..□

□아차 Table1이 아니라 Tabel2인데 □ 라고 생각하셨을때 바로 CRecordset를 상속받은 CMExOdbcSet를 수정하시면 됩니다.

CRecordset에는 표2와 같은 함수들이 있습니다.

(표2) CRecordset의 함수리스트

함수명	내용
MoveFirst	제일 처음 레코드로 이동한다.
MoveLast	마지막 레코드로 이동한다
MoveNext	다음 레코드로 이동한다
MovePrev	이전 레코드로 이동한다.
SetBookmark	현제 레코드를 북마크한다.
AddNew	세로운 필드를 만든다
Delete	핸재 레코드를 지운다
Edit	데이터베이스를 수정가능한 상태로 한다.
Update	CRecordset 에 있는 데이터 맴버들의 내용을 데이터베이스에 업데이트한다.
Requery	SQL언어에 의해서 또는 함수에 의해서 설정된것을 새롭게 query 한다.
IsEOF	현재가 파일 마지막이면 1를 리턴한다.
IsBOF	현재가 파일처음이면 1를 리턴한다.
GetRecordCount	현재 테이블의 레코드 수를 얻는다.
m_pDatabase	CDatabase 클래스형의 맴버로 실제 데이터베이스를 컨트롤 하는 클래스
m_strFilter	SQL언어를 가동시키기 위한 문자열을 저장하는 변수 이곳에 문자열을 저장하고 Requery를 실행하면 SQL언어를 실행할수 있다.

표2의 함수를 보면 데이터베이스를 입력하고 수정하고 검색하는 모든 기능은 CRecosrset에 있다는 것을 느낄수 있을겁니다.

여기에서 다시 정리한다면 CRecordView(MExOdbc에서는 CMExOdbcView)에서는 단순하게 CRecordset(MExOdbc에서는 CMExOdbcSet)를 맴버 변수로 가지고 있고 실제 레코드를 핸들링하는 것은 CRecordset라는 것을 알수 있습니다. CRecordView는 CRecordset에서 설정된 레코드 데이터를 FormView형태로 화면에 출력하는 것이 전부입니다.

<6>CDatabase

초보적인 ODBC를 이용한다고 하였을경우 CRecordset의 기능만 이해해도 가능합니다. 그러나 CRecordset까지 조사해본 이마당에 또하나를 더알아보겠습니다. CRecordset에는 m_pDatabase라는 CDatabase라는 클래스의 맴버 형이 있습니다. 이클래스는 우리가 지금까지 보지못한 새로운 클래스입니다. 이클래스의 기능이 무엇인가를 알기위해서 도움말에서 CDatabase를 찾아보시기 바랍니다. calss member 를 선택하면 그림 3-1-10과 같은 화면이 나오게 될것입니다.

(그림 3-1-10)도움말에서 찾아본 CDatabase Class Members

본책을 끝난후에는 여러분들이 프로그램을 할때 최우선적으로 참고서를 삼아야 할것이 도움말이 될것입니다. 따라서 이제는 도움말을 보고 어떤 클래스인가를 확인할수 있어야 합니다. CDatabase는 실제로 데이터베이스를 컨트롤하는 클래스입니다. 우리가 선택한 데이터베이스 즉 namecard를 직접 컨트롤하는것은 CRecordView도 아니고 CRecordset도 아닌 CDatabase입니다. 그러나 이 CDatabase는  내부에 가려져 있기 때문에 우리가 알수가 없었던것입니다. 프로그래밍을 할때 CDatabase만가지고도 ODBC를 핸들링할수 있습니다. MFC에서는 CDatabase가지고 핸들링할때 다소 복잡한것을 막기 위하여 데이터베이스 프로그래밍을 그립 3-1-11과 같은 구조로 설정하도록 권고를 한것입니다.

(그림 파일)

(그림3-1-11) 데이터베이스 핸들링 클래스 형태

그림 3-1-11에서 보는것처럼 원하는 데이터베이스를 오픈하는 실제 클래스는 CDatabase이며 이클래스를 이용하여 데이터베이스를 오픈하고 해당 레코드 정보를

로드하는 클래스가 CRecordset이며 로드된 레코드들을 화면에 출력하는 것이 CReocrdView입니다. 이런 개념을 이해하고 예제를 작성해 나가면 데이터베이스의 개념을 이해할수 있을것입니다.

<7>CRecordView의 자원과 CRecordSet 연결

이제 프로젝트 MExOdbc에 새로운 데이터베이스를 핸들링하는 예제를 만들어 보겠습니다.

MExOdbc에서 CMExOdbcView와 연결된 대화상자 자원 ID는 IDD_MEXODBC_FORM 입니다. 그림 3-1-12은 IDD_MEXODBC_FOMR을 수정한 화면입니다.

(그림 3-1-12)IDD_MEXODBC_FORM

IDD_MEXODBC_FORM 에는 이름,전화번호,주소,비고등을 기입할수 있고 또는 출력할수 있는 에디터 박스를 설정하였습니다. 비고는 여러 줄을 입력할수 있도록 multiline형태로 설정하였습니다. 위와 같이 설정된 각 레코드를 CRecordset형인 CMExOdbcSet의 변수로 연결시켜 보겠습니다.

그림 3-1-13은 이름을 CMExOdbcSet의 m_name로 연결하는 형태입니다.

(그림 3-1-13) 이름의 자원을 m_pSet->m_name로 연결시킨형태

이름을 입력하는 에디터박스에 마우스를 위치하고Ctrl 키를 누른상태에서 좌측 마우스 버튼을 클릭하게 되면 그림 3-1-13과 같은 화면이 나타납니다. CRecoreView로 설정하면 Member variable name에 콤보박스형태로 나타나게 되고 m_pSet(CMExOdbcSet의 형)의 필드 데이터를 저장하는 변수들의 리스트가 보이게 됩니다. 이때 이름의 필드와 연결하기 위해서 m_pSet->m_name를 선택한것입니다. 이와 같은 방법으로 전화번호,주소 비고등을 연결합니다. 각 필드와 연결되는 m_pSet의 맴버의 리스트는 표3과 같습니다.

ID	내용	m_pSet과 연결
IDC_NAME	이름 필드	m_name
IDC_TEL	전화번호	m_tel
IDC_ADDRESS	주소	m_address
IDC_BIGO	비고	m_bigo

(표3) m_pSet과 연결되는 대화상자 ID

위와 같이 하면 DoDataExchange 함수는 다음과 같이 설정됩니다.

void CMExOdbcView::DoDataExchange(CDataExchange* pDX)

{

CRecordView::DoDataExchange(pDX);

//{{AFX_DATA_MAP(CMExOdbcView)

DDX_FieldText(pDX, IDC_NAME, m_pSet->m_name, m_pSet);

DDX_FieldText(pDX, IDC_TEL, m_pSet->m_tel, m_pSet);

DDX_FieldText(pDX, IDC_ADDRESS, m_pSet->m_address, m_pSet);

DDX_FieldText(pDX, IDC_BIGO, m_pSet->m_bigo, m_pSet);

//}}AFX_DATA_MAP

}

DDX_Text가 아닌 DDX_FieldText형태로 두개의 데이터가 서로 연결될것입니다. DDX_FieldText 함수는 한개의 인자를 더필요합니다. 이인자는 CRecordset형의 현재 RecordView에 설정된 변수입니다.

이와 같이 한후 컴파일하고 실행하면 그림 3-1-14와 같은 화면이 출력될것입니다.

(그림 3-1-14) MExOdbc 실행결과

<8>데이터 입력

현재까지는 namecard에 설정되어 있는 데이터를 화면에 출력하고 도구바에 있는 이동버튼을 이용하여 레코드를 이용할뿐입니다. 이제 이 프로그램에서 입력을 설정해 보겠습니다. 메뉴의 레코드항목에 □입력□ 이라는 항목을 추가합니다. 그림 3-1-15은 □입력□이라는 항목이 추가된 메뉴자원의 형태입니다.

(그림 3-1-15) 메뉴에 입력항목을 추가한 형태

그림 3-1-15처럼 ID가 IDD_ADDDATA인 입력이라는 항목을 추가하고 이항목을 선택하였을때 실행되는 함수를 CMExOdbcView에 설정합니다. 설정된 함수는 다음과 같습니다.

void CMExOdbcView::OnAdddata()

{

// TODO: Add your command handler code here

}

이제 이함수에서 기록된 데이터를 새로 입력하도록 합니다. 새로 입력할때는 데이터베이스에 빈 레코드를 하나 만들어야 합니다. 빈레코드를 만들어야 이레코드에 데이터를 입력할수 있는것입니다. 그럼 빈레코드를 어느곳에 만들어야 할까요? 당연히 레코드 맨마지막으로 이동해서 만들어야 합니다. 이렇게 빈 레코드를 만들고난후 에디터에 입력한 값을 이 빈레코드에 넣으면 됩니다. 이렇게 한후 Requery를 실행시킵니다. 이것을 실행하지 않으면 데이터베이스가 갱신되지 않습니다. 이렇게 하여 데이터베이스를 갱신한후 데이터베이스 특정 레코드에 위치를 해야 합니다. Requery를 하게 되면 보통 맨처음 레코드로 이동하게됩니다. 이렇게 되면 현래 레코드 데이터를 그대로 화면에 보여주게 해야 합니다. 이런 루틴으로 OnAdddata함수를 수정한 것이 다음과 같습니다.

void CMExOdbcView::OnAdddata()

{

// TODO: Add your command handler code here

m_pSet->MoveLast();//마지막 레코드로 이동한후

m_pSet->AddNew();//빈 레코드를 하나 만든다

UpdateData(TRUE);//자원에 있는 데이터를 m_pSet으로 전송

m_pSet->Update();//빈레코드에 데이터를 넣는다.

m_pSet->Requery();//데이터베이스를 갱신한다.

m_pSet->MoveFirst();//처음으로 이동한다.

UpdateData(FALSE);//m_pSet에 있는 데이터를 자원으로 전송

}

CRecordset의 MoveLast함수는 마지막 레코드로 이동하는 함수입니다. 이렇게 마지막으로 이동한후 AddNew()함수를 이용하여 빈레코드를 만듭니다. 이렇게 빈레코드를 만들면 m_pSet의 m_name,m_tel등의 맴버들은 모두 아무데이터도 들어있지 않습니다. 이멤버들에 현재 화면에서 입력한 데이터를 넣어야 합니다. 이럴때 사용하는 게 UpdateData(TRUE)입니다. 이렇게 하면 m_pSet의 m_name,m_tel,m_address,m_bigo 등에 입력한 데이터가 저장되게 됩니다. m_pSet의 맴버 변수의 데이터를 DB에 저장하고자 할때 사용하는 함수가 Update입니다. Update를 하면 m_name등에 저장된 데이터가 바로 데이터베이스로 전송되는 것입니다. RecordSet이 snapshot 형태이므로 데이터베이스를 갱신해야 설정된 데이터가 바로 파일에 기록됩니다. 이렇게 갱신할때 사용하는 함수가 Requery()입니다.

Requery함수에 의해서 데이터베이스가 갱신되고 MoveFirst로 이동하게 되면 m_pSet의 맴버 변수들에는 첫번째 데이터가 저장될것입니다. 이 데이터를 화면에 출력하기위해서 UpdateData(FALSE);함수를 호출한것입니다.

이와같이 하고 컴파일한후 새로운 데이터를 입력하고 메뉴에서 □입력□항목을 클릭하여 보세요. 그림 3-1-16는 입력루틴을 설정한 MExOdbc의 출력결과입니다.

(그림 3-1-16)MExOdbc에 입력루틴 추가한 모습

<9>데이터 수정

수정하기는 입력하기보다 매우 간단합니다. 메뉴판에 입력하기와 비슷하게 수정하기항목을 IDD_MODEDATA라는 아이디로 만든다음 이항목을 선택하면 실행하는 함수를 만들고 이함수를 다음과 같이 수정합니다.

void CMExOdbcView::OnModedata()

{

// TODO: Add your command handler code here

m_pSet->Edit();

UpdateData(TRUE);

m_pSet->Update();

m_pSet->Requery();

m_pSet->MoveFirst();

UpdateData(FALSE);

}

수정을 하고자 할경우에는 CRecordSet의 Edit()함수를 호출하면 됩니다. 이함수를 호출하면 현재 레코드를 수정가능한 상태로 만듭니다. 이렇게 한후 화면에서 입력한 내용을 받고 Update 함수를 이용하여 현재 레코드에 업데이트합니다. 이렇게 한후 Requery를 이용하여 데이터베이스를 갱신하고 MoveFirst를 이용하여 데이터베이스 첫번째 레코드로 간다음 첫번째 레코드의 데이터를 화면에 보여주는 것입니다.

컴파일하고 실행한후 현재 입력된 데이터를 수정해 보세요. 수정되는 것을 보실수 있을것입니다.

<10>데이터 검색

데이터 검색방법은 CRecordset의 m_strFilter를 이용합니다. 예를 들어 name가 □이상엽□ 인데이터를 찾아라라고 할경우 m_strFilter에 다음과 같이 설정한후 Requery함수를 호출합니다.

m_pSet->m_strFilter="name= '이상엽' ";

m_pSet->Requery();

m_strFilter에 설정하는 내용은 SQL문입니다. SQL문이란 데이터베이스를 컨트롤하는 데이터베이스 언어라고 생각하시면 됩니다. SQL문에 대해서는 본책에서 언급하지 않습니다. 데이터베이스에 관련된 책자에는 SQL문에 대해서 자세히 나와있을것입니다. 실제로 ODBC는 SQL문을 드라이버에 전송하고 이 SQL문에 의해서 구동하는것입니다. 지금까지 사용한 MoveFirst나 MoveLast등 모두 내부적으로 보면 SQL문을 호출하고 있습니다. CDatabase를 직접 핸들링하여 데이터베이스를 구동한다고 할경우에는 CDatabase의 맴버함수인 ExcuteSql이라는 함수에 SQL문을 넣어서 수행하면 데이터베이스를 직접 핸들링할수 있습니다.

이제 MExOdbc에 검색항목을 설정하겠습니다. 메뉴에 □검색□ 이라는 항목을 설정하고 ID를 IDD_SEEKNAME로 설정합니다. 이렇게 설정하고 본 메뉴항목을 선택하였을때 실행하는 함수를 CMExOdbcView에 만듭니다.

다음은 CMExOdbcView에 설정한 함수입니다.

void CMExOdbcView::OnSeekname()

{

// TODO: Add your command handler code here

UpdateData(TRUE);

m_pSet->m_strFilter.Format("name= '%s' ",m_pSet->m_name);

m_pSet->Requery();

UpdateData(FALSE);

}

먼저 화면에 입력한 데이터를 받아두고 m_strFilter에 현재 이름란에 입력한 데이터를 받아서 검색하게 합니다. 예를 들어 이름란에 □홍길동□ 이라고 입력하였다면 m_strFilter에는 다음과 같은 내용이 설정될것입니다.

"name= '홍길동' □

이와 같이한후 Requery함수를 호출하면 □홍길동□ 이란 이름이 있는 레코드를 모두 로드합니다.

여기에서 주의할점은 □홍길동□이라는 이름이 있는 레코드만 유효화고 나머지 레코드는 무시한다는 것입니다. □홍길동□ 이라는 레코드가 단순하게 한개일경우에는 현재 설정된 레코드는 단 한개입니다. 프로그램을 컴파일 하고 실행시켜서 □홍길동□이라는 레코드를 설정하고 찾아보시면 □홍길동□은 찾게 될것입니다. 이렇게 찾게된후 도구바에서 레코드 이동 도구바를 클릭하면 이동이 되지 않습니다. 왜냐하면 현재 로드한 레코드는 오로지 □홍길동□ 이라는 이름이 있는 레코드이기 때문입니다.

이부분에 대해서 웹서버에 질문이 많이 들어옵니다. □레코드를 찾으면 레코드는 찾는데 다른데이터가 사라집니다!□ 라는 질문입니다.

m_strFilter에 검색 SQL 문을 사용하게 되면 검색된 데이터만 유효하게 로드되기 때문에 전체 데이터를 로드하는 것이 아닙니다. 비로 데이터베이스 □홍길동,이상엽,이상돌,만득이□ 라는 데이터가 있다 하더라도 □홍길동□ 에 해당하는 데이터는 1개이기 때문에 한개만 로드하는 것입니다.

검색이 되었다면 검색을 해제 하여야 합니다. 검색을 해제 하는것은 매우 간단합니다. m_strFilter에 ""값을 입력한후 Requery함수를 실행시키면 됩니다.

메뉴에 □검색해제□ 라는 항목을 두고 이항목의 ID를 IDD_FREESEEK라고 설정한후 이항목을 선택하였을때의 함수를 CMExOdbcView에 설정합니다. 이렇게 만든 함수는 다음과 같습니다.

void CMExOdbcView::OnFreeseek()

{

m_pSet->m_strFilter="";

m_pSet->Requery();

UpdateData(FALSE);

}

OnFreeseek()함수는 먼저 m_strFilter에 ""값을 설정하고 Requery함수를 실행 한것입니다. 이렇게 검색해제를 하면 모든 데이터가 다 로드됩니다.

위와 같이 한후 컴파일하고 실행하면 그림3-1-17과 같은 화면이 나타납니다. 직접 지금까지의 내용을 프로그래밍하시기 바랍니다.

<11>레코드 삭제

레코드 삭제는 단한개의 함수로 이루어집니다. CRecordset의 Delete라는 함수를 호출하면 됩니다. 메뉴항목에 □삭제□라는 항목을 만들고 ID를 IDD_DELDATA 로 설정한다음 이항목을 선택하엿을때의 함수를 CMExOdbcView에 만듭니다.

이함수는 다음과 같습니다.

(그림 3-1-17) MExOdbc실행결과

void CMExOdbcView::OnDeldata()

{

// TODO: Add your command handler code here

m_pSet->Delete();

m_pSet->Requery();

UpdateData(FALSE);

}

"삭제□ 항목을 클릭하면 m_pSet의 Delete함수가 호출되고 현재 데이터가 삭제됩니다. 그리고 난후에 Requery함수를 이용하여 데이터베이스를 갱신한다음 현재 선택된 데이터를 화면에 출력하기 위해서 UpdateData함수를 호출한것입니다.

OnDeldata함수를 만들고 난후에 컴파일하고 실행을 해보시기 바랍니다.

<12>레코드 이동

지금까지 데이터를 수정,검색,삭제,입력 등의 중요 4가지 항목을 설명하였습니다. 이제 남은 내용이 레코드이동입니다. 레코드이동에서 처음으로 이동하는 함수 MoveFirst와 마지막으로 이동하는 함수 MoveLast는 이미 설명하였습니다.

현재 레코드에서 다음 레코드로 이동하는 함수와 현재 레코드에서 바로 이전으로 이동하는 함수는 다음과 같습니다.

m_pSet->MoveNext(); //다음으로 이동

m_pSet->MovePrev(); //이전으로 이동

MExOdbc에서는 도구바의 레코드 이동 버튼을 이용하여 이동하기 때문에 위의 두함수를 사용하지 않았습니다. 그러나 실제로 이함수들을 기억해야할때가 있을겁니다. 도구바에서 버튼을 클릭하면 제일먼저 실행되는 함수가 OnMove함수입니다. 이함수는 CRecordView에 내장되어 있는 함수입니다. 이함수의 인자로 nIDMoveCommand 가 들어오는데 처음 버튼을 클릭하면 ID_RECORD_FIRST가 마지막 버튼을 클릭하면 ID_RECORD_LAST가 다음 버튼을 클릭하면 ID_RECORD_NEXT가 이전 버튼을 클릭하면 ID_RECORD_PREV가 전해집니다. 이렇게 전해진 값에 의해서 MoveFirst,MoveLast,MoveNext,MovePrev가 실행되는 것입니다.

클래스 위저드를 실행시키면 CMExOdbcView에서 OnMove 가 보일것입니다.

버튼의 동작에 따라 실행을 바꾸고자 한다면 이함수를 만들고 수정을 하면 될것입니다.

<13>CRecordView와 도큐먼트와의 관계

지금까지 제작한 프로그램은 모두 CRecordView를 상속받은 CMExOdbcView에서만 하였습니다. 원래규칙대로라면 데이터베이스자료는 CDocument 를 상속받은 CMExOdbcDoc에 설정되어 있어야 할것인데 왜 View에 설정되어 있을까 하는 의문을 갖지는 않으셨는지요? 그런 의문을 가졌다면 여기에서 해답이 나옵니다. 실제 데이터베이스 레코드를 핸들링하는 CMExOdbcSet은 도큐먼트에 설정되어 있습니다. CMExOdbcDoc헤더에 보시면 다음과 같은 변수가 있을것입니다.

CMExOdbcSet m_mExOdbcSet;

그렇다면 CMExOdbcView에서 사용하였던 m_pSet은 무엇일까요? 이것은 View의

OnInitialUpdate() 함수를 보시면 답이 나옵니다.

void CMExOdbcView::OnInitialUpdate()

{

m_pSet = &GetDocument()->m_mExOdbcSet;

CRecordView::OnInitialUpdate();

}

View에 있는 m_pSet은 도큐먼트에 있는 m_mExOdbcSet의 포인터일뿐입니다.

즉 실제 레코드컨트롤 클래스는 도큐먼트에 있다는 의미입니다.

CRecordView에서 m_pSet과 대화상자 폼과 연결기키기위해서 포인터만 받은것입니다.

지금까지 MExOdbc를 제작하는 전반적인 방법에 대해서 설명하였습니다. 프로그램의 소스 부분은 생략합니다. 제공되는 CD를 참조하시기 바랍니다.

2.데이터베이스와 레코드자원과의 분리

전항목에서는 ODBC를 이용할때 CRecordSet의 맴버들을 DDX_FieldText 를 이용하여 바로 연결하였습니다. 본장에서는 CRecordSet과 바로 연결하지 않고 대화상자 폼에 있는 데이터를 View의 맴버에서 받은다음 데이터베이스에 저장할필요가 있을경우 이때 데이터베이스에 저장하는 예제를 작성해보겠습니다. 본항목에서 작성하는 예제는 Exodb2입니다.

<1>Exodb2 프로그램

Exodb2프로그램의 전체구조는 그림 3-1-18과 같습니다.

(그림 파일)

(그림 3-1-18)Exodb2의 전체 구조

그림 3-1-18의   ⓐ항목이 Exodb2의 실행모습니다.  Exodb2에는 여러기능들이 들어가 있습니다. 그림 3-1-18의  ①항목처럼 4개의 필드를 이용하여 검색을 할수 있도록 설정이 되어있습니다. ①의 4개의 버튼중에서 검색하고자 하는 버튼을 클릭하게 되면 입력 대화상자가 출력되고 검색하고자 하는 명을 입력하도록 합니다. 이때 검색하고자 하는 명을 쓰면 레코드에서 해당 검색명과 일치하는 레코드들만 현재 데이터베이스의 레코드로 설정합니다. 에를 들어 □홍길동,강길동,홍길동,남길동□ 이렇게 4개의 레코드가 있을경우 □홍길동□이란 이름을 가진 데이터를 찾고자 하여 □이름으로 검색□ 이라는 버튼을 클릭하여 □홍길동□ 을 입력하게 되면 현제 레코드는 □홍길동,홍길동□ 두개라는 의미입니다. 이상태에서 이동버튼을 이용하여 다음 이전으로 이동을 하여도 이데이터 두개만 출력이 됩니다. 이상태에서 다시 원래대로 복귀하고자 한다면 그림 3-1-18의 ⑦ 항목을 클릭하면 됩니다. 이렇게 되면 다시 모든 레코드들을 데이터로 설정합니다.②항목 리스트 보기를 클릭하게 되면 그림 3-1-18의 ⓑ항목과 같은 대화상자가 출력되고 리스트중 해당 항목을 클릭하게 되면 ⓐ화면에서 해당 레코드로 설정이 됩니다. ③항목은 현재 레코드에서 데이터를 수정하였을때 활성화 됩니다. 이렇게 활성화 되었을때 수정 버튼을 클릭하게 되면 해당 내용이 수정됩니다. ④항목은 현재 레코드를 삭제하는 기능이며 ⑤항목은 새로운 레코드를 하나 만드는 항목입니다. 이렇게 레코드를 만든후에 데이터를 입력하고 ③항목을 클릭하게 되면 새로운 데이터가 설정됩니다. 그림3-1-18과 같은 예제를 만들고자 할때는 CRecordSet의 맴버와 자원과를 분리하는 것이 좋습니다.

이렇게 하면 입력되는 데이터가 데이터베이스와 바로 연결되지 않고 중간에 한단계가 있기 때문에 데이터보안에 좋은 잇점이 있습니다. 그림 3-1-19은 그림 3-1-18의 ⓐ항목에 설정되어 있는 각 필드와 m_pSet의 맴버와 연결형태를 도시한 내용입니다.

(그림 파일)

(그림 3-1-19)자원의 각필드와 m_pSet과 연결형태

그림 3-1-19과 같이  m_pSet과 대화상자의 자원사이에 ⓑ항목과 같이 View의 맴버들이 중간에 위치합니다. 이렇게 된후에 ⓑ항목의 데이터를   ⓒ항목으로 전송하고자 할경우에는 View의 UpdateDb함수를 이용하고( ①항목참조) ⓒ항목에서 ⓑ항목으로 전송할경우에는 UpdateReource함수를 (②항목 참조) 이용합니다. 이렇게 하면 자원의 에디터 박스의 내용을 UpdateData(FALSE)나 UpdateData(TRUE)를 사용하였을때 바로 데이터베이스로 가지않고 ⓑ항목으로 넘어가기 때문에 □아차 이게 아닌가부다□ 라고 느꼈을때에도 큰문제가 없게 되는 것입니다. 그림 3-1-19의 ①항목 함수는 다음과 같습니다.

//레코드에 있는 데이타를 리소스로 옮긴다.

void CExOdb2View::UpdateResource()

{

char temp[80];

sprintf(temp,"%ld",m_pSet->m_ID);

m_pNumber=temp;

m_pName=m_pSet->m_name;

m_pHTel=m_pSet->m_tel;

m_pHPostNumber=m_pSet->m_postnumber;

m_pHAddress=m_pSet->m_address;

m_pOffice=m_pSet->m_office;

m_pOTel=m_pSet->m_officetel;

m_pOPostNumber=m_pSet->m_officepostnumber;

m_pOAddress=m_pSet->m_officeaddress;

m_pBeepNum=m_pSet->m_beep;

m_pHandNum=m_pSet->m_handpone;

m_pDescription=m_pSet->m_description;

UpdateData(FALSE);

}

다음은 그림 3-1-19의 ②항목 함수의 내용입니다.

//리소스에 있는 데이터를 모드 m_pSet멤버로

//등록한다.

void CExOdb2View::UpdateDb()

{

//리소스에 있는데이터를 View멤버로 옴긴다.

UpdateData(TRUE);

//리소스 데이터 -> m_pSet멤버로

m_pSet->m_name=m_pName;

m_pSet->m_tel=m_pHTel;

m_pSet->m_postnumber=m_pHPostNumber;

m_pSet->m_address=m_pHAddress;

m_pSet->m_office=m_pOffice;

m_pSet->m_officetel=m_pOTel;

m_pSet->m_officepostnumber=m_pOPostNumber;

m_pSet->m_officeaddress=m_pOAddress;

m_pSet->m_beep=m_pBeepNum;

m_pSet->m_handpone=m_pHandNum;

m_pSet->m_description=m_pDescription;

}

Exodb2프로그램는 프로그램이 실행되면 데이터베이스 레코드의 제일 처음으로 이동하고 첫번재 레코드의 내용을 그림 3-1-19의 ⓐ항목으로 전송합니다. FormView에서는 (CRecordView 는 CFormView를 상속받았음을 생각하세요) OnInitDialog함수 대신에 OnInitialUpdate 함수를 사용합니다. 이함수는 다음과 같습니다.

//초기에 업데이트 함수

void CExOdb2View::OnInitialUpdate()

{

m_pSet = &GetDocument()->m_exOdb2Set;

//레코드를 리소스에 업데이트한다.

UpdateResource();

CRecordView::OnInitialUpdate();

//레코드를 화일 처음으로 이동

OnMove(ID_RECORD_FIRST);

GetDlgItem(IDC_MODE)->EnableWindow(FALSE);

}

이와 같이 하면 데이터베이스의 필드와 현재 프로그램과 연결되는 기본적인 형태가 설정된것입니다.

<2>각버튼에 해당하는 함수 만들기

이제 각버튼을 클릭했을때 해당하는 함수를 만들어 보겠습니다. 먼저 그림 3-1-18의 ①항목의 버튼을 클릭했을때의 내용입니다.

//이름으로 검색

void CExOdb2View::OnSeekname()

{

char temp[80];

CSeekWord *pDlg = new CSeekWord;

//검색어 찾는 대화상자 가동

if(pDlg->DoModal()==IDOK)

{

//SQL 문을 실행

sprintf(temp,"name = '%s'",pDlg->m_strWord.operator const char*());

m_pSet->m_strFilter = temp;

m_pSet->Requery();

UpdateResource();

}

delete pDlg;

}

//집전화번호로

void CExOdb2View::OnSeekhtel()

{

char temp[80];

CSeekWord *pDlg = new CSeekWord;

if(pDlg->DoModal()==IDOK)

{

sprintf(temp,"tel = '%s'",pDlg->m_strWord.operator const char*());

m_pSet->m_strFilter = temp;

m_pSet->Requery();

UpdateResource();

}

delete pDlg;

}

//직장명으로

void CExOdb2View::OnSeekoffice()

{

char temp[80];

CSeekWord *pDlg = new CSeekWord;

if(pDlg->DoModal()==IDOK)

{

sprintf(temp,"office = '%s'",pDlg->m_strWord.operator const char*());

m_pSet->m_strFilter = temp;

m_pSet->Requery();

UpdateResource();

}

delete pDlg;

}

//직장 전화번호로

void CExOdb2View::OnSeekotel()

{

char temp[80];

CSeekWord *pDlg = new CSeekWord;

if(pDlg->DoModal()==IDOK)

{

sprintf(temp,"officetel = '%s'",pDlg->m_strWord.operator const char*());

m_pSet->m_strFilter = temp;

m_pSet->Requery();

UpdateResource();

}

delete pDlg;

}

4개의 항목 모두 CSeekWord 라는 대화상자를 출력하고 대화상자의 에디터 박스에서 입력받은 데이터를 이용하여 SQL문을 작성하고 이 SQL문을 m_pSet의 m_strFilter의 명으로 설정한다음 Requery를 수행하여 SQL문을 실행시킵니다. 이렇게 되면 검색하고자 하는 레코드만 데이터베이스에서 찾게 됩니다.

이렇게 검색을 한후에 현재 설정된 데이터의 전체 리스트를 보고자 할때는 그림 3-1-18의  ②항목을 클릭합니다. 이때 수행되는 내용은 다음과 같습니다.

//리스트 보기

void CExOdb2View::OnList()

{

CDataList *pDlg = new CDataList;

pDlg->DoModal();

delete pDlg;

UpdateResource();

}

리스트 보기를 선택하면 CDataList대화상자가 출력되는 것이 전부입니다. 이대화상자는 데이터베이스에 설정되어 있는 전체 레코드를얻는 대화상자입니다. 이대화상자에서 전체 레코드를 어는 OnInitDialog함수는 다음과 같습니다.

BOOL CDataList::OnInitDialog()

{

CDialog::OnInitDialog();

char temp[200];

//CExOdb2View를 pView로 설정한다.

CExOdb2View* pView =

( CExOdb2View* )((CMainFrame*)

(AfxGetApp()->m_pMainWnd))->GetActiveView();

//레코드를 처음으로 이동하고

pView->m_pSet->MoveFirst();

for(;;)

{

//이름 회사 전화번호 회사전화번호를 넣은 temp를 만든다음

sprintf(temp,"%s %s %s %s",

pView->m_pSet->m_name,pView->m_pSet->m_office,

pView->m_pSet->m_tel,pView->m_pSet->m_officetel);

//리스트에 넣는다.

m_listBox.AddString(temp);

//다음 레코드로 이동

pView->m_pSet->MoveNext();

//만일 다음 레코드가 없으면 탈출

if(pView->m_pSet->IsEOF())

break;

}

//다시 처음으로 이동

pView->m_pSet->MoveFirst();

m_listBox.SetCurSel(0);

return TRUE;  // return TRUE unless you set the focus to a control

// EXCEPTION: OCX Property Pages should return FALSE

}

위와 같이 하면 그림 3-1-18의   ⓑ항목같이 대화상자가 출력되고 현재의 레코드들이 모두 출력됩니다. 이상태에서 특정 항목을 선택하고 OK버튼을 클릭했을때 해당 레코드로 이동합니다. 이렇게 이동되는 OnOk함수는 다음과 같습니다.

//현서 특정 레코드를 선택하고 Ok버튼을 클릭하면

void CDataList::OnOK()

{

int num;

//선택한 커서를 받고

num=m_listBox.GetCurSel();

CExOdb2View* pView =

( CExOdb2View*)((CMainFrame*)

(AfxGetApp()->m_pMainWnd))->GetActiveView();

//커서와 같은 레코드가 될때까지 MoveNext함수 실행

for(int i=0;i<num;i++)

{

pView->m_pSet->MoveNext();

if(pView->m_pSet->IsEOF())

break;

}

CDialog::OnOK();

}

위의 예는 현재 선택된 데이터의 위치를 얻고 MoveNext함수를 이용하여 다음으로 계속 이동한것입니다. 이방법외에 m_pSet의 함수에서 SetBookMark와 GetBookMark를 이용하셔도 됩니다.

전체 리스트를 클릭하면 그림 3-1-18의 ①항목을 이용하여 검색한 데이터를 해제하고 현재 설정되어 있는 모든 레코드들을 로드합니다. 이내용은 다음과 같습니다.

//전체 레코드로 가는 함수

void CExOdb2View::OnAllseek()

{

m_pSet->m_strFilter = "";

m_pSet->Requery();

UpdateResource();

}

전체 레코드로 가는 방법은 매우 간단합니다. m_pSet의 맴버인 m_strFilter에 ""값을 설정하고 Requery함수를 실행한후에 UpdateReosource를 이용하여 현재 m_pSet의 맴버에 설정되어 있는 대화상자에 전송하면 됩니다. m_strFilter에 아무값도 설정하지 않는다는 의미는 모든레코드를 전부로드하고 옵션을 설정하지 않는다는 의미가 됩니다.

그림 3-1-18의 ③항목은 레코드의 데이터가 수정이 되었을때 활성화 됩니다. 그림 3-1-18 의 모든 에디터박스에서 EN_CHANGE 메시지가 발생되면 이것은 레코드가 수정되었다는 의미입니다. 이때 ③항목을 활성화 시키면 됩니다. 다음은  이름을 입력하는 에디터박스에서 EN_CHANGE메시지가 발생되었을때의 함수입니다.

void CExOdb2View::OnChangeName()

{

if(!m_bMode)

{

GetDlgItem(IDC_MODE)->EnableWindow(TRUE);

m_bMode=TRUE;

}

}

이름에 데이터를 입력하였을때 이때 현재 수정된 적이 없으면 즉 m_bMode가 FASLE이면 IDC_MODE(수정/입력완료 버튼) 를 활성화 시키고 m_bMode를 TRUE로 설정한것입니다. 그림 3-1-18의 모든 에디터박스를 위의 OnChangeName과 같이 수정해야 합니다.

위와 같이 하면 수정/입력 완료 버튼이 활성화 되고 이버튼을 클릭하면 현재 대화상자에 입력된 내용을 데이터베이스에 저장해야 합니다. 이내용은 다음과 같습니다.

//수정입력 완료시

void CExOdb2View::OnMode()

{

//수정 가능하게 한다.

m_pSet->Edit();

//리소스->레코드로

UpdateDb();

//업데이트 한다.

m_pSet->Update();

//수정모드 비활성화

GetDlgItem(IDC_MODE)->EnableWindow(FALSE);

m_bMode=FALSE;

}

데이터베이스를 수정이 가능한 상태로 만들기 위해서 CRecordSet의 맴버함수이 Edit를 호출한후에 UpdateDb함수를 이용하여 대화상자 폼에 설정된 데이터를 레코드롤 전송합니다. CRecordSet의 Update함수는 CRecordSet의 레코드 멤버에 있는 데이터를 파일에 저장하는 기능을 합니다. 이렇게 하면 데이터가 그대로 파일에 저장되게 됩니다. 이후에 다시 □수정/입력 완료□ 버튼을 비활성화 하고 m_bMode를 FALSE로 설정합니다. 이렇게 하면 □수정/입력 완료□ 기능에 대한 내용 수행이 끝납니다.

삭제 버튼을 클릭하게 되면 현재 설정된 데이터를 삭제 합니다. 이내용은 다음과 같습니다.

//삭제

void CExOdb2View::OnDel()

{

m_pSet->Delete();

m_pSet->MoveNext();

if(m_pSet->IsEOF())

m_pSet->MoveLast();

if(m_pSet->IsBOF())

m_pSet->SetFieldNull(NULL);

UpdateResource();

}

Delete함수를 이용하여 현재 데이터를 삭제하고 다음 레코드로 이동합니다. 만일 다음레코드가 없으면 즉 IsEOF함수가 1를 리턴하게 되면 MoveLast함수가 가동되고 이렇게 되면 맨 마지막 필드로 위치합니다. 마지막 필드에 있는데 이때상태가 데이터의 맨처음이 되면 결국 데이터가 하나도 없다는 의미가 됩니다. 이럴경우에는 SetFildNull함수를 실행합니다. SetFiledNull에 인자를 NULL 로 설정하는 것은 현재 데이터베이스에 레코드가 하나도 없다는 의미입니다. 이렇게 한후에 m_pSet에 설정되어 있는 레코드 맴버를 대화상자에 전송합니다.

□추가□ 버튼을 클릭하면 새로운 데이터를 추가하는 것입니다. 이때의 내용은 다음과 같습니다.

//새로운 레코드 설정시

void CExOdb2View::OnAdd()

{

//새로운 레코들 만들고

m_pSet->AddNew();

//자원에서 m_pSet으로 이동

UpdateDb();

//데이터 입력

m_pSet->Update();

//정보 갱신

m_pSet->Requery();

//m_pSet에서 다시 자원으로 이동

UpdateResource();

}

OnAdd함수는 새로운 레코드를 하나 만든다음 현재 자원에 입력된 데이터를 그대로 데이터에 설정합니다. 즉 에디터박스에 데이터를 입력하고 추가 버튼을 클릭하면 현재 데이터는 새로운 레코드로 추가되는 것입니다.

□SQL 실행□ 버튼을 클릭하면 대화상자에 입력된 내용을 m_strFilter에 설정하고 Requery함수를 이용하여 SQL문을 실행시키면 됩니다. 이내용은 다음과 같습니다.

void CExOdb2View::OnSqlrun()

{

char temp[80];

CSeekWord *pDlg = new CSeekWord;

if(pDlg->DoModal()==IDOK)

{

sprintf(temp,"%s",pDlg->m_strWord.operator const char*());

m_pSet->m_strFilter = temp;

m_pSet->Requery();

UpdateResource();

}

delete pDlg;

}

<3>이동

이동은 MExOdbc보다 다소 복잡합니다. 리소스와 레코드간에 분리가 되어 있기 때문에 이동 동작을 모두 써 주어야 합니다. ExOdb2프로그램은 데이터 입력란에 데이터를 수정하였을 경우 수정/입력 완료 버튼이 활성화 됩니다. 만일 이동을 한다면 현재 입력한 것을 무시하겠다는 것이니까 이것을 다시 비활성화 되도록 해야 합니다. 다음으로 이동 선택에 따라 모든 것을 직접 기입해야 합니다. 그이유는 데이터베이스를 구동하는 m_pSet과 화면에 입력한 데이터를 저장하는 변수가 분리 되어 있기 때문입니다.

//레코드가 이동시 함수

BOOL CODBC2View::OnMove(UINT nIDMoveCommand)

{

//현재 수정이 되었다가 이동을 하면

//무시한다.

if(m_bMode)

{

m_bMode=FALSE;

GetDlgItem(IDC_MODE)->EnableWindow(TRUE);

}

//이동 모드에 따라

switch(nIDMoveCommand)

{

//헤더로 이동시

case ID_RECORD_FIRST:

if(m_pSet->IsBOF())

break;

//처음 으로 이동

m_pSet->MoveFirst();

break;

//마지막으로 이동시

case ID_RECORD_LAST:

if(m_pSet->IsEOF())

break;

//마지막으로 이동

m_pSet->MoveLast();

break;

case ID_RECORD_NEXT:

//다음으로 이동시

m_pSet->MoveNext();

break;

case ID_RECORD_PREV:

//이전으로 이동시

m_pSet->MovePrev();

break;

}

UpdateResource();

return TRUE;

}

<4>프로그램 소스

Exodb2에서 View클래스 소스는 전체적으로 보는것이 좋을것 같습니다. 다음은 Exodb2의 View클래스의 소스입니다.

(프로그램 소스)

// ExOdb2Vw.h : interface of the CExOdb2View class

//

/////////////////////////////////////////////////////////////////////////////

class CExOdb2Set;

class CExOdb2View : public CRecordView

{

protected: // create from serialization only

CExOdb2View();

DECLARE_DYNCREATE(CExOdb2View)

public:

//{{AFX_DATA(CExOdb2View)

enum{ IDD = IDD_EXODB2_FORM };

CExOdb2Set* m_pSet;

CString m_pNumber;

CString m_pName;

CString m_pHTel;

CString m_pHPostNumber;

CString m_pHAddress;

CString m_pOffice;

CString m_pOTel;

CString m_pOPostNumber;

CString m_pOAddress;

CString m_pBeepNum;

CString m_pHandNum;

CString m_pDescription;

BOOL m_bMode;

//}}AFX_DATA

// Attributes

public:

CExOdb2Doc* GetDocument();

void UpdateResource();

void UpdateDb();

// Operations

public:

// Overrides

// ClassWizard generated virtual function overrides

//{{AFX_VIRTUAL(CExOdb2View)

public:

virtual CRecordset* OnGetRecordset();

virtual BOOL PreCreateWindow(CREATESTRUCT& cs);

virtual BOOL OnMove(UINT nIDMoveCommand);

protected:

virtual void DoDataExchange(CDataExchange* pDX);    // DDX/DDV support

virtual void OnInitialUpdate(); // called first time after construct

virtual BOOL OnPreparePrinting(CPrintInfo* pInfo);

virtual void OnBeginPrinting(CDC* pDC, CPrintInfo* pInfo);

virtual void OnEndPrinting(CDC* pDC, CPrintInfo* pInfo);

virtual void OnPrint(CDC* pDC, CPrintInfo* pInfo);

//}}AFX_VIRTUAL

// Implementation

public:

virtual ~CExOdb2View();

#ifdef _DEBUG

virtual void AssertValid() const;

virtual void Dump(CDumpContext& dc) const;

#endif

protected:

// Generated message map functions

protected:

//{{AFX_MSG(CExOdb2View)

afx_msg void OnAdd();

afx_msg void OnMode();

afx_msg void OnRecordFirst();

afx_msg void OnRecordLast();

afx_msg void OnRecordNext();

afx_msg void OnRecordPrev();

afx_msg void OnUpdateRecordFirst(CCmdUI* pCmdUI);

afx_msg void OnUpdateRecordLast(CCmdUI* pCmdUI);

afx_msg void OnSeekname();

afx_msg void OnAllseek();

afx_msg void OnDel();

afx_msg void OnSeekhtel();

afx_msg void OnSeekoffice();

afx_msg void OnSeekotel();

afx_msg void OnSqlrun();

afx_msg void OnChangeBeepnum();

afx_msg void OnChangeDescription();

afx_msg void OnChangeHaddress();

afx_msg void OnChangeHandnum();

afx_msg void OnChangeHpostnumber();

afx_msg void OnChangeHtel();

afx_msg void OnChangeName();

afx_msg void OnChangeOaddress();

afx_msg void OnChangeOffice();

afx_msg void OnChangeOpostnumber();

afx_msg void OnChangeOtel();

afx_msg void OnList();

//}}AFX_MSG

DECLARE_MESSAGE_MAP()

};

#ifndef _DEBUG  // debug version in ExOdb2Vw.cpp

inline CExOdb2Doc* CExOdb2View::GetDocument()

{ return (CExOdb2Doc*)m_pDocument; }

#endif

/////////////////////////////////////////////////////////////////////////////

// ExOdb2Vw.cpp : implementation of the CExOdb2View class

//

#include "stdafx.h"

#include "ExOdb2.h"

#include "ExOdb2St.h"

#include "ExOdb2Dc.h"

#include "ExOdb2Vw.h"

#include "seekword.h"

#ifdef _DEBUG

#define new DEBUG_NEW

#undef THIS_FILE

static char THIS_FILE[] = __FILE__;

#endif

/////////////////////////////////////////////////////////////////////////////

// CExOdb2View

IMPLEMENT_DYNCREATE(CExOdb2View, CRecordView)

BEGIN_MESSAGE_MAP(CExOdb2View, CRecordView)

//{{AFX_MSG_MAP(CExOdb2View)

ON_BN_CLICKED(IDC_ADD, OnAdd)

ON_BN_CLICKED(IDC_MODE, OnMode)

ON_COMMAND(ID_RECORD_FIRST, OnRecordFirst)

ON_COMMAND(ID_RECORD_LAST, OnRecordLast)

ON_COMMAND(ID_RECORD_NEXT, OnRecordNext)

ON_COMMAND(ID_RECORD_PREV, OnRecordPrev)

ON_UPDATE_COMMAND_UI(ID_RECORD_FIRST, OnUpdateRecordFirst)

ON_UPDATE_COMMAND_UI(ID_RECORD_LAST, OnUpdateRecordLast)

ON_BN_CLICKED(IDC_SEEKNAME, OnSeekname)

ON_BN_CLICKED(IDC_ALLSEEK, OnAllseek)

ON_BN_CLICKED(IDC_DEL, OnDel)

ON_BN_CLICKED(IDC_SEEKHTEL, OnSeekhtel)

ON_BN_CLICKED(IDC_SEEKOFFICE, OnSeekoffice)

ON_BN_CLICKED(IDC_SEEKOTEL, OnSeekotel)

ON_BN_CLICKED(IDC_SQLRUN, OnSqlrun)

ON_EN_CHANGE(IDC_BEEPNUM, OnChangeBeepnum)

ON_EN_CHANGE(IDC_DESCRIPTION, OnChangeDescription)

ON_EN_CHANGE(IDC_HADDRESS, OnChangeHaddress)

ON_EN_CHANGE(IDC_HANDNUM, OnChangeHandnum)

ON_EN_CHANGE(IDC_HPOSTNUMBER, OnChangeHpostnumber)

ON_EN_CHANGE(IDC_HTEL, OnChangeHtel)

ON_EN_CHANGE(IDC_NAME, OnChangeName)

ON_EN_CHANGE(IDC_OADDRESS, OnChangeOaddress)

ON_EN_CHANGE(IDC_OFFICE, OnChangeOffice)

ON_EN_CHANGE(IDC_OPOSTNUMBER, OnChangeOpostnumber)

ON_EN_CHANGE(IDC_OTEL, OnChangeOtel)

ON_BN_CLICKED(IDC_LIST, OnList)

//}}AFX_MSG_MAP

// Standard printing commands

ON_COMMAND(ID_FILE_PRINT, CRecordView::OnFilePrint)

ON_COMMAND(ID_FILE_PRINT_DIRECT, CRecordView::OnFilePrint)

ON_COMMAND(ID_FILE_PRINT_PREVIEW, CRecordView::OnFilePrintPreview)

END_MESSAGE_MAP()

/////////////////////////////////////////////////////////////////////////////

// CExOdb2View construction/destruction

CExOdb2View::CExOdb2View()

: CRecordView(CExOdb2View::IDD)

{

//{{AFX_DATA_INIT(CExOdb2View)

// NOTE: the ClassWizard will add member initialization here

m_pSet = NULL;

m_pNumber="";

m_pName="";

m_pHTel="";

m_pHPostNumber="";

m_pHAddress="";

m_pOffice="";

m_pOTel="";

m_pOPostNumber="";

m_pOAddress="";

m_pBeepNum="";

m_pHandNum="";

m_pDescription="";

m_bMode=FALSE;

//}}AFX_DATA_INIT

// TODO: add construction code here

}

CExOdb2View::~CExOdb2View()

{

}

//에디터 박스를 View안에 있는 멤버와

//연결시킨다.

void CExOdb2View::DoDataExchange(CDataExchange* pDX)

{

CRecordView::DoDataExchange(pDX);

//{{AFX_DATA_MAP(CExOdb2View)

DDX_Text(pDX,IDC_NUMBER,m_pNumber); //고객 번호와 연결

DDX_Text(pDX,IDC_NAME,m_pName); //이름과 연결

DDX_Text(pDX,IDC_HTEL,m_pHTel); //전화번호와 연결

DDX_Text(pDX,IDC_HPOSTNUMBER,m_pHPostNumber); //우편번호와 연결

DDX_Text(pDX,IDC_HADDRESS,m_pHAddress); //주소와 연결

DDX_Text(pDX,IDC_OFFICE,m_pOffice); //회사명과 연결

DDX_Text(pDX,IDC_OTEL,m_pOTel); //회사 전화번호아 연결

DDX_Text(pDX,IDC_OPOSTNUMBER,m_pOPostNumber); //회사 우편번호와 연결

DDX_Text(pDX,IDC_OADDRESS,m_pOAddress);//회사 주소와 연결

DDX_Text(pDX,IDC_BEEPNUM,m_pBeepNum);//삐삐 번호와 연결

DDX_Text(pDX,IDC_HANDNUM,m_pHandNum);//핸드폰 번호와 연결

DDX_Text(pDX,IDC_DESCRIPTION,m_pDescription);//비고문

//}}AFX_DATA_MAP

}

BOOL CExOdb2View::PreCreateWindow(CREATESTRUCT& cs)

{

// TODO: Modify the Window class or styles here by modifying

//  the CREATESTRUCT cs

return CRecordView::PreCreateWindow(cs);

}

//레코드에 있는 데이타를 리소스로 옮긴다.

void CExOdb2View::UpdateResource()

{

char temp[80];

sprintf(temp,"%ld",m_pSet->m_ID);

m_pNumber=temp;

m_pName=m_pSet->m_name;

m_pHTel=m_pSet->m_tel;

m_pHPostNumber=m_pSet->m_postnumber;

m_pHAddress=m_pSet->m_address;

m_pOffice=m_pSet->m_office;

m_pOTel=m_pSet->m_officetel;

m_pOPostNumber=m_pSet->m_officepostnumber;

m_pOAddress=m_pSet->m_officeaddress;

m_pBeepNum=m_pSet->m_beep;

m_pHandNum=m_pSet->m_handpone;

m_pDescription=m_pSet->m_description;

UpdateData(FALSE);

}

//리소스에 있는 데이터를 모드 m_pSet멤버로

//등록한다.

void CExOdb2View::UpdateDb()

{

//리소스에 있는데이터를 View멤버로 옴긴다.

UpdateData(TRUE);

//리소스 데이터 -> m_pSet멤버로

m_pSet->m_name=m_pName;

m_pSet->m_tel=m_pHTel;

m_pSet->m_postnumber=m_pHPostNumber;

m_pSet->m_address=m_pHAddress;

m_pSet->m_office=m_pOffice;

m_pSet->m_officetel=m_pOTel;

m_pSet->m_officepostnumber=m_pOPostNumber;

m_pSet->m_officeaddress=m_pOAddress;

m_pSet->m_beep=m_pBeepNum;

m_pSet->m_handpone=m_pHandNum;

m_pSet->m_description=m_pDescription;

}

//초기에 업데이트 함수

void CExOdb2View::OnInitialUpdate()

{

m_pSet = &GetDocument()->m_exOdb2Set;

//레코드를 리소스에 업데이트한다.

UpdateResource();

CRecordView::OnInitialUpdate();

//레코드를 화일 처음으로 이동

OnMove(ID_RECORD_FIRST);

GetDlgItem(IDC_MODE)->EnableWindow(FALSE);

}

/////////////////////////////////////////////////////////////////////////////

// CExOdb2View printing

BOOL CExOdb2View::OnPreparePrinting(CPrintInfo* pInfo)

{

// default preparation

return DoPreparePrinting(pInfo);

}

void CExOdb2View::OnBeginPrinting(CDC* pDC, CPrintInfo* pInfo)

{

}

void CExOdb2View::OnEndPrinting(CDC* /*pDC*/, CPrintInfo* /*pInfo*/)

{

// TODO: add cleanup after printing

}

/////////////////////////////////////////////////////////////////////////////

// CExOdb2View diagnostics

#ifdef _DEBUG

void CExOdb2View::AssertValid() const

{

CRecordView::AssertValid();

}

void CExOdb2View::Dump(CDumpContext& dc) const

{

CRecordView::Dump(dc);

}

CExOdb2Doc* CExOdb2View::GetDocument() // non-debug version is inline

{

ASSERT(m_pDocument->IsKindOf(RUNTIME_CLASS(CExOdb2Doc)));

return (CExOdb2Doc*)m_pDocument;

}

#endif //_DEBUG

/////////////////////////////////////////////////////////////////////////////

// CExOdb2View database support

CRecordset* CExOdb2View::OnGetRecordset()

{

return m_pSet;

}

/////////////////////////////////////////////////////////////////////////////

// CExOdb2View message handlers

//레코드가 이동시 함수

BOOL CExOdb2View::OnMove(UINT nIDMoveCommand)

{

//현재 수정이 되었다가 이동을 하면

//무시한다.

if(m_bMode)

{

m_bMode=FALSE;

GetDlgItem(IDC_MODE)->EnableWindow(TRUE);

}

//이동 모드에 따라

switch(nIDMoveCommand)

{

//헤더로 이동시

case ID_RECORD_FIRST:

if(m_pSet->IsBOF())

break;

//처음 으로 이동

m_pSet->MoveFirst();

break;

//마지막으로 이동시

case ID_RECORD_LAST:

if(m_pSet->IsEOF())

break;

//마지막으로 이동

m_pSet->MoveLast();

break;

case ID_RECORD_NEXT:

//다음으로 이동시

m_pSet->MoveNext();

break;

case ID_RECORD_PREV:

//이전으로 이동시

m_pSet->MovePrev();

break;

}

UpdateResource();

return TRUE;

}

//새로운 레코드 설정시

void CExOdb2View::OnAdd()

{

//새로운 레코들 만들고

m_pSet->AddNew();

//자원에서 m_pSet으로 이동

UpdateDb();

//데이터 입력

m_pSet->Update();

//정보 갱신

m_pSet->Requery();

//m_pSet에서 다시 자원으로 이동

UpdateResource();

}

//수정입력 완료시

void CExOdb2View::OnMode()

{

//수정 가능하게 한다.

m_pSet->Edit();

//리소스->레코드로

UpdateDb();

//업데이트 한다.

m_pSet->Update();

//수정모드 비활성화

GetDlgItem(IDC_MODE)->EnableWindow(FALSE);

m_bMode=FALSE;

}

void CExOdb2View::OnRecordFirst()

{

OnMove(ID_RECORD_FIRST);

}

void CExOdb2View::OnRecordLast()

{

OnMove(ID_RECORD_LAST);

}

void CExOdb2View::OnRecordNext()

{

OnMove(ID_RECORD_NEXT);

}

void CExOdb2View::OnRecordPrev()

{

OnMove(ID_RECORD_PREV);

}

void CExOdb2View::OnUpdateRecordFirst(CCmdUI* pCmdUI)

{

pCmdUI->Enable(1-m_pSet->IsBOF());

}

void CExOdb2View::OnUpdateRecordLast(CCmdUI* pCmdUI)

{

pCmdUI->Enable(1-m_pSet->IsEOF());

}

//이름으로 검색

void CExOdb2View::OnSeekname()

{

char temp[80];

CSeekWord *pDlg = new CSeekWord;

//검색어 찾는 대화상자 가동

if(pDlg->DoModal()==IDOK)

{

//SQL 문을 실행

sprintf(temp,"name = '%s'",pDlg->m_strWord.operator const char*());

m_pSet->m_strFilter = temp;

m_pSet->Requery();

UpdateResource();

}

delete pDlg;

}

//전체 레코드로 가는 함수

void CExOdb2View::OnAllseek()

{

m_pSet->m_strFilter = "";

m_pSet->Requery();

UpdateResource();

}

//삭제

void CExOdb2View::OnDel()

{

m_pSet->Delete();

m_pSet->MoveNext();

if(m_pSet->IsEOF())

m_pSet->MoveLast();

if(m_pSet->IsBOF())

m_pSet->SetFieldNull(NULL);

UpdateResource();

}

//집전화번호로

void CExOdb2View::OnSeekhtel()

{

char temp[80];

CSeekWord *pDlg = new CSeekWord;

if(pDlg->DoModal()==IDOK)

{

sprintf(temp,"tel = '%s'",pDlg->m_strWord.operator const char*());

m_pSet->m_strFilter = temp;

m_pSet->Requery();

UpdateResource();

}

delete pDlg;

}

//직장명으로

void CExOdb2View::OnSeekoffice()

{

char temp[80];

CSeekWord *pDlg = new CSeekWord;

if(pDlg->DoModal()==IDOK)

{

sprintf(temp,"office = '%s'",pDlg->m_strWord.operator const char*());

m_pSet->m_strFilter = temp;

m_pSet->Requery();

UpdateResource();

}

delete pDlg;

}

//직장 전화번호로

void CExOdb2View::OnSeekotel()

{

char temp[80];

CSeekWord *pDlg = new CSeekWord;

if(pDlg->DoModal()==IDOK)

{

sprintf(temp,"officetel = '%s'",pDlg->m_strWord.operator const char*());

m_pSet->m_strFilter = temp;

m_pSet->Requery();

UpdateResource();

}

delete pDlg;

}

void CExOdb2View::OnSqlrun()

{

char temp[80];

CSeekWord *pDlg = new CSeekWord;

if(pDlg->DoModal()==IDOK)

{

sprintf(temp,"%s",pDlg->m_strWord.operator const char*());

m_pSet->m_strFilter = temp;

m_pSet->Requery();

UpdateResource();

}

delete pDlg;

}

void CExOdb2View::OnChangeBeepnum()

{

if(!m_bMode)

{

GetDlgItem(IDC_MODE)->EnableWindow(TRUE);

m_bMode=TRUE;

}

}

void CExOdb2View::OnChangeDescription()

{

if(!m_bMode)

{

GetDlgItem(IDC_MODE)->EnableWindow(TRUE);

m_bMode=TRUE;

}

}

void CExOdb2View::OnChangeHaddress()

{

if(!m_bMode)

{

GetDlgItem(IDC_MODE)->EnableWindow(TRUE);

m_bMode=TRUE;

}

}

void CExOdb2View::OnChangeHandnum()

{

if(!m_bMode)

{

GetDlgItem(IDC_MODE)->EnableWindow(TRUE);

m_bMode=TRUE;

}

}

void CExOdb2View::OnChangeHpostnumber()

{

if(!m_bMode)

{

GetDlgItem(IDC_MODE)->EnableWindow(TRUE);

m_bMode=TRUE;

}

}

void CExOdb2View::OnChangeHtel()

{

if(!m_bMode)

{

GetDlgItem(IDC_MODE)->EnableWindow(TRUE);

m_bMode=TRUE;

}

}

void CExOdb2View::OnChangeName()

{

if(!m_bMode)

{

GetDlgItem(IDC_MODE)->EnableWindow(TRUE);

m_bMode=TRUE;

}

}

void CExOdb2View::OnChangeOaddress()

{

if(!m_bMode)

{

GetDlgItem(IDC_MODE)->EnableWindow(TRUE);

m_bMode=TRUE;

}

}

void CExOdb2View::OnChangeOffice()

{

if(!m_bMode)

{

GetDlgItem(IDC_MODE)->EnableWindow(TRUE);

m_bMode=TRUE;

}

}

void CExOdb2View::OnChangeOpostnumber()

{

if(!m_bMode)

{

GetDlgItem(IDC_MODE)->EnableWindow(TRUE);

m_bMode=TRUE;

}

}

void CExOdb2View::OnChangeOtel()

{

if(!m_bMode)

{

GetDlgItem(IDC_MODE)->EnableWindow(TRUE);

m_bMode=TRUE;

}

}

#include "datalist.h"

//리스트 보기

void CExOdb2View::OnList()

{

CDataList *pDlg = new CDataList;

pDlg->DoModal();

delete pDlg;

UpdateResource();

}

void CExOdb2View::OnPrint(CDC* pDC, CPrintInfo* pInfo)

{

CRecordView::OnPrint(pDC, pInfo);

}

(프로그램 소스끝)

2.DAO

DAO란 Microsoft 사에서 제공하는 단일컴퓨터용 데이터베이스 라이브러리 입니다. DAO클래스는 기본적으로 MDB파일을 구동합니다. 단일 컴퓨터용으로 제작되어 있기 때문에 속도 또한 ODBC보다 빠릅니다. ODBC는 ODBC 디바이스 드라이버를 이용하는 반면에 DAO는 자체 라이브러리를 이용하기 때문입니다. 개인용 컴퓨터에서는 DAO를 많이 사용합니다. 또한 파일을 만들수가 있으며 파일에 새로운 테이블및 필드들을 기록할수도 있습니다.

DAO는 ODBC와 방법이 같습니다. 다만 사용되는 클래스들이 다릅니다. ODBC의 CRecordset클래스는 CDaoRecordset와 대응되고 CRecordView는 CDaoRecordView로 대응됩니다. 실제 프로그래밍을 작성해 가면서 DAO의 기능을 알아보겟습니다.

<1>프로젝트 MExDao만들기

프로젝트명을 MExDao로 선택합니다. 이렇게 설정한후 SDI형태로 설정합니다. MExOdbc 프로젝트를 만들때와 같은 방법으로 AppWizard의 Step2에서 Database view without file support를 선택합니다. 이와 같이 선택한후에 Data Source를 클릭합니다. 그림 3-1-20은 Data Source를 선택하였을 때의 화면입니다.

(그림 3-1-20)Database Options 설정화면

그림 3-1-20과 같이 Database Options화면이 출력되면 DAO를 클릭합니다. 이때 DAO항목의 에디터박스가 활성화 되는데 옆의 버튼을 클릭하면 파일 대화상자가 출력되고 이곳에서 mdb파일을 설정할수 있습니다. 또한 DAO입력란에 전체 경로명을 설정하여 주셔도 됩니다. Record type는 Dynaset로 합니다. 이와 같이 하고 OK버튼을 클릭합니다. OK버튼을 클릭하면 그림 3-1-21와 같이 Table를 선택하는 화면이 나타납니다.

(그림 3-1-21)Select Database Tables 선택화면

이화면에서 Table1을 선택합니다. 이와 같이 선택한후 Next버튼을 클릭하여 Step6까지 가면 그림 3-1-22과 같은 화면이 출력됩니다.

(그림 3-1-22)AppWizard Step6 화면

그림 3-1-22에서 보듯이 CMExDaoView는 CDaoRecordView로 설정되어 있으며 CMExDaoSet는 CDaoRecordset로 설정되어 있습니다. 즉 DAO를 이용하여 데이터베이스를 구동할경우에는 CDaoRecordView와 CDaoRecordset을 이용한다는 것입니다. 이와 같이하고 Finish를 클릭하여 프로젝트를 만듭니다.

<2>CDaoRecordView

CMExDaoView는 CDaoRecordView를 상속받았습니다. 이것은 CFormView를 상속받았으며 DAO데이터베이스를 핸들링하도록 설정된 것입니다. 그림 3-1-23은 CDaoRecordView의 계층도입니다.

(그림 3-1-23)CDaoRecordView의 계층구조

CDaoRecordView는 전항목에 설명한 CRecordView와 기능이 거히 같습니다. 다만 차이는 CDaoRecordView는 DAO를 핸들링하는 CDaoRecordset를 m_pSet으로 맴버를 가지고 있다는 것입니다.

<3>MExOdbc와 같은 방법으로 프로그래밍 하기

이데 MExOdbc와 같은 방법으로 CMExDaoView와 연결된 자원  ID IDD_MEXDAO_FORM 에 그림 3-1-24와 같이 설정합니다.

(그림 3-1-24)IDD_MEXDAO_FORM 수정 화면

위와 같이 설정한후 각 에디터와 m_pSet 의 맴버를 연결시킵니다. 즉 DoDataExchange에 DDX_FieldText 로 연결시키라는 의미입니다. 연결방법은 MExOdbc를 만들때의 방법과 같습니다.

이렇게 연결된 CMExDaoView 의 DoDataExchange 다음과 같습니다.

void CMExDaoView::DoDataExchange(CDataExchange* pDX)

{

CDaoRecordView::DoDataExchange(pDX);

//{{AFX_DATA_MAP(CMExDaoView)

DDX_FieldText(pDX, IDC_NAME, m_pSet->m_name, m_pSet);

DDX_FieldText(pDX, IDC_TEL, m_pSet->m_tel, m_pSet);

DDX_FieldText(pDX, IDC_ADDRESS, m_pSet->m_address, m_pSet);

DDX_FieldText(pDX, IDC_BIGO, m_pSet->m_bigo, m_pSet);

//}}AFX_DATA_MAP

}

위와같이 한후에 메뉴를 수정합니다. 메뉴를 수정한 화면은 그림 3-1-25과 같습니다.

(그림 3-1-25) 수정된 메뉴 형태

즉 MExOdbc 와 같은 기능을 그대로 DAO 로 만들어 보는것입니다.

이와 같이 메뉴를 만든다음 새로 설정한 □입력□,□수정□,□검색□,□검색해제□,□삭제□의 항목을 클릭했을때 실행되는 함수를 CMExDaoView에 설정하고 설정된 함수를 수정합니다. 다음은 설정된 함수를 수정한 내용입니다.

// 입력항목

void CMExDaoView::OnAdddata()

{

// TODO: Add your command handler code here

m_pSet->MoveLast();

m_pSet->AddNew();

UpdateData(TRUE);

m_pSet->Update();

m_pSet->Requery();

UpdateData(FALSE);

}

//수정항목

void CMExDaoView::OnModedata()

{

// TODO: Add your command handler code here

m_pSet->Edit();

UpdateData(TRUE);

m_pSet->Update();

m_pSet->Requery();

UpdateData(FALSE);

}

//검색

void CMExDaoView::OnSeekname()

{

// TODO: Add your command handler code here

UpdateData(TRUE);

m_pSet->m_strFilter.Format("name= '%s' ",m_pSet->m_name);

m_pSet->Requery();

UpdateData(FALSE);

}

//검색 해제

void CMExDaoView::OnFreeseek()

{

// TODO: Add your command handler code here

m_pSet->m_strFilter="";

m_pSet->Requery();

UpdateData(FALSE);

}

//삭제

void CMExDaoView::OnDeldata()

{

// TODO: Add your command handler code here

m_pSet->Delete();

m_pSet->Requery();

UpdateData(FALSE);

}

이와 같이 한후 컴파일 하여 실행하면 그림3-1-26와 같은 화면이 출력됩니다. 이기능은 ODBC 와 같습니다.

(그림 3-1-26)MExDao 출력결과

DAO를 이용한 고객 관리 예제 프로그램

Exodb2 프로그램을 DAO를 이용한 프로그램으로 전환해 보겠습니다.

ODBC와 DAO의 가장 큰 차이점은 클래스가 다르다는 것입니다.

ODBC의 CRecordset이 DAO는 CDaoRecordset로 대응되고 ODBC에서 사용하는 CRecordView는 DAO에서는 CDaoRecordView로 대응됩니다.

그 이외의 모든 기능은 ODBC와 유사하지만  Snapshot으로 설정할 경우 ODBC와 연결된 데이터베이스의 수정 및 삭제가 용이하지 않습니다.

DAO를 이용한다면 Dynaset로 설정하는 것이 일반적입니다.

또다른 차이점은 GetCurrentRecord 함수에 있습니다. 이 함수는 현재의 레코드를 얻는 함수인데 이것은 실제 읽는 레코드수를 리턴하게 됩니다. DAO의 추가 기능 몇 가지를 설명하겠습니다.

□FindFirst() : 특정키값으로 첫번째 레코드를 찾는다.

□FindLast() : 특정키값으로 마지막 레코드를 찾는다.

□FindNext() : 특정키값으로 다음 레코드를 찾는다.

□FindPrev() : 특정키값으로 이전 레코드를 찾는다.

□SetBookmark() : 현재 레코드를 마크한다.

현재 레코드를 마크한다는 이야기는 특정 레코드를 찾아 놓고 다른 것을 찾고 수정하고 하다가 다시 처음에 찾아 놓았던 데이터로 돌아가려고 할 때 설정하는 것입니다. 즉 책갈피 개념으로 보면 됩니다. 어느 중요한 레코드를 마크해 놓는 것이죠. 나중에 마크된 데이터로 돌아가려면 GetBookmark()를 실행하면 됩니다.

CDaoRecordset의 클래스 맴버 함수를 도움말을 이용하여 읽어 보면 DAO와 ODBC의 차이점을 금방 느낄 수 있을 것입니다. DAO는 STAND ALONE 방식이기 때문에 색인 검색이 자유롭습니다. 또한 이동하는 방법이나 데이터에 접근하는 방식이 ISAM 방식을 따라갑니다. 그렇기 때문에 ODBC와는 다르게 매우 자유롭습니다. 승용차가 버스보다는 자유롭게 이동하고 좁은 길도 갈 수 있듯이 DAO는 ODBC보다 제약점을 많이 해제했다는 것이 다릅니다.

또한 속도면에서(다른 사람들 말로는 똑같다고 하는데 필자가 생각하기에는) ODBC보다 빠른 편입니다. 둘을 비교할 때 DAO가 ODBC보다 훨씬 낫다는 이야기를 하는 사람도 있으나 필자가 생각하기에는 서로 장단점이 있습니다. 버스보다 승용차가 낫다는 것은 개인이 운전하기에 편리하다는 이야기이죠. 많은 사람을 운송하는 면에서는 승용차보다는 버스가 낫지 않을까요?

Dao는 고객 관리 프로그램을 DAO로 바꾸어 프로그래밍한 것입니다. 내용은 ODBC와 같습니다.

Dao 예제인 Exdao프로그램은 제공되는 CD를 참조하시기 바랍니다.