Hibernate

Inhalt

Diese Seite beschreibt einige Dinge im Zusammenhang mit der Verwendung des Hibernate Frameworks

Verwendung der Criteria API

Tutorials zum Thema Criteria API: Dabei empfiehlt sich folgende Vorgangsweise:
  • Aus dem Session Objekt ein Criteria Objekt erstellen, in dem die Klasse und im Falle eines Joins ein Alias für die Datenbank-Tabelle angegeben wird.
  • Joins mit der Methode createCriteria realisieren. Dabei wird die Join-Eigenschaft und die zu Joinende Tabelle angegeben.
  • Where Bedingungen mit der Methode add und der Klasse Restrictions hinzufügen.
  • Eine Liste von Objekten mit der Methode list bzw. ein eindeutiges Resultat mit uniqueResult abrufen.
Datenstrukturen für die folgenden Beispiele

Tabelle MyUsers (Zuordnung zu Java Klasse MyUser)
Id FirstName LastName Type
1 Max Mustermann 0
2 Juergen Wanderinformatiker 1
3 Hertha Huhn 0

Tabelle MyPermissions (Zuordnung zu Java Klasse MyPermission)
Id Permission MyUser StartDate EndDate
1 READ 1 01.01.2000 NULL
2 READ 2 01.01.2000 NULL
3 READ 3 01.01.2000 NULL
4 WRITE 1 01.01.2000 NULL
5 WRITE 2 01.01.2000 NULL
6 ADMIN 2 01.01.2000 01.01.2030

Damit die Beispiele funktionieren, muss ein Foreign Key auf die Spalte MyUser in der Tabelle MyPermission gesetzt sein, der auf die Spalte Id in der Tabelle MyUser referenziert.

Beispiel 1 - Abfrage auf Gleichheit

In diesem Beispiel werden die Berechtigungen eines Benutzers abgefragt, in dem der Nachname des Benutzers spezifiziert wird.

          public List<MyPermission> getPermissions(Session s, String userLastName) {

            Criteria c = s.createCriteria(MyPermission.class, "permission");
            c.createAlias("permission.myUser", "user"); // inner join by default
            c.add(Restrictions.eq("user.lastName", userLastName));
            return c.list();
          }
        
In diesem Beispiel wird ein INNER JOIN gesetzt. Würde man einen LEFT JOIN benötigen müsste man folgenden Befehl verwenden:

          c.createAlias("permission.myUser", "user", CriteriaSpecification.LEFT_JOIN); 
        
Beispiel 2 - Mehrere Bedingungen

In dem zweiten Beispiel wird das erste Beispiel erweitert, in dem nur aktuell gültige Permissions berücksichtigt werden. Dabei werden mit der Methode add weitere Bedingungen mit der UND-Logik hinzugefügt. Will man eine ODER-Logik einfügen, so kann das über die Restrictions.or Methode getan werden.

Konkret wird zusätzlich abgefragt, ob das Startdatum der Permission vor dem aktuellen Datum liegt und ob das Endedatum entweder leer ist oder in der Zukunft liegt.

          public List<MyPermission> getPermissions(Session s, String userLastName) {

            Criteria c = s.createCriteria(MyPermission.class, "permission");
            c.createAlias("permission.myUser", "user"); // inner join by default
            c.add(Restrictions.eq("user.lastName", userLastName));
            
            Calendar today = Calendar.getInstance();
            c.add(Restrictions.le("permission.startDate", today));
            c.add(Restrictions.or(Restrictions.isNull("permission.endDate"), Restrictions.ge("permission.endDate", today)));
            return c.list();
          }
        

Beispiel 3 - Abfrage auf Werteliste

In diesem Beispiel werden die Berechtigungen abgefragt, die einen bestimmten Namen aufweisen. (Hier muss kein Alias für die Tabelle MyPermission gesetzt werden, weil man lediglich auf diese Tabelle abfrägt.)

          public List<MyPermission> getPermissions(Session s, List<String> permissionNames) {
            if (permissionNames == null) return new ArrayList<MyPermission>();
            if (permissionNames.size() == 0) return new ArrayList<MyPermission>();

            Criteria c = s.createCriteria(MyPermission.class);
            c.add(Restrictions.in("permission", permissionNames));
            return c.list();
          }
        

Beispiel 4 - Abfrage auf Ähnlichkeit

In diesem Beispiel werden die Benutzer abgefragt, deren Nachname den angegeben Namen enthält.

          public List<MyUser> getUsers(Session s, String lastName) {
            if (lastName == null) return new ArrayList<MyUser>();
            if (lastName.length() == 0) return new ArrayList<MyUser>();
            lastName = "%" + lastName.trim() + "%";

            Criteria c = s.createCriteria(MyUser.class, "user");
            c.add(Restrictions.like("lastName", lastName));
            return c.list();
          }
        

Beispiel 5 - Abfrage auf Ähnlichkeit kombiniert mit ODER-Logik

In diesem Beispiel werden die Benutzer abgefragt, die einen bestimmten Namen entweder im Vornamen oder im Nachnamen aufweisen.

          public List<MyUser> getUsers(Session s, String name) {
            if (name == null) return new ArrayList<MyUser>();
            if (name.length() == 0) return new ArrayList<MyUser>();
            name = "%" + name.trim() + "%";

            Criteria c = s.createCriteria(MyUser.class, "user");
            c.add(Restrictions.or(Restrictions.like("lastName", name), Restrictions.like("firstName", name)));
            return c.list();
          }
        

Beispiel 6 - Ein eindeutiges Objekt abfragen

Hier wird nach einem eindeutigen MyUser Objekt anhand des Vor- und Nachnamens abgefragt. Es wird eine Exception geworfen, wenn es mehrere MyUser mit diesen Eigenschaften gibt bzw. wenn es keinen MyUser gibt, auf den dieser Vor- und Nachname zutrifft.

          public MyUser getUniqueUser(Session s, String firstName, String lastName) throws Exception {
            Criteria c = s.createCriteria(MyUser.class);
            c.add(Restrictions.eq("firstName", firstName));
            c.add(Restrictions.eq("lastName", lastName));
            MyUser uniqueUser = null;
            try {
              uniqueUser = (MyUser) c.uniqueResult();
            }
            catch (Exception e) {
              throw new Exception("More than one user found with this first name and last name!");
            }

            if (uniqueUser == null) throw new Exception("No user found with this first name and last name!");

            return uniqueUser;

          }
        

Verwendung von SQL Abfragen

Tutorials zum Thema Hibernate SQL Abfragen: Hier empfiehlt sich folgende Vorgangsweise:
  • Die SQL Abfrage als String Konstante definieren.
  • Parameter in der Abfrage mit der Notatation :<Camelcase Variablenname> definieren.
  • Die Hibernate API verwenden, um die Abfrage auszuführen und die Ergebnisse in Java Objekten zu erhalten.
Beispiel 1

Im ersten Beispiel werden die Ids von Users zurückgegeben, die von einem bestimmten Typ sind.
          private static final String SQL_GET_USER_IDS = "select id from MyUsers where type = :userType ";

          public List<Long> getUserIdsOfType(Session s, Long userType) {
            SQLQuery query = s.createSQLQuery(SQL_GET_USER_IDS);
            query.setLong("userType", userType);
            query.addScalar("id", StandardBasicTypes.LONG);
            return query.list();
          }
        
In diesem Fall wird eine Liste von Long Elementen zurückgeliefert.

In solchen Fällen hatte ich auch des öfteren die Exception

java.lang.ClassCastException:
java.math.BigDecimal cannot be cast to java.lang.Long


Diese tritt auf, wenn man bei der SQLQuery vergisst addScalar zu setzen, damit man weiß, welcher Datentyp zurückgegeben werden soll.

Beispiel 2

Im nächsten Fall wird abgefragt, ob es Benutzer mit einem bestimmten Nachnamen gibt.

          private static final String SQL_USERS_EXIST = "select count(id) from MyUsers where lastName = :lastName ";

          public boolean usersExist(Session s, String lastName) {
            SQLQuery query = s.createSQLQuery(SQL_USERS_EXIST);
            query.setString("lastName", lastName);
            query.addScalar("id", StandardBasicTypes.INTEGER);
            return ((Integer) query.uniqueResult()).intValue() > 0;
          }
        

Mischen von Criteria API und SQL Abfragen

In folgendem Beispiel wird die Anwendung der Criteria API und der Verwendung von SQL Abfragen gemischt, um die MyUser Objekte eines bestimmten Typs zurückzugeben.
          private static final String SQL_GET_USER_IDS = "select id from MyUsers where type = :userType ";

          public List<MyUser> getUserIdsOfType(Session s, Long userType) {
            SQLQuery query = s.createSQLQuery(SQL_GET_USER_IDS);
            query.setLong("userType", userType);
            query.addScalar("id", StandardBasicTypes.LONG);
            List<Long> validIds query.list();

            if (validAddressIds.size() > 0) {
              Criteria criteria = s.createCriteria(MyUser.class);
              criteria.add(Restrictions.in("id", validIds));
              return criteria.list();

            }
            else {
              return new ArrayList<MyUser>();
            }
          }
        
Diese Mischung ist vor allem dann hilfreich, wenn zum Auffinden der Ids komplexe SQL Abfragen nötig sind. In diesem einfachen Fall wäre man mit der Criteria API natürlich auch schnell ans Ziel gekommen.

          public List<MyUser> getUserIdsOfType(Session s, Long userType) {

            Criteria criteria = s.createCriteria(MyUser.class);
            criteria.add(Restrictions.eq("type", userType));
            return criteria.list();


          }
        

Transaktionen

Das folgende Code Snippet zeigt eine Möglichkeit, wie man Transaktionen verweden kann.

          public void importData(Session s) {

            Transaction tx = null;

             try {
               tx = s.beginTransaction();
               // perform import routines
               tx.commit();
             } catch (Exception e) {
               if (tx != null) {
                 tx.rollback();
               }
             }

          }
        

Laden von Objekten

Ist die Id eines bestimmten Objekts bekannt, kann man es einfach laden.

          public MyUser getUser(Session s, long userId) throws Exception {
              return (MyUser) s.load(MyUser.class, userId);
          }
        

Erstellen eines neuen Objekts


            public void insertNewUser(Session s, String firstName, String lastName) throws Exception {
                MyUser user = new MyUser();
                user.setFirstName(fistName);
                user.setLastName(lastName);
                s.save(user);
            }
          

Löschen von Objekten


            public void removeUsers(Session s, List<Long> ids) throws Exception {
            
              Criteria c = s.createCriteria(MyUser.class);
              c.add(Restrictions.in("id", ids));
              List<MyUser> users = c.list();

              int size = users.size();
              for (int i = 0; i < size; i++) {
                s.delete(users.get(i));
              }
            
            }
          

Update von Objekten

In diesem Beispiel werden bestehende MyUser Objekte, die vom Programm verändert wurden, bzw. neue Objekte in der Datenbank gespeichert. Es wird dabei festgestellt, ob das Objekt schon in der Datenbank vorliegt, dann wird ein Update ausgeführt. Ansonsten wird das Objekt angelegt.

            public void saveUsers(Session s, List<MyUser> users) throws Exception {
            
              int size = users.size();
              for (int i = 0; i < size; i++) {
                s.saveOrUpdate(users.get(i));
              }
            
            }
          

LazyInitializationException - could not initialize proxy - No Session

Ich bezeichne diesen Fehler meist als "No session Error". Ausgeschrieben heißt er aber org.hibernate.LazyInitializationException: could not initialize proxy - no Session.

Zu diesem Thema habe ich folgende Links recht hilfreich gefunden: Im folgenden habe ich das wichtigste der Artikel für mich zusammengefasst.

Der Fehler tritt auf, weil Hibernate meist auf "Lazy Initialization" eingestellt ist. Das bedeutet, dass Referenzen zu anderen Objekten nicht vollständig also "lazy" geladen werden. Beispielsweise, wenn man eine Klasse MyUser hat, die eine Liste von MyPermission Objekte enthält, so werden beim Laden eines MyUser Objektes nicht alle Eigenschaften der MyPermission Objekte mitgeladen sondern nur deren Id. Wird dann die Session geschlossen und man versucht danach auf ein Attribut eines MyPermission Objekts zuzugreifen, so wird der Fehler geworfen. (Hibernate müsste nämlich auf die Datenbank zugreifen, um das Permmission Objekt zu initialisieren, allerdings wurde die Session und damit die Datenbank-Verbindung schon geschlossen.)

Jetzt könnte man natürlich die "Lazy initialization" ausschalten, was aber bei vielen Referenzen bzw. langen Listen von Objekten erhebliche Performance Nachteile und auch eine hohe Speicher-Belegung bringen kann. Das heißt man würde ein eigentliches Feature von Hibernate deaktivieren. Viel besser ist es, den Sourcecode so zu gestalten, dass dieser Fehler nicht mehr auftritt.

Für meine Aufgabenstellungen hat dies bisher folgendes bedeutet:
  • Bevor der Speicher-Prozess abgeschlossen ist, sollten alle Objekte, die danach angesprochen werden, korrekt in Hibernate geladen sein: Dafür war es beispielsweise wichtig ein Objekt mit der get Methode zu laden, damit alle Attribute in Hibernate vorhanden waren. Ansonsten kann es sein, dass mit load nur ein Proxy erzeugt wird, was optimal ist, wenn nur die Id des Objekts benötigt wird. (Weiter unten beschreibe ich den Unterschied zwischen load und get.)
  • Wenn das Objekt an einer Stelle nach dem Schließen einer Session benötigt wird, so kann man die Methode update der Klasse Session verwenden, um dessen Eigenschaften in die aktuelle Session zu laden.
Unterschied zwischen den Methoden load und get

Der Hauptunterschied ist, dass die Methode get einen Datenbank-Zugriff durchführt, wenn ein Object nicht im Cache der Session ist. Dabei wird ein vollständig initialisiertes Objekt in die Session geladen. Damit stellt man sicher, dass man auch auf alle Eigenschaften des Objekts zugreifen kann, auch wenn die Datenbank-Verbindung bereits geschlossen wurde.

Die Methode load hingegen, retourniert nur einen Proxy, der nur die Id des Objekts enthält. Diese Methode ist daher effizienter und nicht so speicher-intensiv. Sie kann verwendet werden, um Beziehungen zwischen Objekten zu erstellen.