GEDOPLAN
Jakarta EE (Java EE)

JPA + REST, Jackson Resolver

Jakarta EE (Java EE)


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.

 

Schreibe einen Kommentar

Deine E-Mail-Adresse wird nicht veröffentlicht. Erforderliche Felder sind mit * markiert

Bitte füllen Sie dieses Feld aus.
Bitte füllen Sie dieses Feld aus.
Bitte gib eine gültige E-Mail-Adresse ein.
Sie müssen den Bedingungen zustimmen, um fortzufahren.

Autor

Diesen Artikel teilen

LinkedIn
Xing

Gibt es noch Fragen?

Fragen beantworten wir sehr gerne! Schreibe uns einfach per Kontaktformular.

Kurse

weitere Blogbeiträge

Work Life Balance. Jobs bei Gedoplan

We are looking for you!

Lust bei GEDOPLAN mitzuarbeiten? Wir suchen immer Verstärkung – egal ob Entwickler, Dozent, Trainerberater oder für unser IT-Marketing! Schau doch einfach mal auf unsere Jobseiten! Wir freuen uns auf Dich!

Work Life Balance. Jobs bei Gedoplan

We are looking for you!

Lust bei GEDOPLAN mitzuarbeiten? Wir suchen immer Verstärkung – egal ob Entwickler, Dozent, Trainerberater oder für unser IT-Marketing! Schau doch einfach mal auf unsere Jobseiten! Wir freuen uns auf Dich!