はじめに
この記事は DevNav Handbook の記事です。
今回は、PostgreSQLに作成した users テーブルを、Spring Boot側で AppUser Entityとして表現します。
Entityは、Spring BootアプリケーションとDBテーブルをつなぐ重要なクラスです。
ただ、@Entity、@Table、@Column、@NoArgsConstructor、@Data まわりは、最初はかなり混乱しやすいです。
この記事では、users テーブルを例に、JPA Entityの基本を整理します。
コードを細かく読むというより、DBの1行をJava側でどう扱えるようにするのか、そして なぜEntityで@Dataを安易に使わないのか を理解することを重視します。
冒頭理解度チェック
以下を説明できる人は、この記事を読まなくても大丈夫です。
@Entity と @Table(name = "users") の違いを説明できる
DBの firebase_uid カラムと Javaの firebaseUid フィールドがどう対応するか説明できる
JPA Entityに引数なしコンストラクタが必要な理由を説明できる
@NoArgsConstructor(access = AccessLevel.PROTECTED) の意味を説明できる
Entityで @Data を安易に使わない理由を説明できる
public AppUser(...) のような値ありコンストラクタを置く理由を説明できる
この記事の後半に、上記チェック項目の答え合わせも入れています。
実装コードをすべて読まなくても、理解度チェックの答え合わせを見るだけで、今回のポイントは復習できます。
この記事のゴール
この記事のゴールは以下です。
PostgreSQLの users テーブルを
Spring Boot側の AppUser Entity として表現する
今回扱う内容は以下です。
@Entity
@Table
@Id
@GeneratedValue
@Column
@Getter
@NoArgsConstructor(access = AccessLevel.PROTECTED)
値ありコンストラクタ
@Dataを避ける理由
今回の記事では、まだ以下は扱いません。
Repository
Controller
GET /api/users
Firebase Token検証
Spring Security連携
Entityの理解だけに集中します。
前提:usersテーブル
今回、PostgreSQL側には以下の users テーブルがある前提です。
CREATE TABLE users (
id BIGSERIAL PRIMARY KEY,
firebase_uid VARCHAR(128) UNIQUE,
email VARCHAR(255) NOT NULL UNIQUE,
display_name VARCHAR(100),
role VARCHAR(50) NOT NULL,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);
このテーブルを、Java側では AppUser というEntityで扱います。
今回作るAppUser Entity
今回作るEntityは以下です。
package com.example.devnav.user;
/**
* アプリ側で利用するユーザー情報を表すEntity。
*/
@Getter
@NoArgsConstructor(access = AccessLevel.PROTECTED)
@Entity
@Table(name = "users")
public class AppUser {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
@Column(name = "firebase_uid", nullable = false, unique = true)
private String firebaseUid;
@Column(nullable = false, unique = true)
private String email;
@Column(name = "display_name")
private String displayName;
@Column(nullable = false)
private String role;
@Column(name = "created_at")
private LocalDateTime createdAt;
/**
* アプリ側の処理からAppUserを新規作成するためのコンストラクタ。
*
* @param firebaseUid Firebase Authenticationで発行されたユーザーUID
* @param email メールアドレス
* @param displayName 画面表示用のユーザー名
* @param role アプリ側で利用する権限
*/
public AppUser(String firebaseUid, String email, String displayName, String role) {
this.firebaseUid = firebaseUid;
this.email = email;
this.displayName = displayName;
this.role = role;
}
}
今回のポイントは、DBの users テーブルの1行を、Java側では AppUser オブジェクトとして扱えるようにすることです。
usersテーブルの1行
↓
AppUser Entity
EntityはDBテーブルとJavaクラスをつなぐ
@Entity は、このクラスをJPA管理対象にするためのアノテーションです。
@Entity
public class AppUser {
}
これを付けることで、JPAは AppUser をDBテーブルと対応するクラスとして扱えるようになります。
一方、@Table(name = "users") は、このEntityがどのテーブルに対応するかを明示するために使います。
@Table(name = "users")
今回のDBテーブル名は users です。
そのため、AppUser クラスに @Table(name = "users") を付けています。
AppUserクラス
↓
usersテーブル
@Entity はJPA管理対象にする指定です。
@Table(name = "users") は対応するテーブル名を指定するものです。
似ていますが、役割は違います。
DBカラムとJavaフィールドの対応
今回のEntityでは、DBのカラムとJavaのフィールドが以下のように対応します。
DBの id
→ Javaの id
DBの firebase_uid
→ Javaの firebaseUid
DBの email
→ Javaの email
DBの display_name
→ Javaの displayName
DBの role
→ Javaの role
DBの created_at
→ Javaの createdAt
Javaでは firebaseUid のようなキャメルケースを使います。
一方、DBでは firebase_uid のようなスネークケースを使うことが多いです。
この名前の違いを対応させるために、@Column(name = "firebase_uid") を使います。
@Column(name = "firebase_uid", nullable = false, unique = true)
private String firebaseUid;
これにより、JPAに以下を伝えています。
Javaの firebaseUid フィールドは
DBの firebase_uid カラムに対応する
なぜprotectedの空コンストラクタが必要なのか
今回のEntityには以下を付けています。
@NoArgsConstructor(access = AccessLevel.PROTECTED)
これは、Lombokによって以下のようなコンストラクタを自動生成する指定です。
protected AppUser() {
}
JPAはDBからデータを取得するときに、DBの1行をJavaのEntityオブジェクトとして組み立てます。
このとき、JPAはまず空のEntityを作る必要があります。
そのため、Entityには引数なしコンストラクタが必要です。
ただし、public AppUser() にすると、アプリ側のどこからでも空の AppUser を作れてしまいます。
AppUser user = new AppUser();
この時点では、email や role などが入っていない不完全な状態です。
そのため、空のコンストラクタはJPA用に残しつつ、通常のアプリ処理からは呼びにくい protected にしておきます。
protected AppUser()
→ JPAがDBからEntityを作るための入口
public AppUser()
→ どこからでも空のEntityを作れてしまうため避けたい
DBからEntityを作るとは何か
例えば、DBの users テーブルに以下の1行があるとします。
id: 1
firebase_uid: abc123
email: admin@example.com
display_name: admin
role: ADMIN
created_at: 2026-06-09 14:00:00
Spring Boot側でRepositoryを使って取得すると、JPAはこのDBの1行を AppUser オブジェクトとして扱えるようにします。
DBの1行
↓
AppUserオブジェクト
もう少し具体的に言うと、以下の対応が行われます。
DBの id
→ AppUserの id
DBの firebase_uid
→ AppUserの firebaseUid
DBの email
→ AppUserの email
DBの display_name
→ AppUserの displayName
DBの role
→ AppUserの role
DBの created_at
→ AppUserの createdAt
このように、DBの値をJavaのフィールドに対応させて、Javaコードから扱える形にするのがJPAの役割です。
なぜ値ありコンストラクタを置くのか
Entityには、値ありコンストラクタも用意しています。
public AppUser(String firebaseUid, String email, String displayName, String role) {
this.firebaseUid = firebaseUid;
this.email = email;
this.displayName = displayName;
this.role = role;
}
これは、Spring Boot側で新しいユーザーを作るときに使います。
例えば、後で初回ログイン時にFirebaseの情報をもとにユーザーをDBへ保存する場合、以下のように使えます。
AppUser user = new AppUser(
firebaseUid,
email,
displayName,
"ADMIN"
);
appUserRepository.save(user);
つまり、このコンストラクタは以下のためにあります。
Spring Boot側で新しいAppUserを作れるようにするため
空のEntityではなく、必要な値が入った状態のEntityを作る入口です。
protected空コンストラクタと値ありコンストラクタの違い
整理すると、役割は以下です。
protected AppUser()
→ JPAがDBの値をAppUserとして扱うために必要
public AppUser(firebaseUid, email, displayName, role)
→ Spring Boot側で新しいユーザーを作るために使う
この2つは役割が違います。
JPA用とアプリ用で、Entityの生成ルートを分けているイメージです。
なぜEntityで@Dataを使わないのか
Lombokの @Data は便利ですが、Entityでは安易に使わない方が安全です。
@Data には以下が含まれます。
@Getter
@Setter
@RequiredArgsConstructor
@ToString
@EqualsAndHashCode
Entityで特に注意したいのは以下です。
@Setter
@ToString
@EqualsAndHashCode
@Setter があると、どこからでもEntityの値を書き換えられます。
user.setRole("ADMIN");
user.setEmail("changed@example.com");
もちろん必要な場合もありますが、無制限にSetterを生やすと、どこで値が変わったのか追いにくくなります。
Entityでは、必要な変更処理だけをメソッドとして用意する方が安全です。
例えば将来的には以下のような形です。
public void changeDisplayName(String displayName) {
this.displayName = displayName;
}
また、@ToString はリレーションが増えたときに、関連先まで出力しようとしてログが膨らんだり、循環参照の原因になったりすることがあります。
@EqualsAndHashCode も、JPA EntityのID採番や永続化前後の状態と相性が悪くなることがあります。
そのため、まずは以下の方針で十分です。
JPA Entityでは @Data を雑に付けない
@Getterから始める
必要な変更メソッドだけ自分で用意する
冒頭理解度チェックの答え合わせ
ここで、冒頭理解度チェックの答えを整理します。
全部読まなくても、ここを見れば今回のポイントを復習できます。
@Entity と @Table(name = "users") の違い
@Entity
→ このクラスをJPA管理対象にする。
@Table(name = "users")
→ このEntityがDBのusersテーブルに対応することを明示する。
DBの firebase_uid カラムと Javaの firebaseUid フィールドの対応
DBでは firebase_uid。
Javaでは firebaseUid。
@Column(name = "firebase_uid") を付けることで、
Javaの firebaseUid と DBの firebase_uid を対応させる。
JPA Entityに引数なしコンストラクタが必要な理由
JPAがDBからデータを取得するとき、
まず空のEntityを作り、そこにDBの値を詰めていくため。
そのため、Entityには引数なしコンストラクタが必要。
@NoArgsConstructor(access = AccessLevel.PROTECTED) の意味
protectedな引数なしコンストラクタをLombokで生成する指定。
JPAには使わせる。
ただし、アプリ側から雑に空のEntityを作らせない。
Entityで @Data を安易に使わない理由
@Dataには @Setter、@ToString、@EqualsAndHashCode などが含まれる。
Setterでどこからでも値を書き換えられる。
ToStringはリレーションで大量出力や循環参照の原因になることがある。
EqualsAndHashCodeはJPA EntityのID採番や永続化前後の状態と相性が悪いことがある。
そのため、Entityではまず @Getter から始める。
public AppUser(...) のような値ありコンストラクタを置く理由
Spring Boot側で新しいAppUserを作るため。
JPA用のprotected空コンストラクタとは役割が違う。
protected AppUser()
→ JPAがDBからEntityを復元するため
public AppUser(...)
→ アプリ側で新しいEntityを作成するため
次回やること
次回は、この AppUser Entityを使って、Spring Bootから users テーブルを取得します。
次回のゴールは以下です。
AppUserRepositoryを作成する
UserControllerを作成する
GET /api/users でusersテーブルのデータを取得する
Entity単体では、まだDBアクセスはできません。
次回はRepositoryとControllerを作り、実際にSpring BootからPostgreSQLの users テーブルを取得できるようにします。
まとめ
今回は、PostgreSQLの users テーブルを、Spring Boot側の AppUser Entityとして表現しました。
Entityは、DBテーブルとJavaクラスを対応させる重要な層です。
今回のポイントは以下です。
@EntityでJPA管理対象にする
@Tableで対応するテーブル名を明示する
@ColumnでDBカラムとJavaフィールドを対応させる
JPA用にprotectedの空コンストラクタを用意する
アプリ側の新規作成には値ありコンストラクタを使う
Entityで@Dataを安易に使わない
まずはこのレベルで腹落ちしていれば、RepositoryやControllerに進めます。