Lazy-Loading ist eine feine Sache, denn Daten nur dann zu Laden wenn sie benötigt werden klingt nach einer optimalen Lösung. Allerdings dürfte die org.hibernate.LazyInitializationException den Meisten ein Begriff sein, wird diese Exception doch ausgelöst wenn auf eine eben solche Lazy-Abhänigkeit zugegriffen wird ohne das sie Initialisiert wurde oder die Möglichkeit besteht dies direkt zu tun. Grundsätzlich kein großes Problem, eine Serialisierung solcher Objekte in ein JSON-Format stellt allerdings erst einmal eine Hürde da…
Schauen wir uns das sehr einfache Beispiel einer Schnittstelle an die eine Entität mittels Hibernate aus einer Datenbank ausliest und als JSON-String ausliefert.
@Path("material") @Produces("application/json") @Consumes("application/json") public class MaterialResourceWS { @Inject private MaterialResource materialResource; @GET public List<Material> getMaterials() { return materialResource.readAllMaterials(); } ...
Was passiert? Bei der Serialisierung unserer Entität werden alle Attribute ermittelt welche serialisiert werden sollen. Anschließend werden diese Attribute abgerufen und in ein JSON Format gespeichert. Das geschieht nicht nur mit primitiven Datentypen sondern auch mit abhängigen Objekten. Das geht so lange gut bis der Parser auf eine Lazy-Abhängigkeit stößt, denn auch hier versucht dieser die Werte zu ermitteln und läuft damit auf eine LazyInitializationException.
Welche Möglichkeiten haben wir?
- @JsonIgnore
- Diese Annotation (am Attribut einer Entität) teilt dem Parser mit das dieses Element ignoriert werden soll. Diese Lösung ist einfach, aber nicht besonders flexibel da wir auf Klassenebene einmal entscheiden welche Attribute ins JSON übertragen werden sollen und welche nicht. Eine Unterscheidung zwischen verschiedenen Szenarien ist hier nicht möglich
- DTOs
- Ein bewährtes Vorgehen ist die Erzeugung von DTOs (Data Transfer Objects). Wir erstellen demnach für jedes Szenario eine entsprechende Java Klasse, ohne JPA spezifischen Annotation und übertragen nur die Werte in das DTO die von der Schnittstelle benötigt werden. Dieses Vorgehen ist sicherlich das Mittel der Wahl wenn es darum geht große Objekte auf das benötigte Sub Set von Attributen zu minimieren, Tools wir z.B. Dozer helfen dabei unnötige Mapping-Methoden zu schreiben.
- Dem Parser „Hibernate beibringen“
- DTOs zu verwenden mag in einigen Fällen eher unhandlich sein da wir selbst für überschaubare Entitäten mit Lazy-Abhängigkeiten entsprechende DTO-Klassen erzeugen müssten. Viel einfacher wäre es doch wenn nicht initialisierte Objekte / Collections einfach ignoriert werden würden:
jackson-datatype-hibernate
Dieses Addon für Jackson tut (unter anderem) genau das: Berücksichtigung von LazyLoading. Die Verwendung ist denkbar einfach. Neben einer entsprechenden Maven-Abhängigkeit (s. Maven ) müssen wir das Modul lediglich registrieren. Hierfür bietet sich eine JAX-RS-Provider Klasse an:
import com.fasterxml.jackson.databind.ObjectMapper; import com.fasterxml.jackson.datatype.hibernate4.Hibernate4Module; import javax.ws.rs.Produces; import javax.ws.rs.core.MediaType; import javax.ws.rs.ext.ContextResolver; import javax.ws.rs.ext.Provider; @Provider @Produces(MediaType.APPLICATION_JSON) public class JacksonHibernate4ModuleConfig implements ContextResolver<ObjectMapper> { private ObjectMapper objectMapper = new ObjectMapper() { private static final long serialVersionUID = 1L; { Hibernate4Module hibernate4Module = new Hibernate4Module(); //hibernate4Module.configure(Hibernate4Module.Feature.SERIALIZE_IDENTIFIER_FOR_LAZY_NOT_LOADED_OBJECTS, true); registerModule(hibernate4Module); } }; @Override public ObjectMapper getContext(Class<?> objectType) { return objectMapper; } }
Dank dieser Konfiguration erkennt der JSON Parser nicht initialisierte Objekte und Collections und setzte diese auf „null“ anstatt eine Exception aus zu lösen:
! Vorsicht ist hier bei schreibenden Schnittstellen geboten. Sollten diese Objekte unverändert an Hibernate übergeben werden kommt es in folgenden beiden Fällen zu einem „Fehler“:
- Collections mit „orphan removal“
- Hibernate wird bei einer solchen Liste die durch Jackson auf „null“ gesetzt wurde einen Fehler auslösen.
- @ManyToOne(fetch = FetchType.LAZY)
- Diese Referenz wird von Jackson entfernt / auf null gesetzt. Hibernate wird dies bei der Speicherung berücksichtigen und ungewollt die entsprechende Referenz aus der Datenbank entfernen.
Im obigen Beispiel wird bereits eine weitere Möglichkeit angedeutet. Durch die Konfiguration des Features: SERIALIZE_IDENTIFIER_FOR_LAZY_NOT_LOADED_OBJECTS werden Lazy-ManyToOne Abhängigkeiten nicht auf „null“ gesetzt sondern mit einem Hinweis auf die Ziel-Entität versehen. Das obige Beispiel würde mit aktivierter Option für das Attribut Purchaser folgendes JSON Format erzeugen:
"purchaser": {
"de.gedoplan.angular.model.Purchaser": 1
}...
(zu beachten ist, dass dieses Format natürlich nicht ohne Eingriff vom Server in ein Material-Objekt zurück gewandelt werden kann)
jackson-datatype-hibernate bietet, wie wir sehen konnten, eine sehr einfache Art und Weise mit Lazy-Loading um zu gehen ohne zusätzliche Klassen oder eigene Parser zu schreiben. Vorsicht ist nur bei schreibenden Schnittstellen geboten, sodass hier gegeben falls noch selber Anpassungen nötig sind.