Back-End

5 jan, 2015

Como e quando utilizar opções de consulta GORM

Publicidade

O Grails Object Relational Mapper (GORM) é um excelente produto ORM. O que começou como um Groovy DSL sobre o Hibernate e modelado com Grails evoluiu para uma persistência agnóstica, uma biblioteca Grails independente para trabalhar com bancos de dados.

O GORM usa design via API e um pouco da magia Groovy por ser especialmente amigável. Ele faz isso através de cinco mecanismos de consulta de dados muito poderosos:

Neste artigo, falarei um pouco sobre como funciona cada mecanismo e, talvez ainda mais importante, quando usar cada um deles. Mas, primeiro, um aviso: as informações apresentadas a seguir são baseadas na documentação GORM e confirmada por minhas próprias experiências. A última versão do Grails é a versão 2.3.7. Se eu perdi ou errei em alguma coisa, por favor me avise.

Com isso fora do caminho, aqui está o resumo de onde cada método deve ser usado:

Finders dinâmicos Cláusulas where Critérios HQL SQL
consultas simples X X X X X
filtros complexos X X X X
associações X X X X
comparações de propriedade X X X X
subconsultas X X X X
fetches com filtros complexos X X X
projeções X X X
consultas com conjuntos de retorno arbitrárias X X
consultas de alta complexidade (como junções automáticas) X
características específicas de banco de dados X
consultas com desempenho otimizado X

A instalação

1. Definir modelo de domínio

Antes que possamos entrar no âmbito do GORM, precisaremos definir alguns objetos de domínio para trabalhar:

gorm-1

Aqui, temos uma empresa e uma loja. A empresa fabrica os produtos, que são vendidos em lojas. Os detalhes de cada venda (qual produto foi vendido e que loja vendeu) são registrados em uma transação.

Aqui está o código:

class Company {
    String name
    String location
}
 
class Store {
    String name
    String city
    String state
}
 
class Product {
    String name
    Company manufacturer
    BigDecimal salesPrice
}
 
class Transaction {
    Product product
    Store store
    Date salesDate
    Integer quantity
}

2. Padrões de mudança GORM (opcional)

Esta é definitivamente uma questão de preferência pessoal, mas eu particularmente não gosto que:

  1. O GORM assuma todas as propriedades que não possam ser desfeitas por padrão e
  2. Ocorram falhas silenciosas e não seja exibido nenhum erro.

Também acho útil ver o SQL gerado pelo Hibernate enquanto estou desenvolvendo e testando meu aplicativo. Então, vou fazer os seguintes ajustes no arquivo Config.groovy e DataSource.groovy:

// *** in Config.groovy ***
// 1. make all properties nullable by default
grails.gorm.default.constraints = {
    '*'(nullable: true)
}
 
// 2. turn off silent GORM errors
grails.gorm.failOnError = true
 
 
// *** in DataSource.groovy ***
// 3. enable logging of Hibernate's SQL queries
test {
    dataSource {
        logSql = true
        // .... other settings
    }
}
 
development {
    dataSource {
        logSql = true
        // .... other settings
    }
}

Ok, estamos finalmente prontos para consultar alguns dados.

1. Finders dinâmicos

A maneira mais simples de consultar em GORM é usando finders dinâmicos, que são métodos em um objeto de domínio que começam com findBy, findAllBy e countBy. Por exemplo, podemos usar finders dinâmicos para fazer uma lista de produtos filtrados de diferentes maneiras:

Company ACME = Company.findByName('ACME')
Product.findAllByManufacturer(ACME)
Product.findAllByManufacturerAndSalesPriceBetween(ACME, 200, 500)

Também podemos criar contadores:

Product.countByManufacturer(ACME)

A coisa mais interessante sobre os finders dinâmicos é que eles realmente não existem no objeto de domínio. Em vez disso, o GORM usa Programação de Meta Objetos do Groovy (MOP) para interceptar chamadas para os métodos e construir consultas em tempo real.

Outra coisa importante a se destacar sobre os finders dinâmicos é que eles estão preguiçosamente carregados por padrão (o que pode deixar o sistema lento). IsSo pode ser alterado, especificando como determinados objetos específicos devem ser obtidos:

Product fluxCapacitor = Product.findByName('flux capacitor')
Transaction.findAllByProduct(fluxCapacitor, [fetch: [product: 'eager', store: 'eager']])

2. Cláusulas Where

Originalmente introduzidas no Grails 2.0, as cláusulas where nos garantem outra forma simples para consultar dados. Veja como os exemplos acima podem ser criados usando-as:

// Product.findAllByManufacturer(ACME)
Product.where {
    manufacturer == ACME
}.list()
 
// Product.findAllByManufacturerAndSalesPriceBetween(ACME, 200, 500)
Product.where {
    manufacturer == ACME && (salesPrice > 200 && salesPrice < 800)
}.list()
 
// Product.countByManufacturer(ACME)
Product.where {
    manufacturer == ACME
}.count()

Filtros complexos

Embora as cláusulas where façam as mesmas coisas que os finders dinâmicos, elas são definitivamente mais poderosas. Por exemplo, você pode definir mais condições de filtragem complexas.

Imagine que você deseja obter uma lista de todas as vendas que possuem 1 item vendido ou vendas de um produto específico durante um determinado período. Fazer isso com um finder dinâmico poderia ser semelhante a findAllByQuantityOrProductAndSalesDateBetween, mas isso na verdade não funciona. Em vez disso, vamos usar uma cláusula where:

Transaction.where {
    quantity == 1 ||
    (product == fluxCapacitor &&
        (salesDate >= '1/1/2014' && salesDate <= '1/10/2014')
    )
}.list()

Consultando associações

Outro lugar onde a cláusula where é extremamente útil é na consulta de associações. Por exemplo, para obter a lista de transações de um fabricante em específico, podemos fazer:

Transaction.where {
    product.manufacturer.name == 'ACME'
}.list()

Note que product.manufacturer faz referência a um objeto associado. Essa consulta resultará no seguinte join SQL:

FROM transaction this_
  INNER JOIN product product_al1_ ON this_.product_id = product_al1_.id
  INNER JOIN company manufactur2_ ON product_al1_.manufacturer_id = manufactur2_.id
WHERE manufactur2_.name = ?

Comparações de propriedade e subconsultas

Há dois outros casos de uso onde a cláusula where pode ser útil: comparação de propriedade e subconsultas:

// find stores named after the city they're located in
Store.where {
    name == city
}.list()
 
// find the largest sales of the flux capacitor
Transaction.where {
    quantity == max(quantity) && product == fluxCapacitor
}.list()

Gostaria de salientar que subqueries para a cláusula where são limitadas a projeções (ou seja, agregados como min, max  ou avg).

3. Critérios

Os dois métodos que cobrimos até agora são certamente simples e diretos, mas podem ser limitantes. Por exemplo, imagine que você queria obter uma lista de todos os produtos e as lojas foram vendidas para um determinado fabricante.

Para fazer isso de forma eficiente, você iria querer todas as informações do produto e da loja para ser recuperada em um tiro (ansiosamente). Infelizmente, cláusulas where (ainda) não permitem que você especifique quais objetos devem ser ansiosamente buscados. Felizmente, há uma maneira de fazer isso usando critérios:

Transaction.createCriteria().list {
    fetchMode 'product', FetchMode.JOIN
    fetchMode 'store', FetchMode.JOIN
 
    product {
        manufacturer {
            eq 'id', ACME.id
        }
    }
}

Nesse exemplo, estamos usando fetchMode do JOIN para indicar que produtos e propriedades devem ser recuperados. Também estamos usando uma condição aninhada para chegar ao fabricante correto.

Tenha em mente que os critérios GORM são atualmente DSL para critérios de construção Hibernate. Portanto, eles permitem que você construa consultas bastante sofisticadas.

Projeções

Além do JOIN, os critérios podem ser úteis para as projeções. As projeções são uma maneira de moldar ainda mais um conjunto de dados e são normalmente utilizadas para funções de agregação como sum(), count() e average().

Por exemplo, aqui está uma projeção que recebe as quantidades de produtos vendidos para um determinado fabricante:

Transaction.createCriteria().list {
    projections {
        groupProperty 'product'
        sum 'quantity'
    }
 
    product {
        manufacturer {
            eq 'id', ACME.id
        }
    }
}

Note que estamos criando uma cláusula de projeção e especificando o agregador (soma) no agrupamento (via groupProperty).

4. HQL

Finders dinâmicos, cláusulas where e critérios nos dão muito poder sobre o que e como podemos realizar consultas. No entanto, existem algumas situações em que precisamos de ainda mais poder, e é aí que entra o HQL.

Mas antes de falarmos sobre HQL e seus casos de uso, há uma coisa importante a destacar. Se você estiver usando as 3 primeiras formas de consulta, o GORM vai sempre lhe retornar objetos de domínio (a menos que você esteja usando contagens ou projeções). Isso não é necessariamente verdade se você estiver usando HQL.

Find() e FindAll()

O GORM proporciona dois modos de usar HQL. A primeira é usá-lo em combinação com os métodos de objeto de domínio find() ou findAll(). Se você estiver usando dessa maneira, estará essencialmente limitado a especificar a cláusula WHERE. Por exemplo:

Transaction.findAll('from Transaction as t where t.product.manufacturer.id = :companyId', [companyId: 1])

Aqui, estamos solicitando todas as transações de um determinado fabricante. Note que, assim como os outros métodos que discutimos até agora, usar HQL dessa forma ainda permite ao GORM retornar objetos de domínio.

Importante: esSe exemplo usa o nome mapeado ([CompanyID: 1]) para passar em parâmetros de consulta. Embora você também possa usar nomes mapeados posicionais, eu definitivamente prefiro nomes mapeados, porque eles são explícitos e você pode usar o mesmo parâmetro várias vezes em sua consulta sem ter que especificá-la novamente várias vezes.

ExecuteQuery()

Até agora, todos os métodos de consulta que usamos retornaram tipos de objetos de domínio. Isso é ótimo, mas às vezes você precisa de algo diferente. É aí que o método executeQuery() entra.

O GORM permite que você execute HQL arbitrário usando executeQuery(). Por exemplo, aqui está uma consulta que retorna nomes de lojas, o fabricante e o produto vendidos durante um determinado período de tempo:

String query = $/
    select
    s.name,
    m.name,
    p.name
    from Transaction as t
    inner join t.product as p
    inner join t.store as s
    inner join p.manufacturer as m
    where t.product.manufacturer.id = :companyId
    and t.salesDate between :startDate and :endDate
/$
 
List queryResults = Transaction.executeQuery(query,
        [companyId: ACME.id, startDate: new Date('1/1/2014'), endDate: new Date('1/31/2014')]
)

O que é realmente notável aqui é que podemos moldar o retorno ajustando os valores conforme precisarmos. Obviamente, isso faz com que seja impossível para o GORM nos dar os objetos de domínio certos, mas em alguns casos a troca é justificada.

Outro ponto que deve interessá-lo é que o conjunto de dados retornado por essa consulta é uma lista de arrays. Para torná-la mais útil, poderíamos processá-la durante o retorno e convertê-la em uma lista de mapas com propriedades nomeadas:

Transaction.executeQuery(query,
        [companyId: ACME.id, startDate: new Date('1/1/2014'), endDate: new Date('1/31/2014')]
).collect {
    [
            storeName: it[0],
            manufacturerName: it[1],
            productName: it[2]
    ]
}

A saída desSa consulta pode, por exemplo, ser facilmente serializada em um script JSON e processada como uma resposta de um controlador.

5. SQL

Bem ou mal, poucos desenvolvedores acreditam que o uso de um ORM significa nunca ter de olhar para o código SQL. Embora isso possa ser verdade para a grande maioria das consultas, certas situações exigem um olho crítico.

Considere uma consulta que quer comparar as vendas de todos os produtos para um determinado fabricante para um período de tempo semelhante no ano passado. EsSa consulta requer juntar a tabela de Transação para si (em diferentes intervalos de tempo).

O HQL faz um JOIN (incluindo auto JOIN) se houver uma associação definida entre os objetos. Em outras palavras, tínhamos necessidade de modificar uma classe de transação como esta:

class Transaction {
    Product product
    Store store
    Date salesDate
    Integer quantity
 
    Transaction baseline
}

Se fizéssemos isso, poderíamos então definir a seguinte consulta HQL:

String query = $/
    select
    t1.product.name,
    sum(t1.quantity),
    sum(t2.quantity)
    from Transaction as t1
    inner join t1.baseline as t2
    where t1.product.manufacturer.id = :companyId
    and t1.salesDate between :startDate and :endDate
    and t2.salesDate between :baselineStartDate and :baselineEndDate
    group by t1.product.name
/$

Agora, mesmo que isso seja possível, acho que é uma solução de mau gosto. Afinal de contas, fazer isso nos obriga a poluir o objeto de domínio com associações quase arbitrárias apenas para fazer o trabalho de uma consulta.

A outra opção é a utilização de SQL nativo:

String query = $/
SELECT p.name,
sum(t1.quantity),
sum(t2.quantity)
FROM transaction t1
LEFT OUTER JOIN transaction t2 ON t1.product_id = t2.product_id
INNER JOIN product p ON t1.product_id = p.id
WHERE p.manufacturer_id = :companyId
AND t1.sales_date between :startDate and :endDate
AND t2.sales_date between :baselineStartDate and :baselineEndDate
GROUP BY p.name
/$
 
new Transaction()
.domainClass
.grailsApplication
.mainContext
.sessionFactory
.currentSession
.createSQLQuery(query)
        .setLong('companyId', 1)
        .setDate('startDate', new Date('1/1/2014'))
        .setDate('endDate', new Date('1/31/2014'))
        .setDate('baselineStartDate', new Date('1/1/2013'))
        .setDate('baselineEndDate', new Date('1/31/2013'))
        .list()

Há algumas coisas que temos de notar aqui. Em primeiro lugar, a fim de executar essa consulta, é preciso obter a sessão atual do Hibernate e chamar o método createSQLQuery(). As duas maneiras de fazer isso são

  1. se sessionFactory tiver sido injetado em nossa classe por Grails ou
  2. se a classe de domínio dentro do método possuir uma longa cadeia de dependências para obtê-lo.

Estou usando a opção 2 aqui porque coloquei o método que implementa essa consulta dentro da minha classe de domínio e queria mantê-la estática:

class Transaction {
   ...
   static List<map> findAllTransactionsForManufacturerAndDateRangeWithBaseline() {
   }
}
</map>

Se você estiver incluindo esse método em algum lugar que não seja a sua classe de domínio (como controlador ou serviço), eu recomendo usar a opção 1.

Outra coisa que eu quero salientar é que, uma vez que estamos usando o método Hibernate real, não podemos passar diretamente um mapa de parâmetros para ele. Em vez disso, temos que usar tipos método set*() do Hibernate.

Banco de dados SQL puros

Além de consultas SQL complexas-ainda-que-genéricas, às vezes precisamos tirar proveito dos condicionais específicos do banco de dados. Por exemplo, o banco de dados PostgreSQL permite armazenar dados como arrays, mapas (hstore) ou JSON. Certos tipos de consultas usando esses tipos de dados são difíceis, se não impossíveis, de serem escritas usando HQL.

Ajuste de desempenho

Há uma outra razão para usar o SQL nativo do GORM: ajuste de desempenho. Embora o Hibernate seja tipicamente excelente no que tange à criação de SQL, ele definitivamente não é perfeito. Assim, há casos raros em que o SQL ajustado à mão pode lhe dar um ganho significativo de desempenho.

Pensamento final

Opções de busca apoiadas pelo GORM são apropriadas nas circunstâncias certas. Eu, pessoalmente, tento usar a opção mais simples sempre que possível (menos código para testar e manter). Por outro lado, se o impensável acontece e HQL (ou SQL) for necessário, é bom saber como as coisas funcionam para entender como fazê-las funcionar.

***

Artigo traduzido pela Redação iMasters com autorização do autor. Publicado originalmente em http://tatiyants.com/how-and-when-to-use-various-gorm-querying-options/