完璧なJPAエンティティを作成する[終了]


422

私はしばらくの間JPA(実装Hibernate)を使用しており、エンティティを作成する必要があるたびに、AccessType、不変のプロパティ、equals / hashCodeなどの問題に悩まされています。
そこで、私は各問題の一般的なベストプラクティスを見つけ出して、個人用に書き留めることにしました。
しかし、誰かがそれについてコメントしたり、どこが間違っているかを教えたりしても構わないと思います。

エンティティークラス

  • Serializableを実装する

    理由:仕様では、そうする必要があると記載されていますが、一部のJPAプロバイダーはこれを強制していません。JPAプロバイダーとしてのHibernateはこれを強制しませんが、Serializableが実装されていない場合は、ClassCastExceptionで胃のどこかで失敗する可能性があります。

コンストラクタ

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

    理由:コンストラクターは、作成されたインスタンスを常に正常な状態にしておく必要があります。

  • このコンストラクターのほかに、パッケージのプライベートデフォルトコンストラクターがある

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

フィールド/プロパティ

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

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

  • 不変フィールドのセッターを省略(アクセスタイプフィールドには不要)

  • プロパティはプライベートである可能性があります
    理由:以前は(Hibernate)パフォーマンスの方がprotectedの方が優れていると聞きましたが、Webで見つけることができるのは:Hibernateがpublic、private、protectedのアクセサーメソッド、およびpublic、private、protectedフィールドに直接アクセスできることです。選択はあなた次第であり、アプリケーション設計に合うようにそれを一致させることができます。

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

}

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

更新

この記事を読んでから、eq / hCの実装方法を変更しました。

  • 不変のシンプルなビジネスキーが利用可能な場合:それを使用します
  • その他すべての場合:uuidを使用します

6
これは質問ではなく、リストの要求を伴うレビューの要求です。さらに、それは非常にオープンエンドで曖昧、または言い換えると、JPAエンティティが完全であるかどうかは、それが何のために使用されるかに依存します。エンティティの考えられるすべての用途でエンティティが必要とする可能性のあるすべてのものをリストする必要がありますか?
メリトン

私が謝罪する明確な質問ではないことを知っています。他の提案を歓迎しますが、それは実際にはリストの要求ではなく、コメント/コメントの要求です。JPAエンティティの可能な用途について詳しく説明してください。
Stijn Geukens、

私はフィールドも欲しいですfinal(セッターの省略率から判断すると、あなたもそうだと思います)。
Sridhar Sarnobat

試してみる必要がありますが、Hibernateがこれらのプロパティに値を設定できるようにする必要があるため、finalは機能しないと思います。
Stijn Geukens 2018年

どこnotNullから来たの?
ブルーノ

回答:


73

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

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

キーはSerializableを実装する必要があります。HttpSessionに入るもの、またはRPC / Java EEによってネットワーク経由で送信されるものは、Serializableを実装する必要があります。その他のもの:それほどではない。重要なことに時間をかけてください。

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

アプリケーションロジックのコンストラクターには、エンティティの作成時に常に認識される重要な「外部キー」または「タイプ/種類」フィールドがいくつか必要です。残りは、セッターメソッドを呼び出すことによって設定する必要があります。

コンストラクターにフィールドを入れすぎないようにしてください。コンストラクタは便利で、オブジェクトに基本的な健全性を与える必要があります。名前、タイプ、および/または親は、通常すべて役に立ちます。

OTOHアプリケーションルール(今日)で顧客に住所を要求する場合は、設定者に任せます。これは「弱いルール」の例です。たぶん来週、「詳細の入力」画面に進む前にCustomerオブジェクトを作成したいですか?自分でつまずかないでください。不明なデータ、不完全なデータ、または「部分的に入力された」データの可能性を残してください。

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

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

フィールド/プロパティ

Hibernateおよびインスタンスの外部からの「プロパティ」フィールドアクセスを使用します。インスタンス内では、フィールドを直接使用します。理由:Hibernateの最も単純で最も基本的な方法である標準リフレクションが機能するようにします。

アプリケーションに対して「不変」のフィールドについては、Hibernateはまだこれらをロードできる必要があります。これらのメソッドを「プライベート」にしたり、注釈を付けたりして、アプリケーションコードが不要なアクセスを行うのを防ぐことができます。

注:equals()関数を作成するときは、「その他」のインスタンスの値にゲッターを使用してください。そうしないと、プロキシインスタンスの初期化されていない/空のフィールドにヒットします。

(Hibernate)のパフォーマンスはProtectedの方が優れていますか?

ありそうもない。

Equals / HashCode?

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

顧客は住所の変更、ビジネスの名前の変更などを行うことができます-一般的ではありませんが、それは起こります。データが正しく入力されなかった場合は、修正もできるようにする必要があります。

通常は不変に保たれるいくつかのことは、子育てとおそらくタイプ/種類です-通常、ユーザーはこれらを変更するのではなく、レコードを再作成します。しかし、これらはエンティティを一意に識別しません!

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

A)「頻繁に変更されないフィールド」で比較/ハッシュする場合、UIから「変更された/バインドされたデータ」を操作するとき、またはB)「 IDで比較/ハッシュする場合、「保存されていないデータ」。

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

はい、これは必要なときに良い戦略です。ただし、UUIDはパフォーマンス面では無料ではなく、クラスタリングによって複雑になることに注意してください。

Equals / HashCode-関連するエンティティを参照しない

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

いいアドバイスですね。

お役に立てれば!


2
Re:コンストラクター、私はしばしば引数が0のみ(つまり、なし)であり、呼び出しコードにはセッターの非常に長いリストがあり、私には少し面倒に見えます。呼び出し側のコードをより簡潔にするために、ニーズに合ったコンストラクターをいくつか持つことに本当に問題がありますか?
ハリケーン

特に俳優については完全に意見が分かれています。より美しいコードは何ですか?objの正常な状態を作成するために必要な値(の組み合わせ)、または何が設定され、どの順序で何の手がかりも得られず、ユーザーの間違いを起こしやすい引数なし​​のctorを知るためのさまざまなctorの束?
mohamnag 2018

1
@mohamnag依存します。内部システムで生成されたデータの場合、厳密に有効なBeanは優れています。ただし、最新のビジネスアプリケーションは、多数のユーザーデータ入力CRUDまたはウィザード画面で構成されています。ユーザーが入力したデータは、少なくとも編集中は、部分的または不完全な形式になることがよくあります。多くの場合、後で完了するために不完全な状態を記録できることにはビジネス価値さえあります-保険アプリケーションのキャプチャ、顧客のサインアップなどを考慮してください。ビジネスの状況。
トーマスW

1
@ThomasW最初に言わなければならないのは、ドメイン主導の設計と、クラス名に名前を使用すること、およびメソッドの完全な動詞を意味することに対して、私は強く意見を述べています。このパラダイムでは、あなたが言及しているのは、実際にはDTOであり、一時的なデータストレージに使用する必要があるドメインエンティティではありません。または、ドメインを誤解/構造化しました。
mohamnag 2018

@ThomasW私が初心者であるとあなたが言おうとしているすべての文をフィルターで除外すると、ユーザーの入力に関する情報を除いて、コメントに残っている情報はありません。前に述べたように、その部分はDTOで行われ、エンティティでは直接行われません。FowlerのようなDDDの背後にある大きな心の5%になる可能性があることについて、さらに50年で話しましょう。歓声:D
モハムナグ2018

144

JPA 2.0仕様は、と述べています:

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

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


13
True、Equals、HashcodeなどはJPAの要件ではありませんが、もちろん推奨されており、優れたプラクティスと見なされています。
Stijn Geukens、

6
@TheStijnまあ、デタッチされたエンティティが等しいかどうかを比較する予定がない限り、これはおそらく不要です。エンティティマネージャは、要求するたびに特定のエンティティの同じインスタンスを返すことが保証されています。したがって、私が理解している限り、管理対象エンティティのID比較で問題なく実行できます。これを良い習慣とみなすシナリオについてもう少し詳しく説明してもらえますか?
Edwin Dalorzo、

2
equals / hashCodeを常に正しく実装するように努めています。JPAには必要ありませんが、エンティティーやセットに追加する場合には、これは良い習慣だと思います。エンティティがセットに追加される場合にのみ等号を実装することを決定できますが、常に事前に知っていますか?
Stijn Geukens、2011年

10
@TheStijn JPAプロバイダーは、常に特定のエンティティのインスタンスが1つだけコンテキスト内に存在することを保証します。したがって、マネージエンティティのみを使用する場合、equals / hascodeを実装しなくてもセットは安全です。エンティティにこれらのメソッドを実装することには問題がないわけではありません。たとえば、このHibernate Articleを見てください。私の要点は、管理対象エンティティでのみ作業する場合は、それらなしで作業するほうがよいでしょう。それ以外の場合は、非常に慎重な実装を提供します。
Edwin Dalorzo、

2
@TheStijnこれは良い混合シナリオです。エンティティが永続化レイヤーの安全性を放棄すると、JPA標準によって適用されるルールを信頼できなくなるため、最初に提案したとおりにeq / hCを実装する必要性が正当化されます。私たちの場合、DTOパターンは最初からアーキテクチャ的に適用されていました。設計上、永続性APIはビジネスオブジェクトと対話するためのパブリックな方法を提供せず、DTOを使用して永続性レイヤーと対話するためのAPIのみを提供します。
Edwin Dalorzo、

13

ここで私の答えに追加した2セントは次のとおりです。

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

  2. Equals / hashCodeの参照:双方向の関係に参加しているエンティティに対してEclipseの自動生成Equals / hashCodeメソッドを使用することはできません。そうでない場合、循環参照が原因で、スタックオーバーフロー例外が発生します。双方向の関係(OneToOneなど)を試し、Equals()または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);
}

すべてのエンティティの基本的な実装により、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
@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と見なされている場合、これはより多くのケースになる可能性があります。


4
ボイラープレートコードを取り出すために親エンティティクラスを使用することは良い方法ですが、equalsメソッドでDB定義のIDを使用することはお勧めできません。あなたの場合、2つの新しいエンティティを比較すると、NPEもスローされます。nullセーフにしても、永続化されるまで、2つの新しいエンティティは常に等しくなります。Eq / hCは不変である必要があります。
Stijn Geukens 2013年

2
Equals()は、DB IDがnullであるかどうかのチェックがあるため、NPEをスローしません。DBidがnullの場合、等式はfalseになります。
ahaaman 2013

3
確かに、コードがnullセーフであることをどのようにして見逃したのかはわかりません。しかし、IDを使用したIMOは依然として悪い習慣です。引数:onjava.com/pub/a/onjava/2006/09/13/...
Stijn Geukens

Vaughn Vernon著の「Implementing DDD」では、「初期PK生成」を使用する場合、equalsにidを使用できると主張されています(データベースを生成させるのではなく、最初にidを生成してエンティティーのコンストラクターに渡します)エンティティーを永続化するときのID。)
Wim Deblauwe

または同等の非永続エンティティの比較を計画していない場合はどうなりますか?なぜはずです...
Enerccio
弊社のサイトを使用することにより、あなたは弊社のクッキーポリシーおよびプライバシーポリシーを読み、理解したものとみなされます。
Licensed under cc by-sa 3.0 with attribution required.