Como customizar queries

De Wiki Expresso V3
Ir para: navegação, pesquisa

Esta é uma implementação de referência para explicar como funciona a abstração de queries, funcionalidade disponível no Tine 2.0 (issue 8914).

A seguir temos um exemplo usando a aplicação Catálogo de Endereços (Addressbook).

A classe Addressbook_Backend_Sql tem o método getByUserId():

    /**
     * fetch one contact of a user identified by his user_id
     *
     * @param   int $_userId
     * @return  Addressbook_Model_Contact
     * @throws  Addressbook_Exception_NotFound if contact not found
     */
    public function getByUserId($_userId)
    {
        $select = $this->_getSelect()
            ->where($this->_db->quoteIdentifier('accounts.id') . ' = ?', $_userId)
            ->limit(1);
       
        Tinebase_Backend_Sql_Abstract::traitGroup($select);
       
        $stmt = $this->_db->query($select);
        $queryResult = $stmt->fetch();
        $stmt->closeCursor();
               
        if (!$queryResult) {
            throw new Addressbook_Exception_NotFound('Contact with user id ' . $_userId . ' not found.');
        }
       
        $contact = $this->_rawDataToRecord($queryResult);
               
        return $contact;
    }


Este método usa o método herdado _getSelect() para criar a expressão SELECT. A consulta gerada não é apropriada para todos os bancos de dados. Originalmente, todas as consultas do Tine 2.0 funcionam para MySQL, pois este foi o banco com o qual o projeto foi utilizado desde o início. Quando o PostgreSQL foi introduzido, a consulta não funcionava porque ela omite colunas usadas na cláusula GROUP BY. Por causa disso, foi criado o método traitGroup() de Tinebase_Sql_Abstract, para modificar a consulta e torná-la funcional para pelo menos três bancos (MySQL, PostgreSQL e Oracle). Esta solução foi aplciada porque o método _getSelect() é muito genérico é não retorna a melhor consulta possível, porque ele pode receber outras partes. Mas realmente, o uso de traitGroup() ainda não é a melhor solução. No momento em que uma grande mudança arquitetural puder ser realizada, ele deve ser eliminado pela correta construção de consultas, aderente ao padrão SQL ANSI.


O exemplo de customização de consulta será feito então de modo a eliminar, para o banco de dados PostgreSQL, a necessidade de usar o método traitGroup(). Assim o método getUserById() seria modificado para ficar deste jeito:

    public function getByUserId($_userId)
    {
        $contact = $this->_rawDataToRecord(Addressbook_Backend_Sql_Query::factory($this->_db)->getQueryByUserId($_userId, $this->_getSelect()));
               
        return $contact;
    }

Observe que está sendo usado um método de fábrica para consultas. A implementação desse método é descrita a seguir. Essa chamada é um modelo que você deve usar, que pode estar presente em cada aplicação que necessite de customização de consultas. A classe Addressbook_Backend_Sql_Query seleciona um adaptador para gerar a consulta apropriada. Ela herda métodos comuns para este tipo de classe

class Addressbook_Backend_Sql_Query extends Tinebase_Backend_Sql_Factory_Abstract
{
}


O relato 8914 gerou uma generalização de classes de fábrica para customização de comandos em banco de dados na aplicação Tinebase:

abstract class Tinebase_Backend_Sql_Factory_Abstract
{
    protected static $_instances = array();
   
    /**
     * @param Zend_Db_Adapter_Abstract $adapter
     * @return mixed
     */
    public static function factory(Zend_Db_Adapter_Abstract $adapter)
    {     
        $className = get_called_class() . '_' . self::_getClassName($adapter);
       
        // @todo find better array key (add loginname and host)
        if (!isset(self::$_instances[$className])) {
            self::$_instances[$className] = new $className($adapter);
        }
       
        return self::$_instances[$className];
    }
   
    /**
     *
     * @return Zend_Db_Adapter_Abstract
     */
    public static function getDb()
    {
       return self::$_db;
    }
   
    /**
     *
     * @param Zend_Db_Adapter_Abstract $adapter
     * @return string
     */
    private static function _getClassName($adapter)
    {
        $completeClassName = explode('_',get_class($adapter));
        $className = $completeClassName[count($completeClassName)-1];
        $className = str_replace('Oci','Oracle',$className);
       
        return $className;
    }
}


Esta classe é usada por Tinebase_Backend_Sql_Command, que já existia. Ela permite que cada aplicação tenha uma fábrica de consultas.

Vamos criar uma generalização em Addressbook que mantenha a implementação original de getByUserId():

abstract class Addressbook_Backend_Sql_Query_Abstract extends Tinebase_Backend_Sql_Query_Abstract implements
Addressbook_Backend_Sql_Query_Interface
{
   /**
    * @param int $userId
    * @param Zend_Db_Select $select
    */   
   public function getQueryByUserId($userId, $select)
   {
      $select->where($this->_db->quoteIdentifier('accounts.id') . ' = ?', $userId)
      ->limit(1);
      
      Tinebase_Backend_Sql_Abstract::traitGroup($select);
      
      $stmt = $this->_db->query($select);
      $queryResult = $stmt->fetch();
      $stmt->closeCursor();
      
      if (!$queryResult) {
         throw new Addressbook_Exception_NotFound('Contact with user id ' . $userId . ' not found.');
      }
      
      return $queryResult;      
   }
   
}

Esta classe herda uma implementação genérica de Tinebase para consultas customizadas:

abstract class Tinebase_Backend_Sql_Query_Abstract
{
   /**
    *
    * @var Zend_Db_Adapter_Abstract
    */
   protected $_db;
   /**
    *
    * @param Zend_Db_Adapter_Abstract $db
    */
   public function __construct(Zend_Db_Adapter_Abstract $db)
   {
      $this->_db = $db;
   }   
}

Assim, mantemos o mesmo código para MySQL e Oracle (por enquanto) apenas estendendo Addressbook_Backend_Sql_Query_Abstract:

class Addressbook_Backend_Sql_Query_Mysql extends Addressbook_Backend_Sql_Query_Abstract
{   
}


class Addressbook_Backend_Sql_Query_Abstract implements Addressbook_Backend_Sql_Query_Interface
{   
}

Então temos a oportunidade de alterar completamente a consulta para PostgreSQL:

class Addressbook_Backend_Sql_Query_Abstract extends Tinebase_Backend_Sql_Query_Abstract implements Addressbook_Backend_Sql_Query_Interface
{
   /**
    * @param int $userId
    * @param Zend_Db_Select $select
    */   
   public function getQueryByUserId($userId, $select)
   {
      // overwrites object with string
      $select = <<<SQL
SELECT "addressbook"."id", MIN("addressbook"."adr_one_countryname") AS "adr_one_countryname", MIN("addressbook"."adr_one_locality") AS "adr_one_locality", MIN("addressbook"."adr_one_postalcode") AS "adr_one_postalcode", MIN("addressbook"."adr_one_region") AS "adr_one_region", MIN("addressbook"."adr_one_street") AS "adr_one_street", MIN("addressbook"."adr_one_street2") AS "adr_one_street2", MIN("addressbook"."adr_one_lon") AS "adr_one_lon", MIN("addressbook"."adr_one_lat") AS "adr_one_lat", MIN("addressbook"."adr_two_countryname") AS "adr_two_countryname", MIN("addressbook"."adr_two_locality") AS "adr_two_locality", MIN("addressbook"."adr_two_postalcode") AS "adr_two_postalcode", MIN("addressbook"."adr_two_region") AS "adr_two_region", MIN("addressbook"."adr_two_street") AS "adr_two_street", MIN("addressbook"."adr_two_street2") AS "adr_two_street2", MIN("addressbook"."adr_two_lon") AS "adr_two_lon", MIN("addressbook"."adr_two_lat") AS "adr_two_lat", MIN("addressbook"."cat_id") AS "cat_id", MIN("addressbook"."assistent") AS "assistent", MIN("addressbook"."bday") AS "bday", MIN("addressbook"."calendar_uri") AS "calendar_uri", MIN("addressbook"."email") AS "email", MIN("addressbook"."email_home") AS "email_home", MIN("addressbook"."freebusy_uri") AS "freebusy_uri", MIN("addressbook"."geo") AS "geo", MIN("addressbook"."label") AS "label", MIN("addressbook"."note") AS "note", MIN("addressbook"."container_id") AS "container_id", MIN("addressbook"."private") AS "private", MIN("addressbook"."pubkey") AS "pubkey", MIN("addressbook"."role") AS "role", MIN("addressbook"."room") AS "room", MIN("addressbook"."tid") AS "tid", MIN("addressbook"."salutation") AS "salutation", MIN("addressbook"."title") AS "title", MIN("addressbook"."tz") AS "tz", MIN("addressbook"."url") AS "url", MIN("addressbook"."url_home") AS "url_home", MIN("addressbook"."n_family") AS "n_family", MIN("addressbook"."n_fileas") AS "n_fileas", MIN("addressbook"."n_fn") AS "n_fn", MIN("addressbook"."n_given") AS "n_given", MIN("addressbook"."n_middle") AS "n_middle", MIN("addressbook"."n_prefix") AS "n_prefix", MIN("addressbook"."n_suffix") AS "n_suffix", MIN("addressbook"."org_name") AS "org_name", MIN("addressbook"."org_unit") AS "org_unit", MIN("addressbook"."tel_assistent") AS "tel_assistent", MIN("addressbook"."tel_car") AS "tel_car", MIN("addressbook"."tel_cell") AS "tel_cell", MIN("addressbook"."tel_cell_private") AS "tel_cell_private", MIN("addressbook"."tel_fax") AS "tel_fax", MIN("addressbook"."tel_fax_home") AS "tel_fax_home", MIN("addressbook"."tel_home") AS "tel_home", MIN("addressbook"."tel_other") AS "tel_other", MIN("addressbook"."tel_pager") AS "tel_pager", MIN("addressbook"."tel_prefer") AS "tel_prefer", MIN("addressbook"."tel_work") AS "tel_work", MIN("addressbook"."created_by") AS "created_by", MIN("addressbook"."creation_time") AS "creation_time", MIN("addressbook"."last_modified_by") AS "last_modified_by", MIN("addressbook"."last_modified_time") AS "last_modified_time", MIN("addressbook"."is_deleted") AS "is_deleted", MIN("addressbook"."deleted_by") AS "deleted_by", MIN("addressbook"."deleted_time") AS "deleted_time", MIN("addressbook"."seq") AS "seq", MIN("addressbook"."type") AS "type", MIN((CASE WHEN "addressbook_image"."contact_id" IS NULL THEN 0 ELSE 1 END)) AS "jpegphoto", MIN("accounts"."id") AS "account_id" FROM "tine20_addressbook" AS "addressbook"
 LEFT JOIN "tine20_addressbook_image" AS "addressbook_image" ON "addressbook"."id" = "addressbook_image"."contact_id"
 LEFT JOIN "tine20_accounts" AS "accounts" ON "addressbook"."id" = "accounts"."contact_id" WHERE ("addressbook"."is_deleted" = 0) AND ("accounts"."id" = ?) GROUP BY "addressbook"."id" LIMIT 1
SQL;
    
      $stmt = $this->_db->query($select, array($userId));
      $queryResult = $stmt->fetch();
      $stmt->closeCursor();
      
      if (!$queryResult) {
         throw new Addressbook_Exception_NotFound('Contact with user id ' . $userId . ' not found.');
      }
      
      return $queryResult;      
   }
   
}


Veja que neste método não precisamos ajudar a consulta com Tinebase_Backend_Sql_Abstract::traitGroup() e a consulta está legível. Não precisamos de um log para saber imediatamente o que será feito.

Usamos Addressbook como um exemplo, embora a mudança aqui mostrada possa ser implementada sem problemas, pois já foi testada pelo ambiente de integração contínua do Tine 2.0.


Tunning

Ferramentas pessoais
Espaços nominais

Variantes
Ações
Navegação
Ferramentas