Створіть ідеальну сутність JPA [закрито]


422

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

Клас сутності

  • реалізувати Serializable

    Причина: Специфікація говорить про те, що потрібно, але деякі постачальники JPA цього не застосовують. Зимова сплячка як постачальник JPA цього не застосовує, але вона може вийти з ладу десь глибоко в шлунку за допомогою ClassCastException, якщо Serializable не буде впроваджено.

Конструктори

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

    Причина: Конструктор повинен завжди залишати створений екземпляр у здоровому стані.

  • крім цього конструктора: мати пакет приватного конструктора за замовчуванням

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

Поля / Властивості

  • Використовуйте доступ до поля загалом та доступ до власності, коли це необхідно

    Причина: це, мабуть, найбільш дискусійне питання, оскільки немає чітких та переконливих аргументів для того чи іншого (доступ власності проти доступу до поля); однак, доступ до поля, здається, є загальним улюбленим через чіткіший код, кращу інкапсуляцію та відсутність необхідності створювати задачі для змінних полів

  • Опустіть сеттери для змінних полів (не потрібно для поля типу доступу)

  • властивості можуть бути приватними
    Причина: Я колись чув, що захищений краще для (Hibernate) продуктивності, але все, що я можу знайти в Інтернеті, це: Hibernate може отримати доступ до публічних, приватних та захищених методів доступу, а також до публічних, приватних та захищених полів безпосередньо . Вибір залежить від вас, і ви зможете зіставити його відповідно до дизайну програми.

Дорівнює / hashCode

  • Ніколи не використовуйте згенерований ідентифікатор, якщо цей ідентифікатор встановлений лише при збереженні сутності
  • За перевагою: використовуйте незмінні значення для формування унікального бізнес-ключа та використовуйте це для перевірки рівності
  • якщо унікальний бізнес-ключ недоступний, використовуйте неперехідний UUID, який створюється під час ініціалізації об'єкта; Дивіться цю чудову статтю для отримання додаткової інформації.
  • ніколи не посилайтеся на пов’язані особи (ManyToOne); якщо ця особа (як материнське підприємство) має бути частиною бізнес-ключа, то порівняйте лише ідентифікатори. Виклик getId () на проксі-сервер не спричинить завантаження сутності, якщо ви використовуєте тип доступу до властивості .

Приклад суб'єкта

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

    private static final long serialVersionUID = 1L;

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

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

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

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

    Room() {
        // default constructor
    }

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

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

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

    public Building getBuilding() {
        return building;
    }


    public Integer getId() {
        return id;
    }

    public String getNumber() {
        return number;
    }

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

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

    //no setters for number, building nor id

}

Інші пропозиції додати до цього списку більш ніж вітаються ...

ОНОВЛЕННЯ

Після прочитання цієї статті я адаптував свій спосіб реалізації eq / hC:

  • якщо доступний незмінний простий бізнес-ключ: скористайтеся цим
  • у всіх інших випадках: використовуйте uuid

6
Це не питання, це запит на перегляд із запитом на перелік. Більше того, це дуже відкрито та невиразно, або якщо говорити інакше: чи ідеальною є сутність JPA, залежить від того, для чого вона буде використовуватися. Чи слід перераховувати всі речі, які можуть знадобитися суб'єкту господарювання у всіх можливих сферах використання?
meriton

Я знаю, що це не зовсім чітке вирізане питання, за яке я вибачаюся. Це насправді не запит на список, а прохання про коментарі / зауваження, хоча інші пропозиції вітаються. Не соромтеся детальніше розглянути можливі можливості використання об'єкту JPA.
Штійн Гюкенс

Я також хотів би, щоб поля були final(судячи з вашої відсутності сетерів, я б, мабуть, ви теж зробили).
Шрідхар Сарнобат

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

Звідки береться notNull?
Брюно

Відповіді:


73

Я спробую відповісти на декілька ключових моментів: це на основі довгого сплячого / наполегливого досвіду, включаючи декілька основних програм.

Клас особи: реалізувати серіалізаційний?

Ключі потрібно реалізувати Serializable. Речі, які збираються перейти в HttpSession або надсилатись по протоколу RPC / Java EE, повинні реалізувати Serializable. Інші речі: не так багато. Витратьте свій час на те, що важливо.

Конструктори: створити конструктор з усіма необхідними полями сутності?

Конструктор (и) для логіки додатків повинен мати лише декілька критичних полів "зовнішній ключ" або "тип / вид", які завжди будуть відомі при створенні сутності. Решту слід встановити, зателефонувавши у методи сеттера - ось для чого вони і є.

Уникайте занадто багато полів в конструктори. Конструктори повинні бути зручними та надавати об’єкту основну розумність. Ім'я, тип та / або батьки зазвичай корисні.

OTOH, якщо правила заявки (сьогодні) вимагають від Замовника адреси, залиште її сетеві. Це приклад "слабкого правила". Можливо, на наступному тижні ви хочете створити об’єкт "Клієнт", перш ніж перейти на екран "Ввести деталі"? Не відмовляйтесь від себе, не залишайте можливості для невідомих, неповних або "частково введених" даних.

Конструктори: також пакет приватного конструктора за замовчуванням?

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

Поля / Властивості

Використовуйте доступ до поля "Властивості" для Hibernate та поза екземпляром. У межах примірника використовуйте поля безпосередньо. Причина: дозволяє працювати стандартне відображення, найпростіший та найосновніший метод для сплячки.

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

Примітка. Коли ви пишете функцію equals (), використовуйте getters для значень на іншому екземплярі! В іншому випадку ви потрапите на неініціалізовані / порожні поля на екземплярах проксі.

Чи захищений краще для (сплячої) продуктивності?

Навряд чи.

Дорівнює / HashCode?

Це стосується роботи з організаціями, перш ніж їх зберегти - що є тернистим питанням. Хешинг / порівняння за незмінними значеннями? У більшості бізнес-додатків таких немає.

Клієнт може змінити адресу, змінити назву своєї компанії тощо тощо - нечасто, але це буває. Виправлення також потрібно робити, коли дані були введені неправильно.

Кілька речей, які, як правило, зберігаються незмінними, це Parenting і, можливо, Type / Kind - звичайно користувач відтворює записи, а не змінює їх. Але вони не однозначно ідентифікують сутність!

Отже, довго і коротко, заявлені "незмінні" дані насправді не є. Поля первинного ключа / ідентифікатора створюються з точною метою, щоб забезпечити таку гарантовану стабільність та незмінність.

Вам потрібно спланувати та врахувати свою потребу в порівнянні та хешировании та обробці запитів на робочих фазах, коли A) робота зі "зміненими / прив'язаними даними" з інтерфейсу користувача, якщо ви порівнюєте / хеш на "нечасто змінених полях" або B), що працюють з " незбережені дані ", якщо ви порівнюєте / хеш по ID.

Equals / HashCode - якщо унікальний бізнес-ключ недоступний, використовуйте неперехідний UUID, який створюється при ініціалізації сутності

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

Рівно / HashCode - ніколи не посилайтеся на пов'язані сутності

"Якщо пов’язана особа (як батьківська особа) повинна бути частиною бізнес-ключа, тоді додайте поле, яке не можна вставити, не можна оновити, щоб зберігати батьківський ідентифікатор (з тим самим іменем, як ManytoOne JoinColumn), і використовувати цей ідентифікатор для перевірки рівності "

Звучить як хороша порада.

Сподіваюся, це допомагає!


2
Re: конструктори, я часто бачу лише нульовий аргумент (тобто жоден), і викличний код має великий довгий список сеттерів, який здається мені трохи безладним. Чи справді є якась проблема з парою конструкторів, які відповідають вашим потребам, роблячи код виклику більш лаконічним?
Ураган

абсолютно впевнений, особливо щодо ctor. який гарніший код? купа різних ctors, що дозволяє вам дізнатися, які (комбінації) значення потрібні для створення здорового стану obj або no-arg ctor, який не дає поняття, що потрібно встановити і в якому порядку, і залишає його схильним до помилок користувача ?
mohamnag

1
@mohamnag Залежить. Для внутрішніх даних, створених системою, відмінно підходять чіткі боби; однак сучасні бізнес-програми складаються з великої кількості користувацьких CRUD або екранів майстра. Введені користувачем дані часто частково або погано сформовані, принаймні під час редагування. Досить часто є навіть бізнес-цінність у змозі записати незавершений стан для подальшого завершення - подумайте про страхування заявки на страхування, реєстрації клієнтів тощо. Зведення обмежень до мінімуму (наприклад, первинний ключ, бізнес-ключ та стан) дозволяє збільшити гнучкість у реальній ділові ситуації.
Thomas W

1
@ThomasW спочатку маю сказати, що я сильно переконаний у створенні доменного дизайну та використанні імен для назв класів та значень повних дієслів для методів. У цій парадигмі, на яку ви звертаєтесь, насправді є DTO, а не доменні об'єкти, які слід використовувати для тимчасового зберігання даних. Або ви просто неправильно зрозуміли / структурували свій домен.
mohamnag

@ThomasW, коли я фільтрую всі пропозиції, які ти намагаєшся сказати, що я новачок, у твоєму коментарі не залишилося жодної інформації, окрім введення користувачів. Ця частина, як я вже говорив раніше, повинна бути виконана в довідкових організаціях, а не безпосередньо в утворенні. давайте говорити ще через 50 років, що ви можете стати 5% того, що великий розум за DDD, як Фоулер, пережив! ура: D
mohamnag

144

У специфікації JPA 2.0 зазначено, що:

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

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


13
Щоправда, рівне, хеш-код, ... не є вимогою JPA, але, звичайно, рекомендується і вважається хорошою практикою.
Stijn Geukens

6
@TheStijn Ну, якщо ви не плануєте порівнювати окремі сутності за рівністю, це, мабуть, непотрібно. Менеджер суб'єктів гарантовано повертає той самий екземпляр даної сутності кожного разу, коли ви просите про це. Тож, наскільки я розумію, ви можете просто зіставити особистість для керованих організацій. Чи можете ви, будь ласка, детальніше розповісти про ті сценарії, в яких ви вважаєте це гарною практикою?
Едвін Далорцо

2
Я прагну завжди мати правильну реалізацію equals / hashCode. Не потрібно для JPA, але я вважаю це хорошою практикою для тих, хто створює або додає до наборів. Ви можете вирішити застосовувати рівняння лише тоді, коли до наборів будуть додані сутності, але чи завжди ви знаєте заздалегідь?
Штійн Геукенс

10
@TheStijn Постачальник JPA гарантує, що в будь-який момент часу в контексті є лише один екземпляр даної сутності, тому навіть ваші набори безпечні, не застосовуючи рівний / код коду, за умови, що ви використовуєте лише керовані об'єкти. Реалізація цих методів для суб'єктів господарювання не позбавлена ​​труднощів, наприклад, погляньте на цю статтю, що перебуває у сплячому режимі. Моя думка, якщо ви працюєте лише з керованими структурами, вам краще без них, інакше забезпечуйте дуже ретельну реалізацію.
Едвін Далорцо

2
@TheStijn Це хороший змішаний сценарій. Це обґрунтовує необхідність впровадження eq / hC, як ви спочатку пропонували, оскільки після того, як суб'єкти відмовляться від безпеки стійкого шару, ви більше не можете довіряти правилам, що застосовуються стандартом JPA. У нашому випадку модель DTO архітектурно застосовувалася з самого початку. Замисливши наш API стійкості не пропонує відкритий спосіб взаємодії з бізнес-об'єктами, лише API для взаємодії з нашим шаром стійкості за допомогою DTO.
Едвін Далорцо

13

Мої 2 копійки додаються до відповідей тут:

  1. З посиланням на доступ до поля чи власності (осторонь міркувань щодо продуктивності) обидва мають законний доступ за допомогою геттерів та сетерів, тому моя логіка моделі може встановлювати / отримувати їх однаково. Різниця виникає тоді, коли постачальнику постійної тривалості (Hibernate, EclipseLink або іншому) потрібно зберегти / встановити деякий запис у Таблиці A, який містить зовнішній ключ, що посилається на якийсь стовпець у Таблиці B. У випадку типу доступу до власності, збереження система виконання використовує метод кодованого сеттера, щоб призначити комірці в стовпці таблиці нового значення. У випадку типу доступу до поля система виконання постійної тривалості встановлює комірку в стовпці таблиці B безпосередньо. Ця різниця не має важливого значення в контексті однонаправлених відносин, однак ОБОВ'ЯЗКОВО використовувати власний кодований метод сеттера (тип доступу до властивості) для двонаправлених відносин за умови, що метод сеттера добре розроблений для обліку узгодженості. Послідовність є критичним питанням для двонаправлених відносин, які стосуються цьогопосилання на простий приклад для добре розробленого сеттера.

  2. З посиланням на Equals / hashCode: Неможливо використовувати автоматично створені Eclipse методи Equals / hashCode для суб'єктів, що беруть участь у двонаправленому співвідношенні, інакше вони матимуть циркулярну посилання, що призводить до виключення stackoverflow. Як тільки ви спробуєте двонаправлене відношення (скажімо, OneToOne) і автоматично генерує рівне () або hashCode () або навіть toString (), ви потрапите в цей виняток stackoverflow.


9

Інтерфейс сутності

public interface Entity<I> extends Serializable {

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

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

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

Основна реалізація для всіх об'єктів, спрощує реалізацію рівних / хеш-кодів:

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

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

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

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

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

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

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

Номер особи імпл:

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

private static final long serialVersionUID = 1L;

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

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

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

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

Room() {
    // default constructor
}

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

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

public Integer getId(){
    return id;
}

public Building getBuilding() {
    return building;
}

public String getNumber() {
    return number;
}


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

//no setters for number, building nor id
}

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


4
Незважаючи на те, що для вилучення коду пластини котла використовувати клас батьківської сутності, це не є ідеєю використовувати визначений БД ідентифікатор у вашому методі рівних. У вашому випадку порівняння двох нових об'єктів навіть кине NPE. Навіть якщо ви зробите це недійсним, тоді два нові об'єкти завжди будуть рівними, поки вони не збережуться. Екв / гС має бути незмінним.
Stijn Geukens

2
Рівень () не кидає NPE, оскільки є перевірка, чи ідентифікатор БД недійсний, чи ні, якщо ідентифікатор БД недійсний, рівність була б помилковою.
ахааман

3
Дійсно, я не бачу, як я пропустив, що код є нульовим. Але ІМО, що використовує ідентифікатор, все ще є поганою практикою. Аргументи: onjava.com/pub/a/onjava/2006/09/13/…
Stijn Geukens

У книзі "Реалізація DDD" Вона Вернона стверджується, що ви можете використовувати id для рівних, якщо ви використовуєте "ранню генерацію ПК" (Створіть ідентифікатор спочатку та передайте його в конструктор сутності на відміну від дозволу базі даних генерувати ідентифікатор, коли ви зберігаєте сутність.)
Вім Деблауве

або якщо ви не плануєте рівних порівняти непостійні сутності? Навіщо тобі ...
Enerccio
Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.