JPA + REST, Jackson Resolver


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.

 

Advertisements

Über Dominik Mathmann
Dominik Mathmann arbeitet als Berater, Entwickler und Trainer bei der GEDOPLAN GmbH. Auf Basis seiner langjährigen Erfahrung in der Implementierung von Java-EE-Anwendungen leitet er Seminare, hält Vorträge und unterstützt Kunden bei der Konzeption und Realisierung von Webanwendungen vom Backend bis zum Frontend. Sein derzeitiger Schwerpunkt liegt dabei auf der Entwicklung von JSF-Anwendungen. Er sieht die stärker werdende Position von JavaScript-Frameworks jedoch positiv und beschäftigt sich darüber hinaus mit Webframeworks wie AngularJS.

Kommentar verfassen

Trage deine Daten unten ein oder klicke ein Icon um dich einzuloggen:

WordPress.com-Logo

Du kommentierst mit Deinem WordPress.com-Konto. Abmelden /  Ändern )

Google+ Foto

Du kommentierst mit Deinem Google+-Konto. Abmelden /  Ändern )

Twitter-Bild

Du kommentierst mit Deinem Twitter-Konto. Abmelden /  Ändern )

Facebook-Foto

Du kommentierst mit Deinem Facebook-Konto. Abmelden /  Ändern )

Verbinde mit %s

%d Bloggern gefällt das: