Utwórz idealny byt JPA [zamknięty]

422

Od jakiegoś czasu pracuję z JPA (implementacja Hibernacja) i za każdym razem, gdy muszę tworzyć encje, mam problemy z AccessType, niezmiennymi właściwościami, equals / hashCode, ....
Postanowiłem więc znaleźć ogólną najlepszą praktykę dla każdego numeru i zapisać ją na własny użytek.
Nie miałbym jednak nic przeciwko komentowaniu go lub mówieniu, gdzie się mylę.

Klasa podmiotu

  • implementować Serializable

    Powód: specyfikacja mówi, że musisz, ale niektórzy dostawcy JPA tego nie egzekwują. Hibernacja jako dostawca JPA nie wymusza tego, ale może zawieść gdzieś głęboko w brzuchu z ClassCastException, jeśli Serializable nie został zaimplementowany.

Konstruktory

  • utwórz konstruktor ze wszystkimi wymaganymi polami encji

    Powód: Konstruktor powinien zawsze pozostawić instancję utworzoną w stanie normalnym.

  • poza tym konstruktorem: mieć pakiet prywatny konstruktor domyślny

    Powód: Domyślny konstruktor jest wymagany, aby Hibernacja zainicjowała jednostkę; private jest dozwolone, ale widoczność pakietu prywatnego (lub publicznego) jest wymagana do generowania proxy środowiska wykonawczego i efektywnego wyszukiwania danych bez oprzyrządowania z kodem bajtowym.

Pola / właściwości

  • Użyj ogólnego dostępu do pola i dostępu do nieruchomości w razie potrzeby

    Powód: jest to prawdopodobnie najbardziej dyskusyjna kwestia, ponieważ nie ma jasnych i przekonujących argumentów dla jednego lub drugiego (dostęp do nieruchomości a dostęp do pola); jednak dostęp do pól wydaje się być ulubionym ze względu na bardziej przejrzysty kod, lepszą enkapsulację i brak potrzeby tworzenia ustawień dla niezmiennych pól

  • Pomiń setery dla niezmiennych pól (niewymagane dla pola typu dostępu)

  • właściwości mogą być prywatne
    Powód: Kiedyś słyszałem, że funkcja chroniona jest lepsza pod kątem (Hibernacji) wydajności, ale wszystko, co mogę znaleźć w Internecie, to: Hibernacja może uzyskać dostęp do publicznych, prywatnych i chronionych metod akcesorów, a także bezpośrednio do pól publicznych, prywatnych i chronionych . Wybór należy do Ciebie i możesz dopasować go do swojego projektu aplikacji.

Equals / hashCode

  • Nigdy nie używaj wygenerowanego identyfikatora, jeśli ten identyfikator jest ustawiony tylko podczas utrwalania encji
  • Preferencje: użyj niezmiennych wartości, aby utworzyć unikalny klucz biznesowy i użyj go do przetestowania równości
  • jeżeli unikalny Klucz Biznesowy nie jest dostępny, należy użyć przejściowego UUID, który jest tworzony podczas inicjowania jednostki; Zobacz ten świetny artykuł, aby uzyskać więcej informacji.
  • nigdy nie odnoszą się do powiązanych podmiotów (ManyToOne); jeśli ten podmiot (podobnie jak podmiot nadrzędny) musi być częścią klucza biznesowego, porównaj tylko identyfikatory. Wywołanie getId () na proxy nie spowoduje załadowania encji, o ile korzystasz z typu dostępu do właściwości .

Przykładowa jednostka

@Entity
@Table(name = "ROOM")
public class Room implements Serializable {

    private static final long serialVersionUID = 1L;

    @Id
    @GeneratedValue
    @Column(name = "room_id")
    private Integer id;

    @Column(name = "number") 
    private String number; //immutable

    @Column(name = "capacity")
    private Integer capacity;

    @ManyToOne(fetch = FetchType.LAZY, optional = false)
    @JoinColumn(name = "building_id")
    private Building building; //immutable

    Room() {
        // default constructor
    }

    public Room(Building building, String number) {
        // constructor with required field
        notNull(building, "Method called with null parameter (application)");
        notNull(number, "Method called with null parameter (name)");

        this.building = building;
        this.number = number;
    }

    @Override
    public boolean equals(final Object otherObj) {
        if ((otherObj == null) || !(otherObj instanceof Room)) {
            return false;
        }
        // a room can be uniquely identified by it's number and the building it belongs to; normally I would use a UUID in any case but this is just to illustrate the usage of getId()
        final Room other = (Room) otherObj;
        return new EqualsBuilder().append(getNumber(), other.getNumber())
                .append(getBuilding().getId(), other.getBuilding().getId())
                .isEquals();
        //this assumes that Building.id is annotated with @Access(value = AccessType.PROPERTY) 
    }

    public Building getBuilding() {
        return building;
    }


    public Integer getId() {
        return id;
    }

    public String getNumber() {
        return number;
    }

    @Override
    public int hashCode() {
        return new HashCodeBuilder().append(getNumber()).append(getBuilding().getId()).toHashCode();
    }

    public void setCapacity(Integer capacity) {
        this.capacity = capacity;
    }

    //no setters for number, building nor id

}

Inne sugestie dotyczące dodania do tej listy są mile widziane ...

AKTUALIZACJA

Od czasu przeczytania tego artykułu dostosowałem swój sposób implementacji eq / hC:

  • jeśli dostępny jest niezmienny prosty klucz biznesowy: użyj tego
  • we wszystkich innych przypadkach: użyj UUID
Stijn Geukens
źródło
6
To nie jest pytanie, to prośba o sprawdzenie z prośbą o listę. Co więcej, jest on bardzo otwarty i niejasny, lub mówiąc inaczej: To, czy jednostka JPA jest idealna, zależy od tego, do czego będzie używana. Czy powinniśmy wymieniać wszystkie rzeczy, których jednostka może potrzebować we wszystkich jej możliwych zastosowaniach?
meriton
Wiem, że nie jest to jednoznaczne pytanie, za które przepraszam. Nie jest to tak naprawdę prośba o listę, lecz prośba o komentarze / uwagi, choć inne sugestie są mile widziane. Zachęcamy do omówienia możliwych zastosowań podmiotu WZP.
Stijn Geukens
Chciałbym też, aby były to pola final(sądząc po twoim pominięciu seterów, zgaduję, że ty też).
Sridhar Sarnobat
Musiałbym spróbować, ale nie sądzę, że wersja ostateczna zadziała, ponieważ Hibernacja wciąż musi być w stanie ustawić wartości dla tych właściwości.
Stijn Geukens
Skąd notNullpochodzi?
bruno,

Odpowiedzi:

73

Spróbuję odpowiedzieć na kilka kluczowych punktów: pochodzi z długiego doświadczenia w Hibernacji / trwałości, w tym z kilku głównych aplikacji.

Klasa jednostki: implementować Serializable?

Klucze muszą implementować Serializable. Rzeczy, które zostaną wprowadzone w sesji HttpSession lub wysłane przez RPC / Java EE, muszą zaimplementować Serializable. Inne rzeczy: nie bardzo. Poświęć czas na to, co ważne.

Konstruktory: utworzyć konstruktor ze wszystkimi wymaganymi polami encji?

Konstruktor (y) logiki aplikacji powinien mieć tylko kilka krytycznych pól „klucz obcy” lub „typ / rodzaj”, które zawsze będą znane podczas tworzenia encji. Resztę należy ustawić, wywołując metody ustawiające - po to są.

Unikaj umieszczania zbyt wielu pól w konstruktorach. Konstruktory powinny być wygodne i zapewniać obiektowi zdrowy rozsądek. Imię, typ i / lub rodzice są zwykle przydatni.

OTOH, jeśli reguły aplikacji (dzisiaj) wymagają, aby Klient miał Adres, pozostaw to ustawiającemu. To jest przykład „słabej reguły”. Może w przyszłym tygodniu chcesz utworzyć obiekt klienta przed przejściem do ekranu Wprowadź szczegóły? Nie potykaj się, pozostaw możliwość nieznanych, niekompletnych lub „częściowo wprowadzonych” danych.

Konstruktory: także, pakiet prywatny konstruktor domyślny?

Tak, ale używaj opcji „chroniony” zamiast pakietu prywatnego. Podklasowanie jest prawdziwym bólem, gdy niezbędne elementy wewnętrzne nie są widoczne.

Pola / właściwości

Użyj dostępu do pola „właściwość” dla Hibernacji i spoza instancji. W instancji użyj pól bezpośrednio. Powód: umożliwia działanie standardowego odbicia, najprostszej i najbardziej podstawowej metody hibernacji.

Jeśli chodzi o pola „niezmienne” dla aplikacji - Hibernacja wciąż musi być w stanie je załadować. Możesz spróbować ustawić te metody jako „prywatne” i / lub umieścić na nich adnotację, aby zapobiec niepożądanemu dostępowi kodu aplikacji.

Uwaga: pisząc funkcję equals (), używaj getters dla wartości w instancji „other”! W przeciwnym razie trafisz na niezainicjowane / puste pola na instancjach proxy.

Protected jest lepszy dla (hibernacji) wydajności?

Mało prawdopodobne.

Equals / HashCode?

Dotyczy to pracy z bytami, zanim zostaną one zapisane - co jest drażliwym problemem. Mieszanie / porównywanie niezmiennych wartości? W większości aplikacji biznesowych nie ma żadnych.

Klient może zmienić adres, nazwę firmy itp. - nie jest to powszechne, ale zdarza się. Korekty muszą być również możliwe, gdy dane nie zostaną wprowadzone poprawnie.

Nieliczne rzeczy, które są zwykle niezmienne, to Rodzicielstwo i być może Typ / Rodzaj - zwykle użytkownik odtwarza rekord, zamiast go zmieniać. Ale nie jednoznacznie identyfikują bytu!

Tak długo i krótko, twierdzone, że „niezmienne” dane nie są tak naprawdę. Pola klucza głównego / identyfikatora są generowane właśnie w celu zapewnienia takiej gwarantowanej stabilności i niezmienności.

Musisz zaplanować i wziąć pod uwagę potrzebę faz prac związanych z porównywaniem, mieszaniem i przetwarzaniem żądań, gdy A) pracujesz z „zmienionymi / powiązanymi danymi” z interfejsu użytkownika, jeśli porównujesz / mieszasz „rzadko zmieniane pola” lub B) pracujesz z „ niezapisane dane ”, jeśli porównasz / hash na ID.

Równa się / HashCode - jeśli unikalny klucz biznesowy nie jest dostępny, użyj nieprzemijającego UUID, który jest tworzony podczas inicjowania encji

Tak, w razie potrzeby jest to dobra strategia. Należy pamiętać, że identyfikatory UUID nie są bezpłatne, ale pod względem wydajności - a grupowanie komplikuje sprawy.

Equals / HashCode - nigdy nie odnoszą się do powiązanych jednostek

„Jeśli jednostka powiązana (jak jednostka nadrzędna) musi być częścią klucza biznesowego, dodaj pole, którego nie można wstawić i nie można jej zaktualizować, aby przechowywać identyfikator nadrzędny (o tej samej nazwie co kolumna Jointo ManytoOne) i użyj tego identyfikatora w kontroli równości „

Brzmi jak dobra rada.

Mam nadzieję że to pomoże!

Thomas W.
źródło
2
Re: konstruktory, często widzę tylko zero argumentów (tj. Brak), a kod wywołujący ma wielką długą listę zestawów parametrów, co wydaje mi się nieco niechlujne. Czy naprawdę jest jakiś problem z posiadaniem kilku konstruktorów, które odpowiadają Twoim potrzebom, dzięki czemu kod wywołujący jest bardziej zwięzły?
Huragan
całkowicie uparty, szczególnie o ctor. co jest piękniejszym kodem? kilka różnych wskaźników, które pozwalają dowiedzieć się, które (kombinacje) wartości są potrzebne do stworzenia zdrowego stanu obj lub ctor bez argumentu, który nie daje pojęcia, co należy ustawić, w jakiej kolejności i pozostawia podatne na błędy użytkownika ?
mohamnag
1
@mohamnag Zależy. W przypadku danych generowanych przez system świetnie pasują do siebie fasole; jednak nowoczesne aplikacje biznesowe składają się z dużej liczby ekranów CRUD lub kreatora do wprowadzania danych użytkownika. Dane wprowadzone przez użytkownika są często częściowo lub źle sformułowane, przynajmniej podczas edycji. Bardzo często nawet wartość biznesowa polega na możliwości rejestrowania niepełnego stanu do późniejszego ukończenia - pomyśl przechwytywanie aplikacji ubezpieczeniowych, rejestracje klientów itp. Ograniczenie ograniczeń do minimum (np. Klucz podstawowy, klucz biznesowy i stan) pozwala na większą elastyczność w rzeczywistości sytuacje biznesowe.
Thomas W
1
@ThomasW najpierw muszę powiedzieć, że jestem zdecydowanie zwolennikiem projektowania opartego na domenach i używania nazw dla nazw klas i oznaczania pełnych czasowników dla metod. W tym paradygmacie chodzi o DTO, a nie podmioty domeny, które powinny być używane do tymczasowego przechowywania danych. Albo źle zrozumiałeś / ustrukturyzowałeś swoją domenę.
mohamnag
@ThomasW kiedy odfiltrowuję wszystkie zdania, które próbujesz powiedzieć, że jestem nowicjuszem, w twoim komentarzu nie ma żadnych informacji oprócz informacji wprowadzanych przez użytkownika. Ta część, jak już powiedziałem, zostanie wykonana w DTO, a nie bezpośrednio w jednostce. porozmawiajmy w ciągu następnych 50 lat, aby stać się 5% tego, czego doświadczył wielki umysł stojący za DDD, jak Fowler! na zdrowie: D
mohamnag
144

Specyfikacja JPA 2.0 stanowi, że:

  • Klasa encji musi mieć konstruktor bez argumentu. Może mieć także innych konstruktorów. Konstruktor no-arg musi być publiczny lub chroniony.
  • Klasa encji musi być klasą najwyższego poziomu. Wyliczenia lub interfejsu nie można wyznaczać jako bytu.
  • Klasa encji nie może być ostateczna. Żadne metody ani trwałe zmienne instancji klasy encji nie mogą być ostateczne.
  • Jeśli instancja encji ma być przekazywana przez wartość jako odłączony obiekt (np. Przez interfejs zdalny), klasa encji musi implementować interfejs Serializable.
  • Zarówno klasy abstrakcyjne, jak i konkretne mogą być bytami. Jednostki mogą rozszerzać klasy nie będące jednostkami, a także klasy jednostek, a klasy nie będące jednostkami mogą rozszerzać klasy jednostek.

Specyfikacja nie zawiera żadnych wymagań dotyczących implementacji metod equals i hashCode dla encji, tylko dla klas kluczy podstawowych i kluczy mapowania, o ile mi wiadomo.

Edwin Dalorzo
źródło
13
To prawda, równe, hashcode, ... nie są wymaganiem JPA, ale są oczywiście zalecane i uważane za dobrą praktykę.
Stijn Geukens
6
@TheStijn Cóż, chyba że planujesz porównać odłączone byty dla równości, prawdopodobnie nie jest to konieczne. Menedżer encji gwarantuje, że zwróci tę samą instancję danej encji za każdym razem, gdy o nią poprosisz. Tak więc, o ile rozumiem, można sobie poradzić z porównaniami tożsamości dla zarządzanych podmiotów. Czy mógłbyś bardziej szczegółowo opisać te scenariusze, w których uznałbyś to za dobrą praktykę?
Edwin Dalorzo
2
Staram się zawsze mieć poprawną implementację equals / hashCode. Nie jest to wymagane w przypadku JPA, ale uważam, że jest to dobra praktyka, gdy jednostki lub dodane do zestawów. Możesz zdecydować się na zastosowanie równości tylko wtedy, gdy elementy zostaną dodane do zestawów, ale czy zawsze wiesz z góry?
Stijn Geukens
10
@TheStijn Dostawca JPA zapewni, że w danym momencie istnieje tylko jedna instancja danego podmiotu w kontekście, dlatego nawet twoje zestawy są bezpieczne bez implementacji equals / hascode, pod warunkiem, że używasz tylko zarządzanych podmiotów. Wdrożenie tych metod dla podmiotów nie jest wolne od trudności, na przykład spójrz na ten Hibernacyjny artykuł na ten temat. Chodzi mi o to, że jeśli pracujesz tylko z podmiotami zarządzanymi, lepiej bez nich, lepiej zapewnij bardzo staranne wdrożenie.
Edwin Dalorzo
2
@TheStijn To jest dobry mieszany scenariusz. Uzasadnia to potrzebę wdrożenia eq / hC, jak początkowo zasugerowałeś, ponieważ gdy podmioty porzucą bezpieczeństwo warstwy trwałości, nie będziesz już mógł ufać regułom wymuszonym przez standard JPA. W naszym przypadku wzorzec DTO był architektonicznie egzekwowany od samego początku. Z założenia nasz interfejs API trwałości nie oferuje publicznego sposobu interakcji z obiektami biznesowymi, a jedynie interfejs API do interakcji z naszą warstwą trwałości za pomocą DTO.
Edwin Dalorzo
13

Moje 2 centy oprócz odpowiedzi tutaj to:

  1. W odniesieniu do dostępu do pola lub właściwości (poza kwestiami wydajności) oba są legalnie dostępne za pomocą metod pobierających i ustawiających, dlatego moja logika modelu może ustawiać / uzyskiwać je w ten sam sposób. Różnica pojawia się, gdy dostawca środowiska wykonawczego trwałości (Hibernate, EclipseLink lub inny) musi utrwalić / ustawić rekord w tabeli A, który ma klucz obcy odnoszący się do niektórych kolumn w tabeli B. W przypadku typu dostępu do właściwości trwałość system wykonawczy używa mojej metody setter do przypisania komórce w kolumnie tabeli B nowej wartości. W przypadku typu dostępu do pola system wykonawczy trwałości ustawia komórkę bezpośrednio w kolumnie tabeli B. Ta różnica nie ma znaczenia w kontekście relacji jednokierunkowej, jednak NALEŻY użyć mojej własnej metody kodowania (typ dostępu do właściwości) dla relacji dwukierunkowej, pod warunkiem, że metoda metody ustawiania jest dobrze zaprojektowana w celu uwzględnienia spójności. Spójność jest kluczowym zagadnieniem w odniesieniu do relacji dwukierunkowychlink do prostego przykładu dla dobrze zaprojektowanego setera.

  2. W odniesieniu do Equals / hashCode: Niemożliwe jest użycie automatycznie wygenerowanej metody Eclipse Equals / hashCode dla podmiotów uczestniczących w relacji dwukierunkowej, w przeciwnym razie będą miały okrągłe odwołanie, co spowoduje wyjątek przepełnienia stosu. Gdy spróbujesz relacji dwukierunkowej (powiedzmy OneToOne) i automatycznie wygenerujesz Equals () lub hashCode () lub nawet toString (), zostaniesz złapany na tym wyjątku przepełnienia stosu.

Sym-Sym
źródło
9

Interfejs jednostki

public interface Entity<I> extends Serializable {

/**
 * @return entity identity
 */
I getId();

/**
 * @return HashCode of entity identity
 */
int identityHashCode();

/**
 * @param other
 *            Other entity
 * @return true if identities of entities are equal
 */
boolean identityEquals(Entity<?> other);
}

Podstawowa implementacja dla wszystkich jednostek, upraszcza implementacje Equals / Hashcode:

public abstract class AbstractEntity<I> implements Entity<I> {

@Override
public final boolean identityEquals(Entity<?> other) {
    if (getId() == null) {
        return false;
    }
    return getId().equals(other.getId());
}

@Override
public final int identityHashCode() {
    return new HashCodeBuilder().append(this.getId()).toHashCode();
}

@Override
public final int hashCode() {
    return identityHashCode();
}

@Override
public final boolean equals(final Object o) {
    if (this == o) {
        return true;
    }
    if ((o == null) || (getClass() != o.getClass())) {
        return false;
    }

    return identityEquals((Entity<?>) o);
}

@Override
public String toString() {
    return getClass().getSimpleName() + ": " + identity();
    // OR 
    // return ReflectionToStringBuilder.reflectionToString(this, ToStringStyle.MULTI_LINE_STYLE);
}
}

Entity Room impl:

@Entity
@Table(name = "ROOM")
public class Room extends AbstractEntity<Integer> {

private static final long serialVersionUID = 1L;

@Id
@GeneratedValue(strategy = GenerationType.AUTO)
@Column(name = "room_id")
private Integer id;

@Column(name = "number") 
private String number; //immutable

@Column(name = "capacity")
private Integer capacity;

@ManyToOne(fetch = FetchType.LAZY, optional = false)
@JoinColumn(name = "building_id")
private Building building; //immutable

Room() {
    // default constructor
}

public Room(Building building, String number) {
    // constructor with required field
    notNull(building, "Method called with null parameter (application)");
    notNull(number, "Method called with null parameter (name)");

    this.building = building;
    this.number = number;
}

public Integer getId(){
    return id;
}

public Building getBuilding() {
    return building;
}

public String getNumber() {
    return number;
}


public void setCapacity(Integer capacity) {
    this.capacity = capacity;
}

//no setters for number, building nor id
}

Nie widzę sensu porównywanie równości podmiotów na podstawie pól biznesowych w każdym przypadku podmiotów JPA. Może to być bardziej przypadkowe, jeśli te jednostki JPA są uważane za ValueObjects oparte na domenie, zamiast encji opartych na domenie (dla których te przykłady kodu są).

ahaaman
źródło
4
Chociaż dobrym rozwiązaniem jest użycie klasy encji nadrzędnej do pobrania kodu płyty kotła, nie jest dobrym pomysłem stosowanie identyfikatora zdefiniowanego w DB w metodzie równości. W twoim przypadku porównanie 2 nowych podmiotów wyrzuciłoby nawet NPE. Nawet jeśli uczynisz go zerowym bezpiecznym, wówczas 2 nowe byty będą zawsze równe, dopóki nie zostaną utrzymane. Eq / hC powinny być niezmienne.
Stijn Geukens
2
Funkcja Equals () nie wyrzuci NPE, ponieważ sprawdzane jest, czy identyfikator DB jest zerowy, a jeśli identyfikator DB jest zerowy, równość byłaby fałszywa.
ahaaman
3
Rzeczywiście, nie widzę, jak mi umknęło, że kod jest bezpieczny. Ale IMO używające identyfikatora to nadal zła praktyka. Argumenty: onjava.com/pub/a/onjava/2006/09/13/…
Stijn
W książce „Implementing DDD” autorstwa Vaughna Vernona argumentuje się, że można użyć id dla równości, jeśli użyjesz „wczesnego generowania PK” (Wygeneruj najpierw identyfikator i przekaż go do konstruktora jednostki, a nie pozwalając na generowanie bazy danych identyfikator, kiedy
upierasz
lub jeśli nie planujesz równego porównywania nietrwałych bytów? Dlaczego powinieneś ...
Enerccio,