Importação e exportação de dados no Oracle

Que saudade do MySQL.

Se usar um console interativo altamente capenga como o SQLPlus não te faz odiar o Oracle, os mecanismos de importação e exportação de dados certamente farão.

Como eu descobri recentemente que era possível, via SQL, descobrir a DDL original de uma tabela, pensei: deve ser possível exportar a definição de todas as tabelas, índices, seqüências etc. para um arquivo SQL, certo?

Até aí sim, tudo certo, a coisa funciona. É só digitar uma seqüência super simples de comandos: desligar a paginação, configurar o tamanho das linhas, definir o arquivo de saída, definir quando a saída deve ser impressa em arquivo, quando ela deve deixar de ser, além de dizer que, sim, senhor Oracle, você deve colocar um ponto-e-vírgula ao fim de cada instrução (pra quê mais eu estaria pedindo a DDL, pra colar na minha testa?).

Bom, o problema é que não fica muito claro que essas definições não podem ser utilizadas para criar um novo banco de dados, pois as tabelas não podem ser importadas em uma ordem tal que as restrições de integridade entre as tabelas sejam respeitadas no momento de sua re-criação (se duvida, saiba que isso já é sabido desde 2003 por consultores muito bons em Oracle). Minhas buscas pelo Google me deixaram concluir que nada mudou de lá pra cá.

Então, como exportar e importar “coisas” no Oracle, de verdade? exp e imp.

O exp serve para exportar objetos e dados. Para descobrir todas as opções possíveis do comando, faça:

$ exp help=yes

Se você quiser exportar tudo o que existe em uma schema/usuário, faça:

$ exp usuario/senha@//host/banco owner=\(schema-que-voce-quer-exportar\) file=dump.dos.dados

Então o Oracle exporta tudo o que ele conseguir para o arquivo dump.dos.dados. Ah, você achou que ia conseguir olhar o código SQL desse arquivo, né? Esqueça, o arquivo é binário. Valeu, Oracle! O procedimento pode não funcionar corretamente para todos os seus objetos (hoje, por exemplo, todas as minhas visões foram corrompidas, mas por serem visões nem me incomodou muito).

Bom, ao menos o Oracle te deixa exportar tudo menos os dados (ideal quando você começar tudo do zero em uma nova base):

$ exp usuario/senha@//host/banco owner=\(schema-que-voce-quer-exportar\) rows=no file=dump.dos.dados

Depois de exportar, cedo ou tarde você precisará importar os dados. Aí entra o imp. Você deve indicar de qual schema você quer importar (eu já não disse isso para o exp?), e para onde quer importar:

$ imp usuario/senha@//host/banco fromuser=schema-de-onde-voce-exportou touser=schema-para-onde-deseja-importar file=dump.dos.dados

Não é garantido que o Oracle conseguirá importar tudo, fique de olho nas mensagens que ele vai exibir durante o processo (coisas como “erros de compilação” não podem ser bom sinal).

Para saber quais as opções válidas do imp, faça:

$ imp help=yes

De hoje em diante, sempre que eu puder escolher, já sei que SGBD não vou utilizar em um projeto.


Configurando o SQLPlus

O SQLPlus possui um mecanismo quase ideal para carregar configurações de usuário durante o login, que podem ser definidas para todos os usuários ou para um usuário individual.

Quando um usuário abre o SQLPlus, o Oracle procura por dois arquivos: $ORACLE_HOME/sqllplus/admin/glogin.sql e login.sql. Se o primeiro for encontrado, as instruções encontradas no arquivo serão executadas. Esse é o arquivo onde você deve colocar configurações que serão aplicadas às sessões de todos os usuários. O segundo arquivo, login.sql, é procurado no diretório corrente, e caso encontrado, suas instruções também são executadas. Essa ordem de procura e execução dos scripts faz com que as instruções encontradas em login.sql tenham precedência sobre aquelas encontradas em glogin.sql.

Esses arquivos foram bastante úteis para mim quando precisei configurar o formato padrão para exibição de datas nas minhas sessões do SQLPlus. Adicionei o seguinte comando no script login.sql:

alter session set NLS_DATE_FORMAT='DD/MM/YYYY HH24:MI:SS';

Por motivos inerentes ao projeto onde trabalho não pude inserir esse comando em glogin.sql, e portanto, para que essas configurações locais sejam carregadas é preciso abrir o SQLPlus sempre a partir do diretório onde login.sql foi posicionado. Isso é chato, mas não há como configurar onde o Oracle irá procurar esse arquivo.


Selecionando os últimos registros de uma tabela Oracle

Como recuperar os N últimos registros de uma tabela Oracle? A consulta abaixo dá conta do recado.

select
    *
from
    (select
        rownum a,
        coluna1,coluna2,...,columnn
    from
        minha_tabela)
where
    a > (select
            (max(rownum) - N)
         from
             minha_tabela);

Você deve trocar:

  • “coluna1,coluna2,…,colunan” pelas colunas que deseja visualizar.
  • “N” pela quantidade de registros que deseja visualizar.
  • “minha_tabela” pela tabela que deseja visualizar.

Peguei a dica aqui.

Por último, nesse caso, quero dizer os últimos registros por ordem de inserção. Deve haver outras formas estranhas de fazer a mesma coisa, mas não parece existir nenhuma forma simples.

Se você quisesse os N primeiros então a coisa seria bem mais prática:

select * from minha_table where rownum <= N;

Modificando uma tabela Oracle para conversar com o Rails

Minha saga e dos meus colegas de projeto com o Oracle continua. Estamos tentando configurar um framework web (o Rails) para que ele interfira o mínimo possível na definição original das tabelas do nosso banco de dados. Ela é uma base Oracle com convenções próprias que, idealmente, não deveria ter seu esquema modificado de forma a agradar nenhum framework. Não deveria.

Mas eis que o Rails não suporta tabelas com chaves primárias compostas (e a extensão Composite Primary Keys, que resolveria o problema, atualmente não está funcionando). Para quem não tem muita experiência com banco de dados, vou ilustrar com um exemplo de uma tabela que possui uma chave primária composta de 2 colunas (e não 1, que seria o ideal para se trabalhar com o Rails).

Em uma instituição de ensino, por exemplo, poderíamos ter alunos e cursos (disciplinas) vinculados entre si por meio da seguinte tabela:

create table matricula (
    cd_estudante int REFERENCES estudante (cd_estudante),
    cd_curso int REFERENCES curso (cd_curso),
    nota number,
    CONSTRAINT matricula_pk PRIMARY KEY (cd_estudante, cd_curso)
);

Nossa chave primária é, portanto, composta. Para que o Rails consiga interagir com essa tabela é necessário que ela tenha 1 única chave primária (neste caso, uma chave artificial, que chamarei de cd_artificial). Os campos cd_estudante e cd_curso podem continuar formando uma dupla de campos únicos, com a adição de uma restrição de integridade. Nossa tabela “Rails friendly” deveria ter, portanto, a seguinte definição:

create table matricula (
    cd_artificial int,
    cd_estudante int REFERENCES estudante (cd_estudante),
    cd_curso int REFERENCES curso (cd_curso),
    nota number,
    CONSTRAINT matricula_pk UNIQUE (cd_estudante, cd_curso)
);

O problema do nosso projeto é que algumas das tabelas que precisam sofrer essa metamorfose já possuem milhões de registros (ou seja, definir uma nova tabela, migrar os dados da antiga para a nova e remover a antiga custa muito tempo).

Nossa única regalia é poder parar o sistema para realizar as modificações. O que segue abaixo, portanto, é um passo-a-passo de como transformar aquela primeira definição de matricula na segunda, garantindo que, após o procedimento:

  • Todos os registros tenham um cd_artificial populado corretamente
  • Qualquer código-fonte existente que manipule a tabela continue funcionando, pois não precisará saber da existência de cd_artifical
  • Novos registros possam ser inseridos tanto a partir da aplicação web, quanto a partir do sql*plus, sem que um valor para o campo cd_artificial precise ser definido explicitamente.

Para ilustrar melhor o exemplo, apresento agora a definição de todas as 3 tabelas do sistema fictício, e também os registros inseridos para possibilitar o teste do procedimento.

create table estudante (
    cd_estudante int primary key,
    nm_estudante char(30)
);

create table curso (
    cd_curso int primary key,
    nm_curso char(30)
);

create table matricula (
    cd_estudante int references estudante (cd_estudante),
    cd_curso int references curso (cd_curso),
    nota number,
    constraint matricula_pk primary key (cd_estudante, cd_curso)
);

insert into estudante values (1, 'Caio');

insert into curso values (1, 'AWK');
insert into curso values (2, 'PHP');
insert into curso values (3, 'Python');
insert into curso values (4, 'Ruby');

insert into matricula values (1, 1, 5);
insert into matricula values (1, 2, 9);

Observe as colunas e os registros da tabela matricula, para poder comparar com a versão final:

SQL> select * from matricula;

CD_ESTUDANTE   CD_CURSO       NOTA
------------ ---------- ----------
           1          1          5
           1          2          9

Lembre-se de que os passos a seguir se aplicam somente ao Oracle. Se você estiver utilizando MySQL o procedimento é muito mais simples, e não envolve a criação nem de uma seqüência e nem de um gatilho. Provavelmente no PostgreSQL as coisas devem ser mais simples, mas não tenho o conhecimento necessário para dizer com certeza.

Vamos aos passos.

Passo 1 - Remover a restrição de chave primária de cdestudante,cdcurso

Para que a nova chave primária possa ser definida, a antiga deve ser eliminada.

alter table matricula drop constraint matricula_pk;

Passo 2 - Definir a restrição unique para cdestudante,cdcurso

A nossa antiga chave primária garantia que um mesmo aluno não poderia ser vinculado ao mesmo curso mais de uma vez. Como a chave foi eliminada, essa restrição de integridade não existe mais. Precisamos defini-la novamente.

alter table matricula add constraint matricula_pk unique(cd_estudante,cd_curso);

Passo 3 - Adicionar a coluna cd_artificial na tabela

Agora podemos adicionar a coluna que se tornará nossa nova chave primária.

alter table matricula add cd_artificial numeric(10);

Note que, neste momento, a coluna cd_artificial não possui valores para nenhum dos registros de matricula:

CD_ESTUDANTE   CD_CURSO       NOTA CD_ARTIFICIAL
------------ ---------- ---------- -------------
           1          1          5
           1          2          9

Passo 4 - Popular a coluna cd_artificial

Precisamos, portanto, popular a coluna recém-criada. Neste caso queremos atribuir valores a partir de 1 até o número total de registros da tabela.

update matricula set cd_artificial = rownum;

Veja o resultado:

CD_ESTUDANTE   CD_CURSO       NOTA CD_ARTIFICIAL
------------ ---------- ---------- -------------
           1          1          5             1
           1          2          9             2

Passo 5 - Adicionar a restrição de chave primária à coluna cd_artificial

Agora que a nova coluna já possui dados, ela pode se tornar nossa chave primária.

alter table matricula add constraint matricula_pk primary key (cd_artificial);

Passo 6 - Definir a seqüência que populará o campo cd_artificial

Campos auto incrementais não existem nativamente no Oracle da forma como existem no MySQL. Eles precisam ser definidos com um pouco mais de esforço: deve-se criar uma seqüência e um gatilho.

A seqüência fica responsável por guardar, de forma transacional, um número atual, e gerar um próximo número quando solicitada. Neste caso, como nossa tabela já possui 2 registros, precisamos que a seqüência inicie em 3:

create sequence matricula_seq start with 3;

Passo 7 - Popular o campo cd_artificial automaticamente

O gatilho (trigger) será responsável por interceptar a inserção de um registro na tabela matricula. Todo registro terá seu campo cd_artificial definido como matricula_seq.nextval antes da inserção (before insert), sem que seja preciso defini-lo explicitamente.

create or replace trigger matricula_pk_trig
   before insert
   on matricula
   for each row
begin
   select matricula_seq.nextval
   into :new.cd_artificial
   from dual;
end;
/ -- Esta contra-barra marca o fim da declaração da trigger.

Na verdade, é possível forçar um valor diferente de matricula_seq.nextval para o novo registro, desde que esse novo valor não viole a restrição de integridade da chave primária.

Para testar o gatilho, vamos inserir um novo registro que não especifica cd_artificial:

SQL> insert into matricula (cd_estudante,cd_curso,nota) values (1,3,7);

SQL> select * from matricula;

CD_ESTUDANTE   CD_CURSO       NOTA CD_ARTIFICIAL
------------ ---------- ---------- -------------
           1          1          5             1
           1          2          9             2
           1          3          7             3

Pronto! Agora o Rails será capaz de manipular essa tabela sem problemas.


Sequence CurrVal no Oracle

Estou tentando construir uma aplicação Rails sobre uma base de dados Oracle já construída, com suas próprias convenções de nome, sem chaves artificiais para tabelas que fazem relacionamentos NxM… enfim, estou tendo um certo trabalho. A pior parte, no entanto, é que não sou aquilo que se pode chamar de “expert” em Oracle, muito pelo contrário. E foi aí que começaram meus problemas na tarde de hoje.

Eu estava tentando entender como o Rails gerenciava um campo “auto incremental” no Oracle. Para quem não sabe, esse conceito de “auto incremental” é recorrente no MySQL, onde existe uma instrução específica pra isso. No MySQL, você marca uma chave primária como um campo inteiro, auto incremental, e nunca mais se preocupa. Acontece que no Oracle não é bem assim. Você precisa simular esse comportamento em alguns passos.

O primeiro passo é criar uma tabela que possua como chave primária um campo de tipo inteiro. Por exemplo (todos os comandos SQL do artigo são executados diretamente na péssima interface interativa do Oracle, o sqlplus):

create table students (id int, name char(255));

O segundo passo é criar uma seqüência. O que realmente é uma seqüência não é muito importante neste momento: basta entender que ela guarda um valor corrente, e que é capaz de gerar seu próximo valor. O seguinte comando cria a seqüência que eu utilizei para popular o campo id de todos os registros da tabela student.

create sequence students_seq;

Para inserir um registro em students com o campo id definido por students_seq devemos utilizar a propriedade nextval da seqüência, que retorna o próximo valor da mesma e o define como seu valor atual. Veja o exemplo:

insert into students (id,name) values (students_seq.nextval, 'Caio');
> 1 registro criado.
select id from students;
> 1
select students_seq.currval from dual;
> 1
commit;

Ou seja, tudo funcionando como esperado. Para interagirmos com essa tabela a partir de uma aplicação Rails seria preciso apenas definir a seguinte classe em app/models/student.rb:

class Student < ActiveRecord::Base
end

Podemos agora interagir com nossa base Oracle a partir do console do Rails. Vejamos os seguintes comandos no console:

>> Student.find :all
=> [#<Student id: 1, name: "Caio                                               ...">]
>> joao = Student.new(:name => 'Joao')
=> #<Student id: nil, name: "Joao">
>> joao.save
=> true
>> joao.id
=> 2

Na primeira linha recuperamos todos os registros, e nos é retornado apenas um, que criamos anteriormente diretamente via Oracle. Na terceira linha definimos um novo objeto, e na quinta linha salvamos esse objeto no banco. Nesse momento, a consulta que o Rails executou foi, na prática, a seguinte:

insert into students (id,name) values (students_seq.nextval, 'Joao');

Ou seja, antes da inserção o valor corrente de students_seq avançou 1 unidade, tornando-se 2. Esse valor foi retornado para o comando insert, e Joao ganhou um id igual a 2.

Se agora abrirmos o console do Oracle, você esperaria que o valor corrente de students_seq fosse 2, certo? Mas veja o que o Oracle nos diz:

select students_seq.currval from dual;
> A seqüência students_seq.currval ainda não foi definida nesta sessão

De início não dei muita importância pra essa mensagem, pensei: “mas como, se o Rails atribuiu valor 2 pro último registro, essa consulta TEM que retornar 2!”. Mas a mensagem do Oracle diz tudo o que precisamos saber (embora de uma forma que lembra uma mensagem de erro genérica que você também gera para os seus sistemas quando está com preguiça).

O problema é que o currval de uma seqüência realmente depende da sessão onde estamos. Por sessão, neste caso, entende-se o contexto em que as consultas estão sendo executadas. Se eu abro um console do Oracle (sqlplus) crio uma sessão. Se o fecho e o abro de novo, acabei de gerar uma nova sessão.

Na sessão Oracle mantida pelo console do Rails, esse valor certamente é 2. Mas esse valor não é o mesmo nas outras sessões, acredito eu por conta do controle de concorrência. Pois se, enquanto eu escrevo este artigo, o nextval da seqüência foi solicitado 20 vezes por outras transações, é claro que currval não poderá continuar sendo 2. Além disso, calcular esse valor real não me parece trivial e nem muito útil - talvez por isso o Oracle exiba apenas o valor da sessão.

select students_seq.nextval from dual;
> 3

Como o Oracle me informou “3″, sei que nenhuma outra sessão manipulou essa seqüência nesse meio tempo. Para quem entende de Oracle isso certamente era óbvio, mas pra mim demorou um tempo pra cair a ficha.