Jdbi: Torne @CreateSqlObject menos confuso.

Criado em 1 fev. 2018  ·  20Comentários  ·  Fonte: jdbi/jdbi

Problema
A documentação oficial do JDBI leva os desenvolvedores a acreditar que a anotação @CreateSqlObject é o mecanismo para adicionar suporte transacional a DAOs diferentes.

http://jdbi.org/#__createsqlobject

O que parece um pouco enganador para o comportamento real que pode levar a erros de estado quando usado com o mecanismo de criação jdbi.onDemand preferido.

Houve algumas mensagens no fórum que ilustram essa confusão:

Solicitar
Não tenho certeza de qual seria a(s) melhor(es) solução(ões) para esse problema. Alguns que me vêm à mente:

  • Suspensão
  • Melhor documentação sobre as deficiências
  • Outra solução para dividir consultas em vários DAOs, mas permite que a semântica transacional lógica os una. Especificamente, no suporte a uma combinação de transações somente leitura/gravação.
cleanup improvement

Comentários muito úteis

Desculpe comentar isso novamente. Existe alguma coisa em seu roteiro que vai ajudar com isso?

Nenhum problema. Isso é claramente uma fonte de confusão e algo que gostaríamos de corrigir - nós realmente queremos ter certeza de acertar desta vez, já que não fizemos da última vez :)

(Como mencionado por @svlada , é bastante comum em outros frameworks usar @Transactional em um nível mais alto)

Sim, mas até onde eu entendo, tudo isso é feito por meio de ganchos AOP em sua estrutura DI (como Spring). Como o JDBI não "empacota" todos os seus objetos de serviço, não temos nenhuma oportunidade de fornecer soluções do tipo AOP para objetos que não criamos. Mesmo a antiga implementação cglib notaria apenas @Transactional nos próprios daos, nunca teria funcionado em uma classe de serviço separada.

Se for útil, podemos considerar adicionar, por exemplo, ligações Spring e/ou Guice AOP para permitir transações em um nível mais alto. Isso não faria parte de core mas, por exemplo, parte das extensões spring . Não é provável que isso seja algo que os desenvolvedores principais pulem, mas uma contribuição seria seriamente considerada para inclusão. Talvez isso resolva seu problema de uma maneira "mais limpa"?

Decidimos mudar de cglib para Proxy principalmente por motivos de compatibilidade -- Proxy é uma API jdk suportada, enquanto cglib (ou mais particularmente asm ) tende a quebrar com cada lançamento principal (quebrou em 8, 9, 11, ...), o que se transforma em uma enorme dor de cabeça de manutenção.

Todos 20 comentários

Obrigado por relatar isso. Suspeito que terminaremos com uma mudança tecnicamente importante aqui, mas acho que o comportamento existente é ruim o suficiente para fazermos uma mudança tecnicamente importante no início do ciclo de lançamento 3.x. Sinalize esse problema se você depender do comportamento existente ou não concordar com o envio de uma pequena alteração importante aqui.

Apenas para contextualizar _why_ @CreateSqlObject não funciona bem com o sob demanda:

  • On-demand é uma abstração de nível central, implementada usando proxies. Quando você chama um método em um proxy sob demanda, um objeto SQL real é criado e a chamada do método é delegada ao objeto SQL real. O identificador que suporta essa instância real é fechado após o retorno da chamada do método delegado.
  • @CreateSqlObject é implementado usando handle.attach(sqlObjectType) , com o identificador de suporte do Objeto SQL original.

Assim, o identificador de apoio para o objeto SQL criado é fechado antes mesmo que o objeto SQL possa ser retornado.

Nenhuma das opções acima está definida - é apenas como é implementado agora.

Não estou convencido de que teremos que quebrar a compatibilidade para corrigir isso.

Desculpe, eu realmente não entendi... Qual é o problema com onDemand e CreateSqlObject exatamente? Nunca tive problemas com isso, e algo na explicação do @qualidafial está apenas fazendo minha mente entrar em tilt...

image

Uma conexão é adquirida para a chamada para fooProxy.usecase . Foo.usecase chama o método createSqlObject bar , que retorna um novo Bar após anexá-lo ao handle atual ( do caso de uso ). O handle do usecase é fechado quando o usecase retorna. Contanto que você não esteja fazendo nada assíncrono com o Bar retornado, como o handle do bar pode expirar muito cedo? O ciclo de vida e uso de bar é limitado ao corpo de Foo.usecase , assim como o handle...

Não posso falar com os internos do JDBI, porém posso comentar sobre o comportamento que vimos na produção.

Basicamente, em nossos ambientes inferiores as coisas estavam bem. Mas com o aumento da carga na produção, veríamos continuamente instruções SQL não serem executadas no nó de banco de dados adequado. Por exemplo, uma seleção somente leitura atingiria o nó mestre e uma inserção atingiria uma réplica de leitura. Além disso, obteríamos erros de conexão fechada no meio de uma operação.

Essencialmente, parecia que @CreateSqlObject criou um problema de segurança de thread - onde o sinalizador somente leitura estava sendo alterado por solicitações concorrentes.

Para "resolver" o problema - removemos todo o uso de @CreateSqlObject e acabamos com algo assim:

  • class FooDao
  • class BarDao
  • class CombinedDao extends FooDao, BarDao

E então nós jdbi.onDemand(CombinedDao) e só usamos o acesso através disso.
Mudar para esse padrão, embora não tão bonito, eliminou todos os erros mencionados na produção.

Apenas para informação, estou pensando em voltar ao JDBI 2 porque todo o meu código usa onDemand com classes abstratas no momento, e não consigo descobrir como reestruturar de uma maneira que me agrade!

Costumo ter interfaces de serviço, com classes de implementações abstratas contendo lógica, mas sem SQL, com métodos anotados como transacionais, usando CreateSqlObject para fornecer acesso a classes DAO que são puramente SQL. Um exemplo simplificado:

Interface

public interface AccountService {
    void addAccount(Account account, User user);
}  

Implementação

public abstract class AccountServiceJdbi implements AccountService {

    <strong i="11">@Override</strong>  
    <strong i="12">@Transaction</strong>  
    public final void addAccount(@BindBean() Account account, User user) {
        long accountId =  accountDao().insertAccount(account);
        accountDao().linkAccountToOwner(accountId, user.getId());
    }

    <strong i="13">@CreateSqlObject</strong>
    abstract AccountDao accountDao();
}

(Você pode imaginar o Dao)

Isso oferece uma separação muito boa entre lógica e acesso a dados, ao mesmo tempo em que permite transações em vários métodos DAO. Os testes de unidade para o serviço são fáceis de escrever e entender implementando as interfaces DAO.

Já tentei fazer semelhante no JDBI 3, mas acho que significa que a implementação da minha classe de serviço tem que ser uma interface com métodos padrão para aqueles que contêm lógica. Os métodos padrão não podem ser finalizados, então o código não parece tão conciso e me dá menos controle sobre como minha classe pode ser usada.

Existe alguma maneira de estruturar meu código no JDBI 3 para ter métodos transacionais finais?

Você está correto ao dizer que não há como definir um método de interface como final.

Você _pode_ no entanto definir suas próprias anotações de método SQL a par com @SqlQuery , @SqlUpdate , etc, e fornecer uma implementação estática para métodos com essa anotação.

Você _pode_ no entanto definir suas próprias anotações de método SQL a par com @SqlQuery , @SqlUpdate , etc, e fornecer uma implementação estática para métodos com essa anotação.

Obrigado pela resposta - eu não entendo muito bem como isso me ajuda. Qual é a maneira recomendada de ter consultas em vários DAOs, mas executá-las na mesma transação?

maneira de ter consultas em vários DAOs, mas executá-las na mesma transação?

Pessoalmente eu uso algo como

interface Dao1 {
  @SqlQuery("...")
  void query1();
}

interface Dao2 {
  @SqlQuery("...")
  void query2();
}

interface JdbiServiceImpl extends Service {
  <strong i="8">@CreateSqlObject</strong>
  Dao1 dao1();
  <strong i="9">@CreateSqlObject</strong>
  Dao2 dao2();

  <strong i="10">@Transaction</strong>
  <strong i="11">@Override</strong>
  void businessCase() {
    dao1().query1();
    dao2().query2();
  }
}

Service service = handle.attach(JdbiServiceImpl.class);
service.businessCase();

Funciona muito bem para mim desta forma. @CreateSqlObject é basicamente como a injeção de dependência do spring por getter/setter. Eu coloco instâncias onDemand no meu contexto de primavera como beans para que funcione exatamente como serviços regulares, os chamadores não precisam saber sobre jdbi ou a interface de implementação.

image

A coisa StockReductionCase anti-padrão que você vê é um exemplo de "injeção de dependência" jdbi aninhada com CreateSqlObject. Ele possui dependências próprias dentro dele, assim como a implementação do serviço. É basicamente um serviço por conta própria, eu apenas chamo de Case em vez de Service para evitar dependência cíclica, colocando apenas Cases (necessários em vários lugares) dentro de Services (lógica não reutilizável de nível superior) e Consultas em ambos.

image

Ouvi relatos mistos de sucesso ao fazer transações com o escopo correto com @CreateSqlObject , especialmente quando combinado com onDemand() .

A maneira mais segura de executar vários objetos SQL na mesma transação é executar transações via Handle.inTransaction() ou Jdbi.inTransaction() . Dentro do callback, quaisquer DAOs criados via Handle.attach() farão parte da transação do handle:

jdbi.useTransaction(handle -> {
  Dao1 dao1 = handle.attach(Dao1.class);
  Dao2 dao2 = handle.attach(Dao2.class);

  dao1.doStuff();
  dao2.doMoreStuff();
});

Obrigado pelas respostas
@qualidafial isso parece o caminho que terei que seguir, mas dificulta escrever testes de unidade para a classe de serviço. Como ele instancia o próprio Daos, não posso substituir o Daos simulado por testes de unidade.

@TheRealMarnes obrigado também - isso se parece com o que eu tentei, só não gosto de usar métodos padrão, pois eles podem ser substituídos.

@qualidafial Gostaria de confirmar com você que não podemos usar a anotação @Transaction do jdbi em métodos de serviço que estão invocando vários métodos dao?

Se essa suposição estiver correta, esse comportamento é muito perigoso sem ser devidamente documentado como parte da documentação oficial. O grande número de desenvolvedores tem experiência com o Spring stack e usar a anotação @Transactional é uma maneira muito padrão de oferecer suporte a transações em vários DAOs.

@svlada A anotação @Transaction _only_ funciona em métodos de objetos SQL. Quando você diz "métodos de serviço", tenho a impressão de que você está usando a anotação em algum objeto fora da influência do Jdbi, por exemplo, uma classe injetada.

Um exemplo do nosso conjunto de testes:

<strong i="9">@Test</strong>
public void testInsertAndFind() {
    Foo foo = handle.attach(Foo.class);
    Something s = foo.insertAndFind(1, "Stephane");
    assertThat(s).isEqualTo(new Something(1, "Stephane"));
}

<strong i="10">@Test</strong>
public void testTransactionPropagates() {
    Foo foo = dbRule.getJdbi().open().attach(Foo.class);

    assertThatExceptionOfType(Exception.class)
        .isThrownBy(() -> foo.insertAndFail(1, "Jeff"));

    Something n = foo.createBar().findById(1);
    assertThat(n).isNull();
}

public interface Foo {
    <strong i="11">@CreateSqlObject</strong>
    Bar createBar();

    @SqlUpdate("insert into something (id, name) values (:id, :name)")
    int insert(@Bind("id") int id, @Bind("name") String name);

    <strong i="12">@Transaction</strong>
    default Something insertAndFind(int id, String name) {
        insert(id, name);
        return createBar().findById(id);
    }

    <strong i="13">@Transaction</strong>
    default Something insertAndFail(int id, String name) {
        insert(id, name);
        return createBar().explode();
    }
}

public interface Bar {
    @SqlQuery("select id, name from something where id = :id")
    Something findById(@Bind("id") int id);

    default Something explode() {
        throw new RuntimeException();
    }
}

Também quero esclarecer com relação a onDemand() + @CreateSqlObject :

  • @CreateSqlObject DAOs só podem ser usados ​​a partir de métodos internos do DAO que os criou - como no exemplo acima.
  • Chamar fooDao.createBar().findById() lançará uma exceção informando que a conexão está fechada.

Desculpe comentar isso novamente. Existe alguma coisa em seu roteiro que vai ajudar com isso?

Ainda sinto muita falta de poder usar CreateSqlObject em classes abstratas. Ter que fazer tudo nas interfaces ainda parece restritivo; muitas vezes eu gostaria de tornar um método do tipo "serviço" transacional, mas isso significaria que minhas classes de serviço também teriam que ser interfaces usando métodos padrão e eu começaria a perder a clareza da separação de interesses, métodos finais concretos etc.

@tamslinn no que diz respeito ao seu problema com interfaces em vez de classes abstratas, acho que posso dizer com segurança que não voltaremos dessa decisão em um futuro próximo. Jdbi3 usa proxies jdk, que suporta apenas interfaces.

Eu não acho que um módulo separado contribuído construído no cglib ou algum outro que ofereça esse recurso esteja totalmente fora de questão, mas não é uma preocupação no momento.

Se os axiomas sqlobject combinam com você, você sempre pode refatorar seus serviços para serem classes regulares não aprimoradas pelo jdbi, mas sim ter uma instância Jdbi injetada para usar a API fluente.

Quanto ao assunto original do comportamento entre nosso suporte a transações, onDemand, etc, isso está se tornando cada vez mais um foco para trabalhar, embora eu não ache que tenhamos planos realmente concretos ainda. Mas estamos chegando a isso.

Muito obrigado pela atualização. Totalmente faz sentido re os proxies.
A limitação específica para mim é não poder iniciar/terminar transações fora das classes JDBI.
(Como mencionado por @svlada é bastante comum em outros frameworks usar @Transactional em um nível mais alto)
Eu realmente gosto de tudo o mais sobre JDBI, então vou continuar, talvez eu crie um padrão de design com o qual eu esteja feliz para minhas classes de serviço :)

Desculpe comentar isso novamente. Existe alguma coisa em seu roteiro que vai ajudar com isso?

Nenhum problema. Isso é claramente uma fonte de confusão e algo que gostaríamos de corrigir - nós realmente queremos ter certeza de acertar desta vez, já que não fizemos da última vez :)

(Como mencionado por @svlada , é bastante comum em outros frameworks usar @Transactional em um nível mais alto)

Sim, mas até onde eu entendo, tudo isso é feito por meio de ganchos AOP em sua estrutura DI (como Spring). Como o JDBI não "empacota" todos os seus objetos de serviço, não temos nenhuma oportunidade de fornecer soluções do tipo AOP para objetos que não criamos. Mesmo a antiga implementação cglib notaria apenas @Transactional nos próprios daos, nunca teria funcionado em uma classe de serviço separada.

Se for útil, podemos considerar adicionar, por exemplo, ligações Spring e/ou Guice AOP para permitir transações em um nível mais alto. Isso não faria parte de core mas, por exemplo, parte das extensões spring . Não é provável que isso seja algo que os desenvolvedores principais pulem, mas uma contribuição seria seriamente considerada para inclusão. Talvez isso resolva seu problema de uma maneira "mais limpa"?

Decidimos mudar de cglib para Proxy principalmente por motivos de compatibilidade -- Proxy é uma API jdk suportada, enquanto cglib (ou mais particularmente asm ) tende a quebrar com cada lançamento principal (quebrou em 8, 9, 11, ...), o que se transforma em uma enorme dor de cabeça de manutenção.

Oi,

Obrigado pela informação @stevenschlansker. Na verdade, não estou usando Spring com JDBI para meu projeto atual, foi mencionado apenas para comparação. Eu entendo totalmente as coisas do AOP, apenas tentando encontrar uma maneira de contornar isso, o que significa que posso dividir o código como quiser - as classes abstratas costumavam funcionar para mim, mas faz todo o sentido se afastar de cglib

Estou pensando agora que vou construir minhas classes de serviço quando precisar delas, dessa forma posso construí-las dentro de uma transação.

         try (Handle handle = jdbi.open()) {
                handle.useTransaction(h -> {
                    AccountService accountService = new AccountServiceImpl(h.attach(AccountDao.class));                    
                    accountService.addAccount(a, u);
                });
          }

Acho que isso significa que posso testar o serviço com um Mock Dao, manter os métodos de serviço finais e não depender de métodos padrão em interfaces para gerenciamento de transações, e a sobrecarga adicional de instanciação de classe de serviço não deve ter um impacto significativo.

Apenas uma resposta rápida ao exemplo de código em seu último comentário: você pode pular o bloco try e apenas chamar jdbi.useTransaction() diretamente. Este método aloca um identificador temporário que é fechado automaticamente quando seu retorno de chamada retorna.

Olá a todos, há um PR #1579 -- a partir do jdbi 3.10.0, CreateSqlObject e onDemand devem (finalmente) funcionar bem juntos.

Esta página foi útil?
0 / 5 - 0 avaliações