неділя, 1 квітня 2012 р.

JPA 2.0: Entities design

При проектировании модели данных с использованием JPA, особенных правил, которые были бы определены стандартом, фактически нет. Очевидно, что класс должен

  1. Быть сериализуемым
  2. Cодержать реализацию equals и hashCode
  3. Некоторые рекомендуют перегружать toString
  4. Иметь публичные getters & setters для своих полей

Но это фактически требования к стандартным JavaBean's, чем конкретно к JPA entity beans. А между тем, именно JPA entity имеют свои особенности, о которых было очень полезно знать непостредственно при проектировании. И снова источником вдохновения стала статья Stijn Geukens на StackOwerflow

implements java.io.Serializable

Хотя это и очевидно, но далеко не все и не всегда так делают. Стандарт же говорит, что JPA entities должны быть сериализуемыми. Несмотря на то, что некотрые JPA-реализации (привер Hibenate) явно такой проверки не делают, рано или поздно все провайдеры валятся с исключениями (и даже Hibernate)

Default constructor

Опять таки, древняя спецификация на JavaBeans говорит о том, что любой Java бин должен иметь конструктор по-умолчанию. Спецификация JPA 2.0 также требует наличия такового конструктора. И, в конце-концов, ciglib конструктор по-умолчанию необходим для создания proxy-классов. Так что для каждого entity бина конструктор по-умочанию необходим. В случае, если классы или класс модели используется только JPA, конструктор по-умочанию может иметь область видимости package private.

Required (not nullable) fields constrcutor

Отличной идеей является иметь как минимум один конструктор, который содержит все поля класса, помеченные как @Column(nulable = false) и/или @NotNull. Таким образом мы предоставляем программистам (в том числе и себе), возможность создать готовый entity, а также четко и ясно определяем не нулевые поля.

relationParentId property

Еще одной хорошей, хотя на мой взгляд спорной, идеей является определение полей класса, содержащих значения ключа родительской сущности. Таким образом, мы получаем возможность работать со связанной сущностью не выбирая ее из базы явно. Здесь просто необходим пример в коде

public class Entity implements Seializable {     
    @ManyToOne(fetch = FetchType.LAZY, optional = false) 
    @JoinColumn(name = "parentId") 
    private Parent parent;  

    @Column(name = "parentId", insertable = false, udaptable = false) 
    private Long parentId; 
}

В данном примере parentId определена как read-only, но я не уверен, что это единственный возможный способ. Пока проверить не было возможности, но мне кажется, что если есть возможность использовать два поля одного и того же объекта, для работы со свазанными сущностями, то откроются очень интересные перспективы.

No auto generated keys in equals & hashCode

Идея очень проста - каждый entity представляет собой некий набор полей, который является значимым для бизнесс-логики. Автоматически (очень-очень важно понимать, что только автоматически) генерируемый ключ является внутренним идентификатором записи в базе данных. Таким образом, его лучше исключить из методов, которые определяют эквивалетность объектов. Есть еще одна причина не включать автоматически сгенерированный ключ в equals & hashCode. Иногда данные, которые приходят в систему из вне, содержат фактически суперпозицию полей одного или нескольких entity, в таком случае занчительно проще сравнить два объекта - один, созданный на основе внешних данных, и второй, выбранный из базы данных на основании уникального поля или нескольких полей - вместо того, чтобы сравнивать каждое поле с каждым.

Never refers to related entities in equals & hashCode

Никогда не включать свазанные сущности в equals & hashCode. Во-первых, это помогает избежать  бесконечных циклов, в случае определения двусторонней связи. Во-вторых, в случае lazy initalized collections две, даже одинаковые, сущности, будут в лучшем случае не равны, в худешем - будет выброшен LazyIntializationException. Если связная сущность значима для бизнесс-логики конекретной сущности, следует сравнивать значения ключей связанных сущностей, а не сами сущности. Например

public class Entity implements Seializable {
    private Class<T> type;

    @Column(name = "name") 
    private String name;  

    @ManyToOne(fetch = FetchType.LAZY, optional = false) 
    @JoinColumn(name = "parentId") 
    private Parent parent;  

    @Column(name = "parentId", insertable = false, udaptable = false) 
    private Long parentId;  

    @Override public boolean equals(final Object other) { 
        if ((other == null) || !(other instanceof type)) { 
        return false; 
    } 

    final T that = (T) other; 
        if (name != null ? !name.equals(that.name) : that.name != null) return false; 
        if (parentId != null ? !parentId.equals(that.parentId) : that.parentId != null) return false; 
        return true; 
    }  

    @Override public int hashCode() { 
        int result = 31 * (name != null ? name.hashCode() : 0); 
        result = result + 31 * (parentId != null ? !parentId.hashCode() : 0); return resutl; 
    } 
}

Date.compareTo(otherDate) instead of Date.equals(otherDate)

Если поле базы данных имеет тип date или datetime, его мэппинг в объекте рекомендуется дополнительно объявлять @Temporal(TemporalType.TIMESTAMP), то при выборке из базы поле будет иметь тип… java.sql.Timestamp. Даже если оно объявлено как java.util.Date. А следует помнить, что метод equals, класса Date поддерживает даты и только даты. А вот метод compareTo можно использовать с любыми потомками класса. Таким обpазом, такой код

if (date != null ? !date.equals(that.date) : that.date != null) return false;

следует заменить на такой

if (date != null ? date.compareTo(that.date) != 0 : that.date != null) return false;

@NotNull & @Column(nullable = false)

Тут все довольно просто - @NotNull является одной из аннотаций пакета javax.validation. Эта аннотация действительно используется для проверки значечия поля на NULL при операции с persistence layer. Но вместе с тем, никакого воздействия на физическую структуры базы аннотация не оказывает. Если необходимо пометить поле как NOT NULL в базе - нужно использовать @Column.

Default values in default constructor

Если значения по-умочанию не нулевых полей сущности четко определены, эти поля можно проинициализировать в конструкторе по-умолчанию.