PHP+LOB 두개 이상의 CLOB 이용하기

PHP와 Oracle을 연동할 때는 다음과 같은 두가지의 문제가 가장 크게 대두됩니다.

* 페이징 처리
* LOB 타입의 처리

페 이징 처리야 사실 쿼리문을 개발하는 문제이니 Oracle SQL문만 익숙해지면 처리하는데 크게 어렵지 않습니다. 그런데 LOB타입은 쿼리문말고도 별도의 추가작업을 해줘야 하므로 아주 골치를 겪게 됩니다. 그래서 PHP와 Oracle 연동할 때 컬럼 타입을 되도록 LOB 타입으로 안잡으려 노력합니다만... 그게 그렇게 잘 되지 않죠... -_-ㅋ

요즘 그누보드 4를 Oracle 버전으로 포팅하는 작업을 하고 있습니다. 오랜만에 PHP와 Oracle을 연동하다보니 MYSQL의 text 타입을 전부 CLOB으로 처리해줘야 되더군요. 물론 그 중 상당수는 varchar2(4000)로 충분히 커버되는 것이지만, 최대한 그누보드 4의 골격을 해치지 않기 위해서 모두 CLOB으로 변환했습니다. 그러다 보니 한번에 두개 이상의 CLOB을 insert/update하는 일이 발생하게 되었습니다.

이전에도 PHP상에서 CLOB 타입을 핸들링한 적이 많았지만, 보통 한 테이블에 하나정도의 LOB타입이 있는 경우가 많았습니다. 그래서 두개 이상 해본적이 거의 없었던 것 같아서 이리저리 테스트해봤습니다. 되도록 한방에 처리할 수 있는 방안쪽으로 방향을 잡았습니다.

결국 한방에는 되지 않는다로 결론이 나버렸지만, 이 기회에 두개 이상의 CLOB을 핸들링하는 방법을 정리하게 되어 그리 헛된 고생을 한 것은 아닌 것 같습니다. 아 참! Oracle 10g 기반 최신 OCI를 받으면 MYSQL의 text 필드에 내용을 넣듯이 바로 한번에 된다는 글을 검색하는 도중 찾게 되었습니다. 다음에 한번 테스트 해봐야 겠네요.

각설하고... Oracle에서 LOB 처리를 하기 위해서는 다음과 같은 함수들이 추가적으로 필요합니다.

string OCINewDescriptor ( int connection [, int type])
int OCIBindByName ( int stmt, string ph_name, mixed & variable, int length [, int type])

그리고 두개 이상의 CLOB을 insert할 경우의 처리 순서는 다음으로 요약할 수 있습니다.

1. ROWID에 대한 Descriptor를 생성한다.
2. clob 칼럼을 제외한 나머지 칼럼이 포함된 insert 구문을 실행한다. 이 때 insert된 행의 ROWID를 Descriptor와 연결해 놓는다.
3. ROWID를 이용해 clob 칼럼을 하나씩 update를 한다.
4. 3의 과정을 CLOB 칼럼수만큼 반복한다.
5. 작업이 끝났으면 불필요한 리소스를 반환한다.

update의 경우는 위의 3~5과정과 동일하므로 생략하겠습니다. 그럼 위의 과정을 직접 코딩해보겠습니다. 먼저 테스트 테이블을 만듭니다.

create table aaa ( rnum number(7), clob1 clob, clob2 clob);

그리고 위의 테이블에 데이터를 넣어보겠습니다.

$dbconn = @OCILogon('test', 'test123', 'ora10g');

$sql = "insert into aaa (rnum) values (1) returning ROWID into :rid"; --- [1]

$stmt1 = OCIParse($dbconn, $sql); --- [2]
$rowid = OCINewDescriptor($dbconn, OCI_D_ROWID); --- [3]
OCIBindByName($stmt1, ":rid", &$rowid, -1, OCI_B_ROWID); --- [4]
OCIExecute($stmt1, OCI_DEFAULT); --- [5]
OCIFreeStatement($stmt1); --- [6]

$clob = OCINewDescriptor($dbconn, OCI_D_LOB); --- [7]

$content = array(); --- [8]
$content[0] = "CLOB 첫번째 내용입니다..";
$content[1] = "CLOB 두번째 내용입니다.";

$result = true;
for($i=0; $i<2; $i++) { --- [9]
$update = "update aaa set clob".($i+1)." = empty_clob() where ROWID = :rid returning clob".($i+1)." into :clob"; --- [10]
$stmt2 = OCIParse($dbconn, $update);
echo $update."
";
OCIBindByName($stmt2, ":rid", &$rowid, -1, OCI_B_ROWID);
OCIBindByName($stmt2, ":clob", &$clob, -1, OCI_B_CLOB); --- [11]
OCIExecute($stmt2, OCI_DEFAULT);

if($clob->save($content[$i])) { --- [12]
echo "

CLOB".($i+1)." 입력 성공

";
$result &= true; --- [13]
} else {
echo "

CLOB".($i+1)." 입력 실패

";
$result &= false;
}
OCIFreeStatement($stmt2);
}

echo "최종결과: ".(($result)? "true": "false")."
";
if($result) OCICommit($dbconn); --- [14]
else OCIRollback($dbconn);

OCIFreeDesc($rowid); --- [15]
OCIFreeDesc($clob); --- [16]
OCILogoff($dbconn);
?>

[1]: clob 칼럼은 나중에 update를 이용해서 집어넣을 것이므로, clob이 아닌 칼럼만 일단 insert를 합니다. 만일 clob 형 타입을 NOT NULL로 잡았다면 empty_clob() 함수를 이용해서 일단 빈공간을 만들어야 겠죠.
[2]: 일단 쿼리를 파싱합니다.
[3]: ROWID를 받아오기 위해 Descriptor를 생성합니다. 나중에 해당행에 CLOB을 집어넣기위한 식별자로 사용할 것입니다.
[4]: 파싱된 쿼리와 Descriptor를 바인딩시킵니다.
[5]: 쿼리 실행! 오라클은 기본으로 트랜잭션을 이용합니다. 이런 장점을 그대로 가지고 가려면, 두번째 인자에 OCI_DEFAULT를 지정하는 것을 잊지 말아야겠죠?
[6]: Statement를 해제합니다.
[7]: 이제 본격적으로 CLOB을 처리하기 위해 우선 CLOB Descriptor를 생성합니다.
[8],[9]: 이부분은 반복적인 업데이트 구문처리를 편하게 하기 위해 그냥 만든겁니다. 꼭 이렇게 해야한다는 법은 없습니다.
[10]: 실제로 CLOB 데이터를 넣기 위한 SQL입니다. 엄밀히 말하면 데이터가 들어갈테니 공간을 마련해 놓으라는 지시를 하는 것이죠. 실제 데이터 저장은 [12]에서 하게 됩니다.
[11]: 파싱된 쿼리에 CLOB Descriptor를 연결시킵니다.
[12]: 쿼리가 성공적으로 실행되면 실제로 데이터를 저장해야 합니다. LOB 객체의 save() 함수를 사용합니다.
[13]: 이건 나중에 트랜잭션의 Commit/Rollback을 정하기 위한 장치입니다. 비트연산을 이용해서 처리했습니다.
[14]: Commit 혹은 Rollback 합니다.
[15],[16]: 이제 모든 작업이 완료되었으므로 필요없는 Descriptor를 해제하는 작업을 합니다.

-------------------------------------------------------

이 것은 두개의 이상의 CLOB 데이터를 처리하기 위한 것입니다. 만약 한개의 CLOB 데이터를 처리한다면 insert 구문에서 모두 처리가 가능합니다.