web-dev-qa-db-ja.com

完璧なJPAエンティティを作成する

私はしばらくの間JPA(Hibernateの実装)に取り組んできたし、AccessType、不変のプロパティ、equals/hashCodeなどの問題に苦しんでいるエンティティを作成する必要があるたびに...
そこで、私は各号の一般的なベストプラクティスを試してみて、個人的な使用のためにこれを書き留めることにしました。
私はだれでもそれについてコメントしたり、私がどこに問題があるのか​​私に言っても構わないと思います。

エンティティクラス

  • 直列化可能を実装する

    理由:仕様書にはあなたがしなければならないと書かれていますが、いくつかのJPAプロバイダはこれを強制しません。 JPAプロバイダとしてのHibernateではこれは強制されませんが、Serializableが実装されていない場合、ClassCastExceptionによって腹部のどこかで失敗する可能性があります。

コンストラクタ

  • エンティティのすべての必須フィールドでコンストラクタを作成します

    理由:コンストラクタは、作成されたインスタンスを常に正しい状態にしておくべきです。

  • このコンストラクタ以外にも、パッケージ専用のデフォルトコンストラクタがあります。

    理由:Hibernateにエンティティを初期化させるにはデフォルトコンストラクタが必要です。プライベートは許可されていますが、ランタイムプロキシの生成とバイトコードインスツルメンテーションなしの効率的なデータ取得には、パッケージのプライベート(またはパブリック)可視性が必要です。

フィールド/プロパティ

  • 必要に応じて一般的なフィールドアクセスとプロパティアクセスを使用する

    理由:どちらにも明確で説得力のある引数がないため(プロパティアクセスとフィールドアクセス)、おそらくこれは最も議論の余地がある問題です。しかし、より明確なコード、より良いカプセル化、および不変フィールド用のセッターを作成する必要がないため、フィールドアクセスは一般的に好まれているようです

  • 不変フィールドのセッターを除外する(アクセスタイプフィールドには不要)

  • プロパティは非公開にすることができます
    理由:(Hibernate)パフォーマンスにはprotectedが良いと聞いたことがありますが、Hibernateはパブリック、プライベート、および保護されたアクセサメソッドにアクセスできます、私用および保護されたフィールドに直接。選択はあなた次第であり、あなたはあなたのアプリケーションデザインに合うようにそれを一致させることができます

Equals/hashCode

  • このIDがエンティティの永続化時にのみ設定されている場合は、生成されたIDを使用しないでください。
  • 好みにより、不変値を使用して固有のビジネスキーを作成し、これを使用して同等性をテストします。
  • 一意のビジネスキーが利用できない場合は、エンティティの初期化時に作成される非一時的なUUIDを使用します。詳細については この素晴らしい記事 をご覧ください。
  • 関連エンティティを参照することは決してありません(ManyToOne)。このエンティティ(親エンティティなど)をビジネスキーの一部にする必要がある場合は、IDのみを比較します。プロキシで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

}

このリストに追加する他の提案は大歓迎です...

UPDATE

この記事を読んで以来私はeq/hCを実装する私のやり方を改めました。

  • 不変の単純なビジネスキーが利用可能な場合:それを使う
  • 他のすべてのケースでは:UUIDを使用
391
Stijn Geukens

JPA 2.0仕様 は次のように述べています。

  • エンティティクラスには引数のないコンストラクタが必要です。他のコンストラクタもあります。引数のないコンストラクタは、パブリックまたは保護されている必要があります。
  • エンティティクラスは最上位クラスである必要があります。列挙型またはインタフェースをエンティティとして指定してはいけません。
  • 実体クラスはfinalであってはいけません。エンティティクラスのメソッドや永続インスタンス変数は、最終的なものではありません。
  • エンティティインスタンスがデタッチオブジェクトとして値渡しされる場合(たとえば、リモートインタフェースを通じて)、エンティティクラスはSerializableインタフェースを実装する必要があります。
  • 抽象クラスも具象クラスも実体になることができます。エンティティはエンティティクラスと同様に非エンティティクラスを拡張することができ、そして非エンティティクラスはエンティティクラスを拡張することができる。

この仕様には、エンティティのequalsおよびhashCodeメソッドの実装に関する要件は含まれていません。私の知る限りでは、主キークラスとマップキーのみが含まれています。

137
Edwin Dalorzo

私はいくつかの重要なポイントに答えようとします:これはいくつかの主要なアプリケーションを含む長いHibernate /永続性の経験からです。

エンティティクラス:Serializableを実装しますか?

キーはSerializableを実装する必要があります。 HttpSessionに入る予定のもの、またはRPC/Java EEによってネットワークを介して送信される予定のものは、Serializableを実装する必要があります。他のもの:そんなにありません。大切なことに時間をかけてください。

コンストラクタ:エンティティのすべての必須フィールドでコンストラクタを作成しますか?

アプリケーションロジックのコンストラクタは、エンティティを作成するときに常に認識される少数の重要な「外部キー」または「タイプ/種類」フィールドのみを持つべきです。それ以外はsetterメソッドを呼び出して設定する必要があります。

あまりにも多くのフィールドをコンストラクタに入れないでください。コンストラクタは便利で、オブジェクトに基本的な健全性を与えるべきです。名前、種類、および/または両親はすべて通常役に立ちます。

適用規則(今日)が顧客に住所の提示を要求している場合は、OTOHを設定者にお任せください。これは「弱い規則」の一例です。来週、Enter Details画面に進む前にCustomerオブジェクトを作成しますか?自分でつまずいたり、未知、不完全、または「部分的に入力された」データの可能性を残してください。

コンストラクタ:また、パッケージのプライベートデフォルトコンストラクタ?

はい、ただしパッケージプライベートではなく「保護」を使用してください。必要な内部構造が見えない場合は、サブクラス化することは本当に苦痛です。

フィールド/プロパティ

Hibernateにはインスタンスの外部から、 'property'フィールドアクセスを使用してください。インスタンス内では、フィールドを直接使用してください。理由:Hibernateの最も単純で基本的な方法である標準反射が機能するようにします。

アプリケーションにとって「不変」なフィールドに関しては - Hibernateはまだこれらをロードできる必要があります。これらのメソッドを 'private'にしたり、アノテーションを付けたりして、アプリケーションコードが不要なアクセスをするのを防ぐことができます。

注意:equals()関数を書くときは、 'other'インスタンスの値にはゲッターを使用してください。そうでなければ、プロキシインスタンスで未初期化/空のフィールドをヒットするでしょう。

保護されている方が(Hibernate)パフォーマンスに向いていますか?

ありそうもない。

Equals/HashCode?

これは、エンティティが保存される前にエンティティを操作することに関連しています - これはとんでもない問題です。不変値をハッシュ/比較するほとんどのビジネスアプリケーションにはありません。

顧客は住所の変更、事業名の変更などを行うことができます - 一般的ではありませんが、それは起こります。データが正しく入力されていない場合は、訂正も可能である必要があります。

通常は不変に保たれるものがいくつかありますが、それは子育て、そしておそらくType/Kindです - 通常、ユーザーはこれらを変更するのではなくレコードを再作成します。しかし、これらは実体を一意に識別しません!

それで、長いと短い、主張された「不変の」データは実際にはありません。主キー/ IDフィールドは、そのような安定性と不変性を保証するという正確な目的のために生成されます。

A) "頻繁に変更されていないフィールド"で比較/ハッシュする場合はA)UIから "変更/バインドされたデータ"を処理する場合、またはB)を使用して処理する場合保存していないデータ "、IDとハッシュを比較した場合.

Equals/HashCode - 一意のビジネスキーが利用できない場合は、エンティティの初期化時に作成される一時的ではないUUIDを使用します

はい、必要なときにこれは良い戦略です。ただし、UUIDはフリーではなく、パフォーマンス的にも問題ありません。クラスタ化すると作業が複雑になります。

Equals/HashCode - 関連エンティティを参照しません

"関連エンティティ(親エンティティなど)をビジネスキーの一部にする必要がある場合は、親ID(ManytoOne JoinColumnと同じ名前)を格納するための挿入不可、更新不可フィールドを追加し、等価性チェックでこのIDを使用します「

良いアドバイスのようですね。

お役に立てれば!

64
Thomas W

私の2セントはここに答えに加えて:

  1. フィールドまたはプロパティへのアクセスを参照すると(パフォーマンスの観点からは離れています)、どちらもゲッターとセッターを使用して合法的にアクセスされます。したがって、私のモデルロジックは同じ方法で設定/取得できます。永続ランタイムプロバイダ(Hibernate、EclipseLinkなど)が、表Bの列を参照する外部キーを持つ表Aのレコードを永続化または設定する必要がある場合に違いが生じます。プロパティアクセスタイプの場合は、永続化ランタイムシステムは私のコード化されたセッターメソッドを使ってTable Bの列のセルに新しい値を割り当てます。フィールドアクセスタイプの場合、永続ランタイムシステムはテーブルB列のセルを直接設定します。この違いは、単方向の関係では重要ではありませんが、一貫性を考慮して設定されている限り、双方向の関係に自分自身のコード化セッターメソッド(プロパティアクセスタイプ)を使用する必要があります。 。一貫性は、双方向の関係にとって重大な問題です。うまく設計されたセッターの簡単な例として、この リンク を参照してください。

  2. Equals/hashCodeを参照すると:双方向の関係に参加するエンティティに対してEclipseの自動生成Equals/hashCodeメソッドを使用することは不可能です。そうしないと、循環参照が発生し、スタックオーバーフロー例外が発生します。双方向の関係(OneToOneなど)を試してEquals()、hashCode()、さらにはtoString()を自動生成すると、このスタックオーバーフローの例外が発生します。

13
Sym-Sym

エンティティインタフェース

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

Room 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エンティティがドメイン駆動型エンティティではなくドメイン駆動型ValueObjectとして考えられている場合、これはより多くの場合に当てはまります(これらのコード例はそのためのものです)。

9
ahaaman