19 fevereiro 2009

Trabalhando com Lobs, Blobs, Clobs do Oracle no PHP

Trabalhar tipos de dados como Varchar2 no Oracle é bastante simples, mas e se for necessário armazenar uma informação que tenha mais de 4000 caracteres?
Para esses casos a solução é um dos tipos LOB(Oracle Long Object), que necessitam um tratamento especial com a utilização da API do PHP para trabalhar com Lobs.

Objetos Long no Oracle
O Oracle fornece os seguintes tipos de dados LOB:
- BLOB, utilizado para armazenar informações binárias
- CLOB, utilizado para armazenar caracteres com a codificação do banco de dados
- NCLOB, utilizado para armazenar informações com codificação UNICODE. Deve-se observar que NCLOBs, atualmente não são suportados pela extensão OCI8 do PHP, que será explicada neste artigo.
- BFILE, utilizado para referenciar arquivos externos do sistema operacional.

Uma subcategoria dos LOBs, são os LOBs temporários, que podem ser tanto um BLOB, quanto um CLOB ou NCLOB, com a diferença que são armazenados temporariamente na seção até você limpar a memória.
Obs.: As versões mais antigas do Oracle fornecem os tipos LONG e LONG RAW, para caracteres e binários respectivamente. Com o Oracle9i, esses tipos ficaram depreciados em favor dos LOBs.


Armazenamento permitido em um LOB
Para os tipos BLOB, CLOB e NCLOB, o Oracle Database 10g é capaz de armazenar até 128TB em um único registro, dependendo das configurações do banco.

Um objeto LOB compreende dois elementos: O conteúdo do LOB e o localizador do LOB, que é um ponteiro para o conteúdo do LOB. Essa separação é necessária para permitir que o Oracle armazene e gerencie os LOBs de maneira eficiente, e isso é refletido na API do PHP, ao realizar consultas nos LOBs.
Para os tipos internos de LOBs(exceto BFILEs), o Oracle irá armazenar o conteúdo na linha dentro da tabela, juntamente com o restante do registro, em LOBs que tem como tamanho menos de 4KB.
Para LOBs com mais de 4KB, as informações são gravadas fora da tabela. Essa medida permite que os registros pequenos de LOBs sejam trabalhados mais rapidamente do que os maiores, que são armazenados em um local separado, e por isso tem um tempo de acesso mais lento.

Há outras opções para o armazenamento e acesso de LOBs - como utilização de cache e buffer -, que pode fazer com que a performance seja aumentada. Para maiores informações veja o Guia de Performance de LOBs.

Restrições em LOBs
Há algumas restrições que se aplicam ao trabalhar com LOBs, as mais importante estão no seu uso nas consultas SQL.
Não é possível utilizar LOB em nenhuma das seguintes consultas:

SELECT DISTINCT
ORDER BY
GROUP BY

Também não é possível utilizar LOBs em junções de tabelas(JOIN), UNION, INTERSECTION e MINUS.
Há ainda outras restrições como não ser possível utilizar um LOB como chave primária de uma tabela. Para maiores informações, veja o Guia do Desenvolvedor de Aplicações Oracle - Large Objects

CLOBs e codificações
A codificação padrão de uma base de dados é determinada com o parâmetro NLS_CHARACTERSET, e um texto em um CLOB utiliza o mesmo tipo de codificação determinado nesse parâmetro.
Para visualizar o tipo de codificação utilizado em uma case de dados, deve-se utilizar o seguinte SQL.

SELECT value FROM nls_database_parameters WHERE parameter = 'NLS_CHARACTERSET'

Para alterar a codificação de uma base utilize:

ALTER DATABASE CHARACTER SET UTF8

Obs.: Cuidado ao realizar essa operação, principalmente se houver informações na base, ou se a aplicação utilizar outro tipo de codificação.

Trabalhando com LOBs
As explicações seguintes focam na extensão OCI8 do PHP. É bom lembrar que o Oracle fornece um pacote chamado DBMS_LOB, que contém funções para trabalhar com LOBs utilizando PL/SQL.

A extensão OCI8 registra uma classe chamada OCI-Lob no escopo global da aplicação. Quando se executa uma consulta do tipo SELECT por exemplo, onde uma das colunas for um tipo LOB, o PHP vai automaticamente transformar esse campo em um objeto OCI-Lob. Assim que houver uma referencia a esse objeto, será possível chamar métodos como load() e save(), para acessar ou modificar o conteúdo do LOB.
Os métodos disponíveis dependem da versão da PHP que está sendo utilizada. O PHP 5 provê métodos como read(), seek() e append(). Para visualizar os métodos disponíveis, utilize o script abaixo.




No PHP 5.0.5, os seguintes métodos são retornados:
OCI-Lob::load()
OCI-Lob::tell()
OCI-Lob::truncate()
OCI-Lob::erase()
OCI-Lob::flush()
OCI-Lob::setbuffering()
OCI-Lob::getbuffering()
OCI-Lob::rewind()
OCI-Lob::read()
OCI-Lob::eof()
OCI-Lob::seek()
OCI-Lob::write()
OCI-Lob::append()
OCI-Lob::size()
OCI-Lob::writetofile()
OCI-Lob::writetemporary()
OCI-Lob::close()
OCI-Lob::save()
OCI-Lob::savefile()
OCI-Lob::free()

Na prática, a extensão OCI8 do PHP 4.x permite a leitura e escrita de LOBs completos somente, que são os usos mais comuns em aplicações Web. O PHP5 extende essas funcionalidades, permitindo que sejam lidos ou gravados pedaços de um LOB, assim como a utilização de buffer com os métodos setBuffering() e getBuffering().
O PHP5 ainda disponibiliza as funções oci_lob_id_equal() e oci_lob_copy().

Os exemplos abaixo utilizam a nova nomenclatura das funções OCI para PHP5. No PHP5 há o caracter underline entre as palavras. Ex.: PHP4 - ociparse(), PHP5: oci_parse().
Nos exemplos será utilizada a sequência e a tabela abaixo:


CREATE SEQUENCE mylobs_id_seq
NOMINVALUE
NOMAXVALUE
NOCYCLE
CACHE 20
NOORDER
INCREMENT BY 1;

CREATE TABLE mylobs (
id NUMBER PRIMARY KEY,
mylob CLOB
)


Na maioria dos exemplos, serão utilizados CLOBs, mas a mesma lógica pode ser aplicada aos BLOBs.

Inserindo um LOB
Para inserir um LOB, antes de mais nada, é necessário inicializar o campo LOB com as funções empty_blob ou empty_clob do Oracle - Não é possível atualizar uma informação em um LOB que contenha um valor NULL.
Uma vez inicializado, é necessário vincular a coluna a um objeto OCI-Lob do PHP e atualizar o conteúdo do LOB através do método save() do objeto.

O seguinte script fornece um exemplo, retornando o campo LOB da consulta INSERT:
save('INSERT: '.date('H:i:s',time())) ) {

// Se houver erro a transação é cancelada
oci_rollback($conn);

} else {

// Se não houver erro a transação é confirmada
oci_commit($conn);

}

// Libera recursos
oci_free_statement($stmt);
$myLOB->free();


// desconecta do banco, etc...
?>

Perceba que esse exemplo utiliza transação, chamando o método oci_execute com a constante OCI_DEFAULT, o que faz com que a alteração no banco somente seja realizada após oci_commit ou oci_rollback.
Isso é importante, pois há dois estágios na inserção. O primeiro cria o registro e o segundo atualiza o conteúdo do LOB.

Se fosse utilizado um campo BLOB, a única mudança necessária seria na chamada ao método oci_bind_by_name:
oci_bind_by_name($stmt, ":mylob_loc", $myLOB, -1, OCI_B_BLOB);

Há uma maneira de vincular um campo LOB sem especificar seu tipo:



Essa maneira simplifica o código significativamente e é adequado quando há a necessidade de escrever um LOB relativamente pequeno. Se for necessário escrever um LOB extenso, é possível nevegar pelo conteúdo chamando as funções write() e flush() para escrever pequenos pedaços de cada vez e não sobrecarregar a memória.

Retornando um LOB
Quando uma consulta SELECT contém uma coluna LOB, o PHP irá automaticamente vincular a coluna com um objeto OCI-Lob. Por exemplo:

load()."\n";
}

// etc.
?>


Esse processo pode ser simplificado com a utilização da constante OCI_RETURN_LOBS na chamada ao método oci_fetch_arrray(), instruindo a substituição dos objetos LOB por seus valores:


while ( $row = oci_fetch_array($stmt, OCI_ASSOC+OCI_RETURN_LOBS) ) {
print "ID: {$row['ID']}, {$row['MYLOB']}\n";
}


Atualizando um LOB
Para atualizar o conteúdo de um LOB, é possível utilizar o comando RETURNING da mesma maneira que na inserção, mas há uma maneira mais simples, que é utilizar a sintaxe SELECT ... FOR UPDATE

truncate() ) {
oci_rollback($conn);
die ("Failed to truncate LOB\n");
}

// Salva o novo valor no campo LOB
if ( !$row['MYLOB']->save('UPDATE: '.date('H:i:s',time()) ) ) {

// Se houver erro a transação é cancelada
oci_rollback($conn);

} else {

// Se não houver erro a transação é confirmada
oci_commit($conn);

}

// Libera recursos
oci_free_statement($stmt);
$row['MYLOB']->free();
?>



Da mesma maneira que no INSERT, é necessário realizar o UPDATE utilizando uma transação. Um passo importante é chamar o método truncate(). Ao atualizar um LOB com save(), ele irá substituir o conteúdo do início do LOB até o tamanho da nova informação.
Isso significa que o conteúdo anterior(se for maior que o novo), poderá continuar no campo LOB.

No PHP 4.x, onde o método truncate() não está disponível, a alternativa é utilizar o método empty_clob() do Oracle para apagar qualquer conteúdo do LOB antes de salvar a nova informação.


save( 'UPDATE: '.date('H:i:s',time()) ) ) {

OCIRollback($conn);
die("Unable to update lob\n");

}

OCICommit($conn);
$mylob->free();
OCIFreeStatement($stmt);

?>


Trabalhando com BFILES
Ao utilizar o tipo BFILE, as consultas INSERT e UPDATE dizem ao Oracle qual a localização do arquivo no sistema de arquivos do servidor(Que pode não ser o mesmo do servidor Web), ao invés de passar o conteúdo do arquivo. Utilizando uma consulta SELECT, é possível ler o conteúdo de um BFILE através do Oracle, ou chamar as funções do pacote DBMS_LOB para buscar informações do arquivo.

O maior benefício dos campos BFILEs é a possibilidade de acessar o arquivo original diretamente do sistema de arquivos e ainda conseguir localizar arquivos via SQL. Isso significa que, por exemplo, imagens podem ser exibidas diretamente do sistema de arquivos enquanto é possível acompanhar o relacionamento entre a tabela contendo o BFILE e uma tabela de usuários que guarda quem fez o upload.

Como um exemplo, primeiramente é preciso atualizar a tabela utilizada nos exemplos anteriores.

ALTER TABLE mylobs ADD( mybfile BFILE )

Após isso é necessário registrar um atalho para um diretório no Oracle e dar permissões de leitura a este.
CREATE DIRECTORY IMAGES_DIR AS '/home/public_html/images'
GRANT READ ON DIRECTORY IMAGES_DIR TO usuario_do_banco

Agora é possível inserir BFILEs com:



Se for necessário, é possível ler o conteúdo de um BFILE através do Oracle utilizando a mesma maneira realizada com os CLOBs. Alternativamente, se for necessário buscar o nome dos arquivos, é possível acessar diretamente do sistema de arquivos, utilizando uma chamada à função DBMS_LOB.FILEGETNAME:



Ainda é possível utilizar a função DBMS_LOB.FILEEXISTS para descobrir quais arquivos foram deletados pelo sistema operacional mas ainda estão referenciados no banco da dados.

Conclusão
O texto original em inglês foi escrito pelo desenvolvedor Harry Fuecks e está disponível no site da Oracle.
No link há outros artigos sobre integração PHP e Oracle.

Nenhum comentário:

Postar um comentário