Jdbi: Haga que @CreateSqlObject sea menos confuso.

Creado en 1 feb. 2018  ·  20Comentarios  ·  Fuente: jdbi/jdbi

Problema
La documentación oficial de JDBI lleva a los desarrolladores a creer que la anotación @CreateSqlObject es el mecanismo para agregar soporte transaccional a DAO dispares.

http://jdbi.org/#__createsqlobject

Lo que parece un poco engañoso para el comportamiento real que puede conducir a errores de estado cuando se usa con el mecanismo de creación jdbi.onDemand preferido.

Ha habido algunos mensajes en el foro que ilustran esta confusión:

Solicitud
No estoy seguro de cuál sería la mejor solución para este problema. Algunas que se me ocurren:

  • Deprecación
  • Mejor documentación en cuanto a las deficiencias.
  • Otra solución para dividir consultas en múltiples DAO, pero permite que la semántica transaccional lógica las vincule. Específicamente, al admitir una combinación de transacciones de solo lectura/escritura.
cleanup improvement

Comentario más útil

Siento comentar esto de nuevo. ¿Hay algo en su hoja de ruta que ayude con esto?

No hay problema. Esto es claramente una fuente de confusión, y algo que nos gustaría arreglar; solo queremos asegurarnos de hacerlo bien esta vez, ya que no lo hicimos la última vez :)

(Como mencionó @svlada , es bastante común en otros marcos usar @Transactional en un nivel superior)

Sí, pero según tengo entendido, todo se hace a través de enlaces AOP en su marco DI (como Spring). Dado que JDBI no "envuelve" todos sus objetos de servicio, no tenemos ninguna oportunidad de proporcionar soluciones similares a AOP para objetos que no creamos nosotros mismos. Incluso la implementación anterior cglib solo notaría @Transactional en los propios daos, nunca habría funcionado en una clase de servicio separada.

Si fuera útil, podríamos considerar agregar, por ejemplo, enlaces Spring y/o Guice AOP para permitir transacciones a un nivel superior. Esto no sería parte de core sino, por ejemplo, parte de las extensiones spring . No es probable que esto sea algo en lo que los desarrolladores principales salten, pero se consideraría seriamente una contribución para su inclusión. ¿Quizás eso resuelva su problema de una manera "más limpia"?

Decidimos cambiar de cglib a Proxy principalmente por razones de compatibilidad -- Proxy es una API jdk compatible, mientras que cglib (o más particularmente asm ) tiende a romperse con cada lanzamiento principal (se rompió el 8, 9, 11, ...) lo que se convierte en un gran dolor de cabeza de mantenimiento.

Todos 20 comentarios

Gracias por informar esto. Sospecho que terminaremos con un cambio técnicamente importante aquí, pero creo que el comportamiento existente es lo suficientemente pobre como para que hagamos un cambio técnicamente importante al principio del ciclo de lanzamiento de 3.x. Marque este problema si depende del comportamiento existente o no está de acuerdo con enviar un pequeño cambio importante aquí.

Solo como contexto en cuanto a _por qué_ @CreateSqlObject no funciona bien con on-demand:

  • On-demand es una abstracción de nivel central, implementada mediante proxies. Cuando llama a un método en un proxy bajo demanda, se crea un objeto SQL real y la llamada al método se delega al objeto SQL real. El identificador que respalda esa instancia real se cierra después de que regresa la llamada al método de delegado.
  • @CreateSqlObject se implementa usando handle.attach(sqlObjectType) , con el identificador de respaldo del objeto SQL original.

Por lo tanto, el identificador de respaldo para el objeto SQL creado se cierra antes de que el objeto SQL pueda devolverse.

Nada de lo anterior está escrito en piedra, es solo cómo se implementa ahora.

No estoy convencido de que tengamos que romper la compatibilidad para arreglar esto.

Lo siento, realmente no lo entiendo... ¿Cuál es el problema con onDemand y CreateSqlObject exactamente? Nunca he tenido ningún problema con eso, y algo en la explicación de @qualidafial está haciendo que mi mente se incline...

image

Se adquiere una conexión para la llamada a fooProxy.usecase . Foo.usecase llama a la barra de métodos createSqlObject, que devuelve una nueva barra después de adjuntarla al identificador actual (de usecase ). El identificador de usecase se cierra cuando usecase regresa. Siempre que no esté haciendo nada asincrónico con la barra devuelta, ¿cómo puede expirar demasiado pronto el identificador de la barra ? El ciclo de vida y el uso de la barra se limitan al cuerpo de Foo.usecase , al igual que el mango...

No puedo hablar sobre el funcionamiento interno de JDBI, sin embargo, puedo comentar sobre el comportamiento que vimos en producción.

Básicamente, en nuestros ambientes inferiores las cosas estaban bien. Pero con el aumento de la carga en producción, veríamos continuamente que las declaraciones de SQL no se ejecutan en el nodo de base de datos adecuado. Por ejemplo, una selección de solo lectura tocaría el nodo maestro y una inserción tocaría una réplica de lectura. Además, obtendríamos errores de conexión cerrada donde estaba en medio de una operación.

Esencialmente, parecía que @CreateSqlObject creó un problema de seguridad de subprocesos, donde el indicador de solo lectura estaba siendo alterado por solicitudes en competencia.

Para "resolver" el problema, eliminamos todo uso de @CreateSqlObject y terminamos con algo como esto:

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

Y luego usamos jdbi.onDemand(CombinedDao) y solo usamos el acceso a través de eso.
Cambiar a ese patrón, aunque no tan bonito, eliminó todos los errores de producción antes mencionados.

Solo como información, estoy pensando en volver a JDBI 2 porque todo mi código usa onDemand con clases abstractas en este momento, ¡y no puedo encontrar la manera de reestructurar de una manera que me satisfaga!

Tiendo a tener interfaces de servicio, con clases de implementaciones abstractas que contienen lógica pero no SQL, con métodos anotados como transaccionales, usando CreateSqlObject para proporcionar acceso a clases DAO que son puramente SQL. Un ejemplo simplificado:

Interfaz

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

Implementación

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();
}

(Puedes imaginar el Dao)

Esto brinda una separación realmente agradable entre la lógica y el acceso a los datos, al tiempo que permite transacciones a través de múltiples métodos DAO. Las pruebas unitarias para el servicio son fáciles de escribir y comprender mediante la implementación de las interfaces DAO.

Intenté hacer algo similar en JDBI 3, pero creo que significa que la implementación de mi clase de servicio debe ser una interfaz con métodos predeterminados para los que contienen lógica. Los métodos predeterminados no se pueden hacer definitivos, por lo que el código no se siente tan conciso y me da menos control sobre cómo se puede usar mi clase.

¿Hay alguna forma en que pueda estructurar mi código en JDBI 3 para tener métodos transaccionales finales?

Tiene razón en que no hay forma de definir un método de interfaz como final.

Sin embargo, _puede_ definir sus propias anotaciones de métodos SQL a la par con @SqlQuery , @SqlUpdate , etc., y proporcionar una implementación estática para los métodos con esa anotación.

Sin embargo, _puede_ definir sus propias anotaciones de métodos SQL a la par con @SqlQuery , @SqlUpdate , etc., y proporcionar una implementación estática para los métodos con esa anotación.

Gracias por la respuesta, no entiendo muy bien cómo me ayuda eso. ¿Cuál es la forma recomendada de tener consultas en múltiples DAO, pero ejecutarlas en la misma transacción?

forma de tener consultas en múltiples DAO, pero ejecutarlas en la misma transacción?

Personalmente 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 muy bien para mí de esta manera. @CreateSqlObject es básicamente como la inyección de dependencia de Spring por getter/setter. Puse instancias onDemand en mi contexto de primavera como beans para que funcionen exactamente como los servicios regulares, las personas que llaman no necesitan saber sobre jdbi o la interfaz de implementación.

image

El StockReductionCase de aspecto antipatrón que ve es un ejemplo de "inyección de dependencia" jdbi anidada con CreateSqlObject. Tiene dependencias propias en su interior, al igual que la implementación del servicio. Básicamente es un servicio en sí mismo, simplemente lo llamo Caso en lugar de Servicio para evitar la dependencia cíclica al colocar solo Casos (necesarios en varios lugares) dentro de Servicios (lógica no reutilizable de nivel superior) y Consultas en cualquiera.

image

Escuché informes mixtos de éxito en la obtención de transacciones en el alcance correctamente con @CreateSqlObject , especialmente cuando se combina con onDemand() .

La forma más segura de ejecutar múltiples objetos SQL en la misma transacción es ejecutar transacciones a través Handle.inTransaction() o Jdbi.inTransaction() . Dentro de la devolución de llamada, cualquier DAO creado a través Handle.attach() será parte de la transacción del identificador:

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

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

gracias por las respuestas
@qualidafial, este parece ser el camino que tendré que seguir, pero hace que sea difícil escribir pruebas unitarias para la clase de servicio. Como instancia el propio Daos, no puedo sustituir Daos simulados por pruebas unitarias.

@TheRealMarnes gracias también: esto se parece a lo que probé, simplemente no me gusta usar métodos predeterminados, ya que pueden anularse.

@qualidafial Me gustaría confirmar con usted que no podemos usar la anotación @Transaction de jdbi en métodos de servicio que invocan múltiples métodos dao.

Si esta suposición es correcta, este comportamiento es muy peligroso si no se documenta adecuadamente como parte de la documentación oficial. La gran cantidad de desarrolladores tienen experiencia con Spring Stack y el uso de la anotación @Transactional es una forma muy estándar de admitir transacciones en múltiples DAO.

@svlada La anotación @Transaction _solo_ funciona en métodos de objetos SQL. Cuando dice "métodos de servicio", tengo la impresión de que está usando la anotación en algún objeto fuera de la influencia de Jdbi, por ejemplo, una clase inyectada.

Un ejemplo de nuestro conjunto de pruebas:

<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();
    }
}

También quiero aclarar con respecto a onDemand() + @CreateSqlObject :

  • @CreateSqlObject Los DAO solo se pueden usar desde métodos internos del DAO que los creó, como en el ejemplo anterior.
  • Llamar a fooDao.createBar().findById() arrojará una excepción que indica que la conexión está cerrada.

Siento comentar esto de nuevo. ¿Hay algo en su hoja de ruta que ayude con esto?

Todavía extraño mucho poder usar CreateSqlObject en clases abstractas. Tener que hacer todo en las interfaces todavía se siente restrictivo; a menudo me gustaría hacer que un método de tipo "servicio" sea transaccional, pero esto significaría que mis clases de servicio también tenían que ser interfaces usando métodos predeterminados y empiezo a perder la claridad de la separación de preocupaciones, métodos finales concretos, etc.

@tamslinn en lo que respecta a su problema con las interfaces en lugar de las clases abstractas, creo que puedo decir con seguridad que no volveremos de esa decisión en el futuro cercano. Jdbi3 usa jdk Proxies, que solo admite interfaces.

No creo que un módulo separado contribuido construido en cglib o algo similar que ofrezca esta función esté completamente fuera de discusión, pero no es una preocupación en este momento.

Si los axiomas de sqlobject le convienen tanto, siempre puede refactorizar sus Servicios para que sean clases regulares no mejoradas por jdbi, sino que se inyecte una instancia de Jdbi para usar la API fluida en su lugar.

En cuanto al tema original del comportamiento entre nuestro soporte de transacciones, onDemand, etc., se está volviendo cada vez más un enfoque en el que trabajar, aunque no creo que tengamos planes realmente concretos todavía. Pero lo estamos consiguiendo.

Muchas gracias por la actualización. Totalmente tiene sentido con respecto a los proxies.
La limitación específica para mí es no poder iniciar/finalizar transacciones fuera de las clases de JDBI.
(Como mencionó @svlada , es bastante común en otros marcos usar @Transactional en un nivel superior)
Sin embargo, me gusta mucho todo lo demás sobre JDBI, así que lo seguiré, tal vez encuentre un patrón de diseño con el que esté contento para mis clases de servicio :)

Siento comentar esto de nuevo. ¿Hay algo en su hoja de ruta que ayude con esto?

No hay problema. Esto es claramente una fuente de confusión, y algo que nos gustaría arreglar; solo queremos asegurarnos de hacerlo bien esta vez, ya que no lo hicimos la última vez :)

(Como mencionó @svlada , es bastante común en otros marcos usar @Transactional en un nivel superior)

Sí, pero según tengo entendido, todo se hace a través de enlaces AOP en su marco DI (como Spring). Dado que JDBI no "envuelve" todos sus objetos de servicio, no tenemos ninguna oportunidad de proporcionar soluciones similares a AOP para objetos que no creamos nosotros mismos. Incluso la implementación anterior cglib solo notaría @Transactional en los propios daos, nunca habría funcionado en una clase de servicio separada.

Si fuera útil, podríamos considerar agregar, por ejemplo, enlaces Spring y/o Guice AOP para permitir transacciones a un nivel superior. Esto no sería parte de core sino, por ejemplo, parte de las extensiones spring . No es probable que esto sea algo en lo que los desarrolladores principales salten, pero se consideraría seriamente una contribución para su inclusión. ¿Quizás eso resuelva su problema de una manera "más limpia"?

Decidimos cambiar de cglib a Proxy principalmente por razones de compatibilidad -- Proxy es una API jdk compatible, mientras que cglib (o más particularmente asm ) tiende a romperse con cada lanzamiento principal (se rompió el 8, 9, 11, ...) lo que se convierte en un gran dolor de cabeza de mantenimiento.

¿Tal vez https://github.com/jdbi/jdbi/pull/1252 es relevante?

Hola,

Gracias por la información @stevenschlansker. En realidad, no estoy usando Spring con JDBI para mi proyecto actual, solo se mencionó para comparar. Entiendo totalmente las cosas de AOP, solo trato de encontrar una forma de evitarlo que significa que puedo dividir el código como quiero: las clases abstractas solían funcionar para mí, pero tiene mucho sentido alejarse de cglib

Estoy pensando que ahora solo construiré mis clases de servicio cuando las necesite, de esa manera puedo construirlas dentro de una transacción.

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

Creo que esto significará que puedo probar el servicio con un Mock Dao, mantener los métodos de servicio definitivos y no depender de los métodos predeterminados en las interfaces para la gestión de transacciones, y la sobrecarga adicional de la creación de instancias de clase de servicio no debería tener un impacto significativo.

Solo una respuesta rápida al ejemplo de código en su último comentario: puede omitir el bloque try y simplemente llamar a jdbi.useTransaction() directamente. Este método asigna un identificador temporal que se cierra automáticamente cuando regresa su devolución de llamada.

Hola a todos, hay un PR #1579 -- a partir de jdbi 3.10.0, CreateSqlObject y onDemand deberían (finalmente) funcionar bien juntos.

¿Fue útil esta página
0 / 5 - 0 calificaciones