Buat entitas JPA yang sempurna [ditutup]

422

Saya telah bekerja dengan JPA (implementasi Hibernate) untuk beberapa waktu sekarang dan setiap kali saya perlu membuat entitas saya menemukan diri saya berjuang dengan masalah sebagai AccessType, properti tidak berubah, sama dengan / kode hash, ....
Jadi saya memutuskan untuk mencoba dan menemukan praktik terbaik umum untuk setiap masalah dan menuliskannya untuk penggunaan pribadi.
Namun saya tidak keberatan bagi siapa pun untuk mengomentarinya atau mengatakan di mana saya salah.

Kelas Entitas

  • mengimplementasikan Serializable

    Alasan: Spesifikasi mengatakan Anda harus melakukannya, tetapi beberapa penyedia JPA tidak menegakkan ini. Hibernate sebagai penyedia JPA tidak menegakkan ini, tetapi bisa gagal di suatu tempat jauh di dalam perutnya dengan ClassCastException, jika Serializable belum dilaksanakan.

Konstruktor

  • membuat konstruktor dengan semua bidang entitas yang diperlukan

    Alasan: Seorang konstruktor harus selalu membiarkan instance dibuat dalam keadaan waras.

  • selain konstruktor ini: memiliki konstruktor default pribadi paket

    Alasan: Konstruktor default diperlukan agar Hibernate menginisialisasi entitas; pribadi diperbolehkan tetapi paket privat (atau publik) diperlukan untuk pembuatan proxy runtime dan pengambilan data yang efisien tanpa instrumentasi bytecode.

Bidang / Properti

  • Gunakan akses lapangan secara umum dan akses properti saat dibutuhkan

    Alasan: ini mungkin masalah yang paling bisa diperdebatkan karena tidak ada argumen yang jelas dan meyakinkan untuk satu atau yang lain (akses properti vs akses lapangan); Namun, akses bidang tampaknya menjadi favorit umum karena kode yang lebih jelas, enkapsulasi yang lebih baik dan tidak perlu membuat setter untuk bidang yang tidak dapat diubah

  • Abaikan setter untuk bidang yang tidak dapat diubah (tidak diperlukan untuk bidang tipe akses)

  • properti mungkin bersifat pribadi
    Alasan: Saya pernah mendengar bahwa dilindungi lebih baik untuk kinerja (Hibernate) tetapi yang dapat saya temukan di web adalah: Hibernate dapat mengakses metode publik, privat, dan terlindungi, serta bidang publik, swasta dan terlindungi secara langsung . Pilihannya terserah Anda dan Anda dapat mencocokkannya agar sesuai dengan desain aplikasi Anda.

Kode yang sama / hash

  • Jangan pernah gunakan id yang dihasilkan jika id ini hanya disetel saat mempertahankan entitas
  • Menurut preferensi: gunakan nilai yang tidak dapat diubah untuk membentuk Kunci Bisnis yang unik dan gunakan ini untuk menguji kesetaraan
  • jika Kunci Bisnis unik tidak tersedia gunakan UUID non-transien yang dibuat ketika entitas diinisialisasi; Lihat artikel hebat ini untuk informasi lebih lanjut.
  • tidak pernah merujuk ke entitas terkait (ManyToOne); jika entitas ini (seperti entitas induk) perlu menjadi bagian dari Kunci Bisnis, maka bandingkan ID saja. Memanggil getId () pada proxy tidak akan memicu pemuatan entitas, selama Anda menggunakan tipe akses properti .

Entitas Contoh

@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

}

Saran lain untuk ditambahkan ke daftar ini lebih dari diterima ...

MEMPERBARUI

Sejak membaca artikel ini saya telah mengadaptasi cara saya menerapkan eq / hC:

  • jika kunci bisnis sederhana yang kekal tersedia: gunakan itu
  • dalam semua kasus lain: gunakan uuid
Stijn Geukens
sumber
6
Ini bukan pertanyaan, ini adalah permintaan untuk ditinjau dengan permintaan untuk daftar. Selain itu, itu sangat terbuka dan tidak jelas, atau dengan kata lain: Apakah entitas JPA sempurna tergantung pada apa yang akan digunakan untuk. Haruskah kita membuat daftar semua hal yang mungkin diperlukan entitas dalam semua kemungkinan penggunaan entitas?
meriton
Saya tahu itu bukan pertanyaan yang jelas yang saya minta maaf. Ini sebenarnya bukan permintaan untuk daftar, melainkan permintaan untuk komentar / komentar meskipun saran lain diterima. Jangan ragu untuk menguraikan kemungkinan penggunaan entitas JPA.
Stijn Geukens
Saya juga ingin bidang menjadi final(menilai oleh setingan Anda setter, saya kira Anda juga).
Sridhar Sarnobat
Harus mencobanya tetapi saya tidak berpikir final akan bekerja karena Hibernate masih perlu dapat mengatur nilai pada properti tersebut.
Stijn Geukens
Dari mana datangnya notNull?
Bruno

Jawaban:

73

Saya akan mencoba menjawab beberapa poin utama: ini dari pengalaman Hibernate / kegigihan yang panjang termasuk beberapa aplikasi utama.

Kelas Entitas: implement Serializable?

Kunci perlu mengimplementasikan Serializable. Hal-hal yang akan masuk dalam HttpSession, atau dikirim melalui kawat oleh RPC / Java EE, perlu mengimplementasikan Serializable. Hal-hal lain: tidak begitu banyak. Luangkan waktu Anda untuk hal-hal penting.

Konstruktor: membuat konstruktor dengan semua bidang yang diperlukan dari entitas?

Konstruktor untuk logika aplikasi, harus memiliki hanya beberapa bidang "kunci asing" atau "jenis / jenis" kritis yang akan selalu diketahui saat membuat entitas. Sisanya harus ditetapkan dengan memanggil metode setter - itulah gunanya.

Hindari menempatkan terlalu banyak bidang ke dalam konstruktor. Konstruktor harus nyaman, dan memberikan kewarasan dasar pada objek. Nama, Jenis dan / atau Orang Tua semuanya biasanya berguna.

OTOH jika aturan aplikasi (hari ini) mengharuskan Pelanggan memiliki Alamat, serahkan kepada setter. Itu adalah contoh dari "aturan lemah". Mungkin minggu depan, Anda ingin membuat objek Pelanggan sebelum pergi ke layar Enter Details? Jangan tersandung, tinggalkan kemungkinan untuk data yang tidak diketahui, tidak lengkap atau "sebagian dimasukkan".

Konstruktor: juga, paket konstruktor default pribadi?

Ya, tetapi gunakan 'protected' daripada paket pribadi. Hal-hal subclass adalah rasa sakit yang nyata ketika internal yang diperlukan tidak terlihat.

Bidang / Properti

Gunakan akses bidang 'properti' untuk Hibernate, dan dari luar instance. Dalam instance, gunakan bidang secara langsung. Alasan: memungkinkan refleksi standar, metode paling sederhana & paling dasar untuk Hibernate, berfungsi.

Adapun bidang 'tidak berubah' ke aplikasi - Hibernate masih harus dapat memuat ini. Anda dapat mencoba membuat metode ini 'pribadi', dan / atau memberikan anotasi pada mereka, untuk mencegah kode aplikasi membuat akses yang tidak diinginkan.

Catatan: saat menulis fungsi equals (), gunakan getter untuk nilai pada instance 'other'! Jika tidak, Anda akan menekan bidang yang tidak diinisialisasi / kosong pada instance proxy.

Terlindungi lebih baik untuk kinerja (Hibernate)?

Tidak sepertinya.

Sama dengan / HashCode?

Ini relevan untuk bekerja dengan entitas, sebelum mereka diselamatkan - yang merupakan masalah pelik. Hashing / membandingkan nilai-nilai yang tidak berubah? Di sebagian besar aplikasi bisnis, tidak ada.

Seorang pelanggan dapat mengubah alamat, mengubah nama bisnis mereka, dll. - tidak umum, tetapi itu terjadi. Koreksi juga perlu dilakukan, ketika data tidak dimasukkan dengan benar.

Beberapa hal yang biasanya tetap tidak berubah, adalah Pola Asuh dan mungkin Tipe / Jenis - biasanya pengguna membuat ulang catatan, alih-alih mengubahnya. Tetapi ini tidak secara unik mengidentifikasi entitas!

Jadi, panjang dan pendek, data "tidak berubah" yang diklaim tidak benar. Bidang Kunci Utama / ID dibuat untuk tujuan yang tepat, untuk memberikan stabilitas & kekekalan yang dijamin.

Anda perlu merencanakan & mempertimbangkan kebutuhan Anda untuk perbandingan & hashing & fase kerja pemrosesan permintaan ketika A) bekerja dengan "data yang diubah / terikat" dari UI jika Anda membandingkan / hash pada "bidang yang jarang diubah", atau B) bekerja dengan " data yang belum disimpan ", jika Anda membandingkan / hash pada ID.

Equals / HashCode - jika Kunci Bisnis unik tidak tersedia, gunakan UUID non-transien yang dibuat ketika entitas diinisialisasi

Ya, ini adalah strategi yang baik saat dibutuhkan. Ketahuilah bahwa UUID tidak bebas, dari segi kinerja - dan pengelompokan mempersulit.

Equals / HashCode - tidak pernah merujuk ke entitas terkait

"Jika entitas terkait (seperti entitas induk) perlu menjadi bagian dari Kunci Bisnis, lalu tambahkan bidang yang tidak dapat dimasukkan, tidak dapat diupdate untuk menyimpan id induk (dengan nama yang sama dengan ManyCone JoinColumn) dan gunakan id ini dalam pemeriksaan kesetaraan "

Kedengarannya seperti saran yang bagus.

Semoga ini membantu!

Thomas W
sumber
2
Re: konstruktor, saya sering melihat nol arg saja (yaitu tidak ada) dan kode panggilan memiliki daftar panjang setter yang tampak agak berantakan bagi saya. Apakah benar-benar ada masalah dengan memiliki beberapa konstruktor yang sesuai dengan kebutuhan Anda, membuat kode panggilan lebih ringkas?
Hurricane
benar-benar berpendapat, khususnya tentang ctor. apa kode yang lebih indah? sekelompok agen yang berbeda yang memberi tahu Anda (kombinasi dari) nilai-nilai yang diperlukan untuk membuat status objek yang waras atau pelaku tanpa argumen yang tidak memberikan petunjuk apa yang harus ditetapkan dan dalam urutan apa dan membiarkannya rentan terhadap kesalahan pengguna ?
mohamnag
1
@mohamnag Tergantung. Untuk data internal yang dihasilkan sistem, kacang yang benar benar bagus; namun aplikasi bisnis modern terdiri dari sejumlah besar layar entri data CRUD atau wisaya pengguna. Data yang dimasukkan oleh pengguna seringkali sebagian atau buruk, setidaknya selama pengeditan. Cukup sering bahkan ada nilai bisnis untuk dapat merekam keadaan yang tidak lengkap untuk penyelesaian nanti - pikirkan pengumpulan aplikasi asuransi, pendaftaran pelanggan, dll. Menjaga batasan seminimal mungkin (mis. Kunci utama, kunci bisnis & negara) memungkinkan fleksibilitas yang lebih besar secara nyata situasi bisnis.
Thomas W
1
@ ThomasW pertama-tama saya harus mengatakan, saya sangat berpendapat terhadap desain berbasis domain dan menggunakan nama untuk nama kelas dan berarti kata kerja penuh untuk metode. Dalam paradigma ini, apa yang Anda maksudkan, sebenarnya adalah DTO dan bukan entitas domain yang harus digunakan untuk penyimpanan data sementara. Atau Anda baru saja salah paham / -struktur domain Anda.
mohamnag
@ ThomasW ketika saya memfilter semua kalimat yang Anda coba katakan saya pemula, tidak ada informasi dalam komentar Anda yang tersisa kecuali mengenai input pengguna. Bagian itu, seperti yang saya katakan sebelumnya, akan dilakukan dalam DTO dan tidak secara langsung. mari kita bicara dalam 50 tahun lagi bahwa Anda dapat menjadi 5% dari apa pikiran besar di balik DDD seperti yang dialami Fowler!
Cheers
144

The JPA 2.0 Keterangan menyatakan bahwa:

  • Kelas entitas harus memiliki konstruktor no-arg. Mungkin ada konstruktor lain juga. Konstruktor no-arg harus bersifat publik atau dilindungi.
  • Kelas entitas harus berupa kelas tingkat atas. Enum atau antarmuka tidak boleh ditetapkan sebagai entitas.
  • Kelas entitas tidak boleh final. Tidak ada metode atau variabel instance persisten dari kelas entitas yang final.
  • Jika instance entitas harus diteruskan oleh nilai sebagai objek terpisah (misalnya, melalui antarmuka jarak jauh), kelas entitas harus mengimplementasikan antarmuka Serializable.
  • Baik kelas abstrak maupun konkret dapat menjadi entitas. Entitas dapat memperluas kelas non-entitas serta kelas entitas, dan kelas non-entitas dapat memperpanjang kelas entitas.

Spesifikasi tidak mengandung persyaratan tentang penerapan metode hashCode sama dan untuk entitas, hanya untuk kelas kunci utama dan kunci peta sejauh yang saya tahu.

Edwin Dalorzo
sumber
13
Benar, sama dengan, kode hash, ... bukan persyaratan JPA tetapi tentu saja direkomendasikan dan dianggap praktik yang baik.
Stijn Geukens
6
@TheStijn Nah, kecuali jika Anda berencana untuk membandingkan entitas yang terpisah untuk kesetaraan, ini mungkin tidak perlu. Manajer entitas dijamin untuk mengembalikan instance yang sama dari entitas yang diberikan setiap kali Anda memintanya. Jadi, Anda dapat melakukan perbandingan identitas dengan baik untuk entitas yang dikelola, sejauh yang saya mengerti. Bisakah Anda jelaskan sedikit lebih banyak tentang skenario di mana Anda akan menganggap ini praktik yang baik?
Edwin Dalorzo
2
Saya berusaha keras untuk selalu memiliki implementasi yang sama dengan kode / hash. Tidak diperlukan untuk JPA tapi saya menganggapnya sebagai praktik yang baik ketika entitas atau ditambahkan ke Sets. Anda bisa memutuskan untuk hanya menerapkan sama ketika entitas akan ditambahkan ke Set tetapi apakah Anda selalu tahu sebelumnya?
Stijn Geukens
10
@TheStijn Penyedia JPA akan memastikan bahwa pada waktu tertentu hanya ada satu instance dari entitas yang diberikan dalam konteks, oleh karena itu bahkan set Anda aman tanpa menerapkan sama dengan / kode has, asalkan Anda hanya menggunakan entitas yang dikelola. Menerapkan metode-metode ini untuk entitas tidak bebas dari kesulitan, misalnya, lihatlah Artikel Hibernate tentang subjek ini. Maksud saya adalah, jika Anda hanya bekerja dengan entitas yang dikelola, Anda lebih baik tanpa mereka, jika tidak memberikan implementasi yang sangat hati-hati.
Edwin Dalorzo
2
@ TheStijn Ini adalah skenario campuran yang bagus. Ini membenarkan perlunya menerapkan eq / hC seperti yang Anda usulkan pada awalnya karena begitu entitas mengabaikan keamanan lapisan persistensi Anda tidak dapat lagi mempercayai aturan yang diberlakukan oleh standar JPA. Dalam kasus kami, pola DTO secara arsitektur ditegakkan sejak awal. Dengan merancang API kegigihan kami tidak menawarkan cara publik untuk berinteraksi dengan objek bisnis, hanya API untuk berinteraksi dengan lapisan kegigihan kami menggunakan DTO.
Edwin Dalorzo
13

Tambahan 2 sen saya untuk jawaban di sini adalah:

  1. Dengan mengacu pada akses Field atau Properti (jauh dari pertimbangan kinerja) keduanya secara sah diakses melalui getter dan setter, dengan demikian, logika model saya dapat mengatur / mendapatkannya dengan cara yang sama. Perbedaannya muncul ketika penyedia runtime persistence (Hibernate, EclipseLink atau yang lain) perlu bertahan / menetapkan beberapa catatan dalam Tabel A yang memiliki kunci asing yang merujuk ke beberapa kolom pada Tabel B. Dalam kasus tipe akses Properti, persistence sistem runtime menggunakan metode penyetel kode saya untuk menetapkan nilai baru pada sel di kolom B. Dalam kasus tipe akses Field, sistem runtime persistensi mengatur sel dalam kolom Tabel B secara langsung. Perbedaan ini tidak penting dalam konteks hubungan satu arah, namun itu HARUS menggunakan metode setter kode saya sendiri (tipe akses properti) untuk hubungan dua arah asalkan metode setter dirancang dengan baik untuk memperhitungkan konsistensi. Konsistensi adalah masalah penting untuk hubungan dua arah yang merujuk pada hal initautan untuk contoh sederhana untuk setter yang dirancang dengan baik.

  2. Dengan mengacu pada Equals / hashCode: Tidak mungkin untuk menggunakan metode Equals / hashCode yang dihasilkan otomatis Eclipse untuk entitas yang berpartisipasi dalam hubungan dua arah, jika tidak mereka akan memiliki referensi melingkar yang menghasilkan Pengecualian stackoverflow. Setelah Anda mencoba hubungan dua arah (katakanlah OneToOne) dan hasil-otomatis sama dengan () atau kode hash () atau bahkan toString () Anda akan terjebak dalam pengecualian stackoverflow ini.

Simbol-Simbol
sumber
9

Antarmuka entitas

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);
}

Implementasi dasar untuk semua Entitas, menyederhanakan implementasi 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);
}
}

Entitas Entitas Kamar:

@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
}

Saya tidak melihat titik membandingkan kesetaraan entitas berdasarkan bidang bisnis dalam setiap kasus Entitas JPA. Itu mungkin lebih merupakan kasus jika entitas JPA ini dianggap sebagai Domain-Driven ValueObjects, bukan Domain-Driven Entities (yang contoh kode ini untuk).

ahaaman
sumber
4
Meskipun ini adalah pendekatan yang baik untuk menggunakan kelas entitas induk untuk mengeluarkan kode pelat boiler, itu bukan ide yang baik untuk menggunakan id yang didefinisikan DB dalam metode equals Anda. Dalam kasus Anda membandingkan 2 entitas baru bahkan akan melempar NPE. Bahkan jika Anda membuatnya nol aman maka 2 entitas baru akan selalu sama, sampai mereka bertahan. Persamaan / hC harus abadi.
Stijn Geukens
2
Sama dengan () tidak akan melempar NPE karena ada memeriksa apakah DB id adalah nol atau tidak & dalam kasus DB id adalah nol, kesetaraan akan salah.
ahaaman
3
Memang, saya tidak melihat bagaimana saya melewatkan bahwa kode ini aman-nol. Tetapi IMO menggunakan id masih merupakan praktik yang buruk. Argumen: onjava.com/pub/a/onjava/2006/09/13/…
Stijn Geukens
Dalam buku 'Menerapkan DDD' oleh Vaughn Vernon, dikemukakan bahwa Anda dapat menggunakan id untuk sama jika Anda menggunakan "generasi PK awal" (Hasilkan id pertama dan berikan ke konstruktor entitas sebagai lawan membiarkan basis data menghasilkan id ketika Anda bertahan entitas.)
Wim Deblauwe
atau jika Anda tidak berencana untuk membandingkan entitas non persisten yang setara? Mengapa Anda harus ...
Enerccio