JPA OneToOne Relation

hioctane

Mitglied
Hallo allerseits

anbei ein vereinfachtes Model meines Problems:

Java:
@Entity
public class Datei {

	public Datei(String filename, String folder) {
		this.filename = filename;
		this.ordner = new Ordner(folder);
	}
	
	@Id
	@GeneratedValue(strategy = GenerationType.AUTO)
	private long id;

	@NotNull
	private String filename;

	@NotNull
	@OneToOne
	private Ordner ordner;

	// ... getter & setter
}

Java:
@Entity
public class Ordner {

	public Ordner(String folder) {
		directory = folder;
	}

	@Id
	@GeneratedValue(strategy = GenerationType.AUTO)
	private long id;

	@NotNull
	private String directory;

	// ... getter & setter
}

Java:
	@Inject EntityManager em;
	
	public void persist(){
		Datei file = new Datei("Datei.txt", "C:\\");
		Datei file2 = new Datei("File.txt", "C:\\");
		
		em.persist(file);
		em.persist(file2);
	}

Folgendes Problem:
Ich möchte beim speichern die Tabelle Ordner automatisch befüllen lassen. Hier soll jedoch nicht für jedes Datei Entity ein Ordner Entity erstellt werden sondern nur ein Ordner für beide Entities dieser ja identisch für beide ist.

Kurz:
Falls es bereits ein Foldereintrag gibt soll auf diesen referenziert werden, falls nicht soll ein neuer Eintrag erstellt werden. In der Tabelle Datei soll jeweils nur die Id des Ordners als Referenz gespeichert werden.

Wie muss ich mein Ordner anotieren damit das oben genannte Problem abgedeckt wird?
 
N

nillehammer

Gast
Du hast keine OneToOne-Beziehung, sondern eine ManyToOne mit Datei als Besitzer. Ändere das Mapping in Datei wie folgt:
[JAVA=16]
@NotNull
@ManyToOne(cascade=CascadeType.ALL)
private Ordner ordner;
[/code]
Als nächstes musst Du dem Framework die Möglichkeit geben, zu erkennen, wann Order/Dateien jeweils gleich sind. Überschreibe also in Ordner und Datei jeweils die equals()- und hashCode()-Methode.

Und als letztes ändere noch den Konstruktor von Datei wie folgt:
Java:
 public Datei(String filename, Ordner folder) {
        this.filename = filename;
        this.ordner = folder;
    }
So erzeugst Du erst den Ordner und übergibst ihn dann an alle Dateien in diesem Ordner.
 

hioctane

Mitglied
Das hat soweit geklappt, danke. Jedoch ist die Lösung noch suboptimal.

Ich möchte erreichen, dass die Tabelle Ordner sich selbst verwaltet.
Das heißt:
  • Falls es noch kein Ordner mit diesem Pfad gibt ein neuen Tabelleneintrag erstellen
  • Falls bereits ein Ordner mit diesem Pfad gibt die Id des Ordners in die File Tabelle übernehmen
  • Beim löschen einer Datei soll geprüft werden ob sie die letzte Datei im referenzierten Ordner war und wenn ja dann soll der Ordner mitgelöscht werden

Meine equals & hashCode Routinen sehen wie folgt aus:

Java:
	@Override
	public boolean equals(Object that) {
		System.out.println("equals called");
		if (this == that) {
			return true;
		}
		if (that == null) {
			return false;
		}
		if (getClass() != that.getClass()) {
			return false;
		}
		if (folder != null) {
			return folder.equals(((Ordner) that).folder);
		}
		return super.equals(that);
	}

	@Override
	public int hashCode() {
		
		if (folder != null) {
			int hash = folder.hashCode();
			System.out.println("hashCode: " +hash);
			return hash;
		}
		
		return super.hashCode();
	}

Was mich etwas verwundert, ist das equals beim em.persist Aufruf nicht genutzt wird. Es wird jeweils für die beiden zu vergleichenden Entities hashCode aufgerufen. Beide Hash-Codes sind identisch. Trotzdem wird für jede Datei, ein Eintrag in Ordner geschrieben.

Hier nochmal ein Beispiel:
Java:
		Datei file = new Datei();
		Datei file2 = new Datei();
		Datei file3 = new Datei();
		Ordner folder = new Ordner();
		Ordner folder2 = new Ordner();
		Ordner folder3 = new Ordner();
		
		folder.setFolder("C:\\");
		folder2.setFolder("C:\\");
		folder3.setFolder("C:\\Subfolder\\");
		
		file.setFilename("eins.txt");
		file.setOrdner(folder);
		file2.setFilename("zwei.txt");
		file2.setOrdner(folder2);
		file3.setFilename("drei.txt");
		file3.setOrdner(folder3);
		
		em.persist(file);
		em.persist(file2);
		em.persist(file3);

Erwarten würde ich folgende Datenstrukturen:

Tabelle Datei:
Code:
ID   FILENAME   ORDNER_ID
1    eins.txt       1
2    zwei.txt       1
3    drei.txt       2


Tabelle Ordner:
Code:
ID FOLDER
 1    C:\
 2    C:\Subfolder\
 
N

nillehammer

Gast
Ergänzungen zum Mapping:
Java:
@Entity
public class Ordner {
 
    @Id
    @GeneratedValue(strategy = GenerationType.AUTO)
    private long id;
 
    @Column(unique=true) // <-- Verzeichnisnamen müssen eindeutig sein
    @NotNull
    private String directory;
Mit dem Mapping stellst Du zwar noch nicht unbedingt sicher, dass ein bereits vorhandener Ordner beim Speichern von File geladen wird, aber zumindest können keine zwei Ordner selben Namens in der DB landen.

Java:
@Entity
// Kombination von Ordner und filename muss eindeutig sein (geht bei Hibernate auch mit @NaturalId)
@Table(uniqueConstraints=@UniqueConstraint(columnNames={"filename", "ordner_id"}))
public class Datei {
    
    @Id
    @GeneratedValue(strategy = GenerationType.AUTO)
    private long id;
 
    @NotNull
    private String filename;
 
    @NotNull
    @ManyToOne(cascade=CascadeType.ALL, orphanRemoval=true ) //<-- Hier ist was dazugekommen
    private Ordner ordner;
}
Hiermit stellst du sicher, dass bei Datei die Kombination aus Ordner und filename eindeutig sein muss. Die columNames müssen genau so sein, wie in der Datenbank. Passe sie ggf. an. Merke: Ich habe beim Mapping von ordner noch ein orphanRemoval=true eingefügt (wegen Punkt 3 Deiner Anforderungen)

Zu equals/hashCode:
Da komme ich jetzt etwas ins Schwimmen. Du hast offensichtlich zwischendurch Instanzvariablen umbenannt. folder gibt es im Code deines ersten Posts nicht. Wenn ich das richtig deute, handelt es sich aber um den namen eines Ordners, welcher ursprünglich directory hieß. Du berüchsichtigst bei equals/hashCode also den Namen. Das hätte ich auch so gemacht. Allerdings ist der abschließende super-Aufruf in beiden Methoden falsch. Der muss auf jeden Fall raus. Der korrigierte Code könnte so aussehen:
Java:
   @Override
    public boolean equals(Object obj) {
        System.out.println("equals called");

        if (this == obj) {
            return true;
        }

        if (obj == null || getClass() != obj.getClass()) {
            return false;
        }

        final Ordner that = (Ordner) obj;

        if (folder == null) {
            return that.folder == null;
        }

        return folder.equals(that.folder);
    }
 
    @Override
    public int hashCode() {
        
        final int hash;

        if (folder != null) {
            hash = folder.hashCode();
        }
        else {
            hash = 0;
        }
        System.out.println("hashCode: " +hash);

        return hash;
    }
So, Ordner passt, fehlt also noch Datei.equals/hashCode. Dat ganze kann man noch etwas schöner machen, wenn man die EqualsBuilder/HashCodeBuilder von Apache commons-lang benutzt, aber das ist eine andere Baustelle...

Zu Deinen Anforderungen:
Falls es noch kein Ordner mit diesem Pfad gibt ein neuen Tabelleneintrag erstellen
Ist mit cascade=CascadeType.ALL erfüllt
Falls bereits ein Ordner mit diesem Pfad gibt die Id des Ordners in die File Tabelle übernehmen
Das erreichtst Du, indem Du statt em.persist(), em.merge() aufrufst. Achte darauf, dass merge einen Rückgabewert hat. Das ist Deine attached Entity, mit der Du weiterarbeiten solltest.
Beim löschen einer Datei soll geprüft werden ob sie die letzte Datei im referenzierten Ordner war und wenn ja dann soll der Ordner mitgelöscht werden
Müsste jetzt mit orphanRemoval=true sicher gestellt sein.
 
Zuletzt bearbeitet von einem Moderator:

Ähnliche Java Themen


Oben