Autorenarchiv

Pitfall Exceptions bei annotierten Spring-Transaktionen

Donnerstag, 02. September 2010

Spring ist eines der beliebtesten – wenn nicht das beliebteste – Framework in der Java-Welt. Es bietet für nahezu jede vorstellbare Komponente/Facette bei der Anwendungsentwicklung Unterstützung und hilft von weiteren verwendeten Frameworks (z.B. Web-Frameworks) zu abstrahieren. Eine Paarung die man häufig in Java-Projekten zur Realisierung der Persisterung findet, ist Spring in Kombination mit Hibernate.

Spring hilft hier die Integration von Hibernate in die Anwendung zu vereinfachen und spielt vor allem beim Transaktions-Management eine wichtige Rolle. Egal ob nun Services oder DAOs, Spring erlaubt es einfach per Annotations die Transaktionen zu deklarieren und zu steuern. Dafür genügt – bei entsprechender Konfiguration – eine @Transactional Annotation an einem Method-Body:

@Transactional
public void doInTransaction() {
    writeSomething1();
    writeSomething2();
}

Die so deklarierte Methode lässt writeSomething1() und writeSomething2() in einer Transaktion ablaufen. Schlägt writeSomething2() fehl, so wird writeSomething1() zurückgerollt. Möchte man nun Auskunft über den Erfolg der Methode bzw. über gewisse, aufgetretene Fehler geben, gibt es mehrere Möglichkeiten. Die erste ist der eher veraltete, aber noch immer anzutreffende Status-Code. Hier wird zumeist ein Integer-Wert benutzt, um bestimmte Ereignisse (Erfolg, Fehler1, Fehler2 …) abzubilden. Für unser obiges Beispiel könnte dass dann etwa so ausschauen (reduziert auf einen Fehler- und Erfolgsfall):

@Transactional
public int doInTransaction() {
  try {
    writeSomething1();
    writeSomething2();    
  } catch (WriteSomething2Exception) {
      return 1:
  }
  return 0;
}
 
class WriteSomething2Exception extends Exception {}

Das problematische an dieser Implementierung ist, dass sie das Transaktionsmanagement aushebelt. Denn wirft writeSomething2() die Exception, wird sie nicht weitergeleitet und so kann Spring auch nicht erkennen, dass ein Fehler vorliegt und die Transaktion zurückgerollt werden muss.

Statt die Fehler mit Status-Codes abzubilden, kann man natürlich auch Exceptions für die Fehlerfälle verwenden:

@Transactional
public int doInTransaction() throws WriteSomething2Exception {
  writeSomething1();
  writeSomething2();    
}
 
class WriteSomething2Exception extends Exception {}

In diesem Fall wird die Exception weitergeleitet und alles sollte klappen. Sollte man mindestens meinen… Aber der obige Code hat noch immer den gleichen Effekt. Warum? Nun, um das herauszufinden muss man lediglich das java-doc der @Transactional Annotation genau lesen:

/**
 * Describes transaction attributes on a method or class.
 *
 * <p>This annotation type is generally directly comparable to Spring's
 * {@link org.springframework.transaction.interceptor.RuleBasedTransactionAttribute}
 * class, and in fact {@link AnnotationTransactionAttributeSource} will directly
 * convert the data to the latter class, so that Spring's transaction support code
 * does not have to know about annotations. If no rules are relevant to the exception,
 * it will be treated like
 * {@link org.springframework.transaction.interceptor.DefaultTransactionAttribute}
 * (rolling back on runtime exceptions).
 * 
 * @author Colin Sampaleanu
 * @author Juergen Hoeller
 * @since 1.2
 * @see org.springframework.transaction.interceptor.DefaultTransactionAttribute
 * @see org.springframework.transaction.interceptor.RuleBasedTransactionAttribute
 */
@Target({ElementType.METHOD, ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
@Inherited
@Documented
public @interface Transactional {
...
}

Und siehe da, die letzte Zeile im Kommentar enthält die Lösung. Spring rollt lediglich bei RuntimeExceptions zurück! Da unsere Exception jedoch eine checked Exception ist, passiert nichts. Die Lösung ist also nur RuntimeExceptions zu verwenden. Was aber, wenn wir das nicht wollen? Für diesen Fall bietet die Annotation uns das Attribut rollbackFor. Dort kann man dann als Wert die Klassen der Exceptions angeben kann, für die ein Rollback erfolgen soll. Für unser Beispiel sieht die Lösung dann wie folgt aus:

@Transactional(rollbackFor = WriteSomething2Exception.class)
public int doInTransaction() throws WriteSomething2Exception {
  writeSomething1();
  writeSomething2();    
}
 
class WriteSomething2Exception extends Exception {}

Christian Schätzlein


Hibernate, HashCode und Sets

Montag, 29. März 2010

Verwendet man Hibernate in seinem Projekt als O/R-Mapper, zählt es zu einer der ersten Aufaben eine adäquate Implementierung von equals und hashCode für die Entitäten bereitzustellen. Eine verbreitete Möglichkeit für die HashCode-Methode ist dabei die folgende:

    @Override
    public int hashCode() {
        return getId();
    }

Dabei liefert getId() die durch Hibernate generierte Id der Entität, sprich den PrimaryKey des Datenbank-Eintrages. Soweit verständlich. Allerdings bricht diese Implementation mit der ersten Anforderung des Hash-Code Contracts (Javadoc Auszug):

   /**
     * Returns a hash code value for the object. This method is 
     * supported for the benefit of hashtables such as those provided by 
     * <code>java.util.Hashtable</code>. 
     * <p>
     * The general contract of <code>hashCode</code> is: 
     * <ul>
     * <li>Whenever it is invoked on the same object more than once during 
     *     an execution of a Java application, the <tt>hashCode</tt> method 
     *     must consistently return the same integer, provided no information 
     *     used in <tt>equals</tt> comparisons on the object is modified.
     *     This integer need not remain consistent from one execution of an
     *     application to another execution of the same application. 
     * <li>If two objects are equal according to the <tt>equals(Object)</tt>
     *     method, then calling the <code>hashCode</code> method on each of 
     *     the two objects must produce the same integer result. 
     * <li>It is <em>not</em> required that if two objects are unequal 
     *     according to the {@link java.lang.Object#equals(java.lang.Object)} 
     *     method, then calling the <tt>hashCode</tt> method on each of the 
     *     two objects must produce distinct integer results.  However, the 
     *     programmer should be aware that producing distinct integer results 
     *     for unequal objects may improve the performance of hashtables.
     * </ul>
     * <p>
     * As much as is reasonably practical, the hashCode method defined by 
     * class <tt>Object</tt> does return distinct integers for distinct 
     * objects. (This is typically implemented by converting the internal 
     * address of the object into an integer, but this implementation 
     * technique is not required by the 
     * Java<font size="-2"><sup>TM</sup></font> programming language.)
     *
     * @return  a hash code value for this object.
     * @see     java.lang.Object#equals(java.lang.Object)
     * @see     java.util.Hashtable
     */

Da Hibernate die Id erst erzeugt, wenn die Entität gespeichert wird, verändert sich der Hash-Code während des Lebenszykluses des Objektes. In vielen Fällen stellt das kein Problem da. Arbeitet man allerdings mit HashSets als Collection für eine Assoziation und verwendet gleichzeitig Speicher-Kaskaden, führt diese Implementierung zu Schwierigkeiten. Beispiel:

@Entity
public class FunkyVO extents BaseVO {
 
    @OneToMany(mappedBy = "funky", fetch = FetchType.LAZY)
    @Cascade({ org.hibernate.annotations.CascadeType.ALL})
    private final Set<GrooveVO> grooves= new HashSet<GrooveVO>();
 
}

Und dazu folgender Pseudocode:

FunkyVO funkyVO = new FunkyVO();
GrooveVO grooveVO = new GrooveVO();
funkyVO.getGrooves().add(grooveVO);
PersistService.persist(funkyVO);
funkyVO.getGrooves().contains(grooveVO); // => false
funkyVO.getGrooves().remove(grooveVO); // => false
funkyVO.getGrooves().add(grooveVO); // => false

Da die Id erst durch das Persistieren erzeugt wird, ändert sich der Hash-Code nachdem das Objekt zur Liste hinzugefügt wurde. Die Konsequenz ist, dass das Objekt nun nicht mehr wiedergefunden wird (contains), nicht mehr entfernt werden kann (remove) und bei einem erneuten Hinzufügen auch nicht als Duplikat erkannt wird (add).

Und wie sieht die Lösung aus?

Eine Möglichkeit wäre, im ganzen Projekt darauf zu achten, dass die Entitäten erst nach dem Speichern den Listen hinzugefügt werden…

Hibernate verweist dagegen bei diesem Problem darauf, dass die generierte Id nicht in equals oder hashCode verwendet werden soll. Als Lösung wird dort ein expliziter BusinessKey vorgeschlagen, der nichts mit dem PrimaryKey zu tun hat und somit auch beim Erzeugen der Entität angelegt werden könnte.

Möchte man weiterhin den PrimaryKey benutzen, kann man alternativ auch die Id selbst generieren. Allerdings muss man Hibernate dann beibringen, anhand eines anderen Attributes zu entscheiden, ob die Entität neu ist und ein INSERT ausgeführt werden muss oder ob lediglich ein UPDATE nötig ist. Ein solches Attribut könnte das Version Attribut – welches beim OptimisticLocking verwendet wird – oder etwa ein CreatedAt Feld sein, dass beim Insert durch die Datenbank gefüllt wird.


Christian Schätzlein


Ich weiß nicht, ob ihr schon wusstet…

Freitag, 11. Dezember 2009

aber ich bin neulich über eine Sache gestolpert, die mir so nicht bewusst war:

Was passiert wohl beim Ausführen des folgenden Java-Codes?

List<Integer> list = Arrays.asList(1, 2, 3, 4, 5);
List<Integer> list2 = Arrays.asList(8, 9, 10);
 
list.remove(0);
list.add(2);
list.addAll(list2);

Na wisst ihr es? Man bekommt eine UnsupportedOperationException. Und zwar für jede der list manipulierenden Methoden. Seltsam oder vielleicht doch nicht? Ich für meinen Teil war überrascht. Und auch die Java-Doc für Arrays.asList half mir auf dem ersten Blick nicht sofort weiter:

    /**
     * Returns a fixed-size list backed by the specified array.  (Changes to
     * the returned list "write through" to the array.)  This method acts
     * as bridge between array-based and collection-based APIs, in
     * combination with {@link Collection#toArray}.  The returned list is
     * serializable and implements {@link RandomAccess}.
     *
     * This method also provides a convenient way to create a fixed-size
     * list initialized to contain several elements:
     * 
     *     List<String>; stooges = Arrays.asList("Larry", "Moe", "Curly");
     * 
     *
     * @param a the array by which the list will be backed
     * @return a list view of the specified array
     */
    public static <T> List<T> asList(T... a) {...}

Doch beim genaueren Hinsehen bzw. Überlegen wird es klarer: fixed-size list, a list view of the specified array sowie der Methodenname asList sagen aus, dass es sich nur um eine Ansicht des Arrays bzw. der Eingaben handelt. Die Arrays-Klasse benutzt nämlich unter der Haube die eigene Implementierung java.utils.Arrays.ArrayList und diese implementiert weder add noch remove aus dem List-Interface. Hmm… Irgendwie schon blöd, denn es gibt weder Arrays.toList, noch bietet etwa die konkrete Implementierung ArrayList den komfortablen Var-Args Parameter im Konstruktor. Zudem hätte ich mir eine eindeutigere Dokumentation gewünscht, die ausdrücklich darauf hinweist, dass das Hinzufügen bzw. Entfernen nicht möglich ist.

Möchte man nun trotzdem nicht auf den syntaktischen Zucker verzichten, hilft ein kleiner – zugegebenermasen unperformanter – Workaround:

List<Integer> list = new ArrayList<Integer>(Arrays.asList(1, 2, 3, 4, 5));

Und, habt ihr es gewusst?


Christian Schätzlein


Single und Double-Clicks auf einem HTML-Element mit Prototype

Freitag, 24. Juli 2009

Möchte man auf einem HTML-Element (bzw. DOM-Element) einen einfachen und einen Doppel-Klick registrieren und
jeweils unterschiedliche Aktionen ausführen, steht man schnell vor einem Problem. Es gibt zwar ‘onclick’ und ‘ondblclick’ jedoch
löst ein Doppel-Klick immer auch einen einfachen Klick mit aus. Die Lösung besteht darin, nach einem einfachen Klick ein definierte Zeit zu warten und erst, wenn in dieser Zeit kein Doppel-Klick Event aufgetreten ist, die Aktion für den einfachen Klick wirklich auszuführen.
Tritt in dieser Zeit jedoch ein Doppel-Klick auf, wird die Aktion nicht durchgeführt und stattdessen die Aktion des Doppel-Klicks prozessiert. Mit Hilfe des beliebten Javascript Frameworks Prototype kann man dafür leicht eine generische Lösung entwickeln, die diese beiden Events für ein Element oder mehrere registriert:

 
var FunkyTools = {	
/* Registriert einen einfachen klick und einen Doppel-Klick auf den selben Elementen.
 * Stellt sicher, dass bei einem Doppel-Klick nicht auch der Event-Handler des einfachen
* Klicks getriggert wird (gilt nur für den Event-Handler der über diese Methode 
* für den einfachen Klick registriert wurde).  
* 
* Parameter:
* 
* element         = ein DOM-Element, ein String(id des Elementes) oder ein Array 
*                       mit DOM-Elementen bzw. Strings(id der Elemente). 
* singleClickFunc = der Even-Handler für einfache Klicks
* dblClickFunc    = der Even-Handler für doppelte Klicks         
* 
* */
registerClicks: function(element, singleClickFunc, dblClickFunc) {
       var elements = $(element);
 
	if (! Object.isArray(elements)) {
		elements = [element];
	}
 
	elements.each(function(element){
		var doubleClick = false;
 
		element.observe('click', function(event){
			doubleClick = false;
			singleClickFunc.wrap(function(proceed, event){
					if (doubleClick) {
						event.stop();
					} else {
						proceed(event);
					}
				}).delay(0.3, event);			
			});		
 
			element.observe('dblclick', dblClickFunc.wrap(function(proceed, event){
				doubleClick = true;
				proceed(event);
			}));				
		});			
	}
}

Christian Schätzlein