Donnerstag, 26. Februar 2009

Hibernate Basisklasse für saubere Datenbanken und einfaches Testen

Ich stolpere öfters über Entity Objekte aus Hibernate, die zum Teil String, int, long, Integer oder auch Long als Datentyp haben. Ich will nicht auf die Nachteile von Strings oder ints als Primary Key eingehen, aber auf einen Weiteren: Einfaches Testen von Apps mit InMemory Mockdaten. Wer für jedes Entity Objekt einen eigenen InMemory Service schreiben muss, produziert zwar viele Zeilen Code, tut dabei aber nichts sinnvolles. Wenn man seine Entities von einer Abstrakten Basislasse ableitet, welche den PK, die Version, hashCode() und equals() enthält, kann man auch mit einer generischen InMemoryService Implementation arbeiten. Hier der Code:

@MappedSuperclass
public abstract class PersistantObject implements Serializable {
 private static final String TO_STRING_FORMAT = "ID: {0}, Version: {1}";
 private static final long serialVersionUID = -2782842909081144965L;
 protected Long id;
 protected Long version;

 public PersistantObject() {
 }

 @Id
 @GeneratedValue(strategy = GenerationType.IDENTITY)
 public Long getId() {
  return id;
 }

 public void setId(Long id) {
  this.id = id;
 }

 @Version
 public Long getVersion() {
  return version;
 }

 public void setVersion(Long version) {
  this.version = version;
 }

 @Override
 public int hashCode() {
  if (id == null)
   return super.hashCode();
  final int prime = 31;
  int result = 1;
  result = prime * result + id.hashCode();
  return result;
 }

 @Override
 public boolean equals(Object obj) {
  if (obj == null)
   return false;
  if (!(obj.getClass().equals(this.getClass())))
   return false;
  PersistantObject checkMe = (PersistantObject) obj;
  if (this.id == null && checkMe.id == null)
   return super.equals(obj);
  if (this.id == null)
   return false;
  if (checkMe.id == null)
   return false;
  if (!id.equals(checkMe.id))
   return false;
  return version.equals(checkMe.version);
 }
 
 @Override
 public String toString() {
  return MessageFormat.format(TO_STRING_FORMAT, id, version);
 }
}

public class InMemoryService {
 private List data;

 public InMemoryService() {
  data = new ArrayList();
 }

 public void insert(T entity) {
  if (data.size() == 0)
   entity.setId(0L);
  else
   entity.setId(data.get(data.size() - 1).getId() + 1L);
  entity.setVersion(0L);
  data.add(entity);
 }

 public T get(Long id) throws ServiceException {
  for (T entity : data)
   if (entity.getId().equals(id))
    return entity;
  throw new ElementNotFoundException();
 }

 public void update(T entity) throws ServiceException {
  T old = get(entity.getId());
  if (!old.getVersion().equals(entity.getVersion()))
   throw new RuntimeException(
     "Concurrent modification");
  data.remove(old);
  entity.setVersion(entity.getVersion() + 1L);
  data.add(entity);
 }

 public void delete(Long id) throws ServiceException {
  data.remove(get(id));
 }

 public List getAll() {
  return data;
 }
}
Natürlich ist der InMemoryService nicht ThreadSafe, aber er ist ja auch nur zum Testen da :-)

Mittwoch, 18. Februar 2009

JavaScript Injection für einen guten Zweck

Auf Arbeit muss ich meine Zeit Minutengenau mit einem selbst entwickelten Tool auf die einzelnen Tasks meiner Projekte buchen. Dieses Tool hat jedoch einen (gewollten) Bug, wenn man die Tasks vom Vortag nachträgt. Es gibt einen DatePicker, mit dem man das gestrige Datum auswählt und 3 Textfelder für Datum, Start und Ende des zu trackenden Tasks. Das Feld für das Datum wird bei jedem Request auf das aktuelle Datum gesetzt, auch wenn im DatePicker das gestrige ausgewählt ist. Zum Glück ist das Tool anfällig für JS Injection, was mir einen bequemen Einstiegspunkt für meinen Bugfix bietet. An Meinen Vornamen hänge ich also meinen Bugfix Code an: Alter Vorname: Richard Neuer Vorname: Richard<script language="JavaScript">Event.stopObserving(window, 'load');Event.observe(window, 'load', function() { if(document.getElementById('e_date')) {Event.observe($('day_task_beginT'),'focus',function(){var mydate=$('e_date').calendar_date_select.selected_date;$('day_task_date').value=(mydate.getDate() + '.' + (mydate.getMonth()+1) + '.' + mydate.getFullYear())})}});</script>
Dank Prototype ist es leicht ein passendes Event zu binden und das Aktualisieren des Datums-Textfelds auf das im DatePicker ausgewählte Datum zu automatisieren. Also liebe Webentwickler, passt auf, dass Ihr alle Ausgaben aus der Datenbank auch wirklich ordentlich quoted ;-)

Montag, 9. Februar 2009

Update von Hibernate gemappten Beans mit eingebetten Listen weiterer Hibernate Beans und CascadeType DeleteOrphan

Ich bin heute auf ein interessantes Problem gestoßen, als ich einer Mutti eins ihrer Kinder wegnehmen wollte. Ich habe 2 Hibernate gemappte Beans, Mutti und Kind. Die Mutti hat eine Liste von Kindern. Die Kinder eine Referenz auf die Mutti. Die Mutti und die Kinder werden in einem Webservice in eine Datenbank persistiert und in einer separaten Webanwendung bearbeitet. Also sind die Muttis und Kinder nur im Webservice mit einer Hibernate Session assoziiert. Wenn jetzt in der Webanwendung einer Mutti ein Kind genommen wird, kommt eine reine, saubere Mutti ohne Hibernate Proxies in den Webservice. Wenn jetzt der Webservice ein session.update(mutti)[Die Mutti "mutti" ist die mit einem Kind weniger] ausführt, verhält sich Hibernate sehr human und mitfühlend und nimmt der Mutti "mutti", trotz gesetzter Annotation
@Cascade(value = org.hibernate.annotations.CascadeType.DELETE_ORPHAN)
, nicht das Kind weg. In meinen Unit Tests hat das allerdings super funktioniert. Nach vergeblicher Suche nach einer unmenschlichen Hibernate Implementation hab ich das Problem genauer analysiert und festgestellt, dass mein Problem nichts mit dem sozialen Bewusstsein von Hibernate zu tun hat, sondern daran liegt, dass die Mutti "mutti" auf der Reise vom und wieder zurück zum Webservice "nur" eine ArrayList und keine Hibernate PersistantBag-Implementation bekommen hat. Diese war in meinen Unit Tests allerdings vorhanden. Daraus folgt, dass ein DELETE_ORPHAN nur zusammen mit einem PersistantBag funktioniert, den man nur bekommt, wenn das Objekt, in unserem Fall die Mutti "mutti", von einer Hibernate Session erzeugt wurde. Nach Studium der Doku zu session.merge kam ich zu folgender Lösung des Problems:

Mutti backendMutti = (Mutti) session.merge(frontendMutti);
session.update(backendMutti);
Jetzt kann mein Hibernate Muttis auch die Kinder wegnehmen :-)

Donnerstag, 5. Februar 2009

Einfaches und Standardisiertes Exception Handling in Service Beans

Während der letzten beiden Jahre habe ich zu viele Service Klassen wie diese gesehen:

public LoginResponse handleLogin(LoginRequest request) {
 try {
  //Do something
 } catch (AuthenticationException e) {
    logger.warn("authentication exception", e);
    throw e;
 } catch (ServiceException e) {
    throw new ServiceException();
 } catch (Throwable e) {
    logger.fatal("Fatal error " + e.getMessage(), e);
    throw new ServiceException();
 }
}

public LogutResponse handleLogout(LogoutRequest request) {
 try {
  //Do something
 } catch (AuthenticationException e) {
    logger.warn("authentication exception", e);
    throw e;
 } catch (ServiceException e) {
    throw new ServiceException();
 } catch (Throwable e) {
    logger.fatal("Fatal error " + e.getMessage(), e);
    throw new ServiceException();
 }
}

public XYResponse handleXY(XYRequest request) {
 try {
  //Do something
 } catch (AuthenticationException e) {
    logger.warn("authentication exception", e);
    throw e;
 } catch (ServiceException e) {
    throw new ServiceException();
 } catch (Throwable e) {
    logger.fatal("Fatal error " + e.getMessage(), e);
    throw new ServiceException();
 }
}

//Think of plenty more methods like this one, all doing the same try catch stuff
Da ich copy&paste verabscheue habe ich nach einer Lösung für dieses Problem gesucht und im AOP Teil des Guice Frameworks gefunden: Da die meisten Service Beans einheitliche Exceptions werfen ist es ohne weiteres möglich auf das Try Catch in den Service Bean Methoden zu verzichten und diese in einen Interceptor zu packen. Dieser hat nur einen Methodenaufruf der mit einem Try Catch versehen wird, in dem alle relevanten Exceptions gefangen werden: Interceptor

//...
import org.aopalliance.intercept.MethodInterceptor;
import org.aopalliance.intercept.MethodInvocation;
//...
public class ServiceXYExceptionInterceptor implements MethodInterceptor {
private static final Logger logger = Logger.getLogger(ServiceXYExceptionInterceptor.class);

public ServiceXYExceptionInterceptor() {
}

public Object invoke(MethodInvocation invocation) throws Throwable {
 try {
  return invocation.proceed();
 } catch (AuthenticationException e) {
  logger.warn("authentication exception", e);
  throw e;
 } catch (ServiceException e) {
  logger.error(MessageFormat.format("Service exception on {0} with params {1}", invocation.getMethod().getDeclaringClass(),
    Arrays.toString(invocation.getArguments())), e);
  throw e;
 } catch (Throwable e) {
  logger.fatal(MessageFormat.format("Unexpected Throwable on {0} with params {1}", invocation.getMethod().getDeclaringClass(),
    Arrays.toString(invocation.getArguments())), e);
  throw new ServiceException();
 }
}
}
The new Service Class

public LoginResponse handleLogin(LoginRequest request) {
  //Do something
  return response;
}

//Think of plenty more methods like this one, all ignoring the exceptions :-)
Damit man diese schicken Service Klassen auch sicher verwenden kann, muss der Interceptor noch mit Guice an die Service Bean gebastelt werden:

bindInterceptor(Matchers.subclassesOf(ServiceXY.class), Matchers.any(), new ServiceXYExceptionInterceptor());
Weitere Informationen zu diesem Thema sind unter folgenden Sites verfügbar: Tembrel's Tome - Injecting method interceptors in Guice 1.0 Guice Project Home

  © Blogger template 'Morning Drink' by Ourblogtemplates.com 2008

Back to TOP