Tiến thoái lưỡng nan JPA hashCode () / bằng ()


311

Đã có một số cuộc thảo luận ở đây về các thực thể JPA và nên sử dụng hashCode()/ equals()triển khai cho các lớp thực thể JPA. Hầu hết (nếu không phải tất cả) trong số họ phụ thuộc vào Hibernate, nhưng tôi muốn thảo luận về họ về mặt thực thi JPA (tôi đang sử dụng EclipseLink).

Tất cả các triển khai có thể có những ưu điểmnhược điểm riêng liên quan đến:

  • hashCode()/ equals()sự phù hợp hợp đồng (bất biến) cho List/ Sethoạt động
  • Cho dù các đối tượng giống hệt nhau (ví dụ từ các phiên khác nhau, các proxy động từ cấu trúc dữ liệu được tải nhanh) có thể được phát hiện
  • Liệu các thực thể có hành xử đúng trong trạng thái tách rời (hoặc không tồn tại)

Theo tôi có thể thấy, có ba lựa chọn :

  1. Đừng ghi đè lên chúng; dựa vào Object.equals()Object.hashCode()
    • hashCode()/ equals()công việc
    • không thể xác định các đối tượng giống hệt nhau, các vấn đề với proxy động
    • không có vấn đề với các thực thể tách rời
  2. Ghi đè chúng, dựa trên khóa chính
    • hashCode()/ equals()bị hỏng
    • danh tính chính xác (cho tất cả các thực thể được quản lý)
    • vấn đề với các thực thể tách ra
  3. Ghi đè chúng, dựa trên Id doanh nghiệp (các trường khóa không chính; còn khóa ngoại thì sao?)
    • hashCode()/ equals()bị hỏng
    • danh tính chính xác (cho tất cả các thực thể được quản lý)
    • không có vấn đề với các thực thể tách rời

Câu hỏi của tôi là:

  1. Tôi đã bỏ lỡ một tùy chọn và / hoặc pro / con point?
  2. Lựa chọn nào bạn đã chọn và tại sao?



CẬP NHẬT 1:

Bằng cách " hashCode()/ equals()bị phá vỡ", tôi có nghĩa là tiếp hashCode()lời gọi có thể trả về giá trị khác nhau, đó là (khi thực hiện một cách chính xác) không bị hỏng theo nghĩa của Objecttài liệu API, nhưng mà gây ra vấn đề khi cố gắng để lấy một thực thể đã thay đổi từ một Map, Sethoặc khác dựa trên hàm băm Collection. Do đó, các triển khai JPA (ít nhất là EclipseLink) sẽ không hoạt động chính xác trong một số trường hợp.

CẬP NHẬT 2:

Cảm ơn bạn đã trả lời của bạn - hầu hết trong số họ có chất lượng đáng chú ý.
Thật không may, tôi vẫn không chắc chắn cách tiếp cận nào sẽ là tốt nhất cho ứng dụng thực tế hoặc làm thế nào để xác định cách tiếp cận tốt nhất cho ứng dụng của tôi. Vì vậy, tôi sẽ giữ câu hỏi mở và hy vọng có thêm một số thảo luận và / hoặc ý kiến.


4
Tôi không hiểu ý của bạn là gì khi "hashCode () / bằng () bị hỏng"
nanda

4
Sau đó, chúng sẽ không bị "phá vỡ", vì trong tùy chọn 2 và 3, bạn sẽ thực hiện cả bằng () và hashCode () bằng cách sử dụng cùng một chiến lược.
matt b

11
Điều đó không đúng với tùy chọn 3. hashCode () và bằng () nên sử dụng cùng một tiêu chí, do đó, nếu một trong các trường của bạn thay đổi, có, phương thức hashcode () sẽ trả về một giá trị khác cho cùng một ví dụ so với trước đây, nhưng như vậy sẽ bằng (). Bạn đã bỏ phần thứ hai của câu từ mã băm () javadoc: Bất cứ khi nào nó được gọi trên cùng một đối tượng nhiều lần trong khi thực thi một ứng dụng Java, phương thức hashCode phải luôn trả về cùng một số nguyên, không cung cấp thông tin được sử dụng trong so sánh bằng trên đối tượng được sửa đổi .
matt b

1
Trên thực tế, một phần của câu có nghĩa ngược lại - việc gọi hashcode()cùng một đối tượng sẽ trả về cùng một giá trị, trừ khi bất kỳ trường nào được sử dụng trong việc equals()thực hiện thay đổi. Nói cách khác, nếu bạn có ba trường trong lớp và equals()phương thức của bạn chỉ sử dụng hai trường để xác định đẳng thức của các thể hiện, thì bạn có thể mong đợi hashcode()giá trị trả về sẽ thay đổi nếu bạn thay đổi một trong các giá trị của trường đó - điều này hợp lý khi bạn xem xét rằng đối tượng này không còn "bằng" với giá trị mà thể hiện cũ thể hiện.
matt b

2
"Các vấn đề khi cố gắng truy xuất thực thể đã thay đổi từ Bản đồ, Bộ hoặc Bộ sưu tập dựa trên hàm băm khác" ... đây sẽ là "sự cố khi cố truy xuất thực thể đã thay đổi từ HashMap, Hashset hoặc Bộ sưu tập dựa trên hàm băm khác"
nanda

Câu trả lời:


122

Đọc bài viết rất hay này về chủ đề: Đừng để Hibernate đánh cắp danh tính của bạn .

Kết luận của bài viết như sau:

Nhận dạng đối tượng khó có thể thực hiện chính xác khi các đối tượng được duy trì vào cơ sở dữ liệu. Tuy nhiên, các vấn đề hoàn toàn xuất phát từ việc cho phép các đối tượng tồn tại mà không có id trước khi chúng được lưu. Chúng ta có thể giải quyết những vấn đề này bằng cách chịu trách nhiệm gán ID đối tượng khỏi các khung ánh xạ quan hệ đối tượng như Hibernate. Thay vào đó, ID đối tượng có thể được chỉ định ngay khi đối tượng được khởi tạo. Điều này làm cho nhận dạng đối tượng đơn giản và không có lỗi và giảm lượng mã cần thiết trong mô hình miền.


21
Không, đó không phải là một bài viết tốt. Đó là một bài viết tuyệt vời về chủ đề này, và nó cần được đọc cho mọi lập trình viên JPA! +1!
Tom Anderson

2
Yup tôi đang sử dụng cùng một giải pháp. Không để DB tạo ID cũng có những lợi thế khác, chẳng hạn như có thể tạo một đối tượng và đã tạo các đối tượng khác tham chiếu đến nó trước khi duy trì nó. Điều này có thể loại bỏ độ trễ và nhiều chu kỳ yêu cầu / phản hồi trong ứng dụng máy chủ-máy khách. Nếu bạn cần nguồn cảm hứng cho một giải pháp như vậy, hãy xem các dự án của tôi: suid.jssuid-server-java . Về cơ bản suid.jstìm nạp các khối ID suid-server-javamà từ đó bạn có thể nhận và sử dụng phía máy khách.
Stijn de Witt

2
Điều này chỉ đơn giản là điên rồ. Tôi mới làm việc với chế độ ngủ đông dưới mui xe, đang viết bài kiểm tra đơn vị và phát hiện ra rằng tôi không thể xóa một đối tượng khỏi một tập hợp sau khi sửa đổi nó, kết luận rằng đó là do thay đổi mã băm, nhưng không thể hiểu làm thế nào để giải quyết. Bài viết đơn giản tuyệt đẹp!
XMight

Đây là một bài viết tuyệt vời. Tuy nhiên, đối với những người lần đầu tiên nhìn thấy liên kết, tôi sẽ đề xuất rằng nó có thể là quá mức cần thiết cho hầu hết các ứng dụng. 3 tùy chọn khác được liệt kê trên trang này sẽ ít nhiều giải quyết vấn đề theo nhiều cách.
HopeKing

1
Hibernate / JPA có sử dụng phương thức bằng và mã băm của một thực thể để kiểm tra xem bản ghi đã tồn tại trong cơ sở dữ liệu chưa?
Tushar Banne

64

Tôi luôn ghi đè bằng / hashcode và triển khai nó dựa trên id doanh nghiệp. Có vẻ là giải pháp hợp lý nhất cho tôi. Xem liên kết sau .

Để tổng hợp tất cả những thứ này, đây là danh sách những gì sẽ hoạt động hoặc sẽ không hoạt động với các cách khác nhau để xử lý bằng / hashCode: nhập mô tả hình ảnh ở đây

CHỈNH SỬA :

Để giải thích tại sao điều này làm việc cho tôi:

  1. Tôi thường không sử dụng bộ sưu tập dựa trên hàm băm (HashMap / Hashset) trong ứng dụng JPA của mình. Nếu tôi phải, tôi thích tạo giải pháp UniqueList.
  2. Tôi nghĩ rằng việc thay đổi id doanh nghiệp trong thời gian chạy không phải là cách thực hành tốt nhất cho bất kỳ ứng dụng cơ sở dữ liệu nào. Trong những trường hợp hiếm hoi không có giải pháp nào khác, tôi sẽ thực hiện xử lý đặc biệt như loại bỏ phần tử và đưa nó trở lại bộ sưu tập dựa trên hàm băm.
  3. Đối với mô hình của tôi, tôi đặt id doanh nghiệp trên hàm tạo và không cung cấp setters cho nó. Tôi để thực hiện JPA để thay đổi lĩnh vực thay vì tài sản.
  4. Giải pháp UUID dường như là quá mức cần thiết. Tại sao UUID nếu bạn có id doanh nghiệp tự nhiên? Sau cùng, tôi sẽ thiết lập tính duy nhất của id doanh nghiệp trong cơ sở dữ liệu. Tại sao có BA chỉ mục cho mỗi bảng trong cơ sở dữ liệu?

1
Nhưng bảng này thiếu dòng thứ năm "hoạt động với Danh sách / Bộ" (nếu bạn nghĩ đến việc xóa một thực thể là một phần của Tập hợp khỏi ánh xạ OneToMany) sẽ được trả lời "Không" trên hai tùy chọn cuối cùng vì hashCode của nó ( ) thay đổi vi phạm hợp đồng của nó.
MRalwasser

Xem bình luận về câu hỏi. Bạn dường như hiểu nhầm hợp đồng bằng / mã băm
nanda

1
@MRalwasser: Tôi nghĩ bạn có ý đúng, đó không phải là hợp đồng bằng / hashCode () bị vi phạm. Nhưng một bằng / hashCode có thể thay đổi sẽ tạo ra vấn đề với hợp đồng Set .
Chris Lercher

3
@MRalwasser: Mã băm chỉ có thể thay đổi nếu ID doanh nghiệp thay đổi và điểm quan trọng là ID doanh nghiệp không thay đổi. Vì vậy, mã băm không thay đổi và điều này hoạt động hoàn hảo với các bộ sưu tập băm.
Tom Anderson

1
Nếu bạn không có chìa khóa kinh doanh tự nhiên thì sao? Ví dụ trong trường hợp điểm hai chiều, Điểm (X, Y), trong ứng dụng vẽ biểu đồ? Làm thế nào bạn sẽ lưu trữ điểm đó như một thực thể?
jhegedus 6/214

35

Chúng tôi thường có hai ID trong các thực thể của mình:

  1. Chỉ dành cho lớp kiên trì (để nhà cung cấp và cơ sở dữ liệu kiên trì có thể tìm ra mối quan hệ giữa các đối tượng).
  2. Dành cho nhu cầu ứng dụng của chúng tôi ( equals()hashCode()đặc biệt)

Hãy xem:

@Entity
public class User {

    @Id
    private int id;  // Persistence ID
    private UUID uuid; // Business ID

    // assuming all fields are subject to change
    // If we forbid users change their email or screenName we can use these
    // fields for business ID instead, but generally that's not the case
    private String screenName;
    private String email;

    // I don't put UUID generation in constructor for performance reasons. 
    // I call setUuid() when I create a new entity
    public User() {
    }

    // This method is only called when a brand new entity is added to 
    // persistence context - I add it as a safety net only but it might work 
    // for you. In some cases (say, when I add this entity to some set before 
    // calling em.persist()) setting a UUID might be too late. If I get a log 
    // output it means that I forgot to call setUuid() somewhere.
    @PrePersist
    public void ensureUuid() {
        if (getUuid() == null) {
            log.warn(format("User's UUID wasn't set on time. " 
                + "uuid: %s, name: %s, email: %s",
                getUuid(), getScreenName(), getEmail()));
            setUuid(UUID.randomUUID());
        }
    }

    // equals() and hashCode() rely on non-changing data only. Thus we 
    // guarantee that no matter how field values are changed we won't 
    // lose our entity in hash-based Sets.
    @Override
    public int hashCode() {
        return getUuid().hashCode();
    }

    // Note that I don't use direct field access inside my entity classes and
    // call getters instead. That's because Persistence provider (PP) might
    // want to load entity data lazily. And I don't use 
    //    this.getClass() == other.getClass() 
    // for the same reason. In order to support laziness PP might need to wrap
    // my entity object in some kind of proxy, i.e. subclassing it.
    @Override
    public boolean equals(final Object obj) {
        if (this == obj)
            return true;
        if (!(obj instanceof User))
            return false;
        return getUuid().equals(((User) obj).getUuid());
    }

    // Getters and setters follow
}

EDIT: để làm rõ quan điểm của tôi về các cuộc gọi đến setUuid()phương thức. Đây là một kịch bản điển hình:

User user = new User();
// user.setUuid(UUID.randomUUID()); // I should have called it here
user.setName("Master Yoda");
user.setEmail("yoda@jedicouncil.org");

jediSet.add(user); // here's bug - we forgot to set UUID and 
                   //we won't find Yoda in Jedi set

em.persist(user); // ensureUuid() was called and printed the log for me.

jediCouncilSet.add(user); // Ok, we got a UUID now

Khi tôi chạy thử nghiệm và thấy đầu ra nhật ký, tôi đã khắc phục sự cố:

User user = new User();
user.setUuid(UUID.randomUUID());

Ngoài ra, người ta có thể cung cấp một hàm tạo riêng biệt:

@Entity
public class User {

    @Id
    private int id;  // Persistence ID
    private UUID uuid; // Business ID

    ... // fields

    // Constructor for Persistence provider to use
    public User() {
    }

    // Constructor I use when creating new entities
    public User(UUID uuid) {
        setUuid(uuid);
    }

    ... // rest of the entity.
}

Vì vậy, ví dụ của tôi sẽ như thế này:

User user = new User(UUID.randomUUID());
...
jediSet.add(user); // no bug this time

em.persist(user); // and no log output

Tôi sử dụng một hàm tạo mặc định và một setter, nhưng bạn có thể thấy cách tiếp cận hai hàm tạo phù hợp hơn với bạn.


2
Tôi tin rằng, đây là một giải pháp chính xác và tốt. Nó cũng có thể có một chút lợi thế về hiệu suất, bởi vì các số nguyên thường hoạt động tốt hơn trong các chỉ mục cơ sở dữ liệu so với uuids. Nhưng ngoài ra, bạn có thể loại bỏ thuộc tính id số nguyên hiện tại và thay thế nó bằng uuid (ứng dụng được gán)?
Chris Lercher

4
Điều này khác với việc sử dụng mặc định hashCode/ equalsphương thức cho đẳng thức JVM và idcho đẳng thức bền vững như thế nào? Điều này không có ý nghĩa với tôi cả.
Behrang Saeedzadeh

2
Nó hoạt động trong trường hợp khi bạn có một vài đối tượng thực thể trỏ đến cùng một hàng trong cơ sở dữ liệu. Object's equals()sẽ trở lại falsetrong trường hợp này. equals()Lợi nhuận dựa trên UUID true.
Andrew Андрей Листочкин

4
-1 - Tôi không thấy bất kỳ lý do nào để có hai ID và hai loại danh tính. Điều này dường như hoàn toàn vô nghĩa và có khả năng gây hại cho tôi.
Tom Anderson

1
Xin lỗi vì đã chỉ trích giải pháp của bạn mà không chỉ ra một giải pháp tôi muốn. Nói tóm lại, tôi sẽ cung cấp cho các đối tượng một trường ID duy nhất, tôi sẽ triển khai bằng và hashCode dựa trên nó và tôi sẽ tạo ra giá trị của nó khi tạo đối tượng, thay vì khi lưu vào cơ sở dữ liệu. Bằng cách đó, tất cả các hình thức của đối tượng hoạt động theo cùng một cách: không cố chấp, bền bỉ và tách rời. Các proxy Hibernate (hoặc tương tự) cũng sẽ hoạt động chính xác và tôi nghĩ thậm chí không cần phải ngậm nước để xử lý các lệnh gọi bằng và hashCode.
Tom Anderson

31

Cá nhân tôi đã sử dụng tất cả ba chiến lược này trong các dự án khác nhau. Và tôi phải nói rằng tùy chọn 1 theo ý kiến ​​của tôi là khả thi nhất trong một ứng dụng thực tế. Theo kinh nghiệm của tôi, việc phá vỡ tính tuân thủ hashCode () / equals () dẫn đến nhiều lỗi điên rồ vì bạn sẽ luôn gặp phải tình huống thay đổi kết quả sau khi một thực thể được thêm vào bộ sưu tập.

Nhưng có nhiều lựa chọn hơn (cũng với ưu và nhược điểm của họ):


a) hashCode / bằng dựa trên một tập hợp các trường bất biến , không null , hàm tạo được gán , các trường

(+) cả ba tiêu chí đều được đảm bảo

(-) các giá trị trường phải có sẵn để tạo một thể hiện mới

(-) phức tạp xử lý nếu bạn phải thay đổi một trong số đó


b) hashCode / bằng dựa trên khóa chính được gán bởi ứng dụng (trong hàm tạo) thay vì JPA

(+) cả ba tiêu chí đều được đảm bảo

(-) bạn không thể tận dụng các chiến lược tạo ID đáng tin cậy đơn giản như chuỗi DB

(-) phức tạp nếu các thực thể mới được tạo trong môi trường phân tán (máy khách / máy chủ) hoặc cụm máy chủ ứng dụng


c) hashCode / bằng dựa trên UUID được chỉ định bởi hàm tạo của thực thể

(+) cả ba tiêu chí đều được đảm bảo

(-) chi phí sản xuất UUID

(-) có thể có một rủi ro nhỏ khi sử dụng hai lần cùng một UUID, tùy thuộc vào đại số được sử dụng (có thể được phát hiện bởi một chỉ mục duy nhất trên DB)


Tôi fan hâm mộ của Lựa chọn 1phương pháp tiếp cận C cũng có. Không làm gì cho đến khi bạn thực sự cần nó là cách tiếp cận nhanh nhẹn hơn.
Adam Gent

2
+1 cho tùy chọn (b). IMHO, nếu một thực thể có ID doanh nghiệp tự nhiên, thì đó cũng phải là khóa chính của cơ sở dữ liệu. Đó là đơn giản, đơn giản, thiết kế cơ sở dữ liệu tốt. Nếu nó không có ID như vậy, thì cần có khóa thay thế. Nếu bạn thiết lập điều đó khi tạo đối tượng, thì mọi thứ khác đều đơn giản. Đó là khi mọi người không sử dụng khóa tự nhiên không tạo ra khóa thay thế sớm mà họ gặp rắc rối. Đối với sự phức tạp trong việc thực hiện - có, có một số. Nhưng thực sự không nhiều, và nó có thể được thực hiện một cách rất chung chung để giải quyết nó một lần cho tất cả các thực thể.
Tom Anderson

Tôi cũng thích tùy chọn 1, nhưng sau đó làm thế nào để viết bài kiểm tra đơn vị để khẳng định sự bình đẳng hoàn toàn là một vấn đề lớn, bởi vì chúng ta phải thực hiện phương thức bằng cho Collection.
Bóng nước trong

Đừng làm điều đó. Xem Đừng
lộn xộn

29

Nếu bạn muốn sử dụng equals()/hashCode()cho Bộ của mình, theo nghĩa là cùng một thực thể chỉ có thể ở đó một lần, thì chỉ có một tùy chọn: Tùy chọn 2. Đó là vì khóa chính cho một thực thể theo định nghĩa không bao giờ thay đổi (nếu ai đó thực sự cập nhật nó, nó không phải là cùng một thực thể nữa)

Bạn nên hiểu điều đó theo nghĩa đen: Vì bạn equals()/hashCode()dựa trên khóa chính, bạn không được sử dụng các phương thức này, cho đến khi khóa chính được đặt. Vì vậy, bạn không nên đặt các thực thể trong tập hợp, cho đến khi chúng được gán một khóa chính. (Có, UUID và các khái niệm tương tự có thể giúp gán khóa chính sớm.)

Bây giờ, về mặt lý thuyết cũng có thể đạt được điều đó với Tùy chọn 3, mặc dù cái gọi là "khóa kinh doanh" có nhược điểm khó chịu mà họ có thể thay đổi: "Tất cả những gì bạn phải làm là xóa các thực thể đã chèn khỏi bộ ( s) và chèn lại chúng. " Điều đó đúng - nhưng điều đó cũng có nghĩa là, trong một hệ thống phân tán, bạn sẽ phải đảm bảo rằng việc này được thực hiện hoàn toàn ở mọi nơi dữ liệu đã được chèn vào (và bạn sẽ phải đảm bảo rằng việc cập nhật được thực hiện , trước khi những điều khác xảy ra). Bạn sẽ cần một cơ chế cập nhật tinh vi, đặc biệt là nếu một số hệ thống từ xa hiện không thể truy cập ...

Tùy chọn 1 chỉ có thể được sử dụng, nếu tất cả các đối tượng trong các bộ của bạn là từ cùng một phiên Hibernate. Tài liệu Hibernate làm cho điều này rất rõ ràng trong chương 13.1.3. Xem xét danh tính đối tượng :

Trong Phiên, ứng dụng có thể sử dụng == để so sánh các đối tượng một cách an toàn.

Tuy nhiên, một ứng dụng sử dụng == bên ngoài Phiên có thể tạo ra kết quả không mong muốn. Điều này có thể xảy ra ngay cả ở một số nơi bất ngờ. Ví dụ: nếu bạn đặt hai phiên bản tách rời vào cùng một Bộ, cả hai có thể có cùng một danh tính cơ sở dữ liệu (nghĩa là chúng đại diện cho cùng một hàng). Tuy nhiên, định danh JVM là theo định nghĩa không được bảo đảm cho các cá thể ở trạng thái tách rời. Nhà phát triển phải ghi đè các phương thức equals () và hashCode () trong các lớp liên tục và thực hiện khái niệm riêng của chúng về đẳng thức đối tượng.

Nó tiếp tục tranh luận có lợi cho Lựa chọn 3:

Có một cảnh báo: không bao giờ sử dụng định danh cơ sở dữ liệu để thực hiện bình đẳng. Sử dụng khóa doanh nghiệp là sự kết hợp của các thuộc tính duy nhất, thường là bất biến. Mã định danh cơ sở dữ liệu sẽ thay đổi nếu một đối tượng thoáng qua được thực hiện liên tục. Nếu phiên bản tạm thời (thường cùng với các phiên bản tách rời) được giữ trong Tập hợp, việc thay đổi mã băm sẽ phá vỡ hợp đồng của Tập hợp.

Đây là sự thật, nếu bạn

  • không thể gán id sớm (ví dụ: bằng cách sử dụng UUID)
  • và bạn hoàn toàn muốn đặt các đối tượng của mình theo bộ trong khi chúng ở trạng thái thoáng qua.

Nếu không, bạn có thể chọn Tùy chọn 2.

Sau đó, nó đề cập đến sự cần thiết cho một sự ổn định tương đối:

Các thuộc tính cho khóa doanh nghiệp không phải ổn định như khóa chính của cơ sở dữ liệu; bạn chỉ phải đảm bảo sự ổn định miễn là các đối tượng nằm trong cùng một Bộ.

Chính xác. Vấn đề thực tế tôi thấy với điều này là: Nếu bạn không thể đảm bảo sự ổn định tuyệt đối, làm thế nào bạn có thể đảm bảo sự ổn định "miễn là các đối tượng nằm trong cùng một Bộ". Tôi có thể tưởng tượng một số trường hợp đặc biệt (như chỉ sử dụng các bộ cho một cuộc trò chuyện và sau đó ném nó đi), nhưng tôi sẽ đặt câu hỏi về tính khả thi chung của việc này.


Phiên bản ngắn:

  • Tùy chọn 1 chỉ có thể được sử dụng với các đối tượng trong một phiên duy nhất.
  • Nếu bạn có thể, hãy sử dụng Tùy chọn 2. (Gán PK càng sớm càng tốt, vì bạn không thể sử dụng các đối tượng theo bộ cho đến khi PK được chỉ định.)
  • Nếu bạn có thể đảm bảo sự ổn định tương đối, bạn có thể sử dụng Tùy chọn 3. Nhưng hãy cẩn thận với điều này.

Giả định của bạn rằng khóa chính không bao giờ thay đổi là sai. Ví dụ, Hibernate chỉ phân bổ khóa chính khi phiên được lưu. Vì vậy, nếu bạn sử dụng khóa chính làm hashCode thì kết quả của hashCode () trước khi bạn lưu đối tượng trước và sau khi bạn lưu đối tượng trước sẽ khác. Tồi tệ hơn, trước khi bạn lưu phiên, hai đối tượng mới được tạo sẽ có cùng mã băm và có thể ghi đè lên nhau khi được thêm vào bộ sưu tập. Bạn có thể thấy mình phải buộc lưu / xả ngay lập tức vào việc tạo đối tượng để sử dụng phương pháp đó.
William Billingsley

2
@William: Khóa chính của một thực thể không thay đổi. Thuộc tính id của đối tượng được ánh xạ có thể thay đổi. Điều này xảy ra, như bạn đã giải thích, đặc biệt là khi một đối tượng thoáng qua được thực hiện liên tục . Vui lòng đọc kỹ phần câu trả lời của tôi, nơi tôi đã nói về các phương thức bằng / hashCode: "bạn không được sử dụng các phương thức này, cho đến khi khóa chính được đặt."
Chris Lercher

Hoàn toàn đồng ý. Với tùy chọn 2, bạn cũng có thể tính ra mã bằng / mã băm trong một siêu hạng và được sử dụng lại bởi tất cả các thực thể của bạn.
Theo

+1 Tôi mới biết về JPA, nhưng một số ý kiến ​​và câu trả lời ở đây ngụ ý rằng mọi người không hiểu ý nghĩa của thuật ngữ "khóa chính".
Raedwald

16
  1. Nếu bạn có khóa doanh nghiệp , thì bạn nên sử dụng khóa đó cho equals/ hashCode.
  2. Nếu bạn không có khóa doanh nghiệp, bạn không nên để nó với các cài đặt Objectbằng và mặc định hashCode mặc định vì điều đó không hoạt động sau bạn mergevà thực thể.
  3. Bạn có thể sử dụng định danh thực thể như được đề xuất trong bài viết này . Điều hấp dẫn duy nhất là bạn cần sử dụng một hashCodetriển khai luôn trả về cùng một giá trị, như thế này:

    @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 getId() != null && Objects.equals(getId(), book.getId());
        }
    
        @Override
        public int hashCode() {
            return 31;
        }
    
        //Getters and setters omitted for brevity
    }

Mà là tốt hơn: (1) onjava.com/pub/a/onjava/2006/09/13/... hoặc (2) vladmihalcea.com/... ? Giải pháp (2) dễ hơn (1). Vậy tại sao tôi nên sử dụng (1). Là những tác động của cả hai giống nhau? Cả hai có đảm bảo cùng một giải pháp?
nimo23

Và với giải pháp của bạn: "giá trị hashCode không thay đổi" giữa các trường hợp tương tự. Điều này có hành vi tương tự như thể nó là uuid "giống nhau" (từ giải pháp (1)) được so sánh. Tôi có đúng không
nimo23

1
Và lưu trữ UUID trong cơ sở dữ liệu và tăng dấu chân của bản ghi và trong vùng đệm? Tôi nghĩ rằng điều này có thể dẫn đến nhiều vấn đề hiệu suất trong thời gian dài hơn so với hashCode duy nhất. Đối với giải pháp khác, bạn có thể kiểm tra xem liệu nó có cung cấp tính nhất quán trên tất cả các chuyển đổi trạng thái thực thể hay không. Bạn có thể tìm thấy bài kiểm tra kiểm tra trên GitHub .
Vlad Mihalcea

1
Nếu bạn có khóa doanh nghiệp bất biến, hashCode có thể sử dụng nó và nó sẽ được hưởng lợi từ nhiều nhóm, vì vậy nó đáng để sử dụng nếu bạn có. Mặt khác, chỉ cần sử dụng định danh thực thể như được giải thích trong bài viết của tôi.
Vlad Mihalcea

1
Tôi vui vì bạn thích nó. Tôi có hàng trăm bài viết khác về JPA và Hibernate.
Vlad Mihalcea

10

Mặc dù sử dụng khóa nghiệp vụ (tùy chọn 3) là cách tiếp cận được khuyên dùng phổ biến nhất ( wiki cộng đồng Hibernate , "Java Persistence with Hibernate" trang 398), và đây là thứ chúng tôi chủ yếu sử dụng, có một lỗi Hibernate phá vỡ điều này vì háo hức bộ: HHH-3799 . Trong trường hợp này, Hibernate có thể thêm một thực thể vào một tập hợp trước khi các trường của nó được khởi tạo. Tôi không chắc tại sao lỗi này không được chú ý nhiều hơn, vì nó thực sự làm cho cách tiếp cận khóa doanh nghiệp được đề xuất có vấn đề.

Tôi nghĩ cốt lõi của vấn đề là bằng và hashCode phải dựa trên trạng thái bất biến (tham chiếu Oderky và cộng sự ), và một thực thể Hibernate với khóa chính do Hibernate quản lý không có trạng thái bất biến như vậy. Khóa chính được sửa đổi bởi Hibernate khi một đối tượng thoáng qua trở nên liên tục. Khóa nghiệp vụ cũng được sửa đổi bởi Hibernate, khi nó hydrat hóa một đối tượng trong quá trình được khởi tạo.

Chỉ để lại tùy chọn 1, kế thừa các triển khai java.lang.Object dựa trên danh tính đối tượng hoặc sử dụng khóa chính được quản lý ứng dụng theo đề xuất của James Brundege trong "Đừng để Hibernate đánh cắp danh tính của bạn" (đã được tham chiếu bởi câu trả lời của Stijn Geukens ) và bởi Lance Arlaus trong "Tạo đối tượng: Cách tiếp cận tốt hơn để tích hợp ngủ đông" .

Vấn đề lớn nhất với tùy chọn 1 là các trường hợp tách rời không thể so sánh với các thể hiện liên tục bằng cách sử dụng .equals (). Nhưng điều đó ổn thôi; hợp đồng của Equal và hashCode để cho nhà phát triển quyết định ý nghĩa của đẳng thức đối với mỗi lớp. Vì vậy, chỉ cần để bằng và hashCode kế thừa từ Object. Nếu bạn cần so sánh một cá thể tách rời với một cá thể liên tục, bạn có thể tạo một phương thức mới một cách rõ ràng cho mục đích đó, có thể boolean sameEntityhoặc boolean dbEquivalenthoặc boolean businessEquals.


5

Tôi đồng ý với câu trả lời của Andrew. Chúng tôi làm điều tương tự trong ứng dụng của mình nhưng thay vì lưu trữ UUID dưới dạng VARCHAR / CHAR, chúng tôi chia nó thành hai giá trị dài. Xem UUID.getLeastSignificantBits () và UUID.getMostSignificantBits ().

Một điều nữa cần xem xét, đó là các cuộc gọi tới UUID.randomUUID () khá chậm, do đó bạn có thể muốn xem xét việc tạo ra UUID một cách lười biếng khi cần, chẳng hạn như trong khi kiên trì hoặc gọi tới bằng () / hashCode ()

@MappedSuperclass
public abstract class AbstractJpaEntity extends AbstractMutable implements Identifiable, Modifiable {

    private static final long   serialVersionUID    = 1L;

    @Version
    @Column(name = "version", nullable = false)
    private int                 version             = 0;

    @Column(name = "uuid_least_sig_bits")
    private long                uuidLeastSigBits    = 0;

    @Column(name = "uuid_most_sig_bits")
    private long                uuidMostSigBits     = 0;

    private transient int       hashCode            = 0;

    public AbstractJpaEntity() {
        //
    }

    public abstract Integer getId();

    public abstract void setId(final Integer id);

    public boolean isPersisted() {
        return getId() != null;
    }

    public int getVersion() {
        return version;
    }

    //calling UUID.randomUUID() is pretty expensive, 
    //so this is to lazily initialize uuid bits.
    private void initUUID() {
        final UUID uuid = UUID.randomUUID();
        uuidLeastSigBits = uuid.getLeastSignificantBits();
        uuidMostSigBits = uuid.getMostSignificantBits();
    }

    public long getUuidLeastSigBits() {
        //its safe to assume uuidMostSigBits of a valid UUID is never zero
        if (uuidMostSigBits == 0) {
            initUUID();
        }
        return uuidLeastSigBits;
    }

    public long getUuidMostSigBits() {
        //its safe to assume uuidMostSigBits of a valid UUID is never zero
        if (uuidMostSigBits == 0) {
            initUUID();
        }
        return uuidMostSigBits;
    }

    public UUID getUuid() {
        return new UUID(getUuidMostSigBits(), getUuidLeastSigBits());
    }

    @Override
    public int hashCode() {
        if (hashCode == 0) {
            hashCode = (int) (getUuidMostSigBits() >> 32 ^ getUuidMostSigBits() ^ getUuidLeastSigBits() >> 32 ^ getUuidLeastSigBits());
        }
        return hashCode;
    }

    @Override
    public boolean equals(final Object obj) {
        if (obj == null) {
            return false;
        }
        if (!(obj instanceof AbstractJpaEntity)) {
            return false;
        }
        //UUID guarantees a pretty good uniqueness factor across distributed systems, so we can safely
        //dismiss getClass().equals(obj.getClass()) here since the chance of two different objects (even 
        //if they have different types) having the same UUID is astronomical
        final AbstractJpaEntity entity = (AbstractJpaEntity) obj;
        return getUuidMostSigBits() == entity.getUuidMostSigBits() && getUuidLeastSigBits() == entity.getUuidLeastSigBits();
    }

    @PrePersist
    public void prePersist() {
        // make sure the uuid is set before persisting
        getUuidLeastSigBits();
    }

}

Chà, thực ra nếu bạn ghi đè bằng () / hashCode () thì bạn phải tạo UUID cho mọi thực thể (tôi giả sử rằng bạn muốn duy trì mọi thực thể bạn tạo trong mã của mình). Bạn chỉ thực hiện một lần - trước khi lưu trữ vào cơ sở dữ liệu lần đầu tiên. Sau đó, UUID chỉ được cung cấp bởi Nhà cung cấp kiên trì. Vì vậy, tôi không thấy điểm làm việc đó một cách lười biếng.
Andrew Андрей Листочкин

Tôi đã bình chọn câu trả lời của bạn bởi vì tôi thực sự thích những ý tưởng khác của bạn: lưu trữ UUID dưới dạng một cặp số trong cơ sở dữ liệu và không chuyển sang một loại cụ thể bên trong phương thức equals () - đó là một cách thực sự gọn gàng! Tôi chắc chắn sẽ sử dụng hai thủ thuật này trong tương lai.
Andrew Андрей Листочкин

1
Cảm ơn đã bỏ phiếu. Lý do cho việc khởi tạo UUID một cách lười biếng là trong ứng dụng của chúng tôi, chúng tôi tạo ra rất nhiều Thực thể không bao giờ được đưa vào HashMap hoặc tồn tại. Vì vậy, chúng tôi đã thấy hiệu suất giảm 100 lần khi chúng tôi tạo đối tượng (100.000 trong số họ). Vì vậy, chúng tôi chỉ khởi tạo UUID nếu cần thiết. Tôi chỉ muốn có sự hỗ trợ tốt trong MySql cho các số 128 bit để chúng tôi chỉ có thể sử dụng UUID cho id và không quan tâm đến auto_increment.
vẽ

Ồ, tôi hiểu rồi. Trong trường hợp của tôi, chúng tôi thậm chí không khai báo trường UUID nếu thực thể tương ứng sẽ không được đưa vào bộ sưu tập. Hạn chế là đôi khi chúng ta phải thêm nó bởi vì sau đó hóa ra chúng ta thực sự cần phải đưa chúng vào bộ sưu tập. Điều này đôi khi xảy ra trong quá trình phát triển nhưng may mắn là không bao giờ xảy ra với chúng tôi sau khi triển khai ban đầu cho khách hàng nên nó không phải là vấn đề lớn. Nếu điều đó xảy ra sau khi hệ thống hoạt động, chúng ta sẽ cần di chuyển db. UUID lười biếng rất hữu ích trong các tình huống như vậy.
Andrew Андрей Листочкин

Có lẽ bạn cũng nên thử trình tạo UUID nhanh hơn mà Adam đề xuất trong câu trả lời của anh ấy nếu hiệu suất là vấn đề quan trọng trong tình huống của bạn.
Andrew Андрей Листочкин

3

Như những người khác thông minh hơn tôi đã chỉ ra, có rất nhiều chiến lược ngoài kia. Dường như đó là trường hợp mặc dù phần lớn các mẫu thiết kế được áp dụng cố gắng hack theo cách của họ để thành công. Chúng giới hạn quyền truy cập của hàm tạo nếu không cản trở các yêu cầu của hàm tạo hoàn toàn với các hàm tạo chuyên dụng và các phương thức xuất xưởng. Quả thực nó luôn dễ chịu với một API cắt rõ ràng. Nhưng nếu lý do duy nhất là làm cho các phần ghi đè bằng và mã băm tương thích với ứng dụng, thì tôi tự hỏi liệu các chiến lược đó có tuân thủ KISS không (Keep It Simple St ngu).

Đối với tôi, tôi thích ghi đè bằng và mã băm bằng cách kiểm tra id. Trong các phương thức này, tôi yêu cầu id không được rỗng và ghi lại hành vi này tốt. Do đó, nó sẽ trở thành hợp đồng phát triển để duy trì một thực thể mới trước khi lưu trữ anh ta ở một nơi khác. Một ứng dụng không tôn trọng hợp đồng này sẽ thất bại trong vòng một phút (hy vọng).

Mặc dù vậy, nếu thận trọng: Nếu các thực thể của bạn được lưu trữ trong các bảng khác nhau và nhà cung cấp của bạn sử dụng chiến lược tạo tự động cho khóa chính, thì bạn sẽ nhận được các khóa chính trùng lặp trên các loại thực thể. Trong trường hợp như vậy, cũng so sánh các loại thời gian chạy với một lệnh gọi đến Object # getClass () tất nhiên sẽ làm cho hai loại khác nhau được coi là không thể bằng nhau. Điều đó phù hợp với tôi chỉ tốt cho hầu hết các phần.


Ngay cả với các chuỗi thiếu DB (như Mysql), có thể mô phỏng chúng (ví dụ: bảng hibernate_ resultence). Vì vậy, bạn luôn có thể nhận được một ID duy nhất trên các bảng. +++ Nhưng bạn không cần nó. Gọi Object#getClass() là xấu vì H. proxy. Gọi Hibernate.getClass(o)giúp, nhưng vấn đề bình đẳng của các thực thể các loại khác nhau vẫn còn. Có một giải pháp sử dụng canEqual , hơi phức tạp, nhưng có thể sử dụng được. Đồng ý rằng thường thì không cần thiết. +++ Ném eq / hc vào null ID vi phạm hợp đồng, nhưng nó rất thực dụng.
maaartinus

2

Rõ ràng đã có câu trả lời rất nhiều thông tin ở đây nhưng tôi sẽ cho bạn biết những gì chúng tôi làm.

Chúng tôi không làm gì cả (tức là không ghi đè).

Nếu chúng ta cần bằng / mã băm để làm việc cho các bộ sưu tập, chúng ta sử dụng UUID. Bạn chỉ cần tạo UUID trong hàm tạo. Chúng tôi sử dụng http://wiki.fasterxml.com/JugHome cho UUID. UUID là CPU thông minh đắt hơn một chút nhưng lại rẻ so với truy cập serial và db.


1

Tôi đã luôn sử dụng tùy chọn 1 trong quá khứ bởi vì tôi nhận thức được các cuộc thảo luận này và nghĩ rằng tốt hơn là không làm gì cho đến khi tôi biết điều đúng đắn cần làm. Những hệ thống này vẫn đang chạy thành công.

Tuy nhiên, lần tới tôi có thể thử tùy chọn 2 - sử dụng Id được tạo cơ sở dữ liệu.

Hashcode và bằng sẽ ném IllegalStateException nếu id không được đặt.

Điều này sẽ ngăn các lỗi tinh vi liên quan đến các thực thể chưa được lưu xuất hiện bất ngờ.

Mọi người nghĩ gì về phương pháp này?


1

Phương pháp tiếp cận khóa kinh doanh không phù hợp với chúng tôi. Chúng tôi sử dụng ID được tạo DB , tempId tạm thời tạm thời và ghi đè bằng () / hashcode () để giải quyết vấn đề nan giải. Tất cả các thực thể là hậu duệ của thực thể. Ưu điểm:

  1. Không có trường bổ sung trong DB
  2. Không có mã hóa thêm trong các thực thể con cháu, một cách tiếp cận cho tất cả
  3. Không có vấn đề về hiệu năng (như với UUID), việc tạo DB Id
  4. Không có vấn đề gì với Hashmaps (không cần lưu ý việc sử dụng bằng & v.v.)
  5. Hashcode của thực thể mới không thay đổi theo thời gian ngay cả sau khi vẫn tồn tại

Nhược điểm:

  1. Có thể có vấn đề với việc tuần tự hóa và giải tuần tự hóa các thực thể không tồn tại
  2. Hashcode của thực thể đã lưu có thể thay đổi sau khi tải lại từ DB
  3. Các đối tượng không kiên trì được coi là luôn khác nhau (có lẽ điều này đúng?)
  4. Còn gì nữa không

Nhìn vào mã của chúng tôi:

@MappedSuperclass
abstract public class Entity implements Serializable {

    @Id
    @GeneratedValue
    @Column(nullable = false, updatable = false)
    protected Long id;

    @Transient
    private Long tempId;

    public void setId(Long id) {
        this.id = id;
    }

    public Long getId() {
        return id;
    }

    private void setTempId(Long tempId) {
        this.tempId = tempId;
    }

    // Fix Id on first call from equal() or hashCode()
    private Long getTempId() {
        if (tempId == null)
            // if we have id already, use it, else use 0
            setTempId(getId() == null ? 0 : getId());
        return tempId;
    }

    @Override
    public boolean equals(Object obj) {
        if (super.equals(obj))
            return true;
        // take proxied object into account
        if (obj == null || !Hibernate.getClass(obj).equals(this.getClass()))
            return false;
        Entity o = (Entity) obj;
        return getTempId() != 0 && o.getTempId() != 0 && getTempId().equals(o.getTempId());
    }

    // hash doesn't change in time
    @Override
    public int hashCode() {
        return getTempId() == 0 ? super.hashCode() : getTempId().hashCode();
    }
}

1

Vui lòng xem xét cách tiếp cận sau dựa trên định danh loại được xác định trước và ID.

Các giả định cụ thể cho JPA:

  • các thực thể có cùng "loại" và cùng một ID không null được coi là bằng nhau
  • các thực thể không tồn tại (giả sử không có ID) không bao giờ bằng các thực thể khác

Các thực thể trừu tượng:

@MappedSuperclass
public abstract class AbstractPersistable<K extends Serializable> {

  @Id @GeneratedValue
  private K id;

  @Transient
  private final String kind;

  public AbstractPersistable(final String kind) {
    this.kind = requireNonNull(kind, "Entity kind cannot be null");
  }

  @Override
  public final boolean equals(final Object obj) {
    if (this == obj) return true;
    if (!(obj instanceof AbstractPersistable)) return false;
    final AbstractPersistable<?> that = (AbstractPersistable<?>) obj;
    return null != this.id
        && Objects.equals(this.id, that.id)
        && Objects.equals(this.kind, that.kind);
  }

  @Override
  public final int hashCode() {
    return Objects.hash(kind, id);
  }

  public K getId() {
    return id;
  }

  protected void setId(final K id) {
    this.id = id;
  }
}

Ví dụ thực thể cụ thể:

static class Foo extends AbstractPersistable<Long> {
  public Foo() {
    super("Foo");
  }
}

Ví dụ kiểm tra:

@Test
public void test_EqualsAndHashcode_GivenSubclass() {
  // Check contract
  EqualsVerifier.forClass(Foo.class)
    .suppress(Warning.NONFINAL_FIELDS, Warning.TRANSIENT_FIELDS)
    .withOnlyTheseFields("id", "kind")
    .withNonnullFields("id", "kind")
    .verify();
  // Ensure new objects are not equal
  assertNotEquals(new Foo(), new Foo());
}

Ưu điểm chính ở đây:

  • sự đơn giản
  • đảm bảo các lớp con cung cấp danh tính loại
  • dự đoán hành vi với các lớp ủy nhiệm

Nhược điểm:

  • Yêu cầu mỗi thực thể gọi super()

Ghi chú:

  • Cần chú ý khi sử dụng thừa kế. Ví dụ, đẳng thức của class Aclass B extends Acó thể phụ thuộc vào chi tiết cụ thể của ứng dụng.
  • Lý tưởng nhất là sử dụng khóa doanh nghiệp làm ID

Mong muốn được bình luận của bạn.


0

Đây là một vấn đề phổ biến trong mọi hệ thống CNTT sử dụng Java và JPA. Điểm đau vượt ra ngoài việc thực hiện bằng () và hashCode (), nó ảnh hưởng đến cách một tổ chức đề cập đến một thực thể và cách các khách hàng của nó tham chiếu đến cùng một thực thể. Tôi đã thấy đủ đau đớn khi không có chìa khóa kinh doanh đến mức tôi đã viết blog của riêng mình để bày tỏ quan điểm của mình.

Tóm lại: sử dụng ID tuần tự ngắn, dễ đọc của con người với các tiền tố có ý nghĩa làm khóa doanh nghiệp được tạo mà không phụ thuộc vào bất kỳ bộ lưu trữ nào ngoài RAM. Bông tuyết của Twitter là một ví dụ rất hay.


0

IMO bạn có 3 tùy chọn để thực hiện bằng / hashCode

  • Sử dụng một danh tính được tạo ra ứng dụng, tức là một UUID
  • Triển khai nó dựa trên khóa doanh nghiệp
  • Thực hiện nó dựa trên khóa chính

Sử dụng một danh tính tạo ra ứng dụng là cách tiếp cận dễ nhất, nhưng đi kèm với một vài nhược điểm

  • Tham gia chậm hơn khi sử dụng nó làm PK vì 128 Bit đơn giản là lớn hơn 32 hoặc 64 Bit
  • "Gỡ lỗi khó hơn" vì kiểm tra bằng mắt của bạn một số dữ liệu là chính xác là khá khó

Nếu bạn có thể làm việc với những nhược điểm này , chỉ cần sử dụng phương pháp này.

Để khắc phục vấn đề tham gia, người ta có thể sử dụng UUID làm khóa tự nhiên và giá trị chuỗi làm khóa chính, nhưng sau đó bạn vẫn có thể gặp phải các vấn đề triển khai bằng / hashCode trong các thực thể con có cấu trúc có id nhúng vì bạn sẽ muốn tham gia dựa trên trên khóa chính. Sử dụng khóa tự nhiên trong id thực thể con và khóa chính để tham chiếu đến cha mẹ là một sự thỏa hiệp tốt.

@Entity class Parent {
  @Id @GeneratedValue Long id;
  @NaturalId UUID uuid;
  @OneToMany(mappedBy = "parent") Set<Child> children;
  // equals/hashCode based on uuid
}

@Entity class Child {
  @EmbeddedId ChildId id;
  @ManyToOne Parent parent;

  @Embeddable class ChildId {
    UUID parentUuid;
    UUID childUuid;
    // equals/hashCode based on parentUuid and childUuid
  }
  // equals/hashCode based on id
}

IMO đây là cách tiếp cận sạch nhất vì nó sẽ tránh được mọi nhược điểm và đồng thời cung cấp cho bạn một giá trị (UUID) mà bạn có thể chia sẻ với các hệ thống bên ngoài mà không để lộ nội bộ hệ thống.

Triển khai nó dựa trên khóa doanh nghiệp nếu bạn có thể mong đợi rằng từ người dùng là một ý tưởng hay, nhưng cũng đi kèm với một vài nhược điểm

Hầu hết thời gian khóa doanh nghiệp này sẽ là một loại mà người dùng cung cấp và thường không phải là tổng hợp của nhiều thuộc tính.

  • Tham gia chậm hơn vì tham gia dựa trên văn bản có độ dài thay đổi đơn giản là chậm. Một số DBMS thậm chí có thể gặp sự cố khi tạo chỉ mục nếu khóa vượt quá độ dài nhất định.
  • Theo kinh nghiệm của tôi, các khóa nghiệp vụ có xu hướng thay đổi sẽ yêu cầu cập nhật xếp tầng cho các đối tượng đề cập đến nó. Điều này là không thể nếu các hệ thống bên ngoài đề cập đến nó

IMO bạn không nên thực hiện hoặc làm việc với một khóa kinh doanh độc quyền. Đó là một tiện ích bổ sung tốt, tức là người dùng có thể nhanh chóng tìm kiếm theo khóa doanh nghiệp đó, nhưng hệ thống không nên dựa vào nó để vận hành.

Triển khai nó dựa trên khóa chính có vấn đề, nhưng có lẽ nó không phải là vấn đề lớn

Nếu bạn cần đưa id ra hệ thống bên ngoài, hãy sử dụng phương pháp UUID mà tôi đề xuất. Nếu bạn không, bạn vẫn có thể sử dụng phương pháp UUID nhưng bạn không phải làm vậy. Vấn đề sử dụng id DBMS được tạo bằng bằng / hashCode xuất phát từ thực tế là đối tượng có thể đã được thêm vào các bộ sưu tập dựa trên hàm băm trước khi gán id.

Cách rõ ràng để khắc phục điều này là chỉ đơn giản là không thêm đối tượng vào các bộ sưu tập dựa trên hàm băm trước khi gán id. Tôi hiểu rằng điều này không phải lúc nào cũng có thể bởi vì bạn có thể muốn sao chép trước khi gán id. Để vẫn có thể sử dụng các bộ sưu tập dựa trên hàm băm, bạn chỉ cần xây dựng lại các bộ sưu tập sau khi gán id.

Bạn có thể làm một cái gì đó như thế này:

@Entity class Parent {
  @Id @GeneratedValue Long id;
  @OneToMany(mappedBy = "parent") Set<Child> children;
  // equals/hashCode based on id
}

@Entity class Child {
  @EmbeddedId ChildId id;
  @ManyToOne Parent parent;

  @PrePersist void postPersist() {
    parent.children.remove(this);
  }
  @PostPersist void postPersist() {
    parent.children.add(this);
  }

  @Embeddable class ChildId {
    Long parentId;
    @GeneratedValue Long childId;
    // equals/hashCode based on parentId and childId
  }
  // equals/hashCode based on id
}

Tôi đã không tự mình kiểm tra cách tiếp cận chính xác, vì vậy tôi không chắc cách thay đổi các bộ sưu tập trong các sự kiện trước và sau vẫn tồn tại nhưng ý tưởng là:

  • Tạm thời xóa đối tượng khỏi bộ sưu tập dựa trên hàm băm
  • Kiên trì nó
  • Thêm lại đối tượng vào bộ sưu tập dựa trên hàm băm

Một cách khác để giải quyết điều này là chỉ cần xây dựng lại tất cả các mô hình dựa trên hàm băm của bạn sau khi cập nhật / tiếp tục.

Cuối cùng, tùy bạn. Cá nhân tôi sử dụng cách tiếp cận dựa trên trình tự hầu hết thời gian và chỉ sử dụng phương pháp UUID nếu tôi cần đưa ra một định danh cho các hệ thống bên ngoài.


0

Với phong cách mới instanceoftừ java 14, bạn có thể thực hiện equalstrong một dòng.

@Override
public boolean equals(Object obj) {
    return this == obj || id != null && obj instanceof User otherUser && id.equals(otherUser.id);
}

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

-1

Nếu UUID là câu trả lời cho nhiều người, tại sao chúng ta không sử dụng các phương thức xuất xưởng từ lớp nghiệp vụ để tạo các thực thể và gán khóa chính tại thời điểm tạo?

ví dụ:

@ManagedBean
public class MyCarFacade {
  public Car createCar(){
    Car car = new Car();
    em.persist(car);
    return car;
  }
}

bằng cách này, chúng ta sẽ có được một khóa chính mặc định cho thực thể từ nhà cung cấp kiên trì và các hàm hashCode () và equals () của chúng ta có thể dựa vào đó.

Chúng tôi cũng có thể tuyên bố các nhà xây dựng của Xe được bảo vệ và sau đó sử dụng sự phản chiếu trong phương thức kinh doanh của chúng tôi để truy cập chúng. Bằng cách này, các nhà phát triển sẽ không có ý định khởi tạo Xe bằng phương thức mới mà thông qua phương thức xuất xưởng.

Thế nào?


Một cách tiếp cận hiệu quả nếu bạn sẵn sàng thực hiện cả hai cách tạo hướng dẫn khi thực hiện tra cứu cơ sở dữ liệu.
Michael Wiles

1
Còn xe thử nghiệm thì sao? Trong trường hợp này bạn cần một kết nối cơ sở dữ liệu để thử nghiệm? Ngoài ra, các đối tượng miền của bạn không nên phụ thuộc vào sự kiên trì.
jhegedus

-1

Tôi đã cố gắng tự trả lời câu hỏi này và không bao giờ hoàn toàn hài lòng với các giải pháp được tìm thấy cho đến khi tôi đọc bài đăng này và đặc biệt là DREW. Tôi thích cách anh lười tạo UUID và tối ưu lưu trữ nó.

Nhưng tôi muốn thêm linh hoạt hơn nữa, tức là lười biếng tạo UUID CHỈ khi hashCode () / bằng () được truy cập trước khi sự tồn tại đầu tiên của thực thể với các lợi thế của mỗi giải pháp:

  • equals () có nghĩa là "đối tượng đề cập đến cùng một thực thể logic"
  • sử dụng ID cơ sở dữ liệu càng nhiều càng tốt bởi vì tại sao tôi sẽ thực hiện công việc hai lần (mối quan tâm về hiệu suất)
  • ngăn chặn sự cố trong khi truy cập hashCode () / equals () trên thực thể chưa được duy trì và giữ hành vi tương tự sau khi thực sự tồn tại

Tôi thực sự sẽ đánh giá phản hồi về giải pháp hỗn hợp của tôi dưới đây

public class MyEntity { 

    @Id()
    @Column(name = "ID", length = 20, nullable = false, unique = true)
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id = null;

    @Transient private UUID uuid = null;

    @Column(name = "UUID_MOST", nullable = true, unique = false, updatable = false)
    private Long uuidMostSignificantBits = null;
    @Column(name = "UUID_LEAST", nullable = true, unique = false, updatable = false)
    private Long uuidLeastSignificantBits = null;

    @Override
    public final int hashCode() {
        return this.getUuid().hashCode();
    }

    @Override
    public final boolean equals(Object toBeCompared) {
        if(this == toBeCompared) {
            return true;
        }
        if(toBeCompared == null) {
            return false;
        }
        if(!this.getClass().isInstance(toBeCompared)) {
            return false;
        }
        return this.getUuid().equals(((MyEntity)toBeCompared).getUuid());
    }

    public final UUID getUuid() {
        // UUID already accessed on this physical object
        if(this.uuid != null) {
            return this.uuid;
        }
        // UUID one day generated on this entity before it was persisted
        if(this.uuidMostSignificantBits != null) {
            this.uuid = new UUID(this.uuidMostSignificantBits, this.uuidLeastSignificantBits);
        // UUID never generated on this entity before it was persisted
        } else if(this.getId() != null) {
            this.uuid = new UUID(this.getId(), this.getId());
        // UUID never accessed on this not yet persisted entity
        } else {
            this.setUuid(UUID.randomUUID());
        }
        return this.uuid; 
    }

    private void setUuid(UUID uuid) {
        if(uuid == null) {
            return;
        }
        // For the one hypothetical case where generated UUID could colude with UUID build from IDs
        if(uuid.getMostSignificantBits() == uuid.getLeastSignificantBits()) {
            throw new Exception("UUID: " + this.getUuid() + " format is only for internal use");
        }
        this.uuidMostSignificantBits = uuid.getMostSignificantBits();
        this.uuidLeastSignificantBits = uuid.getLeastSignificantBits();
        this.uuid = uuid;
    }

Ý bạn là gì khi "UUID một ngày được tạo trên thực thể này trước khi tôi tồn tại"? bạn có thể vui lòng cho một ví dụ cho trường hợp này?
jhegedus

bạn có thể sử dụng Generationtype được chỉ định? Tại sao danh tính cần loại? nó có một số lợi thế hơn so với giao?
jhegedus 6/214

Điều gì xảy ra nếu bạn 1) tạo MyEntity mới, 2) đưa nó vào danh sách, 3) sau đó lưu nó vào cơ sở dữ liệu sau đó 4) bạn tải thực thể đó trở lại từ DB và 5) thử xem liệu đối tượng đã tải có trong danh sách không . Tôi đoán là nó sẽ không ngay cả khi nó phải như vậy.
jhegedus

Cảm ơn những bình luận đầu tiên của bạn đã cho tôi thấy tôi không rõ ràng như tôi nên làm. Đầu tiên, "UUID một ngày được tạo trên thực thể này trước khi tôi tồn tại" là một lỗi đánh máy ... "trước khi CNTT được duy trì" nên được đọc thay thế. Đối với các nhận xét khác, tôi sẽ sớm chỉnh sửa bài đăng của mình để cố gắng giải thích rõ hơn giải pháp của mình.
dùng2083808

-1

Trong thực tế có vẻ như, Tùy chọn 2 (Khóa chính) được sử dụng thường xuyên nhất. Khóa kinh doanh tự nhiên và NGAY LẬP TỨC là điều hiếm khi, việc tạo và hỗ trợ các khóa tổng hợp quá nặng để giải quyết các tình huống, điều có lẽ không bao giờ xảy ra. Có một cái nhìn về triển khai Spring-data-jpa AbstractPersistable (điều duy nhất: đối với việc sử dụng triển khai HibernateHibernate.getClass ).

public boolean equals(Object obj) {
    if (null == obj) {
        return false;
    }
    if (this == obj) {
        return true;
    }
    if (!getClass().equals(ClassUtils.getUserClass(obj))) {
        return false;
    }
    AbstractPersistable<?> that = (AbstractPersistable<?>) obj;
    return null == this.getId() ? false : this.getId().equals(that.getId());
}

@Override
public int hashCode() {
    int hashCode = 17;
    hashCode += null == getId() ? 0 : getId().hashCode() * 31;
    return hashCode;
}

Chỉ cần biết thao tác với các đối tượng mới trong Hashset / HashMap. Ngược lại, Tùy chọn 1 (vẫnObject thực hiện) bị hỏng ngay sau mergeđó, đó là tình huống rất phổ biến.

Nếu bạn không có khóa kinh doanh và có nhu cầu THỰC SỰ để thao túng thực thể mới trong cấu trúc băm, hãy ghi đè lên hashCodehằng số, như dưới đây Vlad Mihalcea đã được khuyên.


-2

Dưới đây là một giải pháp đơn giản (và đã được thử nghiệm) cho Scala.

  • Lưu ý rằng giải pháp này không phù hợp với bất kỳ trong 3 loại được đưa ra trong câu hỏi.

  • Tất cả các Thực thể của tôi là các lớp con của UUIDEntity vì vậy tôi tuân theo nguyên tắc không lặp lại (DRY).

  • Nếu cần, việc tạo UUID có thể được thực hiện chính xác hơn (bằng cách sử dụng nhiều số giả ngẫu nhiên hơn).

Mã Scala:

import javax.persistence._
import scala.util.Random

@Entity
@Inheritance(strategy = InheritanceType.TABLE_PER_CLASS)
abstract class UUIDEntity {
  @Id  @GeneratedValue(strategy = GenerationType.TABLE)
  var id:java.lang.Long=null
  var uuid:java.lang.Long=Random.nextLong()
  override def equals(o:Any):Boolean= 
    o match{
      case o : UUIDEntity => o.uuid==uuid
      case _ => false
    }
  override def hashCode() = uuid.hashCode()
}
Khi sử dụng trang web của chúng tôi, bạn xác nhận rằng bạn đã đọc và hiểu Chính sách cookieChính sách bảo mật của chúng tôi.
Licensed under cc by-sa 3.0 with attribution required.