Jdbi: Rendez @CreateSqlObject moins déroutant.

Créé le 1 févr. 2018  ·  20Commentaires  ·  Source: jdbi/jdbi

Problème
La documentation officielle de JDBI amène les développeurs à croire que l'annotation @CreateSqlObject est le mécanisme permettant d'ajouter un support transactionnel à des DAO disparates.

http://jdbi.org/#__createsqlobject

Ce qui semble un peu trompeur quant au comportement réel qui peut entraîner des erreurs d'état lorsqu'il est utilisé avec le mécanisme de création jdbi.onDemand préféré.

Il y a eu quelques messages dans le forum qui illustrent cette confusion :

Demande
Je ne sais pas quelle(s) meilleure(s) solution(s) à ce problème serait. Certains qui me viennent à l'esprit :

  • Désapprobation
  • Meilleure documentation sur les lacunes
  • Une autre solution pour diviser les requêtes en plusieurs DAO, mais permettre à la sémantique transactionnelle logique de les lier. Plus précisément, en prenant en charge une combinaison de transactions en lecture seule/écriture.
cleanup improvement

Commentaire le plus utile

Désolé de commenter à nouveau. Y a-t-il quelque chose dans votre feuille de route qui vous aidera à cela ?

Aucun problème du tout. C'est clairement une source de confusion, et quelque chose que nous aimerions corriger - nous voulons vraiment nous assurer de bien faire les choses cette fois, puisque nous ne l'avons pas fait la dernière fois :)

(Comme mentionné par @svlada , il est assez courant dans d'autres frameworks d'utiliser @Transactional à un niveau supérieur)

Oui, mais pour autant que je sache, tout cela se fait via des crochets AOP dans votre cadre DI (comme Spring). Étant donné que JDBI n'"enveloppe" pas tous vos objets de service, nous n'avons aucune possibilité de fournir des solutions de type AOP pour les objets que nous ne créons pas nous-mêmes. Même l'ancienne implémentation cglib ne remarquerait que @Transactional sur les daos eux-mêmes, cela n'aurait jamais fonctionné sur une classe de service distincte.

Si cela s'avère utile, nous pourrions envisager d'ajouter, par exemple, des liaisons Spring et/ou Guice AOP pour autoriser les transactions à un niveau supérieur. Cela ne ferait pas partie de core mais, par exemple, des extensions spring . Il est peu probable que ce soit quelque chose sur lequel les principaux développeurs sautent, mais une contribution serait sérieusement envisagée pour inclusion. Peut-être que cela résout votre problème de manière "plus propre" ?

Nous avons décidé de passer de cglib à Proxy principalement pour des raisons de compatibilité -- Proxy est une API jdk prise en charge, alors que cglib (ou plus particulièrement asm ) a tendance à casser à chaque version majeure (il a cassé le 8, 9, 11, ...) ce qui se transforme en un énorme casse-tête de maintenance.

Tous les 20 commentaires

Merci d'avoir signalé cela. Je soupçonne que nous allons nous retrouver avec un changement techniquement révolutionnaire ici, mais je pense que le comportement existant est suffisamment médiocre pour que nous apportions un changement techniquement décisif au début du cycle de publication 3.x. Veuillez signaler ce problème si vous dépendez du comportement existant ou si vous n'êtes pas d'accord avec l'envoi éventuel d'un petit changement de rupture ici.

Juste pour le contexte, _pourquoi_ @CreateSqlObject ne fonctionne pas bien avec à la demande :

  • La demande est une abstraction au niveau du cœur, implémentée à l'aide de proxys. Lorsque vous appelez une méthode sur un proxy à la demande, un objet SQL réel est créé et l'appel de méthode est délégué à l'objet SQL réel. Le handle qui sauvegarde cette instance réelle est fermé après le retour de l'appel de la méthode déléguée.
  • @CreateSqlObject est implémenté en utilisant handle.attach(sqlObjectType) , avec le handle de sauvegarde de l'objet SQL d'origine.

Ainsi, le descripteur de sauvegarde de l'objet SQL créé est fermé avant même que l'objet SQL puisse être renvoyé.

Rien de ce qui précède n'est gravé dans le marbre - c'est juste la façon dont il est mis en œuvre maintenant.

Je ne suis pas convaincu que nous devrons rompre la compatibilité pour résoudre ce problème.

Désolé, je ne comprends pas vraiment... Quel est le problème avec onDemand et CreateSqlObject exactement ? Je n'ai jamais eu de problèmes avec ça, et quelque chose à propos de l'explication de @qualidafial me fait juste basculer l'esprit...

image

Une connexion est acquise pour l'appel à fooProxy.usecase . Foo.usecase appelle la méthode createSqlObject bar , qui renvoie un nouveau Bar après l'avoir attaché au handle actuel ( usecase ). Le handle de usecase est fermé lorsque usecase revient. Tant que vous ne faites rien d'asynchrone avec le Bar retourné, comment le handle de bar peut-il expirer trop tôt ? Le cycle de vie et l'utilisation de la barre sont limités au corps de Foo.usecase , tout comme la poignée...

Je ne peux pas parler de l'intérieur de JDBI, mais je peux commenter le comportement que nous avons vu en production.

Fondamentalement, dans nos environnements inférieurs, les choses allaient bien. Mais avec l'augmentation de la charge en production, nous voyions continuellement des instructions SQL ne pas s'exécuter sur le nœud de base de données approprié. Par exemple, une sélection en lecture seule toucherait le nœud maître et une insertion toucherait un réplica en lecture. De plus, nous obtenions des erreurs de fermeture de connexion là où c'était au milieu d'une opération.

Essentiellement, il semblait que @CreateSqlObject avait créé un problème de sécurité des threads - où l'indicateur de lecture seule était modifié par des requêtes concurrentes.

Pour "résoudre" le problème - nous avons supprimé toute utilisation de @CreateSqlObject et nous nous sommes retrouvés avec quelque chose comme ceci :

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

Et puis nous jdbi.onDemand(CombinedDao) et n'utilisons que l'accès via cela.
Le passage à ce modèle, même s'il n'est pas aussi joli, a permis d'éliminer toutes les erreurs de production susmentionnées.

Juste pour info, je pense revenir à JDBI 2 car tout mon code utilise onDemand avec des classes abstraites pour le moment, et je n'arrive pas à trouver comment restructurer d'une manière qui me satisfera !

J'ai tendance à avoir des interfaces de service, avec des classes d'implémentations abstraites contenant de la logique mais pas de SQL, avec des méthodes annotées comme transactionnelles, utilisant CreateSqlObject pour fournir un accès aux classes DAO qui sont purement SQL. Un exemple simplifié :

Interface

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

Mise en œuvre

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

(Vous pouvez imaginer le Dao)

Cela donne une très belle séparation de la logique et de l'accès aux données, tout en permettant des transactions sur plusieurs méthodes DAO. Les tests unitaires pour le service sont faciles à écrire et à comprendre en implémentant les interfaces DAO.

J'ai essayé de faire la même chose dans JDBI 3, mais je pense que cela signifie que l'implémentation de ma classe de service doit être une interface avec des méthodes par défaut pour celles contenant de la logique. Les méthodes par défaut ne peuvent pas être rendues finales, de sorte que le code ne semble pas aussi concis et me donne moins de contrôle sur la façon dont ma classe peut être utilisée.

Existe-t-il un moyen de structurer mon code dans JDBI 3 pour avoir des méthodes transactionnelles finales ?

Vous avez raison de dire qu'il n'y a aucun moyen de définir une méthode d'interface comme finale.

Vous _pouvez_ cependant définir vos propres annotations de méthode SQL sur un pied d'égalité avec @SqlQuery , @SqlUpdate , etc., et fournir une implémentation statique pour les méthodes avec cette annotation.

Vous _pouvez_ cependant définir vos propres annotations de méthode SQL sur un pied d'égalité avec @SqlQuery , @SqlUpdate , etc., et fournir une implémentation statique pour les méthodes avec cette annotation.

Merci pour la réponse - je ne comprends pas très bien comment cela m'aide. Quelle est la méthode recommandée pour avoir des requêtes dans plusieurs DAO, mais les exécuter dans la même transaction ?

moyen d'avoir des requêtes dans plusieurs DAO, mais de les exécuter dans la même transaction ?

Personnellement, j'utilise quelque chose comme

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

Fonctionne très bien pour moi de cette façon. @CreateSqlObject est fondamentalement comme l'injection de dépendance de Spring par getter/setter. J'ai mis des instances onDemand sur mon contexte de printemps en tant que beans afin que cela fonctionne exactement comme des services réguliers, les appelants n'ont pas besoin de connaître jdbi ou l'interface d'implémentation.

image

La chose StockReductionCase qui ressemble à un anti-modèle que vous voyez est un exemple d'"injection de dépendances" jdbi imbriquée avec CreateSqlObject. Il a ses propres dépendances à l'intérieur, tout comme l'implémentation du service. C'est fondamentalement un service en soi, je l'appelle juste Case au lieu de Service pour éviter la dépendance cyclique en ne plaçant que des cas (nécessaires à plusieurs endroits) dans les services (logique non réutilisable de niveau supérieur) et les requêtes dans l'un ou l'autre.

image

J'ai entendu des rapports mitigés sur le succès de la portée des transactions avec @CreateSqlObject , en particulier lorsqu'elles sont combinées avec onDemand() .

Le moyen le plus sûr d'exécuter plusieurs objets SQL dans la même transaction consiste à exécuter des transactions via Handle.inTransaction() ou Jdbi.inTransaction() . Dans le rappel, tous les DAO créés via Handle.attach() feront partie de la transaction du handle :

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

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

Merci pour les réponses
@qualidafial cela ressemble à la façon dont je devrai procéder, mais cela rend difficile l'écriture de tests unitaires pour la classe de service. Comme il instancie le Daos lui-même, je ne peux pas remplacer les faux Daos par des tests unitaires.

@TheRealMarnes merci aussi - cela ressemble à ce que j'avais essayé, je n'aime tout simplement pas utiliser les méthodes par défaut car elles peuvent être remplacées.

@qualidafial Je voudrais confirmer avec vous que nous ne pouvons pas utiliser l'annotation @Transaction jdbi sur les méthodes de service qui invoquent plusieurs méthodes dao ?

Si cette hypothèse est correcte, ce comportement est très dangereux sans être correctement documenté dans le cadre de la documentation officielle. Le grand nombre de développeurs ont de l'expérience avec la pile Spring et l'utilisation de l'annotation @Transactional est un moyen très standard de prendre en charge les transactions sur plusieurs DAO.

@svlada L'annotation @Transaction _uniquement_ fonctionne sur les méthodes des objets SQL. Lorsque vous dites "méthodes de service", j'ai l'impression que vous utilisez l'annotation sur un objet en dehors de l'influence de Jdbi, par exemple une classe injectée.

Un exemple de notre suite de tests :

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

Je veux aussi clarifier en ce qui concerne onDemand() + @CreateSqlObject :

  • @CreateSqlObject Les DAO ne peuvent être utilisés qu'à partir des méthodes internes du DAO qui les a créés, comme dans l'exemple ci-dessus.
  • L'appel fooDao.createBar().findById() lèvera une exception indiquant que la connexion est fermée.

Désolé de commenter à nouveau. Y a-t-il quelque chose dans votre feuille de route qui vous aidera à cela ?

Je manque toujours vraiment de pouvoir utiliser CreateSqlObject dans des classes abstraites. Le fait de devoir tout faire dans les interfaces semble toujours restrictif ; souvent, je voudrais rendre une méthode de type "service" transactionnelle, mais cela signifierait que mes classes de service devaient également être des interfaces utilisant des méthodes par défaut et je commence à perdre la clarté de la séparation des préoccupations, des méthodes finales concrètes, etc.

@tamslinn en ce qui concerne votre problème avec les interfaces au lieu des classes abstraites, je pense que je peux dire en toute sécurité que nous ne reviendrons pas de cette décision dans un avenir proche. Jdbi3 utilise des proxys jdk, qui ne prennent en charge que les interfaces.

Je ne pense pas qu'un module séparé contribué construit sur cglib ou un autre qui offre cette fonctionnalité soit totalement hors de question, mais ce n'est pas un problème pour le moment.

Si les axiomes sqlobject vous conviennent si mal, vous pouvez toujours refactoriser vos services pour qu'ils soient des classes régulières non améliorées par jdbi mais plutôt en ayant une instance Jdbi injectée pour utiliser l'API fluide à la place.

En ce qui concerne le sujet initial du comportement entre notre support de transaction, onDemand, etc., cela devient de plus en plus une priorité sur laquelle travailler, bien que je ne pense pas que nous ayons encore des plans vraiment concrets. Mais on y arrive.

Merci beaucoup pour la mise à jour. Tout à fait logique concernant les procurations.
La limitation spécifique pour moi est de ne pas pouvoir démarrer/terminer des transactions en dehors des classes JDBI.
(Comme mentionné par @svlada , il est assez courant dans d'autres frameworks d'utiliser @Transactional à un niveau supérieur)
J'aime vraiment tout le reste de JDBI, donc je vais continuer, peut-être que je trouverai un modèle de conception qui me convient pour mes classes de service :)

Désolé de commenter à nouveau. Y a-t-il quelque chose dans votre feuille de route qui vous aidera à cela ?

Aucun problème du tout. C'est clairement une source de confusion, et quelque chose que nous aimerions corriger - nous voulons vraiment nous assurer de bien faire les choses cette fois, puisque nous ne l'avons pas fait la dernière fois :)

(Comme mentionné par @svlada , il est assez courant dans d'autres frameworks d'utiliser @Transactional à un niveau supérieur)

Oui, mais pour autant que je sache, tout cela se fait via des crochets AOP dans votre cadre DI (comme Spring). Étant donné que JDBI n'"enveloppe" pas tous vos objets de service, nous n'avons aucune possibilité de fournir des solutions de type AOP pour les objets que nous ne créons pas nous-mêmes. Même l'ancienne implémentation cglib ne remarquerait que @Transactional sur les daos eux-mêmes, cela n'aurait jamais fonctionné sur une classe de service distincte.

Si cela s'avère utile, nous pourrions envisager d'ajouter, par exemple, des liaisons Spring et/ou Guice AOP pour autoriser les transactions à un niveau supérieur. Cela ne ferait pas partie de core mais, par exemple, des extensions spring . Il est peu probable que ce soit quelque chose sur lequel les principaux développeurs sautent, mais une contribution serait sérieusement envisagée pour inclusion. Peut-être que cela résout votre problème de manière "plus propre" ?

Nous avons décidé de passer de cglib à Proxy principalement pour des raisons de compatibilité -- Proxy est une API jdk prise en charge, alors que cglib (ou plus particulièrement asm ) a tendance à casser à chaque version majeure (il a cassé le 8, 9, 11, ...) ce qui se transforme en un énorme casse-tête de maintenance.

Peut-être que https://github.com/jdbi/jdbi/pull/1252 est pertinent ?

Salut,

Merci pour l'info @stevenschlansker. Je n'utilise pas réellement Spring avec JDBI pour mon projet actuel, vient d'être mentionné à titre de comparaison. Je comprends parfaitement les trucs AOP, j'essaie juste de trouver un moyen de contourner cela qui signifie que je peux diviser le code comme je le souhaite - les classes abstraites fonctionnaient pour moi, mais il est tout à fait logique de s'éloigner de cglib

Je pense maintenant que je vais juste construire mes classes de service quand j'en ai besoin, de cette façon je peux les construire à l'intérieur d'une transaction.

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

Je pense que cela signifie que je peux tester le service avec un Mock Dao, garder les méthodes de service finales et ne pas compter sur les méthodes par défaut dans les interfaces pour la gestion des transactions, et la surcharge supplémentaire de l'instanciation de la classe de service ne devrait pas avoir d'impact significatif.

Juste une réponse rapide à l'exemple de code dans votre dernier commentaire : vous pouvez ignorer le bloc try et appeler simplement jdbi.useTransaction() directement. Cette méthode alloue un handle temporaire qui est automatiquement fermé lorsque votre rappel revient.

Salut tout le monde, il y a un PR #1579 - à partir de jdbi 3.10.0, CreateSqlObject et onDemand devraient (enfin) bien jouer ensemble.

Cette page vous a été utile?
0 / 5 - 0 notes

Questions connexes

jimmyhmiller picture jimmyhmiller  ·  6Commentaires

buremba picture buremba  ·  5Commentaires

stevenschlansker picture stevenschlansker  ·  4Commentaires

qualidafial picture qualidafial  ·  3Commentaires

dhardtke picture dhardtke  ·  3Commentaires