JAX-RS macht das Erstellen von Webservices leicht. Innerhalb von Minuten ist die Businesslogik und JPA Entitäten über REST im JSON Format verfügbar. Aber Moment! LazyLoadException? Circular Dependencies? Schnell stellen wir fest das unsere komplexen JPA Entitäten eben nicht out of the box in ein JSON Format übertragen werden können wenn Relationen und Bidirektionale Verbindungen im Spiel sind. Die Verarbeitung solche Relationen ist dann oftmals mühsam und mit viel manuellen Aufwand verbunden wenn der Umweg über DTOs gegangen wird. Jackson (JSON Parser, Standardimplementierung z.B. im Wildfly) biete hier eine interessante Möglichkeit: JsonIdentityInfo mit eigenem Resolver.
Folgendes einfaches Datenmodel führt ohne Eingriff bereits zu besagten Problemen:
@Entity public class Talk { @Id @GeneratedValue(strategy = GenerationType.IDENTITY) private Integer id; @NotNull @Size(min = 5) private String title; @ManyToMany(fetch = FetchType.EAGER) private List speakers; } @Entity public class Speaker { @Id @GeneratedValue(strategy = GenerationType.IDENTITY) private Integer id; private String firstname; private String lastname; @ManyToMany(mappedBy = "speakers", fetch = FetchType.EAGER) private List talks;
JSON kennt keine Referenzen und würde beim Verarbeiten dieser Entitäten in eine Endlosschleife münden (Talk > Speaker > Talk > Speaker…). Um dies zu verhindern gibt es eine ganze Reihe von Möglichleiten, z.B. die Verwendung von @JSONIgnore (Referenzen beim JSON erzeugen ignorieren) oder die Entwicklung von einem DTO. Auch Denkbar ist die Verwendung von Jacksons @JsonIdentityInfo Annotation:
@JsonIdentityInfo(generator = ObjectIdGenerators.PropertyGenerator.class, property = "id") @JsonIdentityReference(alwaysAsId = true) @ManyToMany(fetch = FetchType.EAGER) private List speakers = new ArrayList();
Dies führt dazu das Jackson diese Relation immer lediglich mit dem Feld “ID” im JSON versieht:
{
“id”: 1,
“title”: “Power Workshop Java EE”,
“speakers”: [
1,3
],
“talkType”: “WORKSHOP”,
“duration”: 480
},
Diese Varianten haben bisher einen entscheidenden Nachteil: ein einfaches Ändern der Relationen ist ohne manuelle Schritte nicht möglich:
- @JSONIgnore: würde sogar dazu führen das die Referenzen auf Speaker entfernt werden, da sie nicht im JSON geliefert werden und bei einem “merge” = “null” sind
- DTO: manueller Aufbau der Entität und “merge”
Auch @JsonIdentityInfo hilft in diesem Fall noch nicht. Optional kann dieser Annotation aber eine “resolver” übergeben werden, der für die Umwandlung in/von JSON sorgt und der in unserem Fall die entsprechende Entitäts-Referenz aus der Datenbank holt und diese korrekt zuweist:
@JsonIdentityInfo( resolver = JPAResolver.class, generator = ObjectIdGenerators.PropertyGenerator.class, scope = Speaker.class, property = "id" )
Der Resolver:
public class JPAResolver extends SimpleObjectIdResolver { private EntityManager em; public JPAResolver() { this.em = CDI.current().select(EntityManager.class).get(); } @Override public void bindItem(IdKey id, Object pojo) { super.bindItem(id, pojo); } @Override public Object resolveId(IdKey id) { Object resolved = super.resolveId(id); if (resolved == null) { resolved = loadFromDatabase(id); bindItem(id, resolved); } return resolved; } private Object loadFromDatabase(IdKey idKey) { return this.em.getReference(idKey.scope, idKey.key); } @Override public ObjectIdResolver newForDeserialization(Object context) { return new JPAResolver(); } @Override public boolean canUseFor(ObjectIdResolver resolverType) { return resolverType.getClass() == JPAResolver.class; } }
Dieser muss noch (zugegeben etwas umständlich) dem Context hinzugefügt werden. Dies kann z.B. mittels JaxRS Provider geschehen mittels HandlerInstantiator . Die Registrierung ist im GitHub hier: de/gedoplan/jackson/system/JacksonJPAResolverProvider.javazu finden
Das ist charmant! Rest-Clients können nun nicht nur die Entität selber (Talk) verändern, sondern in einem Abwasch auch die Referenz (>Speaker) auf Basis der ID manipulieren. Da der Resolver über das scope-Attribut mit der Referenz-Entity-Klasse versehen ist kann er in dieser Form generisch für alle Referenzen verwendet werden. Cool.
Im nächsten Artikel werfen wir einen dann einen Blick auf JSON-B, dem neuen Standard für JSON in Java EE 8
Live. In Farbe. Zum ausprobieren. Auf GitHub.