Daten mit Java-Datenobjekten beibehalten, Teil 1

"Alles sollte so einfach wie möglich gemacht werden, aber nicht einfacher."

Albert Einstein

Die Notwendigkeit, zur Laufzeit erstellte Daten beizubehalten, ist so alt wie das Rechnen. Und die Notwendigkeit, objektorientierte Daten zu speichern, tauchte auf, als die objektorientierte Programmierung allgegenwärtig wurde. Derzeit verwenden die meisten modernen, nicht trivialen Anwendungen ein objektorientiertes Paradigma, um Anwendungsdomänen zu modellieren. Im Gegensatz dazu ist der Datenbankmarkt stärker gespalten. Die meisten Datenbanksysteme verwenden das relationale Modell, aber objektbasierte Datenspeicher sind in vielen Anwendungen unverzichtbar. Außerdem verfügen wir über Legacy-Systeme, mit denen wir häufig eine Schnittstelle herstellen müssen.

In diesem Artikel werden die Probleme im Zusammenhang mit der Datenpersistenz in Transaktions-Middleware-Umgebungen wie J2EE (Java 2 Platform, Enterprise Edition) beschrieben und gezeigt, wie Java Data Objects (JDO) einige dieser Probleme löst. Dieser Artikel bietet eine Übersicht, kein detailliertes Tutorial, und ist aus der Sicht eines Anwendungsentwicklers und nicht eines JDO-Implementierungsdesigners verfasst.

Lesen Sie die gesamte Serie zu Java-Datenobjekten:

  • Teil 1. Erfassen Sie die Eigenschaften hinter einer idealen Persistenzschicht
  • Teil 2. Sun JDO gegen Castor JDO

Diejenigen Java-Entwickler, Designer und J2EE-Architekten, die auf Systemen arbeiten, die Daten in relationalen oder Objektdatenbanken oder anderen Speichermedien speichern müssen, sollten diesen Artikel lesen. Ich gehe davon aus, dass Sie über Grundkenntnisse in Java verfügen und mit objektrelationalen Problemen und Begriffen vertraut sind.

Transparente Persistenz: Warum sich die Mühe machen?

Mehr als ein Jahrzehnt kontinuierlicher Versuche, objektorientierte Laufzeit und Persistenz zu überbrücken, weisen auf mehrere wichtige Beobachtungen hin (in der Reihenfolge ihrer Wichtigkeit aufgeführt):

  1. Das Abstrahieren von Persistenzdetails und eine saubere, einfache, objektorientierte API zur Durchführung der Datenspeicherung sind von größter Bedeutung. Wir möchten keine Persistenzdetails und interne Datendarstellung in Datenspeichern behandeln, sei es relational, objektbasiert oder etwas anderes. Warum sollten wir uns mit Konstrukten des Datenspeichermodells auf niedriger Ebene wie Zeilen und Spalten befassen und diese ständig hin und her übersetzen? Stattdessen müssen wir uns auf die komplexe Anwendung konzentrieren, die wir bis gestern liefern mussten.
  2. Wir möchten den Plug-and-Play-Ansatz für unsere Datenspeicher verwenden: Wir möchten verschiedene Anbieter / Implementierungen verwenden, ohne eine Zeile des Anwendungsquellcodes zu ändern - und möglicherweise ohne mehr als ein paar Zeilen in der entsprechenden Konfigurationsdatei zu ändern ( s). Mit anderen Worten, wir benötigen einen Industriestandard für den Zugriff auf Daten basierend auf Java-Objekten, der eine ähnliche Rolle spielt wie JDBC (Java Database Connectivity) als Industriestandard für den Zugriff auf SQL-basierte Daten.
  3. Wir möchten den Plug-and-Play-Ansatz mit verschiedenen Datenbankparadigmen verwenden - das heißt, wir möchten mit minimalen Änderungen am Anwendungscode von einer relationalen Datenbank zu einer objektorientierten Datenbank wechseln. Obwohl schön zu haben, ist diese Fähigkeit in der Praxis oft nicht erforderlich.

    Ein Kommentar hier: Während relationale Datenbanken bei weitem die größte Marktpräsenz genießen, ist die Bereitstellung einer einheitlichen Persistenz-API und die Ermöglichung des Wettbewerbs von Datenspeicheranbietern im Wettbewerb um Implementierungsstärken unabhängig vom verwendeten Paradigma sinnvoll. Dieser Ansatz könnte letztendlich dazu beitragen, die Wettbewerbsbedingungen zwischen den beiden dominierenden Datenbankanbietergruppen zu verbessern: dem gut verankerten relationalen Lager und dem objektorientierten Lager, das um Marktanteile kämpft.

Die drei oben aufgeführten Entdeckungen führen dazu, dass wir eine Persistenzschicht definieren , ein Framework, das eine Java-API auf hoher Ebene für Objekte und Beziehungen bereitstellt, um die Lebensdauer der Laufzeitumgebung (JVM) zu überleben. Ein solcher Rahmen muss folgende Eigenschaften aufweisen:

  • Einfachheit
  • Minimales Eindringen
  • Transparenz, dh das Framework verbirgt die Implementierung des Datenspeichers
  • Konsistente, präzise APIs zum Speichern / Abrufen / Aktualisieren von Objekten
  • Transaktionsunterstützung, dh das Framework definiert die Transaktionssemantik, die persistenten Objekten zugeordnet ist
  • Unterstützung sowohl für verwaltete (z. B. anwendungsserverbasierte) als auch für nicht verwaltete (eigenständige) Umgebungen
  • Unterstützung für die erforderlichen Extras wie Caching, Abfragen, Primärschlüsselgenerierung und Zuordnungstools
  • Angemessene Lizenzgebühren - keine technische Voraussetzung, aber wir alle wissen, dass eine schlechte Wirtschaftlichkeit ein hervorragendes Projekt zum Scheitern bringen kann

Ich beschreibe die meisten der oben genannten Eigenschaften in den folgenden Abschnitten.

Einfachheit

Die Einfachheit steht auf meiner Liste der erforderlichen Merkmale für jedes Software-Framework oder jede Bibliothek hoch (siehe das Eröffnungszitat dieses Artikels). Die Entwicklung verteilter Anwendungen ist bereits schwierig genug, und viele Softwareprojekte scheitern an der geringen Komplexität (und damit auch am Risikomanagement). Einfach ist nicht gleichbedeutend mit simpel; Die Software sollte über alle erforderlichen Funktionen verfügen, die es einem Entwickler ermöglichen, seine Arbeit zu erledigen.

Minimales Eindringen

Jedes persistente Speichersystem führt einen gewissen Eingriff in den Anwendungscode ein. Die ideale Persistenzschicht sollte das Eindringen minimieren, um eine bessere Modularität und damit Plug-and-Play-Funktionalität zu erreichen.

Für den Zweck dieses Artikels definiere ich Intrusion als:

  • Die Menge an persistenzspezifischem Code, die über den Anwendungscode verteilt ist
  • Die Notwendigkeit, Ihr Anwendungsobjektmodell zu ändern, indem entweder eine Persistenzschnittstelle (wie Persistableoder ähnliches) implementiert werden muss oder der generierte Code nachbearbeitet wird

Das Eindringen gilt auch für objektorientierte Datenbanksysteme, und obwohl es dort im Vergleich zu relationalen Datenspeichern normalerweise weniger problematisch ist, kann es bei ODBMS-Anbietern (objektorientiertes Datenbankverwaltungssystem) erheblich variieren.

Transparenz

Das Konzept der Transparenz auf persistenter Ebene ist recht einfach: Die Anwendung verwendet dieselbe API, unabhängig vom Datenspeichertyp (Transparenz vom Datenspeichertyp) oder vom Anbieter des Datenspeichers (Transparenz des Datenspeichers). Transparenz vereinfacht Anwendungen erheblich und verbessert ihre Wartbarkeit, indem Details zur Implementierung des Datenspeichers so weit wie möglich ausgeblendet werden. Insbesondere für die vorherrschenden relationalen Datenspeicher müssen Sie im Gegensatz zu JDBC keine SQL-Anweisungen oder Spaltennamen fest codieren oder sich die von einer Abfrage zurückgegebene Spaltenreihenfolge merken. Tatsächlich müssen Sie SQL oder relationale Algebra nicht kennen, da sie zu implementierungsspezifisch sind. Transparenz ist vielleicht das wichtigste Merkmal der Persistenzschicht.

Konsistente, einfache API

Die Persistenzschicht-API beschränkt sich auf eine relativ kleine Anzahl von Operationen:

  • Grundlegende CRUD-Operationen (Erstellen, Lesen, Aktualisieren, Löschen) für erstklassige Objekte
  • Transaktionsmanagement
  • Verwaltung von Anwendungs- und Persistenzobjektidentitäten
  • Cache-Verwaltung (dh Aktualisieren und Entfernen)
  • Erstellung und Ausführung von Abfragen

Ein Beispiel für eine PersistenceLayerAPI:

öffentliche Leere bleibt bestehen (Objekt obj); // obj im Datenspeicher speichern. öffentliche Objektlast (Klasse c, Objekt pK); // obj mit einem bestimmten Primärschlüssel lesen. public void update (Object obj); // Aktualisiere das geänderte Objekt obj. public void delete (Objekt obj); // obj aus der Datenbank löschen. öffentlicher Sammlungsfund (Abfrage q); // Objekte finden, die die Bedingungen unserer Abfrage erfüllen.

Transaktionsunterstützung

Eine gute Persistenzschicht benötigt mehrere elementare Funktionen, um eine Transaktion zu starten, festzuschreiben oder zurückzusetzen. Hier ist ein Beispiel:

// Transaction (tx) demarcation. public void startTx(); public void commitTx(); public void rollbackTx(); // Choose to make a persistent object transient after all. public void makeTransient(Object o) 

Note: Transaction demarcation APIs are primarily used in nonmanaged environments. In managed environments, the built-in transaction manager often assumes this functionality.

Managed environments support

Managed environments, such as J2EE application servers, have grown popular with developers. Who wants to write middle tiers from scratch these days when we have excellent application servers available? A decent persistence layer should be able to work within any major application server's EJB (Enterprise JavaBean) container and synchronize with its services, such as JNDI (Java Naming and Directory Interface) and transaction management.

Queries

The API should be able to issue arbitrary queries for data searches. It should include a flexible and powerful, but easy-to-use, language -- the API should use Java objects, not SQL tables or other data-store representations as formal query parameters.

Cache management

Cache management can do wonders for application performance. A sound persistence layer should provide full data caching as well as appropriate APIs to set the desired behavior, such as locking levels, eviction policies, lazy loading, and distributed caching support.

Primary key generation

Providing automatic identity generation for data is one of the most common persistence services. Every decent persistence layer should provide identity generation, with support for all major primary key-generation algorithms. Primary key generation is a well-researched issue and numerous primary key algorithms exist.

Mapping, for relational databases only

With relational databases, a data mapping issue arises: the need to translate objects into tables, and to translate relationships, such as dependencies and references, into additional columns or tables. This is a nontrivial problem in itself, especially with complex object models. The topic of object-relational model impedance mismatch reaches beyond this article's scope, but is well publicized. See Resources for more information.

The following list of extras related to mapping and/or relational data stores are not required in the persistence layer, but they make a developer's life much easier:

  • A GUI (graphical user interface) mapping tool
  • Code generators: Autogeneration of DDL (data description language) to create database tables, or autogeneration of Java code and mapping files from DDL
  • Primary key generators: Supporting multiple key-generation algorithms, such as UUID, HIGH-LOW, and SEQUENCE
  • Support for binary large objects (BLOBs) and character-based large objects (CLOBs)
  • Self-referential relations: An object of type Bar referencing another object of type Bar, for example
  • Raw SQL support: Pass-through SQL queries

Example

The following code snippet shows how to use the persistence layer API. Suppose we have the following domain model: A company has one or more locations, and each location has one or more users. The following could be an example application's code:

PersistenceManager pm =PMFactory.initialize(..); Company co = new Company("MyCompany"); Location l1 = new Location1 ("Boston"); Location l2 = new Location("New York"); // Create users. User u1 = new User("Mark"); User u2 = new User("Tom"); User u3 = new User("Mary"); // Add users. A user can only "belong" to one location. L1.addUser(u1); L1.addUser(u2); L2.addUser(u3); // Add locations to the company. co.addLocation(l1); co.addLocation(l2); // And finally, store the whole tree to the database. pm.persist(c); 

In another session, you can look up companies employing the user Tom:

PersistenceManager pm =PMFactory.initialize(...) Collection companiesEmployingToms = pm.find("company.location.user.name = 'Tom'"); 

Für relationale Datenspeicher müssen Sie eine zusätzliche Zuordnungsdatei erstellen. Es könnte so aussehen:

    Firmenstandort Benutzer             

Die Persistenzschicht kümmert sich um den Rest, der Folgendes umfasst:

  • Abhängige Objektgruppen finden
  • Verwalten der Anwendungsobjektidentität
  • Verwalten persistenter Objektidentitäten (Primärschlüssel)
  • Behalten Sie jedes Objekt in der entsprechenden Reihenfolge bei
  • Bereitstellung der Cache-Verwaltung
  • Bereitstellung des richtigen Transaktionskontexts (wir möchten nicht, dass nur ein Teil des Objektbaums erhalten bleibt, oder?)
  • Bereitstellung von vom Benutzer auswählbaren Sperrmodi