Oracle – eines der verbreitetesten RDBMS seiner Zeit. Ich möchte hier weder Lobeshymnen anstimmen noch Elegien vortragen. Es hat seine Vor- und Nachteile und – das muss man zugeben – funktioniert. Doch neulich stieß ich im Zusammenhang mit Hibernate auf ein sehr merkwürdiges Verhalten.
Oracle unterstützt verschiedene zeichenbasierte Datentypen. Dazu gehören u.A.:
- CHAR
Die Zeichenkettenlänge ist fest und Inhalte werden ggf. mit Blanks aufgefüllt. Die maximale Kapazität beträgt 2000 Byte. Der Typ wird heutzutage selten für längere Texte verwendet. - VARCHAR bzw. VARCHAR2
Die Zeichenkettenlänge ist variabel und Inhalte werden, anders als bei CHAR, nicht durch das System modifiziert. Die maximale Kapazität beträgt 4000 Byte. - LONG (nein, dieser Typ ist nicht –wie man als Entwickler intuitiv vermutet – numerisch.)
Ein Datentyp, der nur noch auf Grund von Abwärtskompatibilität unterstützt wird. Ebenso wie VARCHAR ist die Zeichenkettenlänge variabel. Die maximale Kapazität ist jedoch deutlich größer, sie liegt bei 2^31 – 1 Byte. - CLOB
Wie VARCHAR ein Typ mit variabler Länge. Allerdings ist die maximale Kapazität noch einmal deutlich größer als bei LONG. Sie beträgt (4 GB – 1) * Datenbankblockgröße.
Das Problem
Im Zusammenhang mit großen Textspalten (über 4000 Byte) generiert Hibernate 3.3.2.GA (das ist die Version, die auch in JBoss AS 5.1.0GA verwendet wird) bei Oracle als darunterliegendem DBMS LONG-Spalten. Problematisch dabei ist, dass der Datentyp, ebenso wie CLOB, vielen SQL-Restriktionen unterliegt. Sehr wichtig zu wissen ist z.B., dass man nicht mit DISTINCT selektieren kann.
Seien Clazz und Student Persistent Entities (PE) in einer n:m-Relation mit folgender Definition:
import java.util.HashSet; import java.util.Set; import javax.persistence.Column; import javax.persistence.Entity; import javax.persistence.Id; import javax.persistence.ManyToMany; @Entity public class Clazz { private String name; private String description; private Set<Student> students = new HashSet<Student>(0); @Id public String getName() {return name;} public void setName(String name) {this.name = name;} @Column(length = 10000) public String getDescription() {return description;} public void setDescription(String description) {this.description = description;} @ManyToMany public Set<Student> getStudents() {return students;} public void setStudents(Set<Student> students) {this.students = students;} }
import java.util.HashSet; import java.util.Set; import javax.persistence.Entity; import javax.persistence.Id; import javax.persistence.ManyToMany; @Entity public class Student { private String name; private int iq; private Set<Clazz> clazzes = new HashSet<Clazz>(0); @Id public String getName() {return name;} public void setName(String name) {this.name = name;} public int getIq() {return iq;} public void setIq(int iq) {this.iq = iq;} @ManyToMany(mappedBy = "students") public Set<Clazz> getClazzes() {return clazzes;} public void setClazzes(Set<Clazz> clazzes) {this.clazzes = clazzes;} }
Schreibt man also in JPQL die Abfrage
select distinct c from Clazz c join c.students s where s.iq >= 130
so bekommt man folgende Fehlermeldung bei dessen Ausführung:
java.sql.SQLException: ORA-00997: Unzulässige Verwendung des Datentyps LONG
Ändert man den Datentyp des Attributs Clazz.description auf CLOB
@Lob public String getDescription() {return description;}
und führt die Abfrage erneut aus, so erhält man eine andere Fehlermeldung:
java.sql.SQLException: ORA-00932: Inkonsistente Datentypen: – erwartet, CLOB erhalten
Warum können diese Datentypen nicht in einem SELECT… DISTINCT verwendet werden? Eine Recherche zu dem Thema führt leider zu keinem zufriedenstellenden Ergebnis. Ein Grund könnte der Aspekt der Leistung sein. Mehrere Gigabyte pro Ergebniszeile zu vergleichen, kann mitunter recht imperformant sein.
Maßnahme
Wie kann das Problem umgangen werden? Es ist sehr einfach. Die JPQL wird umgeschrieben:
select c1 from Clazz c1 where c1.name in (select distinct c.name from Clazz c join c.students s where s.iq >= 130)
Diese Abfrage kann problemlos ausgeführt werden.
Fazit
Ein SELECT… DISTINCT für LONG und CLOB lässt sich in vielen Fällen einfach selbst schreiben, wobei man quasi über die Identität auf Gleichheit prüft. Komplizierter wird es beim Vergleich von physisch verschiedenen Datenobjekten. In solchen Fällen sollte man sich aber primär fragen, ob ein Vergleich in dieser Größenordnung sinnvoll ist, denn mit zunehmender Datengröße ist eine Fallanalyse ratsam.
Bei der Lösung stößt man jedoch unter Umständen an andere Grenzen der Oracle DB – nämlich sobald das innere Select mehr als 1000 Zeilen zurückgibt:
ORA-01795: Höchstzahl von Ausdrücken in einer Liste ist 1000
Wir haben bisher leider noch keinen anderen Workaround gefunden.