sexta-feira, 9 de setembro de 2011

Resolvendo Problemas com Aspas Duplas do Zend_Db_Table_Abstract

Resumo,

Quando você tentar apontar o Zend_Db_Table_Abstract do Zend Framework com o Oracle, para gerar comandos SQL é possível que você enfrente o mesmo problema que nós enfrentamos, aspas duplas aparecem e fazem seus comandos SQL inválidos. Para resolver isso adicione  a seguinte linha:

resources.db.params.options.autoQuoteIdentifiers = 0

no seu application.ini e seja feliz.

Versão Longa,

Quando o Zend_Db_Table_Abstract gera erros ao tentar executar seus comandos SQL triviais, gerados automaticamente, qual é o 1º passo? Eu costumo gerar o assemble() do comando SQL para poder testa-lo diretamente no banco de dados. 

Como o problema que temos é com qualquer método, então testamos no mais simples, sobrescrevendo o getAll e fazendo isso:

public function getAll($where = null, $order = null, $count = null, $offset = null)
{
var_export( $this->select()->assemble() );
exit();
}

Então obtemos:

SELECT "tb_estado".* FROM "db_projeto"."tb_estado"

Sabemos agora qual é o problema: As consultas geradas automaticamente pelo Zend estão colocando aspas duplas no nome das tabelas e do schema, o que não é aceito no Oracle.

Após pesquisar um pouco achei esse link http://doczf.mikaelkael.fr/1.10/pt-br/zend.db.select.html que informa:

O método quoteIdentifier() usa aspas no SQL para delimitar o identificador, o que deixa claro que ele é um identificador de uma tabela ou coluna e não parte da síntaxe SQL.

Agora qual é o problema: As consultas geradas automaticamente pelo Zend estão colocando aspas duplas no nomes devido ao método _quoteIdentifier, que funciona do seguinte modo:

1009     /**
1010      * Quote an identifier.
1011      *
1012      * @param  string $value The identifier or expression.
1013      * @param boolean $auto If true, heed the AUTO_QUOTE_IDENTIFIERS config option.
1014      * @return string        The quoted identifier and alias.
1015      */
1016     
protected function _quoteIdentifier($value$auto=false)
1017     {
1018         if (
$auto === false || $this->_autoQuoteIdentifiers === true) {
1019             
$q $this->getQuoteIdentifierSymbol();
1020             return (
$q str_replace("$q""$q$q"$value) . $q);
1021         }
1022         return 
$value;
1023     } 

O nosso problema agora tem um foco bem mais específico. Como fazer o atributo protegido _autoQuoteIdentifiers ser falso. Logicamente se o atributo fosse público ou ao menos tivesse um setAutoQuoteIndentifiers ou algo similar, estaria agora resolvido o problema, na nossa classe pai que herda o Zend_Db_Table_Abstract, algo muito parecido com:

    public function init()
    {
$this->_db->setAutoQuoteIndentifiers( false ); 
    }

Mas, para nossa tristeza e agonia esse método setter não existe. Então precisamos descobrir como esse atributo recebe um valor.

Fazendo uma busca na classe, vemos que esse atributo pode ter seu valor atribuído caso ele venha no Array que é recebido no método construtor da classe 

238 
239         
// obtain quoting property if there is one
240         
if (array_key_exists(Zend_Db::AUTO_QUOTE_IDENTIFIERS$options)) {
241             
$this->_autoQuoteIdentifiers = (bool) $options[Zend_Db::AUTO_QUOTE_IDENTIFIERS];
242         }
243 

Tentando entender como essa variável $options é montada, vemos que essa variável é um Array que é inicializado com os valores padrões:

185         $options = array(
186             
Zend_Db::CASE_FOLDING           => $this->_caseFolding,
187             
Zend_Db::AUTO_QUOTE_IDENTIFIERS => $this->_autoQuoteIdentifiers
188         
);

Mas que tem esses valores alterados conforme os elementos que são recebidos no campo "options" do $config. Sendo $config o Array que é recebido no construtor.

190 
191         
/*
192          * normalize the config and merge it with the defaults
193          */
194         
if (array_key_exists('options'$config)) {
195             
// can't use array_merge() because keys might be integers
196             
foreach ((array) $config['options'] as $key => $value) {
197                 
$options[$key] = $value;
198             }
199         }

Procurando então em que local do Zend_Db_Table_Abstract que é instanciado o atributo $this->_db
Chegamos nas seguintes linhas:

575     
/**
576      * @param  mixed $db Either an Adapter object, or a string naming a Registry key
577      * @return Zend_Db_Table_Abstract Provides a fluent interface
578      */
579     
protected function _setAdapter($db)
580     {
581         
$this->_db self::_setupAdapter($db);
582         return 
$this;
583     } 

Seguindo o rastro, vamos ao método _setupAdapter:

594 
595     
/**
596      * @param  mixed $db Either an Adapter object, or a string naming a Registry key
597      * @return Zend_Db_Adapter_Abstract
598      * @throws Zend_Db_Table_Exception
599      */
600     
protected static function _setupAdapter($db)
601     {
602         if (
$db === null) {
603             return 
null;
604         }
605         if (
is_string($db)) {
606             require_once 
'Zend/Registry.php';
607             
$db Zend_Registry::get($db);
608         }
609         if (!
$db instanceof Zend_Db_Adapter_Abstract) {
610             require_once 
'Zend/Db/Table/Exception.php';
611             throw new 
Zend_Db_Table_Exception('Argument must be of type Zend_Db_Adapter_Abstract, or a Registry key where a Zend_Db_Adapter_Abstract object is stored');
612         }
613         return 
$db;
614     } 

Vemos então que esse método consulta o Zend_Registry, o que já era esperado. Sem querer entrar na complexidade desse Registry, nós sabemos que o Zend_Registry do Banco de Dados é informado no application.ini. É lá que informamos dados como login, senha, nome do banco de dados, etc. Então, será que os dados que o construtor do Zend_Db_Table_Abstract apenas lê os dados vindos do application.ini, na parte dos "resources.db"? 

Para testar essa hipótese eu vou imprir o $config do construtor do Zend_Db_Table_Abstract. Como qualquer alteração feita no código do Zend Framework isso é apenas para auxiliar o nosso entendimento e deverá ser desfeita em breve.

Ao adicionar no construtor a seguinte linha:

    public function __construct($config)
    {
var_export( $config );
exit();
// (...)

Obtemos o seguinte resultado:

array (
  'host' => 'banco_do_projeto',
  'username' => 'usuario_do_servidor',
  'password' => '******',
  'dbname' => 'db_projeto',
  'charset' => 'utf8',
  'profiler' => 
  array (
    'enabled' => '1',
  ),
)

Precisamos adicionar uma linha no nosso application.ini para testar se essa chegará até o $config. Mas qual seria o valor dessa linha. Por semelhança aos demais elementos que chegaram, deve ser algo similar a resources.db.params.options.xxxxxxxxxxx = 0. Mas, para encontrarmos o valor que desejamos, precisamos saber qual é o valor da chave. Esse valor nós podemos ver aqui:

185         $options = array(
186             
Zend_Db::CASE_FOLDING           => $this->_caseFolding,
187             
Zend_Db::AUTO_QUOTE_IDENTIFIERS => $this->_autoQuoteIdentifiers
188         
); 


Usando o CRTL+Click da IDE, já fui direto para a declaração desta constante, conforme:

const AUTO_QUOTE_IDENTIFIERS 'autoQuoteIdentifiers'

Logo a nossa nova linha no application.ini, deve ser:

resources.db.params.options.autoQuoteIdentifiers = 0

Obtemos o seguinte resultado:

array (
  'host' => 'banco_do_projeto',
  'username' => 'usuario_do_servidor',
  'password' => '******',
  'dbname' => 'db_projeto',
  'charset' => 'utf8',
  'options' => 
  array (
    'autoQuoteIdentifiers' => '1',
  ),
  'profiler' => 
  array (
    'enabled' => '1',
  ),
)

Esse novo elemento no $config vai alterar o atributo protegido _autoQuoteIdentifiers retirando as aspas dos SQL gerados e resolvendo o nosso problema.

Espero que isso tenha ajudado.

Um grande abraço.

Nenhum comentário: