Das Abrufen von Daten mit ORM ist einfach! Oder nicht?


Einführung


Fast jedes Informationssystem interagiert auf die eine oder andere Weise mit externen Datenspeichern. In den meisten Fällen handelt es sich um eine relationale Datenbank, und häufig wird eine Art ORM-Framework verwendet, um mit Daten zu arbeiten. ORM eliminiert die meisten Routineoperationen und bietet stattdessen einen kleinen Satz zusätzlicher Abstraktionen für die Arbeit mit Daten.


Martin Fowler veröffentlichte einen interessanten Artikel , einer der wichtigsten Gedanken dort: „ORMs helfen uns, eine große Anzahl von Problemen in Unternehmensanwendungen zu lösen ... Dieses Tool kann nicht als hübsch bezeichnet werden, aber die Probleme, mit denen es sich befasst, sind auch nicht gut. Ich denke, ORM verdient mehr Respekt und Verständnis. “


Wir setzen ORM sehr intensiv im CUBA- Framework ein, daher kennen wir die Probleme und Einschränkungen dieser Technologie aus erster Hand, da CUBA in verschiedenen Projekten auf der ganzen Welt eingesetzt wird. Es gibt viele Themen, die in Bezug auf ORM diskutiert werden können, aber wir werden uns auf eines davon konzentrieren: die Wahl zwischen den „faulen“ (faulen) und den „gierigen“ (eifrigen) Methoden der Datenerfassung. Wir werden anhand von Abbildungen aus der JPA-API und Spring über verschiedene Lösungsansätze für dieses Problem sprechen und auch beschreiben, wie (und warum genau) ORM in CUBA verwendet wird und welche Arbeit wir tun, um die Arbeit mit Daten in unserem Framework zu verbessern.


Datenerfassung: faul oder nicht?


Wenn Ihr Datenmodell nur eine Entität hat, werden Sie bei der Arbeit mit ORM höchstwahrscheinlich keine Probleme bemerken. Schauen wir uns ein kleines Beispiel an. Angenommen, wir haben eine User () mit zwei Attributen: ID und Name () :


 public class User { @Id @GeneratedValue private int id; private String name; //Getters and Setters here } 

Um eine Instanz dieser Entität aus der Datenbank abzurufen, müssen Sie nur eine Methode des EntityManager Objekts aufrufen:


 EntityManager em = entityManagerFactory.createEntityManager(); User user = em.find(User.class, id); 

Etwas interessanter wird es, wenn eine Eins-zu-Viele-Beziehung entsteht:


 public class User { @Id @GeneratedValue private int id; private String name; @OneToMany private List<Address> addresses; //Getters and Setters here } 

Wenn wir eine Benutzerinstanz aus der Datenbank extrahieren müssen, stellt sich die Frage: "Wählen wir auch Adressen aus?". Und die "richtige" Antwort lautet hier: "Kommt auf ... an" In einigen Fällen benötigen wir Adressen, in anderen - nicht. In der Regel bietet ORM zwei Möglichkeiten zum Abrufen abhängiger Datensätze: faul und gierig. Standardmäßig verwenden die meisten ORMs den faulen Weg. Aber wenn wir diesen Code schreiben:


 EntityManager em = entityManagerFactory.createEntityManager(); User user = em.find(User.class, 1); em.close(); System.out.println(user.getAddresses().get(0)); 

... dann bekommen wir die Ausnahme “LazyInitException” , die Neulinge, die gerade mit ORM angefangen haben, schrecklich verwirrt. Und hier kommt der Moment, in dem Sie eine Geschichte darüber beginnen müssen, was "angehängte" und "getrennte" Instanzen einer Entität sind, was Sitzungen und Transaktionen sind.
Ja, das bedeutet, dass die Entität an die Sitzung „angehängt“ werden muss, damit Sie die abhängigen Daten auswählen können. Lassen Sie uns Transaktionen nicht sofort abschließen, und das Leben wird sofort einfacher. Und hier tritt ein weiteres Problem auf: Transaktionen werden länger, was das Risiko eines Deadlocks erhöht. Transaktionen kürzer machen? Es ist möglich, aber wenn Sie viele, viele kleine Transaktionen erstellen, erhalten wir die „Geschichte von Komar Komarovich - eine lange Nase und eine pelzige Mischa - einen kurzen Schwanz“ darüber, wie die Horde winziger Bärenmücken gewonnen hat - es wird mit der Datenbank geschehen. Wenn die Anzahl kleiner Transaktionen erheblich zunimmt, treten Leistungsprobleme auf.
Wie bereits erwähnt, sind beim Abrufen von Daten über einen Benutzer möglicherweise Adressen erforderlich oder nicht. Abhängig von der Geschäftslogik müssen Sie daher entweder die Sammlung auswählen oder nicht. Es ist notwendig, dem Code neue Bedingungen hinzuzufügen ... Hmmm ... Etwas wird irgendwie kompliziert.


Was ist, wenn Sie eine andere Art von Probe ausprobieren?


 public class User { @Id @GeneratedValue private int id; private String name; @OneToMany(fetch = FetchType.EAGER) private List<Address> addresses; //Getters and Setters here } 

Nun ... man kann nicht sagen, dass es viel helfen wird. Ja, wir werden das verhasste LazyInit und es besteht keine Notwendigkeit zu überprüfen, ob die Entität an die Sitzung angehängt ist oder nicht. Jetzt können jedoch Leistungsprobleme auftreten, da wir nicht immer Adressen benötigen, diese Objekte jedoch im Speicher des Servers auswählen.
Noch mehr Ideen?


Frühling jdbc


Einige Entwickler haben ORM so satt, dass sie zu alternativen Frameworks wechseln. Zum Beispiel in Spring JDBC, das die Möglichkeit bietet, relationale Daten im "halbautomatischen" Modus in Objektdaten zu konvertieren. Der Entwickler schreibt Abfragen für jeden Fall, in dem ein bestimmter Satz von Attributen benötigt wird (oder derselbe Code wird für Fälle wiederverwendet, in denen dieselben Datenstrukturen benötigt werden).


Dies gibt uns große Flexibilität. Sie können beispielsweise nur ein Attribut auswählen, ohne das entsprechende Entitätsobjekt zu erstellen:


 String name = this.jdbcTemplate.queryForObject( "select name from t_user where id = ?", new Object[]{1L}, String.class); 

Oder wählen Sie ein Objekt in der üblichen Form aus:


 User user = this.jdbcTemplate.queryForObject( "select id, name from t_user where id = ?", new Object[]{1L}, new RowMapper<User>() { public User mapRow(ResultSet rs, int rowNum) throws SQLException { User user = new User(); user.setName(rs.getString("name")); user.setId(rs.getInt("id")); return user; } }); 

Sie können auch eine Liste mit Adressen für den Benutzer auswählen. Sie müssen lediglich etwas mehr Code schreiben und die SQL-Abfrage korrekt erstellen, um das Problem von n + 1-Abfragen zu vermeiden.


Soooo, wieder kompliziert. Ja, wir steuern alle Abfragen und wie die Daten auf Objekte abgebildet werden, aber wir müssen mehr Code schreiben, SQL lernen und wissen, wie die Abfragen in der Datenbank ausgeführt werden. Persönlich denke ich, dass SQL-Kenntnisse eine erforderliche Fähigkeit für einen Anwendungsprogrammierer sind, aber nicht jeder denkt so, und ich werde mich nicht auf Polemik einlassen. Schließlich ist die Kenntnis der x86-Montageanleitung heutzutage auch optional. Lassen Sie uns besser darüber nachdenken, wie wir Programmierern das Leben erleichtern können.


JPA EntityGraph


Und lassen Sie uns einen Schritt zurücktreten und überlegen, was wir brauchen. Es scheint, dass wir nur genau angeben müssen, welche Attribute wir jeweils benötigen. Nun, lass es uns tun! JPA 2.1 führte eine neue API ein - EntityGraph (Entity Graph). Die Idee ist sehr einfach: Wir verwenden Anmerkungen, um zu beschreiben, was wir aus der Datenbank auswählen werden. Hier ist ein Beispiel:


 @Entity @NamedEntityGraphs({ @NamedEntityGraph(name = "user-only-entity-graph"), @NamedEntityGraph(name = "user-addresses-entity-graph", attributeNodes = {@NamedAttributeNode("addresses")}) }) public class User { @Id @GeneratedValue private int id; private String name; @OneToMany(fetch = FetchType.LAZY) private Set<Address> addresses; //Getters and Setters here } 

Für diese Entität werden zwei Diagramme beschrieben: Das user-only-entity-graph wählt das Addresses (als faul markiert) nicht aus, während das zweite Diagramm ORM anweist, dieses Attribut auszuwählen. Wenn wir Addresses als eifrig markieren, wird das Diagramm ignoriert und Adressen werden trotzdem ausgewählt.


In JPA 2.1 können Sie also Daten wie folgt testen:


 EntityManager em = entityManagerFactory.createEntityManager(); EntityGraph graph = em.getEntityGraph("user-addresses-entity-graph"); Map<String, Object> properties = Map.of("javax.persistence.fetchgraph", graph); User user = em.find(User.class, 1, properties); em.close(); 

Dieser Ansatz vereinfacht die Arbeit erheblich, Sie müssen nicht separat über faule Attribute und die Transaktionslänge nachdenken. Ein zusätzlicher Bonus ist, dass das Diagramm auf der Ebene der SQL-Abfrage angewendet wird, sodass in der Java-Anwendung keine „zusätzlichen“ Daten ausgewählt werden. Es gibt jedoch ein kleines Problem: Sie können nicht sagen, welche Attribute ausgewählt wurden und welche nicht. Es gibt eine API zur Überprüfung. Dies erfolgt mithilfe der PersistenceUtil Klasse:


 PersistenceUtil pu = entityManagerFactory.getPersistenceUnitUtil(); System.out.println("User.addresses loaded: " + pu.isLoaded(user, "addresses")); 

Aber das ist ziemlich langweilig und nicht jeder ist bereit, solche Überprüfungen durchzuführen. Gibt es noch etwas, das Sie vereinfachen und nur keine Attribute anzeigen können, die nicht ausgewählt wurden?


Frühlingsprojektionen


Das Spring Framework hat eine großartige Funktion namens Projektionen (und dies ist nicht dasselbe wie Projektionen im Ruhezustand ). Wenn Sie nur einige Attribute einer Entität auswählen müssen, wird eine Schnittstelle mit den erforderlichen Attributen erstellt, und Spring wählt "Instanzen" dieser Schnittstelle aus der Datenbank aus. Betrachten Sie als Beispiel die folgende Schnittstelle:


 interface NamesOnly { String getName(); } 

Jetzt können Sie ein Spring JPA-Repository zum Abrufen von Benutzerentitäten wie folgt definieren:


 interface UserRepository extends CrudRepository<User, Integer> { Collection<NamesOnly> findByName(String lastname); } 

In diesem Fall erhalten wir nach dem Aufrufen der findByName-Methode in der resultierenden Liste Entitäten, die nur Zugriff auf Attribute haben, die in der Schnittstelle definiert sind! Nach dem gleichen Prinzip kann man abhängige Entitäten auswählen, d.h. Wählen Sie sofort die Beziehung „Master-Detail“ aus. Darüber hinaus generiert Spring in den meisten Fällen "korrektes" SQL, d. H. Aus der Datenbank werden nur die Attribute ausgewählt, die in der Projektion beschrieben sind. Dies ist der Funktionsweise von Entitätsdiagrammen sehr ähnlich.
Dies ist eine sehr leistungsfähige API. Bei der Definition von Schnittstellen können Sie SpEL-Ausdrücke verwenden, Klassen mit einer integrierten Logik anstelle von Schnittstellen verwenden und vieles mehr. In der Dokumentation wird alles ausführlich beschrieben.
Das einzige Problem bei Projektionen besteht darin, dass sie im Inneren als Schlüssel-Wert-Paare implementiert sind, d. H. sind schreibgeschützt. Dies bedeutet, dass wir die Änderungen weder über die CRUD-Repositorys noch über den EntityManager speichern können, selbst wenn wir eine Setter-Methode für die Projektion definieren. Projektionen sind also DTOs, die nur dann wieder in Entity konvertiert und gespeichert werden können, wenn Sie dafür Ihren eigenen Code schreiben.


So wählen Sie Daten in CUBA aus


Von Beginn der Entwicklung des CUBA-Frameworks an haben wir versucht, den Teil des Codes zu optimieren, der mit der Datenbank funktioniert. Bei CUBA verwenden wir EclipseLink als Basis für die Datenzugriffs-API. Das Gute an EclipseLink ist, dass es das Laden von Teilentitäten von Anfang an unterstützte, und dies war ein entscheidender Faktor bei der Auswahl zwischen EclipseLink und Hibernate. In EclipseLink können Sie Attribute angeben, die lange vor dem Erscheinen des JPA 2.1-Standards geladen werden sollen. CUBA hat eine eigene Art, ein Entitätsdiagramm zu beschreiben, das als CUBA-Ansichten bezeichnet wird . Repräsentationen CUBA ist eine ziemlich entwickelte API. Sie können einige Repräsentationen von anderen erben, sie kombinieren und sowohl auf Master- als auch auf Detailentitäten anwenden. Eine weitere Motivation für die Erstellung von CUBA-Ansichten besteht darin, dass wir kurze Transaktionen verwenden wollten, damit wir mit getrennten Entitäten in der Webbenutzeroberfläche arbeiten können.
In CUBA werden Ansichten in einer XML-Datei wie im folgenden Beispiel beschrieben:


 <view class="com.sample.User" extends="_minimal" name="user-minimal-view"> <property name="name"/> <property name="addresses" view="address-street-only-view"/> </property> </view> 

Diese Ansicht wählt die User und ihren lokalen Attributnamen aus und wählt Adressen aus, indem die address-street-only-view . All dies geschieht (Aufmerksamkeit!) Auf der Ebene der SQL-Abfrage. Wenn die Ansicht erstellt wird, können Sie sie bei der Datenauswahl mithilfe der DataManager-Klasse verwenden:


 List<User> users = dataManager.load(User.class).view("user-edit-view").list(); 

Dieser Ansatz funktioniert gut, während der Netzwerkverkehr wirtschaftlich genutzt wird, da nicht verwendete Attribute einfach nicht von der Datenbank an die Anwendung übertragen werden. Wie bei JPA gibt es jedoch ein Problem: Es kann nicht gesagt werden, welche Attribute der Entität geladen wurden. Und in CUBA gibt es eine Ausnahme “IllegalStateException: Cannot get unfetched attribute [...] from detached object” , die wie LazyInit von jedem angetroffen werden muss, der mit unserem Framework schreibt. Wie in der JPA gibt es Möglichkeiten zu überprüfen, welche Attribute geladen wurden und welche nicht, aber das Schreiben solcher Überprüfungen ist wiederum eine mühsame, mühsame Aufgabe, die Entwickler sehr aufregt. Es muss noch etwas anderes erfunden werden, um die Menschen nicht mit Arbeiten zu belasten, die Maschinen theoretisch leisten können.


Konzept - CUBA View-Schnittstellen


Was aber, wenn Sie versuchen, Entitätsdiagramme und Projektionen zu kombinieren? Wir haben uns dazu entschlossen und Schnittstellen für Entity-View-Schnittstellen entwickelt, die dem Spring-Projektionsansatz folgen. Diese Schnittstellen werden beim Start der Anwendung in CUBA-Ansichten übersetzt und können im DataManager verwendet werden. Die Idee ist einfach: Wir beschreiben eine Schnittstelle (oder eine Reihe von Schnittstellen), bei der es sich um ein Entitätsdiagramm handelt.


 interface UserMinimalView extends BaseEntityView<User, Integer> { String getName(); void setName(String val); List<AddressStreetOnly> getAddresses(); interface AddressStreetOnly extends BaseEntityView<Address, Integer> { String getStreet(); void setStreet(String street); } } 

Es ist erwähnenswert, dass Sie in bestimmten Fällen lokale Schnittstellen erstellen können, wie im Fall von AddressStreetOnly aus dem obigen Beispiel, um die öffentliche API Ihrer Anwendung nicht zu "verschmutzen".


Beim Starten einer CUBA-Anwendung (von der der größte Teil den Spring-Kontext initialisiert) erstellen wir programmgesteuert CUBA-Ansichten und fügen sie im Kontext in das interne Bean-Repository ein.
Jetzt müssen Sie die Implementierung der DataManager-Klasse geringfügig ändern, damit sie Schnittstellenansichten akzeptiert, und Sie können Entitäten auf folgende Weise auswählen:


 List<UserMinimalView> users = dataManager.load(UserMinimalView.class).list(); 

Unter der Haube wird ein Proxy-Objekt generiert, das die Schnittstelle implementiert und die aus der Datenbank ausgewählte Entitätsinstanz umschließt (ähnlich wie im Ruhezustand). Wenn der Entwickler den Attributwert aufruft, delegiert der Proxy den Methodenaufruf an die "echte" Instanz der Entität.


Bei der Entwicklung dieses Konzepts versuchen wir, zwei Fliegen mit einer Klappe zu schlagen:


  • Daten, die nicht in der Schnittstelle beschrieben sind, werden nicht in die Anwendung geladen, wodurch Serverressourcen gespart werden.
  • Der Entwickler kann nur die Attribute verwenden, auf die über die Schnittstelle UnfetchedAttribute werden kann (und die daher aus der Datenbank ausgewählt werden), wodurch die oben beschriebenen UnfetchedAttribute Ausnahmen beseitigt werden.

Im Gegensatz zu Spring-Projektionen verpacken wir Entitäten in Proxy-Objekte. Außerdem erbt jede Schnittstelle die Standard-CUBA-Schnittstelle - Entity . Dies bedeutet, dass Entitätsansichtsattribute geändert werden können und diese Änderungen dann mithilfe der Standard-CUBA-API für die Arbeit mit Daten in der Datenbank gespeichert werden.
Übrigens der „dritte Hase“ - Sie können Attribute schreibgeschützt machen, wenn Sie eine Schnittstelle nur mit Getter-Methoden definieren. Daher haben wir die Änderungsregeln bereits auf der Ebene der Entitäts-API festgelegt.
Darüber hinaus können Sie einige lokale Operationen für getrennte Entitäten mithilfe verfügbarer Attribute ausführen, z. B. die Konvertierung von Namenszeichenfolgen, wie im folgenden Beispiel:


 @MetaProperty default String getNameLowercase() { return getName().toLowerCase(); } 

Beachten Sie, dass berechnete Attribute aus dem Entitätsklassenmodell herausgenommen und an Schnittstellen übertragen werden können, die für eine bestimmte Geschäftslogik gelten.


Ein weiteres interessantes Merkmal ist die Vererbung von Schnittstellen. Sie können mehrere Ansichten mit unterschiedlichen Attributgruppen erstellen und diese dann kombinieren. Sie können beispielsweise eine Schnittstelle für eine Benutzerentität mit den Attributen name und email und eine andere mit den Attributen name und address erstellen. Wenn Sie nun Name, E-Mail-Adresse und Adresse auswählen müssen, müssen Sie diese Attribute nicht auf die dritte Schnittstelle kopieren, sondern nur von den ersten beiden Ansichten erben. Und ja, Instanzen der dritten Schnittstelle können an Methoden übergeben werden, die Parameter mit dem Typ der übergeordneten Schnittstellen akzeptieren. Die OOP-Regeln sind für alle gleich.


Eine Konvertierung zwischen Ansichten wurde ebenfalls implementiert - jede Schnittstelle verfügt über eine reload () -Methode, an die Sie die Ansichtsklasse als Parameter übergeben können:


 UserFullView userFull = userMinimal.reload(UserFullView.class); 

UserFullView kann zusätzliche Attribute enthalten, sodass die Entität bei Bedarf aus der Datenbank neu geladen wird. Und dieser Prozess ist verzögert. Der Zugriff auf die Datenbank erfolgt nur, wenn der erste Zugriff auf die Attribute der Entität erfolgt. Dies verlangsamt den ersten Aufruf ein wenig, aber dieser Ansatz wurde absichtlich gewählt. Wenn die Entitätsinstanz im Webmodul verwendet wird, das die Benutzeroberfläche und ihre eigenen REST-Controller enthält, kann dieses Modul auf einem separaten Server bereitgestellt werden. Dies bedeutet, dass die erzwungene Überlastung der Entität zusätzlichen Netzwerkverkehr erzeugt - Zugriff auf das Kernmodul und dann auf die Datenbank. Indem wir die Überlastung bis zu dem Zeitpunkt verschieben, an dem dies erforderlich ist, sparen wir Datenverkehr und reduzieren die Anzahl der Datenbankabfragen.


Das Konzept ist als Modul für CUBA konzipiert. Ein Anwendungsbeispiel kann von GitHub heruntergeladen werden.


Fazit


Es scheint, dass wir ORM in naher Zukunft immer noch massiv in Unternehmensanwendungen einsetzen werden, nur weil wir etwas brauchen, das relationale Daten in Objekte verwandelt. Natürlich werden spezifische Lösungen für komplexe, einzigartige Anwendungen mit ultrahoher Last entwickelt, aber es scheint, dass ORM-Frameworks so lange funktionieren wie relationale Datenbanken.
In CUBA versuchen wir, die Arbeit mit ORM maximal zu vereinfachen, und in zukünftigen Versionen werden wir neue Funktionen für die Arbeit mit Daten einführen. Es wird schwierig sein zu sagen, ob dies Präsentationsschnittstellen oder etwas anderes sein werden, aber ich bin mir einer Sache sicher: Wir werden die Arbeit mit Daten in zukünftigen Versionen des Frameworks weiter vereinfachen.

Source: https://habr.com/ru/post/de451986/


All Articles