Создать идеальный объект JPA [закрыто]

422

Я работаю с JPA (реализация Hibernate) уже некоторое время, и каждый раз, когда мне нужно создать сущности, я сталкиваюсь с такими проблемами, как AccessType, неизменяемые свойства, equals / hashCode, ....
Поэтому я решил попытаться найти общие рекомендации по каждому вопросу и записать их для личного использования.
Однако я не против, чтобы кто-то прокомментировал это или сказал мне, где я неправ.

Класс сущности

  • реализовать Сериализуемый

    Причина: в спецификации сказано, что вы должны это сделать, но некоторые провайдеры JPA этого не соблюдают. Hibernate как провайдер JPA не применяет это, но может потерпеть неудачу где-то глубоко в желудке с ClassCastException, если Serializable не был реализован.

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

  • создать конструктор со всеми обязательными полями объекта

    Причина: конструктор всегда должен оставлять экземпляр, созданный в нормальном состоянии.

  • кроме этого конструктора: есть закрытый конструктор по умолчанию для пакета

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

Поля / Свойства

  • Используйте общий доступ к полю и доступ к свойству при необходимости

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

  • Установщики пропуска для неизменяемых полей (не требуется для поля типа доступа)

  • свойства могут быть частными
    Причина: я однажды слышал, что защищенный лучше для производительности (Hibernate), но все, что я могу найти в Интернете: Hibernate может получить доступ к общедоступным, частным и защищенным методам доступа, а также к публичным, частным и защищенным полям напрямую , Выбор за вами, и вы можете сопоставить его с дизайном вашего приложения.

Равно / хэш-код

  • Никогда не используйте сгенерированный идентификатор, если этот идентификатор установлен только при сохранении объекта
  • По предпочтению: используйте неизменяемые значения для формирования уникального бизнес-ключа и используйте его для проверки равенства
  • если уникальный бизнес-ключ недоступен, используйте непереходный 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
Stijn Geukens
источник
6
Это не вопрос, это запрос на просмотр с запросом на получение списка. Более того, он очень открытый и расплывчатый, или иначе говоря: то, насколько совершенен объект JPA, зависит от того, для чего он будет использоваться. Должны ли мы перечислить все вещи, которые могут понадобиться сущности во всех возможных видах использования сущности?
Меритон
Я знаю, что это непонятный вопрос, за который я прошу прощения. На самом деле это не запрос списка, а запрос комментариев / замечаний, хотя приветствуются и другие предложения. Не стесняйтесь подробно рассказать о возможном использовании объекта JPA.
Stijn Geukens
Я также хотел бы, чтобы поля были final(судя по вашему отсутствию сеттеров, я думаю, вы тоже).
Шридхар Сарнобат
Нужно было бы попробовать, но я не думаю, что final будет работать, поскольку Hibernate все еще должен иметь возможность устанавливать значения для этих свойств.
Stijn Geukens
Откуда notNullберутся?
Бруно

Ответы:

73

Я попытаюсь ответить на несколько ключевых моментов: это из долгого опыта Hibernate / постоянства, включая несколько основных приложений.

Класс сущности: реализовать Сериализуемый?

Ключи должны реализовать Serializable. Материал, который будет идти в HttpSession или передаваться по проводам RPC / Java EE, должен реализовывать Serializable. Другие вещи: не так много. Потратьте свое время на то, что важно.

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

Конструктор (ы) для логики приложения должен иметь только несколько критических полей «внешний ключ» или «тип / вид», которые всегда будут известны при создании сущности. Остальные должны быть установлены путем вызова методов установки - вот для чего они.

Старайтесь не помещать слишком много полей в конструкторы. Конструкторы должны быть удобными и придавать объектам базовый разум. Имя, тип и / или родители, как правило, полезны.

OTOH, если правила приложения (сегодня) требуют, чтобы у Клиента был адрес, оставьте его для установщика. Это пример "слабого правила". Может быть, на следующей неделе вы хотите создать объект Customer перед тем, как перейти к экрану ввода сведений? Не сбивайте себя с толку, оставляйте возможность для неизвестных, неполных или «частично введенных» данных.

Конструкторы: также, упаковывать закрытый конструктор по умолчанию?

Да, но используйте «защищенный», а не закрытый пакет. Создание подклассов - это настоящая боль, когда необходимые внутренние компоненты не видны.

Поля / Свойства

Используйте доступ к полю 'property' для Hibernate и извне экземпляра. Внутри экземпляра используйте поля напрямую. Причина: позволяет стандартному отражению, простейшему и основному методу Hibernate, работать.

Что касается полей, «неизменяемых» для приложения - Hibernate все еще должен иметь возможность загружать их. Вы можете попытаться сделать эти методы «приватными» и / или добавить к ним аннотацию, чтобы предотвратить нежелательный доступ кода приложения.

Примечание: при написании функции equals () используйте значения get для значений в другом экземпляре! В противном случае вы попадете на неинициализированные / пустые поля в экземплярах прокси.

Защищенный лучше для производительности (Hibernate)?

Вряд ли.

Равно / HashCode?

Это имеет отношение к работе с объектами, прежде чем они будут сохранены - что является сложной проблемой. Хеширование / сравнение по неизменным значениям? В большинстве бизнес-приложений их нет.

Клиент может изменить адрес, изменить название своего бизнеса и т. Д. И т. Д. - не часто, но это случается. Необходимо также внести исправления, если данные были введены неправильно.

Несколько вещей, которые обычно остаются неизменными, это Parenting и, возможно, Type / Kind - обычно пользователь воссоздает запись, а не изменяет ее. Но они не однозначно идентифицируют сущность!

Итак, как бы то ни было, заявленные «неизменяемые» данные на самом деле не являются. Поля первичного ключа / идентификатора создаются для точной цели обеспечения такой гарантированной стабильности и неизменности.

Вам нужно спланировать и рассмотреть свои потребности в этапах сравнения, хеширования и обработки запросов, когда A) работа с «измененными / связанными данными» из пользовательского интерфейса, если вы сравниваете / хэширование «редко меняющихся полей», или B) работа с « несохраненные данные ", если сравнить / хешировать по идентификатору.

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

Да, это хорошая стратегия, когда это необходимо. Имейте в виду, что UUID не являются бесплатными, хотя с точки зрения производительности - и кластеризация усложняет ситуацию.

Equals / HashCode - никогда не ссылаться на связанные объекты

«Если связанный объект (например, родительский объект) должен быть частью бизнес-ключа, то добавьте не вставляемое, не обновляемое поле для хранения родительского идентификатора (с тем же именем, что и ManytoOne JoinColumn) и используйте этот идентификатор в проверке равенства "

Похоже, хороший совет.

Надеюсь это поможет!

Томас В.
источник
2
Re: конструкторы, я часто вижу только ноль аргументов (т.е. ни одного) и вызывающий код имеет большой длинный список сеттеров, который мне кажется немного грязным. Есть ли какая-то проблема с парой конструкторов, которые соответствуют вашим потребностям, делая код вызова более лаконичным?
Ураган
полностью самоуверенный, особенно о ctor. какой код красивее? куча разных ctors, которые позволяют вам узнать, какие (комбинации) значений необходимы для создания нормального состояния obj или ctor без аргументов, который не дает подсказки, что должно быть установлено и в каком порядке, и оставляет его подверженным ошибкам пользователя. ?
Мохамнаг
1
@mohamnag Зависит. Для внутренних данных, сгенерированных системой, хороши строго допустимые bean-компоненты; однако современные бизнес-приложения состоят из большого количества пользовательских CRUD-экранов ввода данных или экранов мастера. Введенные пользователем данные часто частично или плохо сформированы, по крайней мере, во время редактирования. Довольно часто бизнес-ценность имеет возможность записи неполного состояния для последующего завершения - например, захват страхового заявления, регистрация клиентов и т. Д. Сохранение ограничений до минимума (например, первичный ключ, бизнес-ключ и состояние) обеспечивает большую гибкость в реальном времени. деловые ситуации.
Томас В.
1
@ThomasW Во-первых, я должен сказать, что я твердо убежден в отношении доменного дизайна и использования имен для имен классов и значения полных глаголов для методов. В этой парадигме вы имеете в виду DTO, а не доменные объекты, которые должны использоваться для временного хранения данных. Или вы просто неправильно поняли / -структурировали свой домен.
Мохамнаг
@ThomasW, когда я отфильтрую все предложения, которые вы пытаетесь сказать, что я новичок, в вашем комментарии не останется никакой информации, кроме как о вводе пользователя. Эта часть, как я уже говорил, должна быть сделана в DTO, а не непосредственно в организации. давайте поговорим еще через 50 лет, что вы можете стать 5% того, что испытал ДДД, такой как Фаулер! ура: D
Мохамнаг
144

Спецификация JPA 2.0 гласит, что:

  • Класс сущности должен иметь конструктор без аргументов. У него могут быть и другие конструкторы. Конструктор без аргументов должен быть открытым или защищенным.
  • Класс сущности должен быть классом верхнего уровня. Перечисление или интерфейс не должны быть обозначены как объект.
  • Класс сущности не должен быть окончательным. Никакие методы или постоянные переменные экземпляра класса сущности не могут быть окончательными.
  • Если экземпляр сущности должен передаваться по значению как отдельный объект (например, через удаленный интерфейс), класс сущности должен реализовывать интерфейс Serializable.
  • И абстрактные, и конкретные классы могут быть сущностями. Сущности могут расширять классы не-сущностей, а также классы сущностей, а классы не-сущностей могут расширять классы сущностей.

Спецификация не содержит требований к реализации методов equals и hashCode для сущностей, только для классов первичного ключа и ключей карты, насколько мне известно.

Эдвин Далорсо
источник
13
Правда, равно, хэш-код, ... не являются требованием JPA, но, конечно, рекомендуется и считается хорошей практикой.
Stijn Geukens
6
@TheStijn Ну, если вы не планируете сравнивать отдельные сущности на равенство, это, вероятно, не нужно. Менеджер сущностей гарантированно возвращает один и тот же экземпляр данной сущности каждый раз, когда вы запрашиваете его. Так что, насколько я понимаю, вы можете прекрасно справиться со сравнениями идентификаторов для управляемых объектов. Не могли бы вы подробнее рассказать о тех сценариях, в которых вы бы сочли это хорошей практикой?
Эдвин Далорсо
2
Я стараюсь всегда иметь правильную реализацию equals / hashCode. Не требуется для JPA, но я считаю, что это хорошая практика, когда сущности или добавляются в наборы. Вы могли бы решить использовать равные только тогда, когда сущности будут добавлены в наборы, но всегда ли вы знаете заранее?
Stijn Geukens
10
@TheStijn Поставщик JPA гарантирует, что в любой момент времени в контексте присутствует только один экземпляр данной сущности, поэтому даже ваши наборы безопасны без реализации equals / hascode, при условии, что вы используете только управляемые сущности. Реализация этих методов для сущностей не свободна от трудностей, например, взгляните на эту статью Hibernate о предмете. Я хочу сказать, что если вы работаете только с управляемыми объектами, вам лучше без них, в противном случае обеспечьте очень осторожную реализацию.
Эдвин Далорсо
2
@TheStijn Это хороший смешанный сценарий. Это оправдывает необходимость внедрения eq / hC, как вы первоначально предложили, потому что, как только сущности покидают уровень сохраняемости, вы больше не можете доверять правилам, установленным стандартом JPA. В нашем случае шаблон DTO был архитектурно реализован с самого начала. По своему дизайну наш постоянный API не предлагает общедоступный способ взаимодействия с бизнес-объектами, а только API для взаимодействия с нашим постоянным уровнем с помощью DTO.
Эдвин Далорсо
13

Мои 2 цента в дополнение к ответам здесь:

  1. Что касается доступа к полю или свойству (в отличие от соображений производительности), доступ к ним законным образом осуществляется с помощью методов получения и установки, поэтому моя логика модели может устанавливать / получать их одинаково. Разница проявляется, когда провайдеру персистентной среды выполнения (Hibernate, EclipseLink или другому) необходимо сохранить / установить некоторую запись в таблице A, которая имеет внешний ключ, относящийся к некоторому столбцу в таблице B. В случае типа доступа к свойству, постоянство система времени исполнения использует мой метод кодированного установщика, чтобы присвоить ячейке в столбце таблицы B новое значение. В случае типа доступа к полю система времени выполнения постоянно устанавливает ячейку в столбце таблицы B. Это различие не имеет значения в контексте однонаправленных отношений, тем не менее, НЕОБХОДИМО использовать мой собственный закодированный метод установки (тип доступа к свойству) для двунаправленных отношений, при условии, что метод установки хорошо разработан для учета согласованности. Последовательность является критической проблемой для двунаправленных отношений, обратитесь к этомуссылка на сайт на простой пример для хорошо разработанного сеттера.

  2. Со ссылкой на Equals / hashCode: невозможно использовать автоматически сгенерированные в Eclipse методы Equals / hashCode для объектов, участвующих в двунаправленном отношении, в противном случае они будут иметь циклическую ссылку, что приведет к исключению переполнения стека. После того, как вы попробуете двунаправленное отношение (скажем, OneToOne) и автоматически сгенерируете Equals () или hashCode () или даже toString (), вы попадете в это исключение stackoverflow.

Sym-Sym
источник
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);
}

Базовая реализация для всех сущностей, упрощает реализации Equals / Hashcode:

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

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

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

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

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

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

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

Номер Entity:

@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 рассматриваются как доменные объекты-значения, а не как доменные объекты (для которых предназначены эти примеры кода).

ahaaman
источник
4
Хотя использование родительского класса сущности для извлечения кода котельной пластины является хорошим подходом, не рекомендуется использовать идентификатор, определенный в БД, в вашем методе equals. В вашем случае сравнение 2 новых сущностей даже бросило бы NPE. Даже если вы сделаете его нулевым, тогда 2 новых объекта всегда будут равны, пока они не будут сохранены. Уравнение / hC должно быть неизменным.
Stijn Geukens
2
Функция Equals () не будет генерировать NPE, поскольку существует проверка, является ли идентификатор БД нулевым или нет, а в случае, если идентификатор БД равен нулю, равенство будет ложным.
Ахаман
3
Действительно, я не понимаю, как я пропустил, что код является нулевым. Но IMO, используя идентификатор, все еще плохая практика. Аргументы: onjava.com/pub/a/onjava/2006/09/13/…
Stijn Geukens
В книге «Реализация DDD» Вона Вернона утверждается, что вы можете использовать id для равных, если вы используете «раннюю генерацию PK» (сначала сгенерируйте id и передайте его в конструктор объекта, а не позволяйте базе данных генерировать идентификатор, когда вы сохраняете сущность.)
Вим Deblauwe
или если вы не планируете на равных сравнивать непостоянные объекты? Почему ты должен ...
Enerccio