エンティティクラスとは、データベースの1テーブルに対応させたクラスです。1インスタンスが1レコード、フィールドが列にあたり、ORM(JPA/Hibernate、Entity Framework Coreなど)はこのクラスをもとにSQLを組み立てます。この記事では、DTO・モデル・テーブルとの違いを先に整理し、Jakarta Persistence 3.2 の仕様が課す要件とアノテーションの書き方、さらに Hibernate ORM 7.4.13 と EF Core 10 で実際に発行されたSQLを見ながら、設計で事故になりやすい点まで扱います。
まとめ
- エンティティクラス=テーブルに対応するクラス。1インスタンス=1レコード、フィールド=列。
- エンティティはORMに変更を追跡される「永続化される状態」を持つ。画面やAPIとの受け渡しはDTOに任せ、両者を兼ねない。
- JPAではエンティティと主キーのマッピング、およびpublic か protected の引数なしコンストラクタが必須。final・record・enum は不可。import は
jakarta.persistence(javax.persistenceは Spring Boot 3以降の標準JPA構成では使わない)。 - Hibernate 7.4.13 では、引数なしコンストラクタが無くても起動とINSERTは通り、別の永続化コンテキストで読み込んだ時点で
InstantiationExceptionになった。起動が通ったことを安全の根拠にしない。 - EF Core はPOCOでよく、コンストラクタバインディングにより引数なしコンストラクタも不要。遅延読み込みは既定で無効。
エンティティクラスの意味とテーブルとの対応
ORM(Object-Relational Mapping)は、リレーショナルDBの行とプログラム上のオブジェクトを相互に変換する仕組みです。その変換先として宣言するクラスがエンティティクラスです。ORMそのものの仕組みはORMとは?O/Rマッパーの仕組み・メリットとデメリットをSQL比較と実行結果で解説で扱っています。
テーブル・レコード・列との対応関係
employees テーブルに id・name・hire_date の3列があれば、エンティティクラス Employee は id・name・hireDate の3フィールドを持ちます。SELECTで取り出した1行が Employee のインスタンス1つになり、そのフィールドを書き換えてトランザクションを確定するとUPDATEが発行されます。
ただし1テーブル=1クラスとは限りません。継承関係を1テーブルに詰める単一テーブル継承(@Inheritance(strategy = InheritanceType.SINGLE_TABLE))や、2テーブルを1クラスに束ねる@SecondaryTableもあります。テーブルは保存の形、エンティティはアプリから見たデータの形で、通常は一致させるが別物、と理解しておくと設計の議論がかみ合います。
「エンティティ」が指す3つの意味
同じ言葉が文脈によって別のものを指します。
| 文脈 | エンティティの意味 | 例 |
|---|---|---|
| DB設計(ER図) | 管理対象の実体・テーブルの候補 | 顧客、注文、商品 |
| ORM(JPA・EF Core) | テーブルにマッピングされるクラス | @Entity を付けた Employee |
| DDD | IDで同一性を判定するドメインオブジェクト | 状態が変わっても同じ会員 |
この記事の「エンティティクラス」は2行目です。DDDのエンティティは永続化の都合と無関係に定義されるので、DDDの議論で@Entityを思い浮かべると話がずれます。DDDの用語はドメイン駆動設計(DDD)とは?意味・基本用語・実装と採用判断をわかりやすく解説で確認できます。
エンティティとDTO・モデルクラスの違い
3語は役割と適用範囲が異なります。エンティティは永続化対象、DTOはデータ転送用の型、モデルは文脈依存の総称です。変更追跡の有無は、エンティティの状態やORMの設定にも依存します。
| 種類 | 役割 | 変更の追跡 | 置き場所 |
|---|---|---|---|
| エンティティ | DBの行を表す永続化対象 | 管理・追跡中の変更を保存時に反映 | 永続化層 |
| DTO | 層の間でデータを運ぶ入れ物 | なし | APIの入出力、画面 |
| モデル | 文脈依存の総称 | 文脈による | MVCのM、設計文書 |
「モデル」は MVC のモデル層全体を指すこともあれば、Rails の ActiveRecord のようにエンティティそのものを指すこともあります。チーム内で「モデル」と言ったときにどれを指すかを決めておくのが先です。DTOの書き方と入力検証はDTOとは?意味・Entityとの違いとNestJSのValidationPipe設定【v12実測】、DTOをJavaの record で書く方法はJavaのレコードクラス(record)とは?使い方・コンストラクタ・継承できない理由をJava 25で実測にまとめています。
JPAエンティティの要件と基本実装
Jakarta Persistence(JPA)はJavaの永続化仕様で、実装として Hibernate ORM が広く使われます。2026年10月時点の最新安定版は Jakarta Persistence 3.2、Hibernate ORM 7.4.13.Final(Maven Central)で、Spring Boot 4.1.1 の依存管理は Hibernate 7.4.5.Final を、4.0.8 は 7.2.24.Final を指定しています。Jakarta Persistence 4.0 はマイルストーン(4.0.0-M7)の段階です。実装の選び方はJava ORM(O/Rマッパー)の種類と選び方|Hibernate・MyBatis・jOOQを比較【2026年10月】を参照してください。
仕様が定めるエンティティクラスの要件
Jakarta Persistence 3.2 仕様の2.1節が課す条件は次のとおりです。
@Entityを付ける(またはXMLで宣言する)。主キーは@Idや@EmbeddedId、または対応するXML設定でマッピングする。- トップレベルクラスか static な内部クラスであること。enum・record・interface はエンティティにできない。
- public か protected の引数なしコンストラクタを持つ。アプリ用の引数付きコンストラクタは追加してよい。
- クラスを final にしない。メソッドと永続化フィールドも final にしない。
record が使えないのは、エンティティがORMに中身を書き換えられる可変オブジェクトだからです。一方、複合主キーのクラスは record で書いてよいと同じ仕様に明記されています。なお Java のORMでも Doma 3 は@Entityを付けた record をイミュータブルなエンティティとして扱えるので、「エンティティ=可変」はJPAの前提だと押さえておきます(Domaの使い方はDoma2とは?Doma 3との違い・移行判断と使い方【3.14.1で実測】)。
最小のエンティティクラスとjakarta名前空間
上位の解説記事にはimport javax.persistence.Entity;のコードが残っていますが、Jakarta Persistence 3.0 でパッケージはjakarta.persistenceに変わり、Spring Boot 3.0 以降の標準JPA構成ではjavax.persistenceの注釈は認識されません。現行の書き方は次のとおりです。
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;
private LocalDate hireDate;
@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "department_id")
private Department department;
protected Employee() { } // JPA用。アプリからは呼ばない
public Employee(String name, LocalDate hireDate, Department department) {
this.name = name;
this.hireDate = hireDate;
this.department = department;
}
}
検証では、Department も同じ形(@Id付きの id と name、protected の引数なしコンストラクタ)で定義し、name を書き換える処理を同じパッケージから呼んでいます。引数なしコンストラクタを protected にすると、別パッケージの通常の呼び出し元からの生成を制限できます。ただし同一パッケージからは呼べるため、必須項目は引数付きコンストラクタなどで別途検証します。
よく使うアノテーションの対応表
| アノテーション | 対応するDB側の要素 | 省略したときの既定 |
|---|---|---|
| @Table(name) | テーブル名 | エンティティ名(クラス名) |
| @Column(name, nullable, length) | 列名・NOT NULL・桁数 | 属性名、NULL可、length=255(対象型のみ) |
| @Id | 主キー | @EmbeddedId・XML等で代替可 |
| @GeneratedValue(strategy) | 採番方式 | AUTO(DBに応じて選択) |
| @Version | 楽観ロック用の版番号列 | なし |
| @Enumerated | enumの保存形式 | 通常ORDINAL(下記の例外あり) |
| @Transient | 永続化対象から除外 | アクセス方式・属性の種類による |
| @ManyToOne / @OneToMany | 外部キーによる関連 | ManyToOneはEAGER |
注意したいのは2つの既定です。@Enumeratedも変換器も指定せず、enumに@EnumeratedValue付きフィールドもない通常のマッピングでは、enumは序数で保存され、enum定数の並びを変えた瞬間に既存データの意味が変わります。EnumType.STRINGを明示するのが安全側です。また@ManyToOneの既定はEAGERなので、上のコードのようにFetchType.LAZYを明示しないと一覧取得のたびに関連を読みにいきます。外部キーそのものの設計は外部キーとは:制約の書き方とON DELETEの選び方・後付け手順を実装目線で解説、関連をまたいだ保存・削除の伝播はJPAのcascade(CascadeType.ALL・PERSIST・REMOVE)の使い分けとON DELETE CASCADEとの違い【Hibernate 7.4実測】、@Versionは楽観ロックと悲観ロックの違いと使い分け|PostgreSQL・MySQLで実測で扱っています。
Hibernate 7.4.13によるエンティティの実行結果
ここからは、上の Employee と Department を Hibernate ORM 7.4.13.Final・H2 2.5.252・JDK 25.0.4 で動かし、show_sqlで出力したSQLをそのまま載せます。
生成されるDDLとSpring Bootの命名変換
Hibernate単体でhireDateフィールドを持つエンティティからテーブルを生成すると、列名はフィールド名のままです。
create table employees (hireDate date, department_id bigint,
id bigint generated by default as identity, name varchar(100) not null, primary key (id))
alter table if exists employees add constraint FKgy4qe3dnqrm3ktd76sxp7n4c2
foreign key (department_id) references departments
Spring Boot はキャメルケースをスネークケースへ変える物理命名戦略を既定で設定します。公式ドキュメントはその名前をCamelCaseToUnderscoresNamingStrategyと書いていますが、Spring Boot 4.1.1 のソース(HibernateProperties)が指定しているのはPhysicalNamingStrategySnakeCaseImplです。前者は Hibernate 7 で@Deprecated(since = "7", forRemoval = true)となり、後者を継承しただけのクラスになっています。PhysicalNamingStrategySnakeCaseImplを設定して実行すると、列名はhire_dateに変わりました。application.ymlに旧クラス名を書き写している場合は、削除予定のクラスを参照していることになります。Spring Boot で動かしたエンティティを素のHibernateやバッチへ持ち出すと、列名が合わずにエラーになるのはこの差です。どちらの環境でも同じ名前にしたいなら@Column(name = "hire_date")を明示します。
管理中エンティティの変更検出とUPDATE
トランザクション内でfindしたエンティティのnameを書き換えただけで、saveもupdateも呼ばずに次のSQLが出ました。
select e1_0.id,e1_0.department_id,e1_0.hireDate,e1_0.name from employees e1_0 where e1_0.id=?
update employees set department_id=?,hireDate=?,name=? where id=?
永続化コンテキストが読み込み時の状態を覚えていて、コミット時に差分を検出する仕組み(ダーティチェック)です。エンティティを画面表示用に加工するつもりで書き換えると、そのままDBに書き戻されます。DTOへ詰め替える理由の1つがこれです。
4つの状態とmerge()の戻り値
エンティティは new(transient)・managed・detached・removed の4状態を移ります。EntityManager#containsで確かめた結果は次のとおりです。
new: contains=false id=null
managed: contains=true id=1
detached: contains=false
detached中の変更はコミットしても反映されない: 営業部
merge後: 戻り値contains=true 引数contains=false
removed: contains=false
detached のインスタンスを書き換えてもDBは変わりません。merge()は引数を管理下に戻すのではなく、管理下にある別のインスタンスを返します。戻り値を捨てて引数を触り続けるコードは、変更が保存されません。Hibernate ORM 7.0 でSession#save/update/saveOrUpdateが削除され、detached の復帰はmerge()に一本化されたので(Hibernate 7.0移行ガイド)、Spring Boot 3 から 4 へ上げるときにこの書き方の違いがまとめて表に出ます。
引数なしコンストラクタ欠落時の読み込みエラー
引数付きコンストラクタしか持たないエンティティを用意したところ、起動時はINFOログが1行出ただけで、persistによるINSERTも成功しました。例外になったのは、別のトランザクション(新しい永続化コンテキスト)でfindして読み込んだときです。同じコンテキスト内なら管理中のインスタンスがそのまま返るので、この例外は出ません。
INFO: HHH000182: No default (no-argument) constructor for class [demo.NoDefaultCtor]
(class must be instantiated by Interceptor)
insert into NoDefaultCtor (name,id) values (?,?)
select ndc1_0.id,ndc1_0.name from NoDefaultCtor ndc1_0 where ndc1_0.id=?
org.hibernate.InstantiationException: No default constructor for entity 'demo.NoDefaultCtor'
書き込みしか通らないテストでは見つからず、本番で一覧画面を開いた時点で初めて落ちる、という順番で表に出ます。Lombokの@Builderや@AllArgsConstructorだけを付けたエンティティで起きやすいので、@NoArgsConstructor(access = AccessLevel.PROTECTED)を併記し、テストには必ず読み込みを含めてください。
C#のエンティティクラスとDbContext(EF Core 10)
C#では、属性を持たないただのクラス(POCO)をエンティティとし、DbContextを継承したクラスにDbSetとして登録します。EF Core 10 は2025年11月リリースのLTSで、サポートは2028年11月10日までです(NuGetの最新は10.0.13。EF Core 11 は2026年10月時点でRC段階)。
最小構成とコンストラクタバインディング
public class Employee(string name, DateOnly hireDate)
{
public int Id { get; set; }
public string Name { get; set; } = name;
public DateOnly HireDate { get; set; } = hireDate;
public int? DepartmentId { get; set; }
public Department? Department { get; set; }
}
public class AppDbContext(DbContextOptions<AppDbContext> options) : DbContext(options)
{
public DbSet<Employee> Employees => Set<Employee>();
public DbSet<Department> Departments => Set<Department>();
}
// Program.cs(ASP.NET Core)
builder.Services.AddDbContext<AppDbContext>(o =>
o.UseSqlServer(builder.Configuration.GetConnectionString("Default")));
掲載例は ASP.NET Core+SQL Server 用の抜粋で、Microsoft.EntityFrameworkCore.SqlServerパッケージと、Id・Name を持つ Department クラスが別に必要です。以下の実測ではMicrosoft.EntityFrameworkCore.SqliteとUseSqliteに差し替えています。Idという名前のプロパティは規約で主キーになり、[Key]は不要です。JPAと違って引数なしコンストラクタも要りません。EF Core はプロパティ名と型が一致する引数を持つコンストラクタがあればそれを呼んで値を渡す(コンストラクタバインディング)ため、上のプライマリコンストラクタだけのクラスでも EF Core 10.0.13+SQLite で保存・読み込みとも通りました。接続文字列はOnConfiguringに直書きせず、上のようにAddDbContextで依存性注入から渡すのが本番の形です。
遅延読み込みは既定で無効
同じ環境でIncludeを付けずに Employee を読むと、Departmentはnullでした。Include(x => x.Department)を付けると「開発部」が入ります。EF Core の遅延読み込みは、Microsoft.EntityFrameworkCore.Proxiesを入れてUseLazyLoadingProxies()を呼び、ナビゲーションをvirtualにすることで有効化できるオプトインの機能です。EF6 は既定で有効だったため、移行時に関連がnullになる場合は、遅延読み込みの有効化設定と、関連データを取得する処理を確認します。追跡中のエンティティのNameを書き換え、EntryやSaveChanges()などで変更検出が行われるとState=Modifiedになり、SaveChanges()でUPDATEが出る点はJPAのダーティチェックと同じです。
エンティティ設計で事故になる3つの場面
この節では、APIとの境界でDTOを使う設計を勧めます。エンティティの直接公開、可変値による等価性の判定、関連の逐次読み込みが引き起こす問題を確認します。
エンティティのAPI直接公開によるリスク
エンティティをそのままJSONにして返すと、テーブル構造が外部仕様として固定されます。公開するプロパティ名を変更するとAPIの破壊的変更になり得ます。また、パスワードハッシュなどの機密情報を持つプロパティをシリアライズ対象にすると、レスポンスへ露出します。関連を持つエンティティは循環参照や想定外の大量読み込みも起こします。境界を越えるときはDTOへ詰め替えてください。Spring Boot のspring.jpa.open-in-viewが既定 true のままだと、この大量読み込みがビュー層で気づかれずに走ります(spring.jpa.open-in-viewとは?OSIVの既定値trueと起動時警告の消し方・falseの判断基準)。
可変フィールドを使うequals/hashCodeの問題
Lombokの@Dataは既定でstatic・transientを除くフィールドからequals/hashCodeを生成します。id と name でhashCodeを組んだエンティティをHashSetに入れてからpersistしたところ、採番で id が null から 1 に変わり、同じインスタンスなのにcontainsが false に変わりました。
persist前 contains=true
select next value for BadEq_SEQ
insert into BadEq (name,id) values (?,?)
persist後 id=1 contains=false
Setから消せない・重複して入る、という形で表に出ます。エンティティには@Getter/@Setterを個別に付け、equalsは同一参照を等しいとし、それ以外ではエンティティ型とnullでない id を比較します。業務キーを使う場合は、一意で変更されない値を選びます。生成IDで比較する場合のhashCodeはクラスごとの固定値を返す書き方が安全側です。@DataのtoStringが遅延ロードの関連をたどってSQLを発行する問題もあります。
ループ内で関連を参照するN+1
一覧100件のそれぞれが未読込の異なる関連を持ち、キャッシュやバッチ取得が効かずに関連を1件ずつ取得する場合、一覧取得1本と関連取得100本で合計101本になります。JPAならJOIN FETCHか@EntityGraph、EF CoreならIncludeなどで必要な関連の取得を明示します。ただし、EntityGraphはSQL一本化の保証ではなく、EF CoreのIncludeも分割クエリ設定では複数のSQLになります。発行SQL数の数え方とORM別の対策はN+1問題とは?発行クエリ数の実測と検出方法・ORM別の対策にまとめました。エンティティ設計から既存システムの作り直しまでまとめて相談したい場合は、フルスクラッチ開発の支援も行っています。
よくある質問
エンティティクラスとは何ですか?
データベースのテーブルに対応させたクラスです。1インスタンスが1レコード、フィールドが列にあたり、JPAやEF CoreなどのORMがこの対応をもとにSQLを組み立てます。
エンティティとはどういう意味ですか?
英語の entity は「実体」です。DB設計では顧客・注文のような管理対象を、ORMではテーブルにマッピングされるクラスを、DDDではIDで識別するドメインオブジェクトを指します。
ModelとEntityの違いは何ですか?
Entityはテーブルに対応し、ORMに変更を追跡される永続化対象です。Modelは文脈依存の総称で、MVCのモデル層全体を指すこともあれば、Railsのようにエンティティそのものを指すこともあります。
Entityとtableの違いは何ですか?
tableはDBに保存されるデータの形、Entityはアプリから見たデータの形です。通常は1対1で対応させますが、継承を1テーブルに詰めたり2テーブルを1クラスに束ねたりもできます。
エンティティとDTOの違いは何ですか?
エンティティは書き換えるとDBに反映されうる状態を持ち、DTOは層をまたいでデータを運ぶだけの入れ物です。APIや画面との境界ではDTOへ詰め替えます。
Javaのrecordはエンティティクラスにできますか?
JPAではできません。Jakarta Persistence 3.2 の2.1節が record をエンティティに指定することを禁じています。複合主キーのクラスやDTOには使えます。Doma 3 は record をイミュータブルなエンティティとして扱えます。