データベース

エンティティクラスとは|JPAとEF Coreの書き方・DTOとの違いを実例で解説

エンティティクラスとは、データベースの1テーブルに対応させたクラスのことです。1インスタンスが1レコードを表し、フィールドが列に対応します。Java(JPA/Hibernate)でもC#(Entity Framework Core)でも考え方は同じで、テーブルとオブジェクトの対応づけをORMに任せるための「型」がエンティティクラスです。この記事では定義とDTO・モデルクラスとの違いを整理したうえで、Jakarta Persistence 3.2 と EF Core 10 の実際のコード、そして現場で事故になりやすい設計の落とし穴まで扱います。

まとめ

  • エンティティクラス=テーブルに対応するクラス。1インスタンス=1レコード、フィールド=列。
  • エンティティは「DBに永続化される状態」を持つ。画面やAPIに渡す入れ物はDTOであり、両者を兼ねると事故が起きる。
  • JavaではJPAの@Entity@Idを付け、引数なしコンストラクタ(public または protected)が必須。C#ではPOCOにDbSetを持つDbContextを組み合わせる。
  • DbContext(C#)とEntityManager(Java)が、エンティティの変更を追跡してSQLに変換する「コンテキストオブジェクト」。寿命は短く保つ。
  • 遅延読み込みはEF Coreでは既定で無効(EF6は既定で有効)。Hibernate 7.0ではSession.update()が削除され、detachedインスタンスの復帰はmerge()に一本化された。

エンティティクラスの定義とORMにおける役割

ORM(Object-Relational Mapping)は、リレーショナルDBの行とプログラム上のオブジェクトを相互変換する仕組みです。その変換先の型として宣言するクラスがエンティティクラスにあたります。ORMそのものの仕組みはORM(Object-Relational Mapping)とは|仕組み・メリット・デメリットと読み方をやさしく解説で整理しています。

テーブル・レコードとの対応関係

employees テーブルに id・name・hire_date の3列があるなら、エンティティクラス Employee は id・name・hireDate の3フィールドを持ちます。SELECTで取り出した1行は Employee のインスタンス1つになり、そのインスタンスのフィールドを書き換えて確定させるとUPDATEが発行されます。「エンティティのインスタンス」という言い方は、この1レコード分のオブジェクトを指しています。

テーブルとエンティティは1対1が基本ですが、常に一致するとは限りません。継承関係を1テーブルに詰める(単一テーブル継承)、複数テーブルを1エンティティに束ねる(@SecondaryTable)といったマッピングもあり、「テーブルの定義=エンティティの定義」と決めつけないことが設計の出発点です。

エンティティとDTO・モデルクラスの違い

この3語はしばしば混同されますが、責務が違います。

種類 役割 ライフサイクル 典型的な使いどころ
エンティティ DBの行を表す永続化対象 ORMのコンテキストが追跡 永続化層
DTO 層間のデータ運搬 都度生成・使い捨て APIレスポンス、画面入力
モデルクラス 文脈依存の総称 文脈による 設計文書・フレームワーク用語

決定的な違いは「変更が追跡されるかどうか」です。エンティティはコンテキストに紐づいている間、フィールドの書き換えがそのままDBへのUPDATEになります。DTOは単なる値の入れ物で、書き換えてもDBには何も起きません。エンティティをAPIレスポンスにそのまま使うと、この境界が消えます。

DDDのエンティティとJPAのエンティティは同じ言葉の別概念

ドメイン駆動設計(DDD)でいうエンティティは「同一性(ID)で識別され、状態が変わっても同じものとみなされる」ドメインオブジェクトを指し、永続化の都合とは無関係に定義されます。ORMのエンティティクラスは「テーブルにマッピングされるクラス」という技術的な定義です。DDDの文脈で「エンティティ」と言われたときにJPAの@Entityを思い浮かべると設計の議論がかみ合いません。DDDの用語体系はドメイン駆動設計(DDD)とは?意味・基本用語・実装と採用判断をわかりやすく解説で確認してください。

JavaのエンティティクラスとJPAの必須ルール

JavaのORMはJakarta Persistence(旧JPA)仕様に基づき、実装としてHibernateが広く使われます。Spring Boot 4.0(2025年11月)はJakarta Persistence 3.2とHibernate ORM 7.1を前提としています。ORM実装の選び方はJava ORM(O/Rマッパー)の種類と選び方|Hibernate・MyBatis・jOOQを比較【2026年版】にまとめました。

エンティティクラスが満たすべき要件

Jakarta Persistenceがエンティティに課す条件は限られており、これを外すと起動時や実行時に例外になります。

  • クラスに@Entity、主キーとなるフィールドに@Idを付ける。
  • 引数なしコンストラクタを持つ。可視性は public か protected でなければならない(JPAがリフレクションでインスタンス化するため)。
  • クラス・メソッド・永続化フィールドを final にしない。
  • Jakarta Persistence 3.2では、エンティティおよび埋め込み可能クラスを static な内部クラスにできるようになった。主キークラスに public かつ Serializable を求める制約も外れている。

Lombokの@Builderだけを付けて@NoArgsConstructorを書かず「No default constructor for entity」で起動に失敗するのは、この引数なしコンストラクタ要件を落としているためです。

import jakarta.persistence.*;
import java.time.LocalDate;

@Entity
@Table(name = "employees")
public class Employee {

    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;

    @Column(nullable = false, length = 100)
    private String name;

    @Column(name = "hire_date")
    private LocalDate hireDate;

    protected Employee() { }  // JPA用。アプリからは呼ばない

    public Employee(String name, LocalDate hireDate) {
        this.name = name;
        this.hireDate = hireDate;
    }
}

Spring Data JPAと組み合わせる場合に登場する@Repository@Transactionalなど、周辺のアノテーションの役割はSpring Bootの主要なアノテーション一覧と基本的な役割で確認できます。

EntityManagerと永続化コンテキストの4状態

JavaでのコンテキストオブジェクトはEntityManagerです。EntityManagerが持つ永続化コンテキストがエンティティを追跡し、トランザクション終了時に差分をSQLへ変換します。エンティティは次の4状態を移り、どの状態にあるかでpersistmergeの挙動が変わります。

  • transient:newしただけで、コンテキストにもDBにも存在しない。
  • managed:コンテキストの管理下。フィールドを書き換えるだけでフラッシュ時にUPDATEされる。
  • detached:コンテキストが閉じた後のインスタンス。追跡されていない。
  • removed:削除予定としてマークされた状態。

作成日時や更新日時をエンティティ側で自動設定したい場合は、@PrePersist@PreUpdateのコールバック、またはエンティティリスナー(@EntityListeners)を使います。日時列をアプリのあちこちで手書きするより、エンティティに寄せたほうが値の漏れが起きません。

C#のエンティティクラスとDbContext(EF Core 10)

C#では、属性を持たないただのクラス(POCO)をエンティティとして扱い、DbContextを継承したクラスにDbSetプロパティとして登録します。EF Core 10(EF10)は2025年11月リリースのLTSで、.NET 10 ランタイムが必要です。サポート期限は2028年11月10日と公表されています。

エンティティとDbContextの最小構成

public class Employee
{
    public int Id { get; set; }
    public string Name { get; set; } = string.Empty;
    public DateOnly HireDate { get; set; }
}

public class AppDbContext : DbContext
{
    private const string ConnectionString = "Server=localhost;Database=Hr;Trusted_Connection=True;";

    public DbSet<Employee> Employees => Set<Employee>();

    protected override void OnConfiguring(DbContextOptionsBuilder options)
        => options.UseSqlServer(ConnectionString);
}

Idという名前のプロパティは規約で主キーとして扱われ、@Id相当の指定は不要です。規約から外す場合だけ[Key]OnModelCreatingで明示します。

接続設定の既定はOnConfiguringではなく依存性注入

接続文字列をOnConfiguringにじか書きするコードはサンプルで多用されますが、本番のASP.NET Coreアプリでは依存性注入で渡すのが標準です。AddDbContextで登録すればDbContextの寿命がリクエストスコープに揃い、接続文字列も構成(appsettings や環境変数、シークレット)から差し替えられます。

DIに切り替えるときは、上のOnConfiguringのオーバーライドを削除し、DbContextOptionsを受け取るコンストラクタを追加します。これを忘れるとDI経由で渡したオプションが効きません。

public class AppDbContext : DbContext
{
    public AppDbContext(DbContextOptions<AppDbContext> options) : base(options) { }

    public DbSet<Employee> Employees => Set<Employee>();
}

// Program.cs
builder.Services.AddDbContext<AppDbContext>(options =>
    options.UseSqlServer(builder.Configuration.GetConnectionString("Default")));

接続文字列をソースコードに直書きしたままリポジトリへコミットする事故を避けるという意味でも、OnConfiguringは学習用と割り切って構いません。

遅延読み込みの既定値とEF6との差分

GSCでは「ef core disable lazy loading」で当記事が表示されていますが、EF Coreの遅延読み込みはそもそもオプトインです。何もしなければ動きません。有効化するにはMicrosoft.EntityFrameworkCore.Proxiesパッケージを入れてUseLazyLoadingProxies()を呼び、ナビゲーションプロパティをvirtualにし、クラスをsealedにしない、という条件をすべて満たす必要があります。つまり「勝手に遅延ロードされて遅い」という症状がEF Coreで出ているなら、どこかで明示的に有効化されているということです。旧来のEF6は遅延読み込みが既定で有効だったため、移行時にこの差分が混乱の原因になります。

エンティティ設計の4つの落とし穴|API露出・Lombok・N+1・detached

入門記事の多くはここまでで終わりますが、実際に障害になるのは以下です。結論から書くと、エンティティは「DBに近い場所に閉じ込める」ほど安全になります。

エンティティをAPIレスポンスや画面に直接渡さない

エンティティをそのままJSONにして返すと、テーブルの内部構造が外部仕様として固定されます。列を1つリネームしただけでAPIの破壊的変更になり、パスワードハッシュや内部フラグのような外に出してはいけない列も、うっかり増やした瞬間に露出します。関連を持つエンティティをシリアライズすると循環参照や意図しない大量ロードも起きます。境界を越えるときはDTOへ詰め替える、という一手間を省かないでください。

Lombokの@Dataとequals/hashCodeの罠

JavaのエンティティにLombokの@Dataを付けるとequalshashCodetoStringが全フィールドから自動生成されます。これは3つの問題を同時に生みます。toStringが遅延ロードの関連をたどってSQLを撒く。hashCodeが可変フィールドに依存するため、Setに入れた後でIDが採番されると等価判定が壊れる。全フィールドを見るequalsが未初期化の関連に触れ、セッションが閉じていればLazyInitializationExceptionになる。エンティティには@Getter@Setterを個別に付け、equalshashCodeはビジネスキーまたはIDだけを使って自分で書くのが安全側です。

遅延ロードとN+1問題

一覧100件を取得し、各行のループ内で関連を参照すると、遅延ロードが1件ずつSQLを発行して合計101本になります。これがN+1問題です。JPAならJOIN FETCH@EntityGraph、EF CoreならIncludeで、必要な関連を最初のクエリで取り切るのが対処です。ログにSQLを出して本数を数えるだけで検出できるので、性能問題が出たらまず発行SQL数を見てください。

Hibernate 7系ではSession.update()が廃止されmerge()へ一本化

Hibernate ORM 7.0でSession#saveupdatesaveOrUpdateが削除されました。detachedインスタンスをそのまま管理下へ戻すレガシーAPIは無くなり、merge()が返す「コンテキスト管理下の新しいインスタンス」を以降で使う形に一本化されています。merge()の戻り値を捨てて元のインスタンスを触り続けるコードは、変更が保存されません。

あわせて、detached状態のエンティティに対するrefresh()lock()も不可になりました(設定hibernate.allow_refresh_detached_entityごと削除)。CascadeType.SAVE_UPDATEの削除により、cascade指定の関連がdetachedインスタンスを参照しているとフラッシュ時にEntityExistsExceptionが飛びます。Spring Boot 4.0はHibernate 7.1を同梱するため、3系から上げるとこの一連の変更をまとめて踏みます。コンパイルは通るのに実行時に落ちるのが厄介な点です。

よくある質問

エンティティクラスとは何ですか?

データベースのテーブルに対応させたクラスです。1つのインスタンスが1レコード、フィールドが列に対応し、ORM(JPAやEF Core)がこの対応関係をもとにSQLを組み立てます。

エンティティとエンティティクラスの違いは何ですか?

エンティティは「業務上の識別可能なモノ」を指す概念で、DB設計では実体(顧客、注文など)を意味します。エンティティクラスは、その概念をプログラム上で表現し、ORMがテーブルへマッピングできるようにした実装の型です。概念が先、クラスは後です。

エンティティとDTOの違いは何ですか?

エンティティは永続化コンテキストに追跡され、値を書き換えるとDBに反映されうる状態を持ちます。DTOは層をまたいでデータを運ぶだけの入れ物で、書き換えてもDBには影響しません。APIや画面との境界ではDTOへ詰め替えます。

エンティティの具体例を教えてください

employees テーブル(id・name・hire_date)に対する Employee クラスが典型例です。id・name・hireDate の3フィールドを持ち、1人分の従業員データが1インスタンスになります。

JavaとC#でエンティティクラスの書き方はどう違いますか?

Javaは@Entity@Idを付け、引数なしコンストラクタ(public または protected)が必須です。C#のEF Coreは属性なしのPOCOで書け、Idという名前のプロパティが規約で主キーになります。コンテキストはJavaがEntityManager、C#がDbContextです。

EF Coreで遅延読み込みを無効にするには?

EF Coreの遅延読み込みは既定で無効(オプトイン)なので、通常は何もする必要がありません。有効になっている場合はUseLazyLoadingProxies()の呼び出しが入っているはずなので、それを外します。EF6は既定で有効だった点が異なります。

関連記事

資料請求

RELATED POSTS 関連記事