Дилема хеш-коду () / дорівнює () JPA


311

Тут відбулися певні дискусії щодо об'єктів JPA та які hashCode()/ equals()впровадження слід використовувати для класів об'єктів JPA. Більшість із них (якщо не всі) залежать від сплячого режиму, але я хотів би обговорити їх реалізацію JPA нейтрально (я, до речі, використовую EclipseLink).

Усі можливі реалізації мають свої переваги та недоліки щодо:

  • hashCode()/ equals()відповідність договору (незмінність) для List/ Setоперацій
  • Чи можна виявити однакові об'єкти (наприклад, з різних сеансів, динамічні проксі-сервери з ліниво завантажених структур даних)
  • Чи правильно вони поводяться у відстороненому (або непостійному) стані

Наскільки я бачу, є три варіанти :

  1. Не перекривляйте їх; покладатися на Object.equals()іObject.hashCode()
    • hashCode()/ equals()робота
    • не вдається ідентифікувати однакові об'єкти, проблеми з динамічними проксі
    • немає проблем з відокремленими сутностями
  2. Замініть їх на основі первинного ключа
    • hashCode()/ equals()ламаються
    • правильна особистість (для всіх керованих організацій)
    • проблеми з відокремленими сутностями
  3. Замініть їх на основі Business-Id (поля не первинного ключа; що з іноземними ключами?)
    • hashCode()/ equals()ламаються
    • правильна особистість (для всіх керованих організацій)
    • немає проблем з відокремленими сутностями

Мої запитання:

  1. Я пропустив варіант та / або про / con пункт?
  2. Який варіант ви вибрали і чому?



ОНОВЛЕННЯ 1:

До « hashCode()/ equals()зламані», я маю в виду , що послідовні hashCode()виклики може повертати різні значення, що (при правильній реалізації) не порушувати в сенсі Objectдокументації API, але викликає проблеми при спробі отримати змінені суті з Map, Setабо інших хеш на основі Collection. Отже, реалізація JPA (принаймні, EclipseLink) не працюватиме належним чином у деяких випадках.

ОНОВЛЕННЯ 2:

Дякую за відповіді - більшість із них мають неабияку якість.
На жаль, я все ще не впевнений, який підхід буде найкращим для застосування в реальному житті або як визначити найкращий підхід для моєї програми. Отже, я буду тримати питання відкритим і сподіваюся на ще кілька дискусій та / або думок.


4
Я не розумію, що ви маєте на увазі під "hashCode () / equals () зламано"
nanda

4
У цьому сенсі вони не будуть "зламані", як і у варіантах 2 та 3, ви реалізовуватимете і рівняння (), і хеш-код (), використовуючи одну і ту ж стратегію.
мат б

11
Це не вірно з варіантом 3. hashCode () та equals () повинні використовувати однакові критерії, тому, якщо зміниться одне з ваших полів, так, метод hashcode () поверне інше значення для того ж екземпляра, ніж попереднє, але так буде дорівнює (). Ви залишили другу частину речення з хеш-коду () javadoc: кожного разу, коли він викликається одним і тим же об'єктом не раз під час виконання програми Java, метод hashCode повинен послідовно повертати те саме ціле число, за умови, що немає інформації використовується в рівних порівняннях на об'єкті модифіковано .
мат б

1
Насправді ця частина речення означає протилежне - виклик hashcode()одного і того ж об’єкта об'єкта повинен повернути те саме значення, якщо не equals()змінюються поля, використані в реалізації. Іншими словами, якщо у вашому класі три поля, а ваш equals()метод використовує лише два з них для визначення рівності екземплярів, ви можете розраховувати, що hashcode()повернене значення зміниться, якщо ви зміните одне з цих значень поля - що має сенс, якщо врахувати що цей примірник об'єкта вже не "дорівнює" значенню, яке представляв старий екземпляр.
мат б

2
"проблеми під час спроби отримати змінену сутність з колекції на карті, наборі чи інших хеш-колекціях" ... це повинні бути "проблеми при спробі отримати змінену сутність з колекцій на основі HashMap, HashSet або інших хеш-колекцій"
nanda

Відповіді:


122

Прочитайте цю приємну статтю на цю тему: Не дозволяйте сплячому вкрасти вашу особу .

Висновок статті виглядає так:

Ідентифікацію об'єкта обманливо важко правильно реалізувати, коли об’єкти зберігаються в базі даних. Однак проблеми випливають повністю з дозволу об'єктів існувати без ідентифікатора до їх збереження. Ми можемо вирішити ці проблеми, знявши відповідальність за присвоєння ідентифікаторів об'єктів від рамки об'єктно-реляційного відображення, таких як Hibernate. Натомість ідентифікатори об'єкта можуть бути призначені, як тільки об'єкт буде створений. Це робить ідентифікацію об'єкта простою та без помилок та зменшує кількість коду, необхідного в доменній моделі.


21
Ні, це не приємна стаття. Це чудова чудова стаття на цю тему, і її слід прочитати кожному програмісту JPA! +1!
Том Андерсон

2
Так, я використовую те саме рішення. Не дозволяючи БД генерувати ідентифікатор має й інші переваги, такі як можливість створювати об’єкт і вже створювати інші об'єкти, на які посилається, перш ніж зберігати його. Це може видалити затримку та кілька циклів запитів / відповідей у ​​додатках клієнт-сервер. Якщо вам потрібне натхнення для такого рішення, перегляньте мої проекти: suid.js та suid-server-java . В основному suid.jsотримує блоки ідентифікаторів, з suid-server-javaяких потім можна отримати та використовувати клієнтську сторону.
Штійн де Вітт

2
Це просто божевільно. Я новачок у сплячому режимі роботи під капотом, писав тестові одиниці, і з'ясував, що я не можу видалити об'єкт із набору після його зміни, зробив висновок, що це через зміну хеш-коду, але не зміг зрозуміти, як вирішувати. Стаття проста чудова!
XMight

Це чудова стаття. Однак людям, які бачать посилання вперше, я б припустив, що це може бути надлишком для більшості програм. Інші 3 варіанти, перелічені на цій сторінці, повинні більш-менш вирішити проблему кількома способами.
HopeKing

1
Чи використовує Hibernate / JPA метод рівняння та хеш-коду суб'єкта, щоб перевірити, чи запис уже існує в базі даних?
Tushar Banne

64

Я завжди переосмислюю рівний / хеш-код і реалізую його на основі бізнес-ідентифікатора. Здається, для мене найбільш розумне рішення. Дивіться наступне посилання .

Підсумовуючи всі ці дані, ось перелік того, що буде працювати чи не працюватиме з різними способами обробляти рівні / hashCode: введіть тут опис зображення

Редагувати :

Щоб пояснити, чому це працює для мене:

  1. Я зазвичай не використовую колекцію на основі хеш-пам'яті (HashMap / HashSet) у своїй програмі JPA. Якщо треба, я вважаю за краще створити рішення UniqueList.
  2. Я думаю, що зміна ділового ідентифікатора під час виконання не є найкращою практикою для будь-якої програми баз даних. У рідкісних випадках, коли іншого рішення немає, я б провів спеціальну обробку, як-от видалити елемент і повернути його до колекції на основі хедів.
  3. Для своєї моделі я встановлюю бізнес-ідентифікатор на конструкторі та не надаю для нього налаштування. Я дозволяю впровадженню JPA змінити поле замість властивості.
  4. Рішення UUID здається непосильним. Чому UUID, якщо у вас є природний ідентифікатор бізнесу? Зрештою, я б встановив унікальність ділового ідентифікатора в базі даних. Чому тоді в БД є три індекси для кожної таблиці?

1
Але в цій таблиці не вистачає п'ятого рядка "працює зі списком / наборами" (якщо ви думаєте видалити об'єкт, який є частиною набору з відображення OneToMany), на який би відповіли "ні" у двох останніх варіантах, оскільки його хеш-код ( ) зміни, що порушують його договір.
MRalwasser

Дивіться коментар до питання. Ви, здається, неправильно розумієте договір рівності / хеш-
коду

1
@MRalwasser: Я думаю, ви маєте на увазі правильну річ, це просто не договір equals / hashCode (), який порушується. Але змінний дорівнює / hashCode створює проблеми із встановленим контрактом.
Кріс Лерчер

3
@MRalwasser: хеш-код може змінюватися лише в тому випадку, якщо ідентифікатор компанії зміниться, і справа в тому, що ідентифікатор компанії не змінюється. Таким чином, хеш-код не змінюється, і це прекрасно працює з хеш-колекціями.
Том Андерсон

1
Що робити, якщо у вас немає природного бізнес-ключа? Наприклад, у випадку двовимірної точки, точка (X, Y), у програмі для малювання графіків? Як би ви зберігали цю точку як Сутність?
jhegedus

35

Зазвичай у нас є два посвідчення особи:

  1. Призначений лише для шару стійкості (щоб постачальник стійкості та база даних могли з'ясувати взаємозв'язки між об'єктами).
  2. Для наших потреб ( equals()і hashCode()зокрема)

Поглянь:

@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: щоб уточнити мою думку щодо викликів setUuid()методу. Ось типовий сценарій:

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

Коли я запускаю свої тести і бачу вихід журналу, я вирішую проблему:

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

Крім того, можна надати окремий конструктор:

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

Тож мій приклад виглядав би так:

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

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

Я використовую конструктор за замовчуванням і сеттер, але ви можете виявити, що підхід для двох конструкторів є більш підходящим для вас.


2
Я вважаю, що це правильне і хороше рішення. Це також може мати невелику перевагу в продуктивності, оскільки цілі числа, як правило, краще в індексах баз даних, ніж ууїди. Але крім цього, ви могли, ймовірно, усунути поточну властивість цілого id та замінити його на (додаток, призначений) uuid?
Кріс Лерчер

4
Чим це відрізняється від використання стандартних hashCode/ equalsметодів рівності JVM та idпостійної рівності? Це для мене зовсім не має сенсу.
Бехранг Саедзаде

2
Він працює в тих випадках, коли у вас є кілька об'єктів об'єкта, що вказують на один рядок у базі даних. Object«S equals()повернеться falseв цьому випадку. equals()Повернення на основі UUID true.
Андрій Андрей Листочкин

4
-1 - я не бачу жодної причини мати два посвідчення особи, а значить, два види посвідчення особи. Це здається мені абсолютно безглуздим і потенційно шкідливим.
Том Андерсон

1
Вибачте за те, що ви критикували ваше рішення, не вказуючи на те, що я вважаю за краще. Коротше кажучи, я дав би об'єктам одне поле ідентифікатора, я би реалізував рівний і хеш-код на його основі, і я би генерував його значення при створенні об'єкта, а не при збереженні в базі даних. Таким чином, усі форми об'єкта працюють однаково: непостійні, стійкі та відсторонені. Hibernate проксі (або подібні) також повинні працювати коректно, і я думаю, навіть не потрібно було би зволожуватися для обробки рівних і хеш-кодів дзвінків.
Том Андерсон

31

Я особисто вже використовував усі ці три державництва в різних проектах. І мушу сказати, що варіант 1, на мою думку, є найбільш практичним у реальному житті. На мій досвід, відповідність hashCode () / equals () призводить до появи багатьох шалених помилок, оскільки ви щоразу опинятиметесь у ситуаціях, коли результат рівності змінюється після додавання сукупності до колекції.

Але є й інші варіанти (також з їх плюсами та мінусами):


а) hashCode / дорівнює на основі набору незмінних , ненулевих , призначених конструкторів полів

(+) всі три критерії гарантовані

(-) значення поля повинні бути доступними для створення нового примірника

(-) ускладнює обробку, якщо ви повинні змінити один із них


б) hashCode / дорівнює на основі первинного ключа, який призначається додатком (у конструкторі) замість JPA

(+) всі три критерії гарантовані

(-) ви не можете скористатися простими надійними стаціонарами генерації ідентифікаторів, як послідовності БД

(-) складно, якщо нові об'єкти створені в розподіленому середовищі (клієнт / сервер) або кластерному сервері додатків


в) хеш-код / ​​дорівнює на основі UUID, призначений конструктором сутності

(+) всі три критерії гарантовані

(-) накладні витрати на покоління UUID

(-) може бути невеликий ризик, що два рази використовується той самий UUID, залежно від використовуваного алгоритму (може бути виявлено за унікальним індексом у БД)


Я прихильник Варіант 1 і підхід C також. Не робіть нічого, поки вам абсолютно не потрібно, це більш спритний підхід.
Адам Гент

2
+1 для варіанту (b). IMHO, якщо суб'єкт господарювання має природний ідентифікатор бізнесу, то це також повинен бути основним ключем його бази даних. Це простий, простий, хороший дизайн бази даних. Якщо у нього немає такого посвідчення особи, тоді потрібен сурогатний ключ. Якщо ви встановите це при створенні об'єкта, то все інше просто. Коли люди не користуються природним ключем і не створюють сурогатного ключа рано, вони потрапляють у біду. Щодо складності в реалізації - так, є деякі. Але насправді не багато, і це можна зробити дуже загальним способом, який вирішує це один раз для всіх сутностей.
Том Андерсон

Я також віддаю перевагу варіант 1, але тоді, як написати одиничний тест, щоб стверджувати повну рівність, є великою проблемою, оскільки ми повинні реалізувати метод рівних для Collection.
OOD Waterball

Просто не робіть цього. Дивіться, щоб не
возитися зі сплячкою

29

Якщо ви хочете використовувати equals()/hashCode()для своїх наборів, в тому сенсі, що одна і та ж сутність може бути там тільки один раз, тоді є лише один варіант: Варіант 2. Це тому, що первинний ключ для сутності за визначенням ніколи не змінюється (якщо хтось дійсно оновлює це, це вже не та сама сутність)

Ви повинні сприймати це буквально: З вашого equals()/hashCode() базується на первинному ключі, ви не повинні використовувати ці методи, поки не буде встановлений первинний ключ. Таким чином, ви не повинні ставити об'єкти в набір, поки їм не буде призначений первинний ключ. (Так, UUID та подібні поняття можуть допомогти завчасно призначити первинні ключі.)

Тепер теоретично також можна досягти цього з Варіантом 3, навіть якщо так звані "бізнес-ключі" мають неприємний недолік, який вони можуть змінити: "Все, що вам потрібно зробити, - це видалити вже вставлені об'єкти з набору ( s) та знову вставити їх. " Це правда - але це також означає, що в розподіленій системі вам доведеться переконатися, що це робиться абсолютно скрізь, де дані були вставлені (і вам доведеться переконатися, що оновлення виконується , перш ніж трапляться інші речі). Вам знадобиться складний механізм оновлення, особливо якщо деякі віддалені системи зараз недоступні ...

Варіант 1 може бути використаний лише в тому випадку, якщо всі об'єкти у ваших наборах з одного і того ж сеансу сплячки. Документація зі сплячого режиму чітко пояснює це у розділі 13.1.3. З урахуванням ідентичності об'єкта :

У межах сесії програма може безпечно використовувати == для порівняння об'єктів.

Однак програма, яка використовує == поза сеансом, може дати неочікувані результати. Це може статися навіть у деяких несподіваних місцях. Наприклад, якщо ви помістите два відокремлених екземпляра в один набір, обидва можуть мати однакову ідентичність бази даних (тобто вони представляють один і той же рядок). Однак ідентифікація JVM за визначенням не гарантується для випадків у відокремленому стані. Розробник повинен перекрити методи equals () та hashCode () у стійких класах та реалізувати власне поняття рівності об'єктів.

Він продовжує стверджувати на користь Варіанту 3:

Є одне застереження: ніколи не використовуйте ідентифікатор бази даних для реалізації рівності. Використовуйте бізнес-ключ, який є поєднанням унікальних, як правило, незмінних атрибутів. Ідентифікатор бази даних зміниться, якщо перехідний об'єкт буде стійким. Якщо перехідний екземпляр (як правило, разом з відокремленими екземплярами) утримується в наборі, зміна хеш-коду розриває контракт набору.

Це правда, якщо ти

  • не може призначити ідентифікатор рано (наприклад, за допомогою UUID)
  • і все ж ви абсолютно хочете поставити свої об'єкти в набори, поки вони перебувають у перехідному стані.

В іншому випадку ви можете вибрати варіант 2.

Тоді йдеться про необхідність відносної стабільності:

Атрибути для бізнес-ключів не повинні бути такими ж стабільними, як первинні ключі бази даних; вам потрібно гарантувати стабільність лише доки об'єкти знаходяться в одному наборі.

Це вірно. Практична проблема, яку я бачу в цьому, полягає в тому, що якщо ви не можете гарантувати абсолютну стабільність, яким чином ви зможете гарантувати стабільність «доки об'єкти знаходяться в одному наборі». Я можу уявити деякі особливі випадки (як, наприклад, використання наборів лише для розмови, а потім їх викидання), але я б поставив під сумнів загальну практичність цього.


Коротка версія:

  • Варіант 1 може використовуватися лише з об'єктами протягом одного сеансу.
  • Якщо ви можете, скористайтеся Варіантом 2. (Призначте ПК як можна раніше, оскільки ви не можете використовувати об'єкти в наборах, поки ПК не буде призначений.)
  • Якщо ви можете гарантувати відносну стабільність, можете скористатися Варіантом 3. Але будьте обережні з цим.

Ваше припущення, що первинний ключ ніколи не змінюється, помилкове. Наприклад, Hibernate виділяє первинний ключ лише тоді, коли сеанс зберігається. Отже, якщо ви використовуєте первинний ключ у якості свого хеш-коду, результат hashCode () перед першим збереженням об'єкта і після першого збереження об'єкта буде іншим. Гірше, що перед тим, як зберегти сеанс, два новостворені об’єкти матимуть однаковий хеш-код і можуть перезаписати один одного, додавши їх у колекції. Можливо, вам доведеться негайно змусити зберегти / вмикати при створенні об'єкта, щоб використовувати такий підхід.
Вільям Біллінгслі,

2
@William: Первинний ключ організації не змінюється. Властивість id відображеного об'єкта може змінюватися. Це відбувається, як ви пояснили, особливо, коли перехідний об'єкт стає стійким . Будь ласка, уважно прочитайте частину моєї відповіді, де я говорив про методи equals / hashCode: "Ви не повинні використовувати ці методи, поки не буде встановлено первинний ключ".
Кріс Лерчер

Повністю згоден. Із варіантом 2 ви також можете визначити рівний / хеш-код у суперкласі та повторно використовувати його для всіх ваших організацій.
Тео

+1 Я новачок у JPA, але деякі коментарі та відповіді тут означають, що люди не розуміють значення терміна "первинний ключ".
Raedwald

16
  1. Якщо у вас є бізнес-ключ , тоді ви повинні використовувати його для equals/ hashCode.
  2. Якщо у вас немає ділового ключа, ви не повинні залишати його з типовими Objectрівнями та реалізаціями хеш-коду, оскільки це не працює після вас mergeта організації.
  3. Ви можете використовувати ідентифікатор особи, як запропоновано в цій публікації . Єдина уловка полягає в тому, що вам потрібно використовувати hashCodeреалізацію, яка завжди повертає те саме значення, як це:

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

Що краще: (1) onjava.com/pub/a/onjava/2006/09/13/… або (2) vladmihalcea.com/… ? Рішення (2) простіше, ніж (1). То чому я повинен використовувати (1). Чи наслідки обох однакові? Чи гарантують обидва рішення однакове рішення?
nimo23

І з вашим рішенням: "значення хеш-коду не змінюється" між тими самими екземплярами. Це така ж поведінка, як якщо б це був "той самий" uuid (з розчину (1)), який порівнюється. Маю рацію?
nimo23

1
І зберігати UUID в базі даних та збільшувати слід запису та в буферному пулі? Я думаю, що це може призвести до більш високих проблем продуктивності в довгостроковій перспективі, ніж унікальний хеш-код. Що стосується іншого рішення, ви можете перевірити його, щоб побачити, чи забезпечує він узгодженість для всіх переходів стану сутності. Ви можете знайти тест, який перевіряє це на GitHub .
Влад Михальча

1
Якщо у вас є незмінний бізнес-ключ, хеш-код може використовувати його, і він отримає користь з декількох відер, тому його варто використовувати, якщо у вас є. В іншому випадку просто використовуйте ідентифікатор сутності, як це пояснено в моїй статті.
Влад Михальча

1
Я радий, що тобі сподобалось. У мене є ще сотні статей про JPA та сплячку.
Влад Міхалча

10

Хоча використання бізнес-ключа (варіант 3) є найбільш рекомендованим підходом ( вікі спільної роботи в сплячому режимі , "Наполегливість Java зі сплячим режимом" стор. 398), і саме це ми в основному використовуємо, є помилка в сплячому режимі, яка порушує це для нетерплячих набори: HHH-3799 . У цьому випадку Hibernate може додати об'єкт до набору до ініціалізації його полів. Я не впевнений, чому ця помилка не приділила більше уваги, оскільки вона справді робить проблематичний рекомендований бізнес-підхід проблематичним.

Я думаю, що суть справи полягає в тому, що рівний і хеш-код повинен базуватися на незмінному стані (посилання Odersky et al. ), А сплячий суб'єкт з первинним ключем, який управляється зімом, не має такого незмінного стану. Первинний ключ змінюється Hibernate, коли перехідний об'єкт стає стійким. Діловий ключ також модифікується Hibernate, коли він зволожує об'єкт у процесі ініціалізації.

Це залишає лише варіант 1, успадковуючи реалізацію java.lang.Object, засновану на ідентичності об'єкта, або використовуючи керований додатком первинний ключ, як запропонував Джеймс Брундже в "Не дозволяйте зимувати вкрасти вашу особу" (на яку вже посилається відповідь Стийна Геккенса ) та Ланс Арлай у "Генерації об'єктів: кращий підхід до сплячої інтеграції" .

Найбільша проблема з варіантом 1 полягає в тому, що відокремлені екземпляри не можна порівнювати зі стійкими екземплярами з використанням .equals (). Але це нормально; договір рівних і хеш-кодів залишає за розробником вирішити, що означає рівність для кожного класу. Тож нехай рівне і hashCode успадковують від Object. Якщо вам потрібно порівняти стоїть окремо екземпляр до стійкого , наприклад, ви можете створити новий метод явно для цієї мети, може бути , boolean sameEntityабо , boolean dbEquivalentабо boolean businessEquals.


5

Я згоден з відповіддю Андрія. Ми робимо те саме в нашій програмі, але замість того, щоб зберігати UUID, як VARCHAR / CHAR, ми розділяємо їх на два довгі значення. Див. UUID.getLeastSignificantBits () та UUID.getMostSignificantBits ().

Ще одне, що слід врахувати, - це те, що виклики до UUID.randomUUID () є досить повільними, тому ви, можливо, захочете вивчити ліниво, що генерує UUID лише в разі необхідності, наприклад, під час постійності або викликів дорівнює () / 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();
    }

}

Ну, насправді, якщо ви переосмислите значення рівняння () / hashCode (), вам все одно доведеться генерувати UUID для кожної сутності (я припускаю, що ви хочете зберегти кожну створену вами сутність у своєму коді). Ви робите це лише один раз - перш ніж зберігати його в базі даних. Після цього UUID просто завантажується постачальником послуг. Таким чином, я не бачу сенсу робити це ліниво.
Андрій Андрей Листочкин

Я відповідав за вашу відповідь, тому що мені дуже подобаються ваші інші ідеї: зберігання UUID як пари чисел у базі даних, а не введення конкретного типу всередині методу equals () - це дійсно охайно! Я точно буду використовувати ці два хитрощі в майбутньому.
Андрій Андрей Листочкин

1
Дякуємо за підсумкове голосування. Причина для ледачої ініціалізації UUID була в тому, що ми додаємо, що ми створюємо багато організацій, які ніколи не ставлять у HashMap і не зберігаються. Таким чином, ми бачили 100-кратне зниження продуктивності, коли ми створювали об'єкт (100 000). Тож ми закликаємо UUID лише у разі потреби. Я просто хочу, щоб у MySql була хороша підтримка для 128-бітових номерів, щоб ми могли просто використовувати UUID для id і не піклуватися про auto_increment.
Дрю

О Я бачу. У моєму випадку ми навіть не оголошуємо поле UUID, якщо відповідне об'єкт не збирається містити у колекції. Недолік полягає в тому, що іноді нам доводиться додавати його, оскільки згодом виявляється, що насправді нам потрібно їх розмістити в колекціях. Це трапляється іноді під час розробки, але, на щастя, ніколи не траплялося з нами після первинного розгортання до клієнта, тому це не було великою справою. Якщо це станеться після того, як система запустила жити, нам знадобиться міграція db. Ледачий UUID дуже допомагає в таких ситуаціях.
Андрій Андрей Листочкин

Можливо, вам слід також спробувати більш швидкий генератор UUID, запропонований Адамом у своїй відповіді, якщо продуктивність є критичною проблемою у вашій ситуації.
Андрій Андрей Листочкин

3

Як вже вказували інші люди, розумніші, ніж я, існує безліч стратегій. Схоже, так і є, що більшість застосованих дизайнерських моделей намагаються зламати шлях до успіху. Вони обмежують доступ конструктора, якщо не перешкоджають виклику конструктора повністю спеціалізованими конструкторами та заводськими методами. Дійсно, це завжди приємно з чітко розрізаним API. Але якщо єдиною причиною є зробити переопрацювання рівнів та хеш-кодів сумісними із програмою, то мені цікаво, чи відповідають ці стратегії KISS (Keep It Simple Stupid).

Для мене я люблю переосмислювати рівняння та хеш-код шляхом вивчення ідентифікатора. У цих методах я вимагаю, щоб ідентифікатор не був нульовим і добре документував цю поведінку. Таким чином, договір розробників стане на зберігання нового об'єкта, перш ніж зберігати його десь в іншому місці. Програма, яка не виконує цей контракт, буде невдалою протягом хвилини (сподіваємось).

Хоча слово обережності: Якщо ваші сутності зберігаються в різних таблицях, а ваш постачальник використовує стратегію автоматичного генерації для основного ключа, ви отримаєте повторювані первинні ключі для типів сутності. У такому випадку також порівняйте типи часу виконання з викликом до Object # getClass (), що, звичайно, унеможливить, що два різних типи вважаються рівними. Це мені підходить здебільшого чудово.


Навіть із відсутністю послідовностей у БД (наприклад, Mysql) можливо їх імітувати (наприклад, таблиця hibernate_sequence). Таким чином, ви завжди можете отримати унікальний ідентифікатор у таблицях. +++ Але це вам не потрібно. Дзвонити Object#getClass() погано через Х. проксі. Покликання Hibernate.getClass(o)допомагає, але проблема рівності сутностей різних видів залишається. Існує рішення, що використовує canEqual , трохи складне, але корисне. Домовились, що зазвичай це не потрібно. +++ Кидання eq / hc на null ID порушує договір, але це дуже прагматично.
maaartinus

2

Очевидно, тут вже є дуже інформативні відповіді, але я розповім, що ми робимо.

Ми нічого не робимо (тобто не перекриваємо).

Якщо нам потрібні рівні / хеш-коди для роботи з колекціями, ми використовуємо UUID. Ви просто створите UUID в конструкторі. Ми використовуємо http://wiki.fasterxml.com/JugHome для UUID. UUID - трохи дорожчий процесор, але він дешевий порівняно з серіалізацією та доступом до db.


1

У минулому я завжди використовував варіант 1, тому що знав про ці дискусії і вважав, що краще нічого не робити, поки не знаю, як правильно робити. Усі ці системи досі успішно працюють.

Однак наступного разу я можу спробувати варіант 2 - використовуючи базу даних, створену Id.

Хеш-код і дорівнює буде кидати IllegalStateException, якщо ідентифікатор не встановлений.

Це запобіжить появі несподіваних тонких помилок, пов’язаних із збереженими об'єктами.

Що люди думають про такий підхід?


1

Підхід до ділових ключів для нас не підходить. Для вирішення дилеми ми використовуємо генерований БД ідентифікатор , тимчасовий перехідний tempId та переопределення рівного () / хеш-коду (). Усі сутності є нащадками Сутності. Плюси:

  1. Без додаткових полів у БД
  2. Ніякого додаткового кодування у нащадків, один підхід для всіх
  3. Немає проблем з продуктивністю (як, наприклад, UUID), генерація ідентифікатора БД
  4. Немає проблем з Hashmaps (не потрібно пам’ятати про використання рівних і т.д.)
  5. Хеш-код нової сутності не змінюється в часі навіть після збереження

Мінуси:

  1. Можуть виникнути проблеми із серіалізацією та десеріалізацією неіснуючих суб'єктів
  2. Хеш-код збереженої сутності може змінитися після перезавантаження з БД
  3. Незбережені об'єкти вважаються завжди різними (можливо, це правильно?)
  4. Що ще?

Подивіться наш код:

@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

Будь ласка, врахуйте наступний підхід на основі попередньо визначеного ідентифікатора типу та ідентифікатора.

Конкретні припущення для JPA:

  • Суб'єкти одного "типу" та одного і того ж ненульового ідентифікатора вважаються рівними
  • непостійні суб'єкти (якщо немає ідентифікатора) ніколи не дорівнюють іншим об'єктам

Абстрактна сутність:

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

Приклад конкретної сутності:

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

Приклад тесту:

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

Основні переваги тут:

  • простота
  • забезпечує підкласи надання ідентичності типу
  • передбачувана поведінка з проксі-класами

Недоліки:

  • Потрібен кожен суб'єкт для виклику super()

Примітки:

  • Потребує уваги при використанні спадщини. Наприклад, рівність екземпляра class Aта class B extends Aможе залежати від конкретних деталей програми.
  • В ідеалі використовуйте бізнес-ключ як ідентифікатор

Чекаємо ваших коментарів.


0

Це поширена проблема у кожній ІТ-системі, яка використовує Java та JPA. Точка болю виходить за рамки впровадження рівних () та хеш-коду (), вона впливає на те, як організація посилається на сутність та як її клієнти посилаються на ту саму сутність. Я бачив достатньо болю, що не маю ділового ключа до того, що я написав власний блог, щоб висловити свою точку зору.

Коротше кажучи: використовуйте короткий, читабельний для людини послідовний ідентифікатор із значущими префіксами як бізнес-ключ, який генерується без залежності від будь-якого сховища, окрім ОЗУ. Сніжинка Twitter - дуже хороший приклад.


0

IMO у вас є 3 варіанти реалізації equals / hashCode

  • Використовуйте ідентифікацію, створену додатком, тобто UUID
  • Реалізуйте його на основі ділового ключа
  • Реалізуйте його на основі первинного ключа

Використання ідентичності, створеної додатком, є найпростішим підходом, але має кілька недоліків

  • Приєднується повільніше при використанні його як ПК, оскільки 128 біт просто більше 32 або 64 біт
  • "Налагодження важче", тому що перевірити власними очима, якщо деякі дані є правильними, досить складно

Якщо ви можете працювати з цими недоліками , просто використовуйте такий підхід.

Для подолання проблеми приєднання можна використовувати UUID як природний ключ, а значення послідовності в якості основного ключа, але тоді ви все ще можете зіткнутися з проблемами рівняння / hashCode реалізації в складових дочірніх об'єктів, які мають вбудовані ідентифікатори, оскільки ви хочете приєднатись на основі на первинному ключі. Використання природного ключа в дочірніх сутностях id та первинного ключа для посилання на батьків є хорошим компромісом.

@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 - це найчистіший підхід, оскільки він дозволить уникнути всіх недоліків і в той же час забезпечить вам значення (UUID), яким ви можете ділитися із зовнішніми системами, не піддаючи внутрішніх систем.

Реалізуйте його на основі ділового ключа, якщо ви можете очікувати, що від користувача це приємна ідея, але також має кілька недоліків

Більшу частину часу цей діловий ключ буде певним кодом, який надає користувач, і рідше - складом з декількох атрибутів.

  • Приєднання відбувається повільніше, оскільки з'єднання на основі тексту змінної довжини просто повільне. Деякі СУБД можуть навіть мати проблеми зі створенням індексу, якщо ключ перевищує певну довжину.
  • На мій досвід, бізнес-ключі, як правило, змінюються, що вимагатиме каскадного оновлення об'єктів, що посилаються на нього. Це неможливо, якщо на нього звертаються зовнішні системи

ІМО, ви не повинні застосовувати або працювати виключно з бізнес-ключем. Це приємне доповнення, тобто користувачі можуть швидко шукати за цим бізнес-ключем, але система не повинна покладатися на нього для роботи.

Втілити його на основі первинного ключа є проблеми, але, можливо, це не така вже й велика справа

Якщо вам потрібно піддати ідентифікатори зовнішній системі, використовуйте запропонований мною підхід UUID. Якщо цього не зробити, ви все одно можете використовувати підхід UUID, але цього не потрібно. Проблема використання ідентифікованого СУБД в рівних / хеш-кодах випливає з того, що об'єкт, можливо, був доданий до колекцій на основі хеш-пам'яті перед призначенням ідентифікатора.

Очевидний спосіб обійти це - просто не додавати об’єкт до колекцій на основі хешів перед призначенням ідентифікатора. Я розумію, що це не завжди можливо, тому що ви, можливо, захочете дедупликації, перш ніж призначати ідентифікатор. Щоб мати можливість використовувати колекції на основі хешу, вам просто потрібно відновити колекції після призначення ідентифікатора.

Ви можете зробити щось подібне:

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

Я ще не перевіряв точний підхід, тому не знаю, як працює зміна колекцій у до- та постперсистентних подіях, але ідея:

  • Тимчасово видаліть об’єкт із колекцій на основі хешу
  • Наполегливо
  • Повторно додайте об’єкт до колекцій на основі хешу

Ще один спосіб вирішити це - просто відновити всі ваші моделі на основі хешу після оновлення / збереження.

Зрештою, це залежить від вас. Я особисто використовую підхід, заснований на послідовності, більшу частину часу і використовую підхід UUID лише в тому випадку, якщо мені потрібно виставити ідентифікатор зовнішнім системам.


0

З новим стилем instanceofвід java 14 ви можете реалізовувати equalsлінію в один рядок.

@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

Якщо UUID - це відповідь для багатьох людей, чому б ми не просто використали фабричні методи з бізнес-рівня для створення об'єктів та призначення первинного ключа під час створення?

наприклад:

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

таким чином, ми отримаємо основний ключ за замовчуванням для сутності від постачальника стійкості, і наші функції hashCode () і equals () можуть покластися на це.

Ми також могли оголосити конструкторів автомобілів захищеними, а потім використати роздуми в нашому бізнес-методі для доступу до них. Таким чином, розробники не мали б наміру створити Автомобіль новим, а фабричним методом.

Як це?


Підхід, який чудово працює, якщо ви готові скористатись ефективністю, як створити орієнтир під час пошуку бази даних.
Майкл Вілз

1
А як щодо тестування автомобілів? У цьому випадку для тестування вам потрібне підключення до бази даних? Також об’єкти вашого домену не повинні залежати від стійкості.
jhegedus

-1

Я сам спробував відповісти на це питання і ніколи не був задоволений знайденими рішеннями, поки не прочитав цей пост, а особливо ДРУГИЙ. Мені сподобалося те, як він ледачий створив UUID і оптимально його зберігав.

Але я хотів додати ще більшу гнучкість, тобто ледачий створення UUID ТОЛЬКО, коли доступ до hashCode () / equals () доступний до першої стійкості сутності з перевагами кожного рішення:

  • equals () означає "об'єкт відноситься до тієї ж логічної сутності"
  • якомога більше використовувати ідентифікатор бази даних, тому що чому я б робив роботу двічі (стурбованість роботою)
  • запобігти проблемі під час доступу до hashCode () / equals () на ще не збереженому об'єкті та зберегти таку ж поведінку після того, як вона дійсно зберігається

Я б дуже вдячний за відгуки про моє змішане рішення нижче

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

що ви маєте на увазі під "UUID за один день, створений для цього об'єкта до того, як мене зберегли"? ви можете, будь ласка, навести приклад для цього випадку?
jhegedus

Ви можете використовувати призначений тип покоління? чому потрібен тип генерування ідентичності? чи є якась перевага перед призначеною?
jhegedus

що трапиться, якщо ви 1) зробите новий MyEntity, 2) помістите його в список, 3) потім збережіть його в базі даних, потім 4) ви завантажите цю сутність з DB і 5) спробуйте дізнатися, чи є завантажений екземпляр у списку . Я здогадуюсь, що це не буде, хоча воно і має бути.
jhegedus

Дякую за ваші перші коментарі, які показали мені, що я не був таким чітким, як повинен. По-перше, "UUID за один день, сформований на цьому об'єкті до того, як я зберігався," був помилковим помилкою ... ", перш ніж ІТ зберігалася", слід було прочитати замість цього. Для інших зауважень я скоро відредагую свій пост, щоб спробувати краще пояснити своє рішення.
користувач2083808

-1

На практиці здається, що варіант 2 (первинний ключ) найчастіше використовується. Природний та непорушний бізнес-ключ - це рідкість, а створення та підтримка синтетичних ключів надто важкі для вирішення ситуацій, які, ймовірно, ніколи не бували. Погляньте на реалізацію spring-data-jpa AbstractPersistable (єдине: для використання в режимі глибокого снуHibernate.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;
}

Тільки в курсі маніпулювання новими об'єктами в HashSet / HashMap. Навпаки, варіант 1 (залишається Objectреалізацією) порушений одразу після mergeцього, це дуже поширена ситуація.

Якщо у вас немає ділового ключа і у вас є РЕАЛЬНІ потреби маніпулювати новим об'єктом у хеш-структурі, замініть hashCodeна постійне, як показано нижче Владу Міхалчею.


-2

Нижче представлено просте (і перевірене) рішення для Scala.

  • Зауважте, що це рішення не входить до жодної з 3 категорій, наведених у питанні.

  • Усі мої особи є підкласами UUIDEntity, тому я слідую принципу "не повторювати себе" (DRY).

  • При необхідності генерація UUID може бути зроблена більш точна (використовуючи більше псевдовипадкових чисел).

Код 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()
}
Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.