JPA verstehen, Teil 2: Beziehungen auf JPA-Weise

Ihre Java-Anwendungen sind von einem Netz von Datenbeziehungen abhängig, die bei unsachgemäßer Behandlung zu einem Wirrwarr werden können. In dieser zweiten Hälfte ihrer Einführung in die Java Persistence API zeigt Ihnen Aditi Das, wie JPA mithilfe von Anmerkungen eine transparentere Schnittstelle zwischen objektorientiertem Code und relationalen Daten erstellt. Die resultierenden Datenbeziehungen sind einfacher zu verwalten und kompatibler mit dem objektorientierten Programmierparadigma.

Daten sind ein wesentlicher Bestandteil jeder Anwendung. Ebenso wichtig sind die Beziehungen zwischen verschiedenen Daten. Relationale Datenbanken unterstützen eine Reihe verschiedener Arten von Beziehungen zwischen Tabellen, die alle darauf ausgelegt sind, die referenzielle Integrität zu erzwingen.

In dieser zweiten Hälfte von Grundlegendes zu JPA erfahren Sie, wie Sie die Java Persistence API und Java 5-Annotationen verwenden, um Datenbeziehungen objektorientiert zu behandeln. Dieser Artikel richtet sich an Leser, die grundlegende JPA-Konzepte und die Probleme der relationalen Datenbankprogrammierung im Allgemeinen verstehen und die objektorientierte Welt der JPA-Beziehungen weiter erforschen möchten. Eine Einführung in JPA finden Sie unter "JPA verstehen, Teil 1: Das objektorientierte Paradigma der Datenpersistenz".

Ein reales Szenario

Stellen Sie sich ein Unternehmen namens XYZ vor, das seinen Kunden fünf Abonnementprodukte anbietet: A, B, C, D und E. Kunden können Produkte in Kombination (zu einem reduzierten Preis) bestellen oder einzelne Produkte bestellen. Ein Kunde muss zum Zeitpunkt der Bestellung nichts bezahlen. Wenn der Kunde am Monatsende mit dem Produkt zufrieden ist, wird eine Rechnung erstellt und zur Abrechnung an den Kunden gesendet. Das Datenmodell für dieses Unternehmen ist in Abbildung 1 dargestellt. Ein Kunde kann null oder mehr Bestellungen haben, und jede Bestellung kann einem oder mehreren Produkten zugeordnet werden. Für jede Bestellung wird eine Rechnung zur Rechnungsstellung erstellt.

Jetzt möchte XYZ seine Kunden befragen, um festzustellen, wie zufrieden sie mit seinen Produkten sind, und muss daher herausfinden, wie viele Produkte jeder Kunde hat. Um herauszufinden, wie die Qualität seiner Produkte verbessert werden kann, möchte das Unternehmen auch eine spezielle Umfrage unter Kunden durchführen, die ihre Abonnements innerhalb des ersten Monats gekündigt haben.

Normalerweise können Sie dieses Problem lösen, indem Sie eine DAO-Schicht (Data Access Object) erstellen, in der Sie komplexe Verknüpfungen zwischen den Tabellen CUSTOMER, ORDERS, ORDER_DETAIL, ORDER_INVOICE und PRODUCT schreiben. Ein solches Design würde auf der Oberfläche gut aussehen, aber es könnte schwierig sein, es zu warten und zu debuggen, da die Komplexität der Anwendung zunimmt.

JPA bietet eine andere, elegantere Möglichkeit, dieses Problem anzugehen. Die Lösung, die ich in diesem Artikel vorstelle, verfolgt einen objektorientierten Ansatz und beinhaltet dank JPA keine SQL-Abfragen. Persistenzanbieter sind dafür verantwortlich, die Arbeit für die Entwickler transparent zu erledigen.

Bevor Sie fortfahren, sollten Sie das Beispielcode-Paket aus dem Abschnitt Ressourcen unten herunterladen. Dies umfasst Beispielcode für die in diesem Artikel erläuterten Eins-zu-Eins-, Viele-zu-Eins-, Eins-zu-Viele- und Viele-zu-Viele-Beziehungen im Kontext der Beispielanwendung.

Eins-zu-eins-Beziehungen

Zunächst muss die Beispielanwendung die Beziehung zwischen Bestellung und Rechnung adressieren. Für jede Bestellung wird eine Rechnung erstellt. In ähnlicher Weise ist jede Rechnung einer Bestellung zugeordnet. Diese beiden Tabellen beziehen sich auf die Eins-zu-Eins-Zuordnung (siehe Abbildung 2), die mithilfe des Fremdschlüssels ORDER_ID verknüpft wird. JPA erleichtert die Eins-zu-Eins-Zuordnung mithilfe der @OneToOneAnmerkung.

Die Beispielanwendung ruft die Bestelldaten für eine bestimmte Rechnungs-ID ab. Die Invoicein Listing 1 gezeigte Entität ordnet alle Felder der INVOICE-Tabelle als Attribute zu und verfügt über ein OrderObjekt, das mit dem Fremdschlüssel ORDER_ID verknüpft ist.

Listing 1. Eine Beispielentität, die eine Eins-zu-Eins-Beziehung darstellt

@Entity(name = "ORDER_INVOICE") public class Invoice { @Id @Column(name = "INVOICE_ID", nullable = false) @GeneratedValue(strategy = GenerationType.AUTO) private long invoiceId; @Column(name = "ORDER_ID") private long orderId; @Column(name = "AMOUNT_DUE", precision = 2) private double amountDue; @Column(name = "DATE_RAISED") private Date orderRaisedDt; @Column(name = "DATE_SETTLED") private Date orderSettledDt; @Column(name = "DATE_CANCELLED") private Date orderCancelledDt; @Version @Column(name = "LAST_UPDATED_TIME") private Date updatedTime; @OneToOne(optional=false) @JoinColumn(name = "ORDER_ID") private Order order; ... //getters and setters goes here }

Die @OneToOneund die @JoinCloumnAnmerkungen in Listing 1 werden vom Persistenzanbieter intern aufgelöst, wie in Listing 2 dargestellt.

Listing 2. SQL-Abfrage zum Auflösen einer Eins-zu-Eins-Beziehung

SELECT t0.LAST_UPDATED_TIME, t0.AMOUNT_PAID, t0.ORDER_ID, t0.DATE_RAISED ,t1.ORDER_ID, t1.LAST_UPDATED_TIME, t1.CUST_ID, t1.OREDER_DESC, t1.ORDER_DATE, t1.TOTAL_PRICE FROM ORDER_INVOICE t0 INNER JOIN ORDERS t1 ON t0.ORDER_ID = t1.ORDER_ID WHERE t0.INVOICE_ID = ?

Die Abfrage in Listing 2 zeigt eine innere Verknüpfung zwischen den Tabellen ORDERS und INVOICE. Aber was passiert, wenn Sie eine äußere Verknüpfungsbeziehung benötigen? Sie können den Verknüpfungstyp sehr einfach steuern, indem Sie das optionalAttribut @OneToOneentweder auf festlegen trueoder falseangeben, ob die Zuordnung optional ist oder nicht. Der Standardwert ist true, was bedeutet, dass das zugehörige Objekt möglicherweise vorhanden ist oder nicht und dass der Join in diesem Fall ein äußerer Join ist. Da jede Bestellung eine Rechnung haben muss und umgekehrt, wurde in diesem Fall das optionalAttribut auf gesetzt false.

Listing 3 zeigt, wie Sie eine Bestellung für eine bestimmte Rechnung abrufen, die Sie schreiben.

Listing 3. Abrufen von Objekten, die an einer Eins-zu-Eins-Beziehung beteiligt sind

.... EntityManager em = entityManagerFactory.createEntityManager(); Invoice invoice = em.find(Invoice.class, 1); System.out.println("Order for invoice 1 : " + invoice.getOrder()); em.close(); entityManagerFactory.close(); ....

Aber was passiert, wenn Sie die Rechnung für eine bestimmte Bestellung abrufen möchten?

Bidirektionale Eins-zu-Eins-Beziehungen

Jede Beziehung hat zwei Seiten:

  • Die besitzende Seite ist dafür verantwortlich, die Aktualisierung der Beziehung zur Datenbank weiterzugeben. Normalerweise ist dies die Seite mit dem Fremdschlüssel.
  • Die inverse Seite ist der Besitzerseite zugeordnet.

Bei der Eins-zu-Eins-Zuordnung in der Beispielanwendung ist das InvoiceObjekt die besitzende Seite. Listing 4 zeigt, wie die inverse Seite - die Order- aussieht.

Listing 4. Eine Entität in der bidirektionalen Eins-zu-Eins-Beziehung des Beispiels

@Entity(name = "ORDERS") public class Order { @Id @Column(name = "ORDER_ID", nullable = false) @GeneratedValue(strategy = GenerationType.AUTO) private long orderId; @Column(name = "CUST_ID") private long custId; @Column(name = "TOTAL_PRICE", precision = 2) private double totPrice; @Column(name = "OREDER_DESC") private String orderDesc; @Column(name = "ORDER_DATE") private Date orderDt; @OneToOne(optional=false,cascade=CascadeType.ALL, mappedBy="order",targetEntity=Invoice.class) private Invoice invoice; @Version @Column(name = "LAST_UPDATED_TIME") private Date updatedTime; .... //setters and getters goes here }

Listing 4 ordnet das Feld ( order) zu, dem die Beziehung gehört mappedBy="order". targetEntityGibt den Namen der besitzenden Klasse an. Ein weiteres Attribut, das hier eingeführt wurde, ist cascade. Wenn Sie Einfüge-, Aktualisierungs- oder Löschvorgänge für die OrderEntität ausführen und dieselben Vorgänge an das untergeordnete Objekt weitergeben möchten ( Invoicein diesem Fall), verwenden Sie die Kaskadenoption. Möglicherweise möchten Sie nur PERSIST-, REFRESH-, REMOVE- oder MERGE-Operationen weitergeben oder alle weitergeben.

Listing 5 zeigt, wie Sie die Rechnungsdetails für eine bestimmte Person abrufen, die OrderSie schreiben.

Listing 5. Abrufen von Objekten, die an einer bidirektionalen Eins-zu-Eins-Beziehung beteiligt sind

.... EntityManager em = entityManagerFactory.createEntityManager(); Order order = em.find(Order.class, 111); System.out.println("Invoice details for order 111 : " + order.getInvoice()); em.close(); entityManagerFactory.close(); ....

Viele-zu-eins-Beziehungen

In the previous section, you saw how to successfully retrieve invoice details for a particular order. Now you'll change your focus to see how to get order details for a particular customer, and vice versa. A customer can have zero or more orders, whereas an order is mapped to one customer. Thus, a Customer enjoys a one-to-many relationship with an Order, whereas an Order has a many-to-one relationship with the Customer. This is illustrated in Figure 3.

Here, the Order entity is the owning side, mapped to Customer by the CUST_ID foreign key. Listing 6 illustrates how a many-to-one relationship can be specified in the Order entity.

Listing 6. A sample entity illustrating a bidirectional many-to-one relationship

@Entity(name = "ORDERS") public class Order { @Id //signifies the primary key @Column(name = "ORDER_ID", nullable = false) @GeneratedValue(strategy = GenerationType.AUTO) private long orderId; @Column(name = "CUST_ID") private long custId; @OneToOne(optional=false,cascade=CascadeType.ALL, mappedBy="order",targetEntity=Invoice.class) private Invoice invoice; @ManyToOne(optional=false) @JoinColumn(name="CUST_ID",referencedColumnName="CUST_ID") private Customer customer; ............... The other attributes and getters and setters goes here } 

In Listing 6, the Order entity is joined with the Customer entity with the help of the CUST_ID foreign key column. Here also the code specifies optional=false, as each order should have a customer associated to it. The Order entity now has a one-to-one relationship with Invoice and a many-to-one relationship with Customer.

Listing 7 illustrates how to fetch the customer details for a particular Order.

Listing 7. Fetching objects involved in a many-to-one relationship

........ EntityManager em = entityManagerFactory.createEntityManager(); Order order = em.find(Order.class, 111); System.out.println("Customer details for order 111 : " + order.getCustomer()); em.close(); entityManagerFactory.close(); ........

But what happens if you want to find out how many orders have been placed by a customer?

One-to-many relationships

Fetching order details for a customer is pretty easy once the owning side has been designed. In the previous section, you saw that the Order entity was designed as the owning side, with a many-to-one relationship. The inverse of many-to-one is a one-to-many relationship. The Customer entity in Listing 8 encapsulates the one-to-many relationship by being mapped to the owning side attribute customer.

Listing 8. A sample entity illustrating a one-to-many relationship

@Entity(name = "CUSTOMER") public class Customer { @Id //signifies the primary key @Column(name = "CUST_ID", nullable = false) @GeneratedValue(strategy = GenerationType.AUTO) private long custId; @Column(name = "FIRST_NAME", length = 50) private String firstName; @Column(name = "LAST_NAME", nullable = false,length = 50) private String lastName; @Column(name = "STREET") private String street; @OneToMany(mappedBy="customer",targetEntity=Order.class, fetch=FetchType.EAGER) private Collection orders; ........................... // The other attributes and getters and setters goes here }

The @OneToMany annotation in Listing 8 introduces a new attribute: fetch. The default fetch type for one-to-many relationship is LAZY. FetchType.LAZY is a hint to the JPA runtime, indicating that you want to defer loading of the field until you access it. This is called lazy loading. Lazy loading is completely transparent; data is loaded from the database in objects silently when you attempt to read the field for the first time. The other possible fetch type is FetchType.EAGER. Whenever you retrieve an entity from a query or from the EntityManager, you are guaranteed that all of its eager fields are populated with data store data. In order to override the default fetch type, EAGER fetching has been specified with fetch=FetchType.EAGER. The code in Listing 9 fetches the order details for a particular Customer.

Listing 9. Abrufen von Objekten, die an einer Eins-zu-Viele-Beziehung beteiligt sind

........ EntityManager em = entityManagerFactory.createEntityManager(); Customer customer = em.find(Customer.class, 100); System.out.println("Order details for customer 100 : " + customer.getOrders()); em.close(); entityManagerFactory.close(); .........

Viele-zu-viele-Beziehungen

Es ist noch ein letzter Abschnitt der Beziehungszuordnung zu berücksichtigen. Eine Bestellung kann aus einem oder mehreren Produkten bestehen, während ein Produkt null oder mehreren Bestellungen zugeordnet werden kann. Dies ist eine Viele-zu-Viele-Beziehung, wie in Abbildung 4 dargestellt.