Tabellen mit mehreren Fremdschlüsseln O/R Mapping mit EJB 3.0 Annotierung

Kaylem

Mitglied
Hallo,
ich habe mich die letzten Monate damit rumgeschlagen mich mal in den EJB 3.0 Standard einzuarbeiten. Ich habe bislang weder ernsthaft mit SQL noch mit EJB gearbeitet und habe nun ein recht banales allerdings ärgerliches Problem.

Ich habe ein Datenbank Schema das ich nun per Annotations mappen will.

In all den Beispielen die ich mir bisher angeschaut habe werden (vermutlich im Zuge der Vereinfachung) Tabellen verwendet die eben jeweils nur eine Fremdschlüsselrelation auf die andere Tabelle besitzt. In der Praxis ist das allerdings nur selten der Fall. Mich würde nun interessieren wie man n:1, 1:n bzw n:m Beziehungen zwischen diversen Tabellen herstellt wenn man in einer Tabelle von mehreren Fremdschlüsseln auf einen Primärschlüssel einer anderen Tabelle per Annotations abbildet.

Grundsätzlich funktioniert bei mir eine einfache ManyToOne Beziehung, für den Fall nur eines Fremdschlüssels auf einen respektiven Primärschlüssel folgendermaßen (ich lasse mich hier auch gerne korrigieren).

Listing: Author
Code:
package entities;

import java.util.ArrayList;
import java.util.Collection;

import javax.persistence.Entity;
import javax.persistence.GeneratedValue;
import javax.persistence.Id;
import javax.persistence.JoinColumn;
import javax.persistence.NamedQueries;
import javax.persistence.NamedQuery;
import javax.persistence.OneToMany;

@Entity
@NamedQueries( { @NamedQuery(name = "Author.findAll", query = "SELECT a FROM Author a ORDER BY a.id") })
public class Author {

    @OneToMany
   @JoinColumn(name="authorId")
    private Collection<Project> projects = new ArrayList<Project>();

    @Id
    @GeneratedValue
    private int authorId; // NotNull; Unique; AutoInc
    private String firstName;
    private String lastName;
    private String department;
    
    [...]

Listing: Project
Code:
package entities;

import java.io.Serializable;

import javax.persistence.Column;
import javax.persistence.Entity;
import javax.persistence.Id;
import javax.persistence.JoinColumn;
import javax.persistence.ManyToOne;
import javax.persistence.NamedQueries;
import javax.persistence.NamedQuery;

@Entity
@NamedQueries( { @NamedQuery(name = "Project.findAll", query = "SELECT a FROM Project a ORDER BY a.id") })
public class Project implements Serializable {

    /**
     * 
     */
    private static final long serialVersionUID = 1L;

    @ManyToOne(optional = false)
   @JoinColumn(name = "leader", nullable = false, insertable = false, updatable = false)
    private Author author;

    @Id
    @GeneratedValue
    @Column(nullable = false, unique = true)
    private int projectId; 
    
    @Column(nullable = false)
    private String title;
    
    @Column(nullable = false)
    private int leader; 

    @Column(nullable = false)
    private int securityOfficer;

    [...]

Der Rest sind nur noch obligatorische getter/setter Methoden die ich der Übersichtlichkeit wegen hier mal nicht aufgeführt habe. Ein kleines Diagramm der Beziehung zwischen den beiden Entities ist angehängt. Die beiden Primärschlüssel sind project_id und author_id. Die beiden Fremdschlüssel project_leader und security_officer sollen nun beide über eine ManyToOne Beziehung mit author_id verknüpft werden. Allerdings kann ich leider nicht mehrere @JoinColumn Annotations angeben, was mir naiv erst mal als logisch erschien. @JoinColumns wiederum scheint sich nur auf zusammengesetzte Schlüssel zu beziehen, was hier eben auch nicht der Fall ist. Das ganze mit einer extra Tabelle, sprich über die @JoinTable Annotation, zu lösen scheint mir wiederum etwas kompliziert.
Ich habe mich hier auch für eine bidirektionale Beziehung entschieden weil ich damit glaube auf der sicheren Seite zu sein. Wann es sich nun jedoch anbietet eine unidirektionale Beziehung zu verwenden ist mir noch nicht ganz klar.

Ein anderes Problem habe ich mit der @GeneratedValue Annotation. Denn sobald ich diese einkommentiere und mir meine Entitäten persistieren lassen, werden gerade die Tabellen nicht erstellt bei denen ein Schlüssel eben auto-inkrementiert werden soll. Ich habe versucht einen Workaround über alle möglichen GenerationTypes zu finden, unter anderem mit @SequenceGenerator, aber leider ohne Erfolg.

Ich bin mir sicher, auch wenn der Standard noch recht neu ist wird sich hier jemand vermutlich schon etwas tiefer mit dem Thema befasst haben, für den diese Probleme trivial erscheinen und der mir hoffentlich weiterhelfen kann. Würde mich freuen wenn mir dieser jemand weiterhelfen könnte :)

Bis denn,
Kay

P.S.: Ich glaube zwar nicht das es eine weiterreichende Relevanz hat aber hier noch meine Systemdaten:
Ubuntu 8.04
MySQL 5.0
JBoss4.2.2
Eclipse3.3 (innerhalb Eclipse mit JBoss IDE)
Java EE 5
 

Anhänge

  • project.jpg
    project.jpg
    21,9 KB · Aufrufe: 106
Zuletzt bearbeitet:
Hallo,

was mir als erstes auffällt:
Bei einer bidirektionalen Beziehung muss ein Ende der Relation als Manager dieser Relation bestimmt werden, d.h. ihr werden alle Angaben entnommen die zu Aktionen auf der Datenbank führen. Eine Relation wird zum Manager dieser Beziehung in dem das andere Ende mit mappedBy gekennzeichnet wird. Das mappedBy Attribut muss das property der verweisenden Entity angeben. Wird eine bidrektionale Beziehung ohne Deklaration eines Managers gemacht (wie in deinem Beispiel) so ist das Verhalten undefiniert. Es wird zwar kein Fehler geworfen aber es kommt zu sehr interessanten Effekten.

Dein Beispiel müsstest du also so abändern:
Code:
public class Author {
   @OneToMany(mappedBy="author")
    private Collection<Project> projects = new ArrayList<Project>();

}
Code:
public class Project implements Serializable {

    @ManyToOne(optional = false)
    @JoinColumn(name = "leader", nullable = false)
    private Author author;
}
Die Join-Column Annotation ist optional und benennt nur die Spalte um die zur Aufnahme des Fremdschlüssels dient. Sie wird für eine Relation angegeben (bei unidirektionalen Beziehungen an dem Ende des Managers).
Die Annotation JoinColumns ist für n:m Beziehungen gedacht, da m:n Relationen über eine Verknüpfungstabelle mit 2 Spalten (Primärschlüssel Tabelle1, Primärschlüssel Tabelle2) abgebildet werden, und nun beide Spalte umbenannt werden können.

Wegen unidirektional vs. bidirektional:
Bidrektionale Beziehungen sind natürlich immer das optimum der Möglichkeiten, erfordern jedoch einen sehr viel höheren Aufwand im Umgang mit der Relation. Beispielsweise wird häufig der Fehler begangen beim Speichern nur das Ende der Relation des Managers zu setzen und diese Entity zu speichern:
Also
project.setAuthor(author);
//speichere project

Es ist aber trotzdem notwendig auch den Author mit den neuen Projekten zu speichern, also zusätzlich:
author.getProjects().add(project);
//speichere author

Ein JPA Provider verkraftet das fehlende speichern von author problemlos und da project die Relation managed werden auch richtige Werte in die Datenbank geschrieben. Witzig wirds dann erst wenn du einen Author lädst und plötzlich nicht mehr alle Projekte dranhängen, weil eben der Author nicht gespeichert wurde (muss nicht immer passieren, aber die Wahrscheinlichkeit dass es passiert ist gegeben). Du musst dir auch im klaren sein, dass diese author.getProjects().add(project) erstmal alle Projekte lädt um das neue Projekt hinzuzufügen.
Ich würde vermuten dass an einem Author sehr viele Projekte hängen, diese Verknüpfung aber lediglich für ein bis zwei Anwendungsfälle benötigt wird. Deshalb denke ich ist eine bidrektionale Beziehung nicht sinnvoll. Reicht es nicht aus wenn man von Project auf Author navigieren kann oder musst du wirklich an sehr vielen Stellen deiner Applikation vom Author auf seine Projekte navigieren?

Für die GeneratedValue Geschichte wäre es noch interessant zu wissen welches Datenbanksystem hintendran läuft. Du solltest keine Basisdatentypen für deine Properties nutzen.
Also statt
private int authorId;
lieber:
private Integer authorId;

EDIT:
Glaube ich habe den Rest jetzt auch verstanden. Du möchtest SecurityOfficer und Leader aus Project auch mit einem Author verknüpfen, richtig?
Diese Properties von Project müssen natürlich vom Typ Author und nicht vom Typ int sein. JPA kennt nur Objekte und Relationen zwischen Objekten, es weiß nichts von Fremdschlüsseln. Das Umbennen über JoinColumn ist lediglich ein Komfort weil man ja vielleicht keine Standardnamen im späteren Datenbankschema mag, sie haben allerdings keinerlei Funktionalität für JPA selbst. Versuchs mal so

Code:
public class Project implements Serializable {
    
    @ManyToOne(optional=false)
    private Author leader; 

    @ManyToOne(optional=false)
    @JoinColumn(name="secOffFK")   //diese Annotation ist optional!!
    private Author securityOfficer;
 
Zuletzt bearbeitet:
Hallo Cojote,

erstmal danke für die ausführliche Antwort. Ich habe mich die letzten Tage mal mit einem Kollegen zusammengesetzt und über mögliche Lösungen unter anderem auch deiner Antwort diskutiert. So recht einig geworden sind wir uns allerdings bislang noch nicht. ;)

Glaube ich habe den Rest jetzt auch verstanden. Du möchtest SecurityOfficer und Leader aus Project auch mit einem Author verknüpfen, richtig?
Exakt, einem Author(leader) soll es also möglich sein ein oder mehrere Projekte in die Datenbank einzutragen. Daneben gibt es noch einen Projektverantwortlichen der eben auch für andere Projekte verantwortlich sein kann, dem Author(securityOfficer).

Code:
public class Project implements Serializable {
    
    @ManyToOne(optional=false)
    private Author leader; 

    @ManyToOne(optional=false)
    @JoinColumn(name="secOffFK")   //diese Annotation ist optional!!
    private Author securityOfficer;

Also dein Code Schnippsel kommt mir jetzt schon etwas dubios vor. Wenn ich die beiden Felder leader und securityOfficer auf den Typ Author ändere so würde es in meiner Project Tabelle dazuführen, dass die beiden Attribute eben nicht mehr atomar wären, was der grundsätzlichen Definition einer relationalen Datenbank und der ersten Normalform widerspräche.

Meine Idee wäre nun die Sache vielleicht mit zwei @JoinTable(s) zu lösen. Allerdings scheint deren Berechtigung normalerweise in Ihrer Nutzung bei n:m Beziehungen zu liegen. Nun kann man natürlich jede n:m Beziehung in n viele 1:m bzw m viele n:1 Beziehungen aufspalten, was für mich dafür spricht das ich diese Annotation möglicherweise auch in meinem Fall anwenden könnte (doppelter Konjunktiv) ;).

Für die GeneratedValue Geschichte wäre es noch interessant zu wissen welches Datenbanksystem hintendran läuft.
Das Ganze soll auf MySQL 5.0 laufen, die Tabellen haben momentan den Typ MyISAM. Die Basistypen werde ich mal vermeiden, obwohl ich irgendwie im Hinterkopf habe das seit Java 1.6 Basistypen bei Bedarf sowieso gewrapped werden.
 
Zuletzt bearbeitet:
Hallo,

ich glaube ich habe jetzt eine mehr oder weniger sinnvolle Lösung gefunden.

Listing: Author
Code:
package entities;

import java.io.Serializable;
import java.util.ArrayList;
import java.util.Collection;

import javax.persistence.Column;
import javax.persistence.Entity;
import javax.persistence.Id;
import javax.persistence.JoinColumn;
import javax.persistence.NamedQueries;
import javax.persistence.NamedQuery;
import javax.persistence.OneToMany;

@Entity
@NamedQueries( { @NamedQuery(name = "Author.findAll", query = "SELECT a FROM Author a ORDER BY a.id") })
public class Author implements Serializable {

    /**
     * 
     */
    private static final long serialVersionUID = 1L;

    @OneToMany(mappedBy = "author")
    @JoinColumn(name="authorId")
    private Collection<Project> leader_projects = new ArrayList<Project>();
    
    @OneToMany(mappedBy = "author2")
    @JoinColumn(name="authorId")
    private Collection<Project> secOff_projects = new ArrayList<Project>();

    @Id
    // @GeneratedValue
    private int authorId; 
    @Column(nullable = false)
    private String firstName;
    @Column(nullable = false)
    private String lastName;
    @Column(nullable = false)
    private String department;
Listing: Project
Code:
package entities;

import java.io.Serializable;

import javax.persistence.Column;
import javax.persistence.Entity;
import javax.persistence.Id;
import javax.persistence.JoinColumn;
import javax.persistence.ManyToOne;
import javax.persistence.NamedQueries;
import javax.persistence.NamedQuery;

@Entity
@NamedQueries( { @NamedQuery(name = "Project.findAll", query = "SELECT a FROM Project a ORDER BY a.id") })
public class Project implements Serializable {

    /**
     * 
     */
    private static final long serialVersionUID = 1L;

    @ManyToOne(optional = false)
    @JoinColumn(name = "leader", insertable = false, updatable = false)
    private Author author;
    
    @ManyToOne(optional = false)
    @JoinColumn(name = "securityOfficer", insertable = false, updatable = false)
    private Author author2;

    @Id
    // @GeneratedValue
    @Column(nullable = false, unique = true)
    private int projectId; // NotNull; Unique; AutoInc
   
    @Column(nullable = false)
    private String title; // NotNull
    
    @Column(nullable = false)
    private int leader; // NotNull
    
    @Column(nullable = false)
    private int securityOfficer; // NotNull
Zunächst mal müssen die Namen der JoinColumns in Project genauso heissen wie die Felder innerhalb der Klasse selbst (hier also "leader" und securityOfficer) da sonst in der Project Tabelle zusätzlich Felder mit dem anderslautenden Namen angelegt werden.
Da es sich um zwei verschiedene Relationen zwischen den beiden Tabellen handelt müssen eben auch jeweils zwei verschiedene Relationsenden angegeben werden.

Code:
@OneToMany(mappedBy = "author")
    @JoinColumn(name="authorId")
    private Collection<Project> leader_projects = new ArrayList<Project>();
    
    @OneToMany(mappedBy = "author2")
    @JoinColumn(name="authorId")
    private Collection<Project> secOff_projects = new ArrayList<Project>();
auf der einen und

Code:
    @ManyToOne(optional = false)
    @JoinColumn(name = "leader", insertable = false, updatable = false)
    private Author author;
    
    @ManyToOne(optional = false)
    @JoinColumn(name = "securityOfficer", insertable = false, updatable = false)
    private Author author2;
auf der anderen Seite der Relation. Was mich noch ein wenig stutzg macht ist jedoch das ich bei den @JoinColumn Annotations die Parameter insertable=false und updatable=false setzen muss da mir sonst folgende Exception geworfen wird.

javax.persistence.PersistenceException: org.hibernate.MappingException: Repeated column in mapping for entity: entities.Project column: leader (should be mapped with insert="false" update="false")

Ob das nun der Weisheit letzter Schluss ist weiss ich natürlich nicht, bin aber immer offen für Verbesserungsvorschläge und Erklärungen.

Ciao
Kay
 

Neue Beiträge

Zurück