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

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

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

  • реализовать Serializable

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

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

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

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

  • кроме этого конструктора: иметь конструктор private private по умолчанию

    Причина: для конструктора по умолчанию требуется инициализация объекта 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 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

}

Другие предложения, которые нужно добавить в этот список, более чем приветствуются...

UPDATE

Считая эту статью, я адаптировал свой способ реализации eq/hC:

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

Ответ 1

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

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

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

Ответ 2

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

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

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

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

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

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

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

Конструкторы: также, конструктор private private default?

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

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

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

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

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

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

Вряд ли.

Равно /Hashcode?

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

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

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

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

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

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

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

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

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

Звучит как хороший совет.

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

Ответ 3

Выразив мое восхищение полу-всеобъемлющим списком Стиджса, 2 исправления:

  • Ссылаясь на доступ к полям или свойствам (вдали от соображений производительности), оба из них законно доступны с помощью геттеров и сеттеров, поэтому моя модельная логика может установить/получить их таким же образом. Разница возникает в том случае, когда поставщику времени выполнения (Hibernate, EclipseLink или еще) необходимо перенести/установить некоторую запись в таблице A, которая имеет внешний ключ, ссылаясь на какой-либо столбец в таблице B. В случае типа доступа к ресурсу постоянство система времени выполнения использует мой метод кодированного сеттера, чтобы назначить ячейке в столбце таблицы B новое значение. В случае типа доступа к полю, система непрерывности выполнения устанавливает ячейку в столбце таблицы B напрямую. Это различие не имеет особого значения в контексте однонаправленных отношений, но оно ДОЛЖНО использовать мой собственный метод кодированного сеттера (тип доступа к свойствам) для двунаправленного отношения, если метод сеттера хорошо разработан для учета согласованности, Согласованность является важной проблемой для двунаправленных отношений, относящихся к этой ссылке для простого примера для хорошо продуманного сеттера.

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

Ответ 4

Интерфейс сущности

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:

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