Jdbi: Machen Sie @CreateSqlObject weniger verwirrend.

Erstellt am 1. Feb. 2018  ·  20Kommentare  ·  Quelle: jdbi/jdbi

Problem
Die offizielle JDBI-Dokumentation lässt Entwickler glauben, dass die Annotation @CreateSqlObject der Mechanismus zum Hinzufügen von Transaktionsunterstützung zu unterschiedlichen DAOs ist.

http://jdbi.org/#__createsqlobject

Was für das tatsächliche Verhalten etwas irreführend erscheint, was zu Zustandsfehlern führen kann, wenn es mit dem bevorzugten jdbi.onDemand -Erstellungsmechanismus verwendet wird.

Es gab einige Nachrichten im Forum, die diese Verwirrung veranschaulichen:

Anfrage
Ich bin mir nicht sicher, was die beste Lösung für dieses Problem wäre. Einige, die mir in den Sinn kommen:

  • Missbilligung
  • Bessere Dokumentation der Mängel
  • Eine weitere Lösung zum Aufteilen von Abfragen in mehrere DAOs, die jedoch durch logische Transaktionssemantik miteinander verbunden werden können. Insbesondere bei der Unterstützung einer Mischung aus Nur-Lese-/Schreibtransaktionen.
cleanup improvement

Hilfreichster Kommentar

Tut mir leid, dies erneut zu kommentieren. Gibt es etwas in Ihrer Roadmap, das Ihnen dabei helfen wird?

Überhaupt kein Problem. Dies ist eindeutig eine Quelle der Verwirrung und etwas, das wir gerne beheben würden – wir wollen nur wirklich sicherstellen, dass es diesmal richtig ist, da wir es letztes Mal nicht getan haben :)

(Wie von @svlada erwähnt, ist es in anderen Frameworks durchaus üblich, @Transactional auf einer höheren Ebene zu verwenden.)

Ja, aber soweit ich verstehe, wird das alles über AOP-Hooks in Ihrem DI-Framework (wie Spring) erledigt. Da JDBI nicht alle Ihre Dienstobjekte "verpackt", haben wir keine Möglichkeit, AOP-ähnliche Lösungen für Objekte bereitzustellen, die wir nicht selbst erstellen. Sogar die alte cglib -Implementierung würde nur @Transactional auf den Daos selbst bemerken, es hätte niemals auf einer separaten Dienstklasse funktioniert.

Wenn es hilfreich wäre, könnten wir erwägen, zB Spring- und/oder Guice-AOP-Bindungen hinzuzufügen, um Transaktionen auf einer höheren Ebene zu ermöglichen. Dies wäre nicht Teil von core sondern zB Teil der spring Erweiterungen. Dies ist wahrscheinlich nicht etwas, worauf die Core-Entwickler aufspringen, aber ein Beitrag würde ernsthaft für die Aufnahme in Betracht gezogen. Vielleicht löst das dein Problem "sauberer"?

Wir haben uns hauptsächlich aus Kompatibilitätsgründen für den Wechsel von cglib zu Proxy entschieden – Proxy ist eine unterstützte jdk-API, während cglib (oder genauer gesagt asm ) neigt dazu, mit jeder größeren Veröffentlichung zu brechen (es brach am 8., 9., 11., ...), was zu großen Kopfschmerzen bei der Wartung führt.

Alle 20 Kommentare

Danke, dass du das gemeldet hast. Ich vermute, dass wir hier mit einer technisch bahnbrechenden Änderung enden werden, aber ich denke, das bestehende Verhalten ist schlecht genug, dass wir früh im 3.x-Veröffentlichungszyklus eine technisch bahnbrechende Änderung vornehmen. Melden Sie dieses Problem bitte, wenn Sie auf das vorhandene Verhalten angewiesen sind oder nicht damit einverstanden sind, möglicherweise eine kleine bahnbrechende Änderung hier zu veröffentlichen.

Nur für den Kontext zu _warum_ @CreateSqlObject passt nicht gut zu On-Demand:

  • On-Demand ist eine Abstraktion auf Kernebene, die mithilfe von Proxys implementiert wird. Wenn Sie eine Methode auf einem On-Demand-Proxy aufrufen, wird ein echtes SQL-Objekt erstellt und der Methodenaufruf an das echte SQL-Objekt delegiert. Das Handle, das diese reale Instanz unterstützt, wird geschlossen, nachdem der Delegate-Methodenaufruf zurückgegeben wurde.
  • @CreateSqlObject wird mit handle.attach(sqlObjectType) implementiert, mit dem Backing-Handle des ursprünglichen SQL-Objekts.

Somit wird das Unterstützungshandle für das erstellte SQL-Objekt geschlossen, bevor das SQL-Objekt überhaupt zurückgegeben werden kann.

Nichts davon ist in Stein gemeißelt – es ist nur so, wie es jetzt implementiert wird.

Ich bin nicht davon überzeugt, dass wir die Kompatibilität aufheben müssen, um dies zu beheben.

Entschuldigung, ich verstehe es nicht wirklich ... Was genau ist das Problem mit onDemand und CreateSqlObject? Ich hatte noch nie Probleme damit, und irgendetwas an der Erklärung von @qualidafial bringt mich einfach um den Verstand...

image

Für den Aufruf von fooProxy.usecase wird eine Verbindung aufgebaut. Foo.usecase ruft die createSqlObject-Methode bar auf, die eine neue Bar zurückgibt, nachdem sie an das aktuelle Handle ( usecase ) angehängt wurde. Das Handle von usecase wird geschlossen, wenn usecase zurückkehrt. Wie kann das Handle von bar zu früh ablaufen, solange Sie nichts asynchrones mit dem zurückgegebenen Bar tun? Der Lebenszyklus und die Verwendung von bar ist auf den Körper von Foo.usecase beschränkt, ebenso wie das Handle ...

Ich kann nicht zu den Interna von JDBI sprechen, aber ich kann das Verhalten kommentieren, das wir in der Produktion gesehen haben.

Im Grunde war in unseren niederen Umgebungen alles in Ordnung. Aber mit der erhöhten Last in der Produktion würden wir ständig sehen, dass SQL-Anweisungen nicht für den richtigen Datenbankknoten ausgeführt werden. Beispielsweise würde eine Read Only-Auswahl den Master-Knoten treffen, und eine Insert würde eine Read Replica treffen. Darüber hinaus würden wir Fehler beim Schließen der Verbindung erhalten, wenn sie sich mitten in einer Operation befand.

Im Wesentlichen schien @CreateSqlObject ein Thread-Sicherheitsproblem verursacht zu haben, bei dem das Nur-Lesen-Flag durch konkurrierende Anforderungen geändert wurde.

Um das Problem zu "lösen", haben wir die gesamte Verwendung von @CreateSqlObject entfernt und am Ende so etwas erhalten:

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

Und dann jdbi.onDemand(CombinedDao) und nutzen nur den Zugriff darüber.
Der Wechsel zu diesem Muster war zwar nicht so hübsch, beseitigte jedoch alle oben genannten Fehler in der Produktion.

Nur zur Information, ich denke darüber nach, zu JDBI 2 zurückzukehren, da mein gesamter Code im Moment onDemand mit abstrakten Klassen verwendet und ich nicht herausfinden kann, wie ich so umstrukturieren kann, dass ich damit zufrieden bin!

Ich neige dazu, Dienstschnittstellen mit abstrakten Implementierungsklassen zu haben, die Logik, aber kein SQL enthalten, mit Methoden, die als transaktional annotiert sind, und die CreateSqlObject verwenden, um Zugriff auf DAO-Klassen zu gewähren, die reines SQL sind. Ein vereinfachtes Beispiel:

Schnittstelle

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

Implementierung

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

(Sie können sich das Dao vorstellen)

Dies ergibt eine wirklich schöne Trennung von Logik- und Datenzugriff, während gleichzeitig Transaktionen über mehrere DAO-Methoden ermöglicht werden. Komponententests für den Dienst sind durch die Implementierung der DAO-Schnittstellen einfach zu schreiben und zu verstehen.

Ich habe versucht, ähnliches in JDBI 3 zu tun, aber ich denke, es bedeutet, dass die Implementierung meiner Dienstklasse eine Schnittstelle mit Standardmethoden für diejenigen sein muss, die Logik enthalten. Standardmethoden können nicht endgültig gemacht werden, daher fühlt sich der Code einfach nicht so prägnant an und gibt mir weniger Kontrolle darüber, wie meine Klasse verwendet werden kann.

Gibt es eine Möglichkeit, meinen Code in JDBI 3 so zu strukturieren, dass er endgültige Transaktionsmethoden enthält?

Sie haben Recht, dass es keine Möglichkeit gibt, eine Schnittstellenmethode als endgültig zu definieren.

Sie _können_ jedoch Ihre eigenen SQL-Methodenannotationen auf Augenhöhe mit @SqlQuery , @SqlUpdate usw. definieren und eine statische Implementierung für Methoden mit dieser Annotation bereitstellen.

Sie _können_ jedoch Ihre eigenen SQL-Methodenannotationen auf Augenhöhe mit @SqlQuery , @SqlUpdate usw. definieren und eine statische Implementierung für Methoden mit dieser Annotation bereitstellen.

Danke für die Antwort - ich kann nicht ganz nachvollziehen, wie mir das hilft. Was ist die empfohlene Methode, um Abfragen in mehreren DAOs zu haben, sie aber in derselben Transaktion auszuführen?

Möglichkeit, Abfragen in mehreren DAOs zu haben, sie aber in derselben Transaktion auszuführen?

Persönlich verwende ich so etwas wie

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

Funktioniert bei mir so super. @CreateSqlObject ist im Grunde wie die Abhängigkeitsinjektion von Spring durch Getter/Setter. Ich habe onDemand-Instanzen als Beans in meinen Spring-Kontext gestellt, damit es genau wie normale Dienste funktioniert, Anrufer müssen nichts über jdbi oder die Implementierungsschnittstelle wissen.

image

Das Anti-Muster-aussehende StockReductionCase-Ding, das Sie sehen, ist ein Beispiel für eine verschachtelte jdbi-"Abhängigkeitsinjektion" mit CreateSqlObject. Es hat seine eigenen Abhängigkeiten, genau wie die Dienstimplementierung. Es ist im Grunde ein eigenständiger Dienst, ich nenne ihn einfach Fall statt Dienst, um zyklische Abhängigkeiten zu vermeiden, indem Fälle (die an mehreren Stellen benötigt werden) immer nur in Dienste (nicht wiederverwendbare Logik auf oberster Ebene) und Abfragen in beide eingefügt werden.

image

Ich habe gemischte Erfolgsberichte darüber gehört, wie Transaktionen mit @CreateSqlObject korrekt erfasst wurden, insbesondere in Kombination mit onDemand() .

Der sicherste Weg, mehrere SQL-Objekte in derselben Transaktion auszuführen, besteht darin, Transaktionen über Handle.inTransaction() oder Jdbi.inTransaction() auszuführen. Innerhalb des Rückrufs sind alle über Handle.attach() erstellten DAOs Teil der Transaktion des Handles:

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

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

Danke für die Antworten
@qualidafial das sieht so aus, als müsste ich gehen, aber es macht es schwierig, Komponententests für die Serviceklasse zu schreiben. Da es das Daos selbst instanziiert, kann ich Mock-Daos nicht für Komponententests ersetzen.

@TheRealMarnes danke auch - das sieht ähnlich aus wie ich es versucht habe, ich mag es einfach nicht, Standardmethoden zu verwenden, da sie überschrieben werden können.

@qualidafial Ich möchte mit Ihnen bestätigen, dass wir die @Transaction -Anmerkung von jdbi nicht für Dienstmethoden verwenden können, die mehrere Dao-Methoden aufrufen?

Wenn diese Annahme zutrifft, ist dieses Verhalten sehr gefährlich, ohne dass es im Rahmen der offiziellen Dokumentation ordnungsgemäß dokumentiert wird. Die große Anzahl von Entwicklern hat Erfahrung mit Spring Stack und die Verwendung der @Transactional -Annotation ist eine sehr gängige Methode, um Transaktionen über mehrere DAOs hinweg zu unterstützen.

@svlada Die Annotation @Transaction funktioniert _nur_ bei Methoden von SQL-Objekten. Wenn Sie "Dienstmethoden" sagen, habe ich den Eindruck, dass Sie die Anmerkung für ein Objekt außerhalb des Einflusses von Jdbi verwenden, z. B. eine injizierte Klasse.

Ein Beispiel aus unserer Testsuite:

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

Ich möchte auch in Bezug auf onDemand() + @CreateSqlObject klarstellen:

  • @CreateSqlObject DAOs dürfen nur innerhalb von Methoden des DAO verwendet werden, das sie erstellt hat – wie im obigen Beispiel.
  • Der Aufruf fooDao.createBar().findById() löst eine Ausnahme aus, die besagt, dass die Verbindung geschlossen ist.

Tut mir leid, dies erneut zu kommentieren. Gibt es etwas in Ihrer Roadmap, das Ihnen dabei helfen wird?

Ich vermisse es immer noch sehr, CreateSqlObject in abstrakten Klassen verwenden zu können. Alles in Schnittstellen tun zu müssen, fühlt sich immer noch restriktiv an; Oft möchte ich eine Methode vom Typ "Service" transaktional machen, aber dies würde bedeuten, dass meine Serviceklassen auch Schnittstellen sein müssten, die Standardmethoden verwenden, und ich beginne, die Klarheit der Trennung von Bedenken, konkreten endgültigen Methoden usw. zu verlieren.

@tamslinn Was Ihr Problem mit Schnittstellen anstelle abstrakter Klassen betrifft, kann ich sicher sagen, dass wir in naher Zukunft nicht von dieser Entscheidung zurückkommen werden. Jdbi3 verwendet jdk-Proxies, die nur Schnittstellen unterstützen.

Ich denke nicht, dass ein beigesteuertes separates Modul, das auf cglib oder ähnlichem basiert und diese Funktion bietet, völlig ausgeschlossen ist, aber es ist im Moment kein Problem.

Wenn die sqlobject-Axiome so schlecht zu Ihnen passen, können Sie Ihre Dienste immer so umgestalten, dass sie reguläre Klassen sind, die nicht von jdbi erweitert werden, sondern stattdessen eine Jdbi -Instanz injiziert wird, um stattdessen die Fluent-API zu verwenden.

Was das ursprüngliche Thema des Verhaltens zwischen unserer Transaktionsunterstützung, OnDemand usw. angeht, so rückt die Arbeit immer mehr in den Fokus, obwohl ich glaube, dass wir noch keine wirklich konkreten Pläne haben. Aber wir kommen dazu.

Vielen Dank für das Update. Völlig sinnvoll bezüglich der Proxys.
Die spezifische Einschränkung für mich besteht darin, dass Transaktionen außerhalb der JDBI-Klassen nicht gestartet/beendet werden können.
(Wie von @svlada erwähnt, ist es in anderen Frameworks durchaus üblich, @Transactional auf einer höheren Ebene zu verwenden.)
Ich mag aber wirklich alles andere an JDBI, also bleibe ich dran, vielleicht finde ich ein Designmuster, mit dem ich für meine Serviceklassen zufrieden bin :)

Tut mir leid, dies erneut zu kommentieren. Gibt es etwas in Ihrer Roadmap, das Ihnen dabei helfen wird?

Überhaupt kein Problem. Dies ist eindeutig eine Quelle der Verwirrung und etwas, das wir gerne beheben würden – wir wollen nur wirklich sicherstellen, dass es diesmal richtig ist, da wir es letztes Mal nicht getan haben :)

(Wie von @svlada erwähnt, ist es in anderen Frameworks durchaus üblich, @Transactional auf einer höheren Ebene zu verwenden.)

Ja, aber soweit ich verstehe, wird das alles über AOP-Hooks in Ihrem DI-Framework (wie Spring) erledigt. Da JDBI nicht alle Ihre Dienstobjekte "verpackt", haben wir keine Möglichkeit, AOP-ähnliche Lösungen für Objekte bereitzustellen, die wir nicht selbst erstellen. Sogar die alte cglib -Implementierung würde nur @Transactional auf den Daos selbst bemerken, es hätte niemals auf einer separaten Dienstklasse funktioniert.

Wenn es hilfreich wäre, könnten wir erwägen, zB Spring- und/oder Guice-AOP-Bindungen hinzuzufügen, um Transaktionen auf einer höheren Ebene zu ermöglichen. Dies wäre nicht Teil von core sondern zB Teil der spring Erweiterungen. Dies ist wahrscheinlich nicht etwas, worauf die Core-Entwickler aufspringen, aber ein Beitrag würde ernsthaft für die Aufnahme in Betracht gezogen. Vielleicht löst das dein Problem "sauberer"?

Wir haben uns hauptsächlich aus Kompatibilitätsgründen für den Wechsel von cglib zu Proxy entschieden – Proxy ist eine unterstützte jdk-API, während cglib (oder genauer gesagt asm ) neigt dazu, mit jeder größeren Veröffentlichung zu brechen (es brach am 8., 9., 11., ...), was zu großen Kopfschmerzen bei der Wartung führt.

Vielleicht ist https://github.com/jdbi/jdbi/pull/1252 relevant?

Hallo,

Danke für die Info @stevenschlansker. Ich verwende Spring mit JDBI eigentlich nicht für mein aktuelles Projekt, wurde nur zum Vergleich erwähnt. Ich verstehe das AOP-Zeug vollkommen und versuche nur, einen Weg zu finden, der bedeutet, dass ich den Code so aufteilen kann, wie ich es möchte - die abstrakten Klassen haben früher für mich funktioniert, aber es ist absolut sinnvoll, sich von cglib zu entfernen

Ich denke, jetzt konstruiere ich einfach meine Serviceklassen, wenn ich sie brauche, damit ich sie innerhalb einer Transaktion konstruieren kann.

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

Ich denke, dies bedeutet, dass ich den Dienst mit einem Mock Dao testen kann, Dienstmethoden endgültig halten und mich nicht auf Standardmethoden in Schnittstellen für die Transaktionsverwaltung verlassen kann, und der zusätzliche Overhead der Serviceklassen-Instanziierung sollte keine wesentlichen Auswirkungen haben.

Nur eine kurze Antwort auf das Codebeispiel in Ihrem letzten Kommentar: Sie könnten den Block try überspringen und einfach direkt jdbi.useTransaction() aufrufen. Diese Methode weist ein temporäres Handle zu, das automatisch geschlossen wird, wenn Ihr Callback zurückkehrt.

Hallo zusammen, es gibt eine PR #1579 – ab jdbi 3.10.0 sollten CreateSqlObject und onDemand (endlich) gut zusammenspielen.

War diese Seite hilfreich?
0 / 5 - 0 Bewertungen

Verwandte Themen

stevenschlansker picture stevenschlansker  ·  4Kommentare

electrum picture electrum  ·  3Kommentare

Shujito picture Shujito  ·  5Kommentare

rherrmann picture rherrmann  ·  4Kommentare

mcarabolante picture mcarabolante  ·  4Kommentare