Spring BootでJPAを使ってエンティティクラスを作成していると、次のようなコードを見かけることがあります。
@Entity
public class Client {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
private String name;
protected Client() {
}
public Client(String name) {
this.name = name;
}
}
ここで疑問になるのが、次の部分です。
protected Client() {
}
なぜ、わざわざ中身のないprotectedコンストラクタを宣言するのでしょうか。
この記事では、Spring BootでJPAを使う場合に、エンティティクラスへprotectedのデフォルトコンストラクタを宣言する理由を整理します。
結論
JPAでは、エンティティクラスに引数なしコンストラクタが必要です。
その理由は、JPAがデータベースから取得した値をもとに、エンティティオブジェクトを生成する必要があるためです。
そして、そのコンストラクタはpublicまたはprotectedである必要があります。
protected Client() {
}
と書くことで、JPAからは利用できる一方で、アプリケーション側から不用意に空のエンティティを作られにくくできます。
JPAはエンティティを生成する必要がある
JPAでは、データベースのレコードをJavaのオブジェクトとして扱います。
例えば、次のようなテーブルがあるとします。
clients
-------------
id | name
-------------
1 | 山田太郎
このデータを取得すると、JPAは内部的に次のようなイメージでエンティティを生成します。
Client client = new Client();
client.id = 1L;
client.name = "山田太郎";
実際にこのようなコードを直接書くわけではありませんが、JPAはリフレクションなどの仕組みを使って、エンティティのインスタンスを作成し、フィールドに値を設定します。
そのため、JPAがエンティティを生成するためには、引数なしのコンストラクタが必要になります。
デフォルトコンストラクタを書かないとどうなるか
Javaでは、コンストラクタを1つも定義していない場合、自動的に引数なしのコンストラクタが用意されます。
例えば、次のクラスには暗黙的にデフォルトコンストラクタがあります。
public class Client {
private String name;
}
しかし、次のように引数ありコンストラクタを定義すると、Javaは自動でデフォルトコンストラクタを作成しません。
@Entity
public class Client {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
private String name;
public Client(String name) {
this.name = name;
}
}
この状態だと、JPAがエンティティを生成できず、実行時エラーの原因になります。
そのため、明示的に引数なしコンストラクタを定義します。
protected Client() {
}
なぜpublicではなくprotectedにするのか
引数なしコンストラクタが必要なら、次のようにpublicでもよさそうです。
public Client() {
}
実際、JPAの仕様上はpublicでも問題ありません。
しかし、エンティティの設計としては、protectedにすることがよくあります。
理由は、アプリケーション側から空のエンティティを自由に作らせたくないためです。
例えば、コンストラクタがpublicだと、次のようなコードが書けてしまいます。
Client client = new Client();
この時点では、nameなどの必要な値が設定されていない可能性があります。
エンティティには、本来「必ず名前を持つ」「必ずメールアドレスを持つ」といったルールがあるかもしれません。
しかし、空のコンストラクタをpublicにしてしまうと、不完全な状態のオブジェクトを簡単に作れてしまいます。
そこで、JPA用の引数なしコンストラクタはprotectedにして、通常の生成には引数ありコンストラクタやファクトリメソッドを使うようにします。
@Entity
public class Client {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
private String name;
protected Client() {
}
public Client(String name) {
if (name == null || name.isBlank()) {
throw new IllegalArgumentException("名前は必須です");
}
this.name = name;
}
}
このようにすると、通常のアプリケーションコードでは次のように生成します。
Client client = new Client("山田太郎");
一方で、JPAはprotectedコンストラクタを使ってエンティティを復元できます。
privateではダメなのか
では、より制限を強くしてprivateにしてもよいのでしょうか。
private Client() {
}
これは避けた方がよいです。
JPAでは、エンティティクラスの引数なしコンストラクタはpublicまたはprotectedである必要があります。
そのため、privateにするとJPAの仕様に合わず、正常に動作しない可能性があります。
基本的には、JPA用のデフォルトコンストラクタはprotectedで宣言するのが無難です。
Lombokを使う場合
Lombokを使っている場合は、次のように書くこともあります。
@NoArgsConstructor(access = AccessLevel.PROTECTED)
@Entity
public class Client {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
private String name;
}
これは、次のコンストラクタをLombokに生成させているのと同じです。
protected Client() {
}
ただし、Lombokを使う場合でも「なぜprotectedの引数なしコンストラクタが必要なのか」は理解しておいた方がよいです。
理由を理解していないと、なんとなく@NoArgsConstructorを付けたり、逆に削除してしまって実行時エラーになることがあります。
エンティティの生成ルールを守る
エンティティは、単なるデータの入れ物ではなく、業務上のルールを持つオブジェクトとして扱うことがあります。
例えば、次のようなルールです。
- クライアント名は必須
- メールアドレスは正しい形式である必要がある
- 案件開始日は終了日より前である必要がある
このようなルールがある場合、空のコンストラクタで自由にインスタンスを作れてしまうと、不正な状態のオブジェクトが生まれやすくなります。
そのため、JPA用のコンストラクタはprotectedにしておき、アプリケーション側では意味のあるコンストラクタやメソッドを使って生成するのがよいです。
public static Client create(String name) {
if (name == null || name.isBlank()) {
throw new IllegalArgumentException("名前は必須です");
}
Client client = new Client();
client.name = name;
return client;
}
ただし、このようなファクトリメソッドを使う場合でも、JPA用のprotectedコンストラクタは必要です。
まとめ
Spring BootでJPAのエンティティクラスを作成する場合、protectedの引数なしコンストラクタを宣言する理由は次の通りです。
- JPAがエンティティを生成するために、引数なしコンストラクタが必要
- JPAの仕様上、引数なしコンストラクタは
publicまたはprotectedである必要がある -
publicにすると、アプリケーション側から不完全なエンティティを生成できてしまう -
protectedにすることで、JPAには利用させつつ、通常のコードからの不用意な生成を防ぎやすくなる - エンティティの生成ルールを守るためにも、JPA用コンストラクタは
protectedにするのがよい
つまり、次のように書くのは、JPAのためであり、同時にエンティティの設計を安全にするためでもあります。
protected Client() {
}
何気なく書いているコードにも、JPAの仕組みとオブジェクト設計上の理由があります。
エンティティクラスを書くときは、「JPAが使うためのコンストラクタ」と「アプリケーションが使うための生成方法」を分けて考えると、より安全で意図の伝わりやすいコードになります。