Donnerstag, September 13, 2018

Links der Woche

UPDATE:

Sonntag, September 09, 2018

JPA Entities, equals und hashCode

Wer hat noch nicht mit seinen Kollegen über die ‘korrekte’ Implementierung von #hashCode() und #equals() diskutiert? Hier kommt meine Zusammenfassung.

Grundlagen

Zunächst die Grundlagen: Welche Bedingungen stellt Java an #hashCode() und #equals()? Da wären:

  1. reflexiv: Für alle x gilt: x.equals(x) liefert immer true zurück.
  2. symmetrisch: Für alle x,y gilt: x.equals(y) == y.equals(x)
  3. transitiv: Für alle x,y,z gilt: x.equals(y) und y.equals(z) dann gilt x.equals(z)
  4. konsistent: Das Ergebnis von #hashCode() und #equals() muss während der gesamten Lebensdauer eines Objekts gleich sein. Konsistenz kann eigentlich nur für unveränderliche Objekte gelten. Oder sagen wir mal so: Es wäre vermutlich ganz praktisch. Allerdings gibt es hier Tricks. Wie z.B. das die Methode #hashCode() eine Konstante zurückliefert. Oder das sich der HashCode nur aus den fachlichen Schlüsseln zusammensetzt.

Das sollte als Einführung reichen. In der JPA Community habe ich 3 Stilrichtungen gefunden, die praktikabel erscheinen. Jede mit gewissen Vor- und Nachteilen. Meine persönliche Präferenz, um das mal vorweg zu nehmen, ist die, die den wenigsten Code erzeugt.

Konsens oder ‘no-go’

Die drei Experten (Gavin King, Vlad Mihalcea, Mark Struberg) sind sich in einer Sache einig. Habe ich eine JPA Entity sollte ich auf gar keinen Fall #equals() und #hashCode() über alle Eigenschaften der Entity ermitteln. Denn wenn ich das tue, was hätte das für die geforderte Eigenschaft ‘konsistent’ zur Folge? Genau. Die Entity müsste unveränderlich sein. D.h. die Code-Generatoren der bekannten IDEs und die Klassen EqualsBuilder oder HashCodeBuilder aus Apache Commons sind tabu.

Variante 1. Die Struberg-Methode | Die Nicht-Implementierung.

Struberg hat einen eigenen Blog. Allein der ist schon empfehlenswert. Der für mich interessante Artikel findet sich hier. Die sogenannte Struberg Variante empfiehlt, auf #equals() und #hashCode zu verzichten. Hauptgrund: Die anderen Varianten (die ich später aufzähle) sind kompliziert und fehleranfällig. Zu dem stellt sich die Frage, benötige ich überhaupt eine spezielle #hashCode() Methode für meine Entity? I.d.R wird dann angeführt, dass die Entities in einem Set gespeichert werden. Also z.B.

@OneToMany 
private Set others;

Aber selbst hier ist die Defaut-Implementierung aus Object absolut ausreichend. Das wird durch zwei Annahmen untermauert. Das erste Argument: Der EntityManager selbst sorgt für die Eindeutigkeit der Objekte im Set. Das zweite Argument, in dem Artikel nicht aufgeführt, aber dennoch interessant: Die Datenbank selbst sorgt mit Unique-Constraints dafür, dass die Elemente im Set eindeutig sind. Allerdings richtig ist der Einwand, dass in diesem Fall das Set nicht mehr selbst in der Lage ist, doppelte Objekte zu erkennen. Aber vielleicht ist in diesem Fall die Validierung in der Geschäftslogik zu lückenhaft?

Wichtig: Das abwägen von Aufwand und Nutzen. Als ‘so-wenig-Code-wie-möglich’ Liebhaber bin ich auf alle Fälle ein Fan dieser Variante.

Variante 2. Hibernate Dokumentation | Der fachliche Schlüssel

In der Hibernate Dokumentation findet sich die Variante 2. Eine Empfehlung lautet aber auch hier, eventuell auf die Implementierung von #hashCode() und #equals() zu verzichten. Will oder kann man das nicht, sollte man den natürlichen Schlüssel der Entity bei der Gestaltung verwenden. Erwähnt wird ebenso, wie bei Struberg oben, dass der EntityManager oder die Session dafür sorgt, dass gleiche Objekte nur einmal instanziert werden. Das Problem kann nur dann auftreten, wenn gleiche Objekte über verschiedene Sessions aus der Datenbank gelesen werden. In diesem Fall würde das #equals(), #hashCode() aus Object zu kurz greifen. Doch man muss sich die Frage stellen, habe ich so eine Situation in meiner Anwendung?

In der Dokumentation findet sich der Hinweis, dass das heranziehen allein der ID aus der Entity i.d.R. nicht ausreicht. Bei Neuinstanzierung einer Entity ist diese Eigenschaft nicht gesetzt und wird erst nach einem flush oder commit der Session in der Entity gesetzt. Damit hätten wir ebenfalls eine Verletzung der Konsistenz. Also bleibt nur der natürliche oder fachliche Schlüssel, der sich nicht ändert und von Anfang an bekannt ist. In dem Beispiel ist es die ISBN eines Buches. Ich verzichte darauf, dass Beispiel hier wiederzugeben. Der interessierte folgt einfach dem Link.

Variante 3. Die Vlad-Methode

Vlad ist der Meinung, dass man #hashCode() und #equals() in jedem Fall implementieren sollte. Er hat für Entities einen sehr guten Test geschrieben, der entscheidet, ob die gewählte Implementierung den Forderungen von #hashCode() und #equals() und den Anforderungen der JPA Spezifikation entspricht. Siehe dafür in seinem GitHub Account oder direkt in der Klasse AbstractEqualityCheckTest.java. Ohne korrekte #hashCode() und #equals() Implementierung wird dieser Test einen Fehler reporten.

In seinem Blog-Artikel unterscheidet Vlad zwischen zwei Typen von Schlüsseln (Identifiables):

Natürliche/Fachliche Schlüssel oder UUIDS

Beiden gemein ist, dass sie bereits vor dem #flush() (Persistierung) in die Datenbank bekannt sind und der Entity zugewiesen werden. In diesen Fällen sollte eine Implementierung so aussehen:

@Entity(name = "Book")
@Table(name = "book")
public class Book 
    implements Identifiable<Long> {

    @Id
    @GeneratedValue
    private Long id;

    private String title;

    @NaturalId
    private String isbn;

    @Override
    public boolean equals(Object o) {
        if (this == o) return true;
        if (!(o instanceof Book)) return false;
        Book book = (Book) o;
        return Objects.equals(getIsbn(), book.getIsbn());
    }

    @Override
    public int hashCode() {
        return Objects.hash(getIsbn());
    }

    //Getters and setters omitted for brevity
}

D.h. die Implementierung zieht nur den fachlichen Schlüssel. Alle anderen Eigenschaft werden ignoriert.

Datenbank generierte Schlüssel

In den anderen Fällen, in denen der Schlüssel von der Datenbank generiert wird, empfiehlt er die folgende Implementierung:

@Entity
public class Book implements Identifiable<Long> {

    @Id
    @GeneratedValue
    private Long id;

    private String title;

    @Override
    public boolean equals(Object o) {
        if (this == o) return true;
        if (!(o instanceof Book)) return false;
        Book book = (Book) o;
        return id != null && id.equals(o.id);
    }

    @Override
    public int hashCode() {
        return 31;
    }

    //Getters and setters omitted for brevity
}

Wichtig: Die #hashCode() Methode liefert eine Konstante zurück. Damit ist sichergestellt, dass der gleiche HashCode über alle Entity-Persistenz-Zustände geliefert wird.

Weitere Erkenntnisse

So, having a cache is really a great idea, but please do not store JPA entities in the cache. At least not as long as they are managed.

Der Dirty-Check von Hibernate funktioniert NICHT über die #equals() Methode. Hibernate verwaltet ein Duplikat der Entity und fährt über dieses Duplikat einen eigenen Vergleich. Siehe Hibernate dirty check.

Another way is to generated a UUID in the constructor or the getId() method. But this is pretty performance intense and also not very nice to handle on the DB side (large Strings as PK consume a lot more storage in the indexes on disk and in memory).

Referenzen

  1. https://vladmihalcea.com/the-best-way-to-implement-equals-hashcode-and-tostring-with-jpa-and-hibernate/
  2. https://vladmihalcea.com/how-to-implement-equals-and-hashcode-using-the-jpa-entity-identifier/
  3. https://vladmihalcea.com/hibernate-facts-equals-and-hashcode/
  4. http://docs.jboss.org/hibernate/orm/5.3/userguide/html_single/Hibernate_User_Guide.html#mapping-model-pojo-equalshashcode
  5. https://github.com/vladmihalcea/high-performance-java-persistence/blob/master/core/src/test/java/com/vladmihalcea/book/hpjp/hibernate/equality/AbstractEqualityCheckTest.java#L55
  6. https://struberg.wordpress.com/2016/10/15/tostring-equals-and-hashcode-in-jpa-entities
  7. https://courses.vladmihalcea.com/?utm_source=blog&utm_medium=banner&utm_campaign=article
  8. Das gleich Buch wie oben, aber bei amazon.de
  9. Thoughts on Java

AssertJ und java.util.List

AssertJ hat eine praktische Möglichkeit, Listen in JUnit Tests abzuprüfen. Insbesondere, wenn in der Liste komplexe Objekte abgelegt sind, s...