How To Cleverly Set Entity's Field Without Knowing its Type

tags

Introduction

I was assigned to a new project last month. The domain revolves around journals and filling it with correct entries. The domain comprises Journal, MainJournal extends Journal, SubJournal extends Journal, and JournalEntry, where both journals can have their own entires.

Using this composition we can define @OneToMany List<JournalEntry> entries in both journals. Now, we can access the entries form the journals. But what about the other way around?

Ideally, the JournalEntry should only know about its parent (journal) and not care about weather it is a main or sub journal. In the database we can’t have a foreign key referencing either table. Therefore we need to define two back-reference columns.

@Entity
class JournalEntry {
    @Id 
    String id;
    
    @ManyToOne 
    MainJournal mainJournal;
    
    @ManyToOne 
    SubJournal subJournal;
}

Now, this is a database mapping done, lets now add the user-friendly representation of a single parent (journal) field. We can achieve this using a getter and setter.

@Entity
class JournalEntry {
    @Id 
    String id;
 
    @Schema(hidden=true)
    @JsonIgnore
    @ManyToOne 
    MainJournal mainJournal;
 
    @Schema(hidden=true)
    @JsonIgnore
    @ManyToOne 
    SubJournal seconaryJournal;
 
    public Journal getJournal() {
        if (mainJournal != null) return mainJournal;
        if (seconaryJournal != null) return seconaryJournal;
        return null;
    }
 
    public void setJournal(Journal journal) {
        if (journal instanceof MainJournal) {
            this.mainJournal = (MainJournal) journal;
        } else if (journal instanceof SubJournal) {
            this.seconaryJournal = (SubJournal) journal;
        } else {
            throw new IllegalStateException("Unknown Journal type");
        }
    }
}

with this code we have defined a new “property” that picks either the main or the sub journal and returns the corresponding Journal. We then use @JsonIgnore to omit the field from response and @Schema(hidden=true) to omit the field from the swagger definition. When setting the field we again pick the corresponding field and set it appropriately.

EntityViews

In our company we have a custom library for DTO generation. The use case is that when we write our entities we want to specify only a subset of fields in our endpoints. for example UserCreate would have { username: string, password: string }, but UserList would only have { username: string }.

The library uses code generation takes the entity fields and keeps only the annotated fields. For example from the above example of user we would have:

@Entity
class User {
    @Id 
    String id;
 
    @ViewablePropery(views={LIST})
    String username;
 
    @ViewablePropery(views={})
    String password;
}

and the generated view would be:

@Entity
class UserList {
    @Id 
    String id;
 
    String username;
}

Relationship mapping

The other great feature is that we can specify what mappings should be used. For example in our entity we can define List<SubJournal> subJournals but want only subjournal ids in our MainJournalDetail. We can achieve this using:

@Entity
class MainJournal {
    @Id 
    String id;
 
    @ViewableProperty(views={DETAIL})
    @ViewableMapping(views={DETAIL}, mappedTo=IDENTIFIED)
    @OneToMany
    List<SubJournal> subJournals;
}

which would generate a view like:

@Entity
class MainJournalDetail {
    @Id 
    String id;
 
    @OneToMany
    List<SubJournalIdentified> subJournals;
}
 
@Entity
class SubJournalIdentified {
    @Id 
    String id;
}

This approach is really useful for generating multiple dtos at once and keeping the fields in sync. I still remember the pain of defining a dto in my personal projects only to realize that it’s not int id but String id, because I changed the type two days ago and didn’t update the dto definition.

The only downside of this approach is how annotaion-heavy this all is. Mixing concepts of Hibernate (@OneToMany), Jackson (@JsonIgnore), Swagger (@Schema), and our EntiyViews (@ViewableProperty) can become really hard to read sometimes.

JournalEntry with @ViewableProperty

Now, lets take a look again at our JournalEntry, we want to include the “journal” field only in certain views, lets say in Detail, but not List. We can do it as follows:

@Entity
class JournalEntry {
    @Id 
    String id;
 
    @Schema(hidden=true)
    @JsonIgnore
    @ManyToOne 
    @ViewableProperty(views={DETAIL})
    MainJournal mainJournal;
 
    @Schema(hidden=true)
    @JsonIgnore
    @ManyToOne 
    @ViewableProperty(views={DETAIL})
    SubJournal seconaryJournal;
 
    @ViewableProperty(views={DETAIL})
    public Journal getJournal() {
        if (mainJournal != null) return mainJournal;
        if (seconaryJournal != null) return seconaryJournal;
        return null;
    }
 
    @ViewableProperty(views={DETAIL})
    public void setJournal(Journal journal) {
        if (journal instanceof MainJournal) {
            this.mainJournal = (MainJournal) journal;
        } else if (journal instanceof SubJournal) {
            this.seconaryJournal = (SubJournal) journal;
        } else {
            throw new IllegalStateException("Unknown Journal type");
        }
    }
}

Tadaa, it is only present in JournalEntryDetail.

But wait, the type of the “journal” field is still Journal, and the journals have List<JournalEntry> entries. This leads to a recursion error when deserializing JSON.

@JsonBackReference / @JsonIgnore

When we want to ignore a field we can use @JsonIgnore to omit it entirely. When we have a bi-directional mapping we use @JsonBackReference, which acts the same way.

This approach however removes the field from all mappings and we can’t unhide it for, let say, MainJournalDetail and hide it in MainJournalList

@JsonIgnoreProperties

After some head scratching my colleague suggested to use @JsonIgnoreProperties. We can use it on a class to ignore certain fields similar to @JsonIgnore, or we can use it on a field to ignore fields inside of that object.

@Entity
class JournalEntry {
    // ...
    @JsonIgnoreProperties({"entries"})
    MainJournal mainJournal;
 
    // ..
    @JsonIgnoreProperties({"entries"})
    SubJournal seconaryJournal;
 
    // ..
    @JsonIgnoreProperties({"entries"})
    public Journal getJournal() {
        // ...
    }
    // ...
}

This approach omits the entries field from journals when serializing that field. I was satisfied with this solution until I looked at the swagger.

Swagger does not (yet) recognize @JsonIgnoreProperties and still shows the entries in the Journal contained in JournalEntry. The simple reason is, that it still is the Journal schema, which does contain the attribute.

@ViewableMapping on getJournal()

Lets try to apply the @ViewableMapping to have only JournalList and therefore no entries.

@Entity
class JournalEntry {
    // ...
 
    // ..
    @ViewableProperty(views={DETAIL})
    @ViewableMapping(views={DETAIL}, mappedTo=LIST)
    MainJournal mainJournal;
 
    // ..
    @ViewableProperty(views={DETAIL})
    @ViewableMapping(views={DETAIL}, mappedTo=LIST)
    SubJournal seconaryJournal;
 
    @ViewableProperty(views={DETAIL})
    @ViewableMapping(views={DETAIL}, mappedTo=LIST)
    public Journal getJournal() {
        if (mainJournal != null) return mainJournal;
        if (seconaryJournal != null) return seconaryJournal;
        return null;
    }
 
    @ViewableProperty(views={DETAIL})
    public void setJournal(Journal journal) {
        if (journal instanceof MainJournal) {
            this.mainJournal = (MainJournal) journal;
        } else if (journal instanceof SubJournal) {
            this.seconaryJournal = (SubJournal) journal;
        } else {
            throw new IllegalStateException("Unknown Journal type");
        }
    }
}

Oh no, this code does not compile. Why? Because of setJournal. The generated view looks like this:

@Entity
class JournalEntryList {
    // ...
 
    // ..
    MainJournalList mainJournal;
 
    // ..
    SubJournalList seconaryJournal;
 
    public JournalList getJournal() {
        if (mainJournal != null) return mainJournal;
        if (seconaryJournal != null) return seconaryJournal;
        return null;
    }
 
    public void setJournal(Journal journal) {
        if (journal instanceof MainJournal) {
            this.mainJournal = (MainJournal) journal;
        } else if (journal instanceof SubJournal) {
            this.seconaryJournal = (SubJournal) journal;
        } else {
            throw new IllegalStateException("Unknown Journal type");
        }
    }
}

We just copy the setJournal code, but the mainJournal is no longer of type MainJournal. Therefore, we get a compile error.

What we really need in the JournalEntryList is the following setter

    public void setJournal(Journal journal) {
        if (journal instanceof MainJournal) {
            this.mainJournal = MainJournalList.toView(journal);
        } else if (journal instanceof SubJournal) {
            this.seconaryJournal = SubJournalList.toView(journal);
        } else {
            throw new IllegalStateException("Unknown Journal type");
        }
    }

I failed to mention this, but the generated views come with a built-in static method toView and toEntity that convert it between the root entity and the view.

After a lot of head scratching I thought of one approach. Reflection.

I would get the field class like mainJournal.getClass() and call .getMethod("toView").invoke(this, journal) on it. But unfortunately mainJournal was null an therefore it’s getClass() through a NullPointerException.

Another problem was that after fixing this issue the method returned an Object. so my assignment code looked like this.mainJournal = (MainJournal) mainJournalClazz.getMethod("toView").invoke(this, journal). This approach worked in the root entity, but guess what, broke in the view, since mainJournal was no longer of type MainJournal.

Final solution

After a lot of head scratching and exploring my options I finally solved the riddle. Using reflection to obtain the field’s type and getting the toView method was part of the solution. The other part was to use reflection to also set the value. The final code looks like this:

public void setJournal(Journal journal) {
    Field journalField;
 
    if (journal instanceof PrimaryJournal) {
        journalField = checked(() -> getClass().getDeclaredField("primaryJournal"));
    } else if (journal instanceof PartialJournal) {
        journalField = checked(() -> getClass().getDeclaredField("partialJournal"));
    } else {
        throw new IllegalArgumentException("Unknown journal type: " + journal.getClass());
    }
 
    Class<?> journalFieldType = journalField.getType();
    if (journalFieldType.getSuperclass().isAssignableFrom(Journal.class)) {
        checked(() -> journalField.set(this, journal));
    } else {
        Method toView = checked(() -> journalFieldType.getMethod("toView", journal.getClass()));
        checked(() -> journalField.set(this, toView.invoke(null, journal)));
    }
}
  • We first obtain the field to be assigned to based on the journal’s type
  • We check wheather the field is the root entity or a view
  • If it is a view, we convert the journal into the view
  • We set the value into the correct field.

With this code we can now freely specify the @ViewablePropery and @ViewableMapping on the journals and keep / hide fields to our hearts contempt.