Recommanded Free YOUTUBE Lecture: <% selectedImage[1] %>

select

이번에는 Select를 해 보자. Select의 경우 쉬울 것 같지만 예상외로 까다로운 점이 있다. 먼저 호스트 변수와 데이터베이스 칼럼의 데이터 타입을 결정해야 한다. 데이터 구조가 간단하다면 좋겠지만 CLOB, BLOB, LONG, RAW 등의 칼럼이 있다면 이 역시 따로 코드를 작성해야 하는 귀찮은 점이 많다. 칼럼의 수만큼 루프도 돌려야 되고 데이터를 뿌려줄 때 칼럼의 이름도 가져와야 한다.(반드시 필요한 것은 아니지만 대부분의 인터페이스에서 칼럼이 이름과 데이터를 같이 보여주지 않는가?)

구구절절 말로 하는 것 보단 실제로 예제를 하나 보도록 하자.
/* --------------------------------------------------------------------------------
파일 이름 : select.pc
개발 일자 : 2002-10-28
작성자 : 류명환
-------------------------------------------------------------------------------- */

#include <stdio.h>
#include <stdlib.h>


/*
 * $ORACLE_HOME/precomp/public/sqlca.h 를 포함해 주기 위해서
 */
exec sql include sqlca;

/*
* 호스트 변수 선언
*/
exec sql begin declare section;
    /*
     * 사용자 ID와 패스워드
     */
    char *username;
    char *password;

    /*
     * emp 테이블의 칼럼 리스트
     */
    int empno;
    int mgr;
    int sal;
    int comm;
    int deptno;
    char ename [11];
    char job [10];
    char hiredate [10];

    /*
     * emp 테이블 칼럼들의 Indicator 변수
     */
    short ind_empno;
    short ind_mgr;
    short ind_sal;
    short ind_comm;
    short ind_deptno;
    short ind_ename;
    short ind_job;
    short ind_hiredate;
exec sql end declare section;

/*
 * SQL 에러 발생시 실행할 함수
 * 에러코드를 프린트 하고 롤백 후에 접속을 종료한다.
 */
void sql_error (void)
{
    printf ("SQL Error Code : %d\n", sqlca.sqlcode);
    printf ("SQL Error Message : %s\n", sqlca.sqlerrm.sqlerrmc);

    exec sql whenever sqlerror continue;
    exec sql rollback work release;

    exit (EXIT_FAILURE);
}

/*
 * Standard Input 에서 문자열을 받아서 저장하는 함수
 * 입력이 없으면 -1을 리턴한다.
*/
int get_value (char *buf, char *prompt)
{
    printf (prompt);
    fgets (buf, 256, stdin);
    buf [strlen (buf) - 1] = '\0';

    if (strlen (buf) == 0)
    {
        return -1;
    }
    else
    {
        return 0;
    }
}

int main (void)
{
char buf [256] = {'\0', };

    username = (char *)strdup ("scott");
    password = (char *)strdup ("tiger");

    /*
     * scott 사용자 계정으로 접속 시도
     * 접속 시도 후에 성공 여부를 위해 에러를 검사한다.
     */
    exec sql
        connect :username identified by :password;
    if (sqlca.sqlcode != 0)
    {
        sql_error ();
    }
    printf ("Connected to Oracle Database\n");

/*
 * 데이터베이스에 EMP 테이블에서 검색한 사원의 사원 번호를 알아온다.
 * 사원번호를 입력하지 않았다면 TYPE 레이블로 돌아와서 다시 입력하도록 한다.
 */
TYPE:

    ind_empno = get_value (buf, "Type empno to find : ");
    empno = atoi (buf);
    if (!empno)
    {
        printf ("You must type empno to find\n");
        goto TYPE;
    }

    /*
     * 해당 사원의 사원 정보를 가져온다.
   * Indicator 변수를 붙여서 NULL이 있는지 검사한다.
     */
exec sql
        select ename, job, mgr, hiredate, sal, comm, deptno
        into :ename:ind_ename, :job:ind_job, :mgr:ind_mgr, :hiredate:ind_hiredate,
             :sal:ind_sal, :comm:ind_comm, :deptno:ind_deptno
        from emp
        where empno = :empno;
    /*
     * SQL 실행후 에러 검사, 코드가 0 이 아닐 경우는 에러
     * 코드가 1403 일경우는 where 절에 해당되는 데이터가 없는 경우
     * 이런 경우는 에러로 처리하기보단 데이터가 없다는 메시지를
     * 뿌려 주는게 더 낫다.
     */
    if (sqlca.sqlcode != 0  && sqlca.sqlcode != 1403)
    {
        sql_error ();
    }

    /*
    * sqlca.sqlerrd [2] 는 위에서도 봤듯이 SQL 문장에 영향을 받은
    * 행의 개수를 가지는 변수이다. Select를 했을 경우에는 sqlcode를 검사해서
     * 1403 인 경우나  sqlerrd [2] 를 검사해서 0 인 경우는 같은 것이다.
 * 이럴 경우는 해당 데이터가 없다는 메시지를 찍어주고 종료한다.
 */
if (sqlca.sqlerrd [2] == 0)
    {
        printf ("No row Selected\n");
    }
    else
    {
        /*
         * Indicator 변수를 검사하여 1 인 경우는 실제 데이터가 NULL 인
         * 경우 이므로 숫자인 경우 1을 문자인 경우 NULL을 찍어 준다.
         */
        printf ("EMPNO ENAME JOB MGR HIREDATE SAL COMM DEPTNO\n");
        printf ("%5d %10s %9s %4d %9s %7d %7d %2d\n",
                (ind_empno    == -1) ? -1 : empno,
                (ind_ename    == -1) ? "NULL" : ename,
                (ind_job      == -1) ? "NULL" : job,
                (ind_mgr      == -1) ? -1 : mgr,
                (ind_hiredate == -1) ? "NULL" : hiredate,
                (ind_sal      == -1) ? -1 : sal,
                (ind_comm     == -1) ? -1 : comm,
                (ind_deptno   == -1) ? -1 : deptno);
    }

    /*
     * 데이터베이스와 접속을 종료한다.
     */
    exec sql
        rollback work release;
    if (sqlca.sqlcode != 0)
    {
        sql_error ();
    }
printf ("Disconnect from Oracle Database\n");
    exit (EXIT_SUCCESS);
}

위 프로그램은 Select의 아주 간단한 예제이다. 위와 같 Select의 경우는 SQL 문장이 이미 결정 되어 있는 상태다. 이런 Select는 상당히 쉽다. 그러나 실제로 프로그램 하다가 보면 SQL 을 실행하기 전까지 SQL 문장이 결정되지 않는 경우가 종종 있다. SQL 문장이 런타임시에 결정되는 경우를 예로 들자면 로그인 한 사람의 직급별로 볼 수 있는 칼럼과 볼 수 없는 칼럼이 구분되어 있다던지 데이터를 가져올 테이블의 이름이 날짜별로 다르다던지 하는 경우를 쉽게 생각 해 볼 수 있다. 이럴 경우 SQL 문장을 날리기 전에서야 SQL 문장의 형태를 알 수 있다. 이러한 상황을 Dynamic SQL 이라하는데 이것은 다음 장에서 좀더 자세히 다루도록 하겠다.

위에서 처럼 한의 행만 가져오지 않고 모든 행을 다 가져와야 하는 경우가 발생하였다 가정한다. 이럴때는 어떻게 해야 할 것인가? 뭐 방법은 많다. SQL 을 조금이라도 해본 사람이라면 먼저 커서를 생각할 것이다. 맞다. 커서를 사용한다면 쉽게 작성할 수 있다. 다른 방법은 없을까? 만약 커서에 대해서 모른다거나 사용할 능력이 없다면 소위 말하는 약간의 꽁수를 동원해도 된다. 먼저 select count(*) from emp 를 수행해서 전체 row이 개수를 얻어온다. 그렇게 해서 for 문을 돌려서 행의 개수만큼 각 행을 가져오면 되는 것이다. 어떤 방법을 쓰던지 그건 프로그래머의 자유지만 이왕 프로그램을 작성하는 것 되도록이면 읽기 쉽고 성능 좋은 프로그램을 작성하는 것이 좋을 것이다. 그것이 프로그래머의 능력차가 아니겠는가? 사실 거창하게 말했지만 누구나 다 알고 있는 기본 사실이다.

이번에는 커서를 이용한 예제를 만들어서 데이터베이스에 대상 테이블의 전체 행을 읽어 오는 프로그램을 작성해 보자.
/* --------------------------------------------------------------------------------
파일 이름 : cursor.pc
개발 일자 : 2002-10-28
작성자 : 류명환
-------------------------------------------------------------------------------- */

#include <stdio.h>
#include <stdlib.h>
#include <stdlib.h>

/*
 * $ORACLE_HOME/precomp/public/sqlca.h 를 포함해 주기 위해서
 */
exec sql include sqlca;

/*
 * 호스트 변수 선언
 */
exec sql begin declare section;
	/*
	 * 사용자 ID와 패스워드
	 */
	char *username;
	char *password;

	/*
	 * 칼럼 리스트
	 */
	int empno;
	int sal;
	int comm;
	char dname [14];
	char ename [11];
	char job [10];

	/*
	 * 칼럼들의 Indicator 변수
	 */
	short ind_empno;
	short ind_sal;
	short ind_comm;
	short ind_dname;
	short ind_ename;
	short ind_job;
exec sql end declare section;

void sql_error (void);

int main (void)
{
char buf [256] = {'\0', };

	username = (char *)strdup ("scott");
	password = (char *)strdup ("tiger");

	/*
     * scott 사용자 계정으로 접속 시도
     * 접속 시도 후에 성공 여부를 위해 에러를 검사한다.
     */
	exec sql
		connect :username identified by :password;
	if (sqlca.sqlcode != 0)
	{
		sql_error ();
	}
	printf ("Connected to Oracle Database\n");

	/*
	 * 커서를 선언한다.
	 * sqlca.sqlcode로 에러 검사
	 */
	exec sql
		declare dept_emp cursor for
			select d.dname, e.empno, e.ename, e.job, e.sal, e.comm
			from dept d, emp e
			where d.deptno = e.deptno;
	if (sqlca.sqlcode != 0)
	{
		sql_error ();
	}

	/*
	 * 커서를 연다.
	 * sqlca.sqlcode로 에러 검사
	 */
	exec sql
		open dept_emp;
	if (sqlca.sqlcode != 0)
	{
		sql_error ();
	}
	
	/*
	 * 칼럼의 이름을 프린트 한다.
	 */
	printf ("DNAME         EMPNO ENAME      JOB       SAL     COMM\n");
	while (1)
	{
		/*
		 * 한행씩 순서대로 패치한다.
		 * sqlca.sqlcode로 에러 검사한다.
		 * 먼저 1403번을 검사하여 모든 행이 패치된 경우를 찾아보고
		 * 행이 남이 있으면서 에러가 난 경우를 검사한다.
		 */
		exec sql
			fetch dept_emp
			into :dname:ind_dname, :empno:ind_empno, :ename:ind_ename,
			     :job:ind_job, :sal:ind_sal, :comm:ind_comm;
		if (sqlca.sqlcode == 1403)
		{
			break;
		}
		else if (sqlca.sqlcode != 0)
		{
			sql_error ();
		}

		/*
		 * 칼럼 내용을 출력한다.
		 * Indicator 변수를 검사하여 NULL이 있는거 보고
		 * 정수일 경우 -1을 문자열인 경우 NULL을 찍는다.
		 */
		printf ("%10s %5d %10s %9s %-7d %-7d\n",
		        (ind_dname == -1) ? "NULL" : dname,
		        (ind_empno == -1) ? -1 : empno,
				(ind_ename == -1) ? "NULL" : ename,
				(ind_job   == -1) ? "NULL" : job,
				(ind_sal   == -1) ? -1 : sal,
				(ind_comm  == -1) ? -1 : comm);
	}

	/*
	 * 커서를 닫는다.
	 * sqlca.sqlcode로 에러 검사
	 */
	exec sql
		close dept_emp;
	if (sqlca.sqlcode != 0)
	{
		sql_error ();
	}

	/*
	 * 데이타베이스와 접속을 종료한다.
	 * sqlca.sqlcode로 에러 검사
	 */
	exec sql
		rollback work release;
	if (sqlca.sqlcode != 0)
	{
		sql_error ();
	}
	printf ("Disconnect from Oracle Database\n");

	exit (EXIT_SUCCESS);
}

/*
 * SQL 에러 발생시 실행할 함수
 * 에러코드를 프린트 하고 커서를 닫고 롤백 후에 접속을 종료한다.
 */
void sql_error (void)
{
	printf ("SQL Error Code : %d\n", sqlca.sqlcode);
	printf ("SQL Error Message : %s\n", sqlca.sqlerrm.sqlerrmc);

	exec sql
		whenever sqlerror continue;
	exec sql
		close dept_emp;
	exec sql
		rollback work release;

	exit (EXIT_FAILURE);
}

커서를 사용한 예제를 보았다. 뭐 특별한 것은 없다. 위에서 보았듯이 Pro*C/C++ 에서 커서를 선언하여 사용할 수 있다는 것이다. 아마 PL/SQL 을 사용해본 독자라면 매우 익숙한 구문일 것이다. 먼서 호스트 변수를 선언하고 exec sql declare <cursor name> cursor for select … … 를 사용하여 커서를 선언한다. 그 후 exec sql open <cursor name> 을 사용하여 커서를 연다. 이때 해당 SQL 문이 실행되어서 각 행들이 메모리에 올라간다. 이 상황에서 순서대로 각 행을 패치해 오면 되는 것이다. 모든 행을 패치하고 더 이상 패치할 내용이 없다면 sqlcode가 1403번으로 설정되며 이때 Loop을 빠져 나가면 되는 것이다. 아주 전형적으로 작성된 구조적 프로그램이다. 위의 예제에 주석을 잘 달아 놓았으므로 어려운 것은 없을 것이다.