SQLいらずで簡単!EntityとRepositoryの基本【Spring Boot】

  • LINEで送る
SQLいらずで簡単!EntityとRepositoryの基本

 Spring Bootでデータベースとやり取りする際、毎回SQLを書くのは大変です。そんなときに役立つのが EntityRepository です。EntityはテーブルをJavaクラスとして表現し、Repositoryはその操作窓口となります。さらに、Spring Data JPAを使えばSQLを書かずにCRUD処理が実現できます。この記事では、EntityとRepositoryの基礎から実装方法、Lombokを使った便利な書き方までを、初心者にも分かりやすく解説します。

Entityの基礎

 Entityは、データベースのテーブルをJavaクラスとして表現したものです。1レコードが1オブジェクトに対応し、クラスのフィールドがテーブルのカラムにマッピングされます。Spring Bootでは @Entity アノテーションを使って定義します。

Entityクラスのイメージ

Entityを使うメリット

  • オブジェクト指向で扱える:SQLを書かずにJavaオブジェクトとしてデータを操作可能
  • 保守性向上:テーブル構造の変更をアノテーションで簡単に反映できる
  • 型安全性:文字列ベースのSQLではなく、クラスとフィールドで扱うので型チェックが効く

よく使うアノテーション

アノテーション説明
@Entityこのクラスがテーブルに対応することを宣言
@Table(name = "users")テーブル名を指定(省略時はクラス名が利用される)
@Id主キーを表すフィールド
@GeneratedValue主キーの自動採番方法を指定(例:IDENTITY, AUTOなど)
@Columnカラムの制御(nullable, length, uniqueなどを指定可能)
@Enumerated(EnumType.STRING)列挙型を扱う場合の設定
@Temporal(TemporalType.TIMESTAMP)日時型を扱う場合の設定

Entityの実装方法(コード例つき)

 ここでは、最小構成 → 実用構成 → 発展構成 の順で、段階的に書き方を深めます。併せて「なぜそうするのか」も補足します。

最小構成(まずは動く形)

import jakarta.persistence.*;

@Entity
@Table(name = "users")
public class User {
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY) // MySQLなどで一般的
    private Long id;

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

    @Column(nullable = false, unique = true, length = 120)
    private String email;

    public User() {} // デフォルトコンストラクタ(必須)

    public User(String name, String email) {
        this.name = name;
        this.email = email;
    }

    public Long getId() { return id; }
    public String getName() { return name; }
    public void setName(String name) { this.name = name; }
    public String getEmail() { return email; }
    public void setEmail(String email) { this.email = email; }
}
  • @GeneratedValue(strategy = GenerationType.IDENTITY) … 主キーをDB側で自動採番する戦略。MySQLなどで一般的に使われます。INSERTのタイミングでIDが決まるため実装はシンプルですが、バルク挿入や一部の最適化は効きにくい場合があります。PostgreSQLやOracleでは SEQUENCE 戦略を使うことが多く、事前にIDを取得できるためパフォーマンス面で有利です。
  • @Column(nullable = false, unique = true) … DB制約を明示的に指定します。nullable = false によって NOT NULL 制約 が付与され、必須入力を保証します。unique = true によって ユニーク制約 が付与され、同じ値が登録されるのを防ぎます。これによりアプリケーションコードだけでなくDBレベルでも一貫性を保つことができ、不整合や重複データを防げます。

Lombokを使った実用構成(ボイラープレート削減)

 最小構成の例では、getter/setter、コンストラクタ、toString、equals/hashCode といった定型的なメソッドをすべて手書きしていました。これはコード量が増えるだけでなく、修正忘れやバグの温床にもなりやすいです。そこで活用できるのが Lombok です。アノテーションを付けるだけでこれらの定型コードを自動生成してくれるため、コードがシンプルかつ保守性が高くなります。

import jakarta.persistence.*;
import lombok.*;

@Entity
@Table(name = "users")
@Getter @Setter
@NoArgsConstructor
@AllArgsConstructor
@Builder
@ToString(exclude = "email") // Lazyロードや個人情報の出力に配慮
@EqualsAndHashCode(onlyExplicitlyIncluded = true)
public class User {
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    @EqualsAndHashCode.Include
    private Long id;

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

    @Column(nullable = false, unique = true, length = 120)
    private String email;
}

@EqualsAndHashCode(onlyExplicitlyIncluded = true) を使い、同一性の比較対象をidだけに限定します。これにより、以下の問題を避けられます。

  • Lazyプロキシの展開による無限再帰/遅延:関連エンティティを比較対象に含めると、equals/hashCode の実行中にLazyローディングが走り、パフォーマンス悪化やスタックオーバーフローの原因になります。
  • 循環参照の比較地獄:双方向関連(例:UserOrder)でお互いが比較中に相手を参照し続け、無限ループに陥ることがあります。
  • 状態変化でハッシュが変わる問題HashSetHashMap のキーに使う場合、可変フィールド(name/emailなど)を比較に含むと、変更後に取り出せなくなるバグの温床に。
@EqualsAndHashCode(onlyExplicitlyIncluded = true)
public class User {
    @EqualsAndHashCode.Include
    private Long id; // 永続化後は不変の識別子として扱う
    // ...
}

💡補足 まだ id が未採番の新規オブジェクト同士の比較が必要なケースでは、ビジネスキー(例:email)を一時的に含める設計もありますが、コレクションのキーに使うなら「採番後のみ利用」などの運用ルールを明確にするのが安全です。

@ToString(exclude = "...")ログ出力時の個人情報漏えい循環参照による無限再帰 を防ぎます。

  • 機密情報の伏せ字emailpassword、トークン類などは toString に出さないのが原則。
  • 双方向関連の無限再帰Userorders を、Orderuser を含むと、toString 呼び出しで相互に呼び合いスタックオーバーフローに。

推奨書き方

@ToString(exclude = {"email", "orders"})
public class User {
    private String email; // 機微情報
    @OneToMany(mappedBy = "user")
    private List<Order> orders; // 双方向関連
}

補足 併せて @JsonIgnore(Jackson)を使い、APIレスポンスからも除外するか、専用のDTOにマッピングするのが実務では一般的です。

Lombokでよく使うアノテーション一覧
アノテーション説明
@Getter/@Settergetter/setterメソッドを自動生成
@NoArgsConstructor引数なしコンストラクタを自動生成
@AllArgsConstructor全フィールドを引数に持つコンストラクタを自動生成
@BuilderBuilderパターンによるインスタンス生成を自動生成
@ToStringtoStringメソッドを自動生成(除外指定も可能)
@EqualsAndHashCodeequals/hashCodeメソッドを自動生成(指定フィールドのみ対象にできる)

列挙型・日時のマッピング(よく使う型)

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

@Entity
public class Task {
    public enum Status { TODO, DOING, DONE }

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

    @Enumerated(EnumType.STRING) // 文字列で保存(数値だと可読性×、順序変更に弱い)
    @Column(nullable = false)
    private Status status;

    @Column(nullable = false)
    private LocalDate dueDate;         // 日付のみ

    @Column(nullable = false)
    private LocalDateTime createdAt;   // 日付+時刻(タイムゾーンなし)

    @Column
    private Instant archivedAt;        // UTC基準のタイムスタンプ

    public Task() {}
}
  • 列挙型は EnumType.STRING が安全(表示・順序変更に強い)。
  • Java 8+ の日時API(LocalDate/LocalDateTime/Instant)が基本。@Temporal は java.util.Date/Calendar 用。

入力バリデーションの付与(実運用で必須)

import jakarta.persistence.*;
import jakarta.validation.constraints.*; // Bean Validation
import lombok.*;

@Entity
@Getter @Setter @NoArgsConstructor @AllArgsConstructor @Builder
public class Customer {
    @Id @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;

    @NotBlank @Size(max = 50)
    private String name;

    @NotBlank @Email @Size(max = 120)
    @Column(unique = true)
    private String email;
}
  • @NotBlank/@Email/@Size入力の早期検証 に有効。Controllerの @Valid と併用する。
  • DB制約(uniquenullable)と Bean Validation を両輪で使うと堅牢。

補足 Bean Validationはアプリケーション層でのチェックを担い、ユーザー入力やリクエストデータに対して即座にバリデーションを行う。これにより、DBに到達する前に不正データを防ぐことができ、エラーメッセージもユーザーに分かりやすく返せる。 入力の早期検証 に有効。Controllerの @Valid と併用する。


監査用カラム(作成・更新の自動記録)

 Spring Data JPAの監査機能を使うと、エンティティの作成日時・更新日時をフレームワークが自動で管理してくれます。これにより、開発者が明示的に日付を設定する必要がなくなり、監査情報が常に正しく記録されます。

import jakarta.persistence.*;
import lombok.*;
import org.springframework.data.annotation.CreatedDate;
import org.springframework.data.annotation.LastModifiedDate;
import org.springframework.data.jpa.domain.support.AuditingEntityListener;
import java.time.LocalDateTime;

@Entity
@EntityListeners(AuditingEntityListener.class)
@Getter @Setter
public class Article {
    @Id @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;

    private String title;

    @CreatedDate
    @Column(updatable = false, nullable = false)
    private LocalDateTime createdAt; // 初回INSERT時に自動設定

    @LastModifiedDate
    @Column(nullable = false)
    private LocalDateTime updatedAt; // UPDATEのたびに自動更新
}
  • @EntityListeners(AuditingEntityListener.class) を付与すると、Springがエンティティのライフサイクルにフックして監査処理を実行します。
  • @CreatedDate は新規保存時にのみ値がセットされ、以降は変更されません。
  • @LastModifiedDate は更新のたびに自動的に更新されます。
  • @Column(updatable = false) にすることで誤ってアプリケーションから上書きされるのを防ぎます。
  • この機能を有効にするには、設定クラスに @EnableJpaAuditing を付ける必要があります。

補足 実務では、監査用のカラム(作成者・更新者・削除フラグなど)をまとめた 基底クラス(BaseEntity) を作り、他のエンティティが継承する形にすると便利です。

Repositoryの基礎

 RepositoryはEntityを操作するためのデータアクセス窓口です。Spring Data JPAではインターフェースを定義するだけで、実装は自動生成されます。

JpaRepositoryの継承

public interface UserRepository extends JpaRepository<User, Long> {
    User findByName(String name);
}
  • JpaRepository<Entity, 主キー型> を継承するだけで、基本的なCRUDメソッドが使えます。
  • 実装クラスを書く必要はありません。

よく使う基本メソッド

メソッド説明
save(entity)新規登録または既存データの更新を行う。引数に渡したEntityをDBに反映する。
findAll()テーブルの全件をリストで取得する。開発時の動作確認や小規模データの参照に便利。
findById(id)主キーで1件を検索する。戻り値は Optional なので存在しない場合の処理が安全に書ける。
deleteById(id)主キーに一致するレコードを削除する。

クエリの自動生成とメソッド名の命名ルール

 Spring Data JPAではメソッド名そのものがクエリを表し、自動的にSQLを組み立ててくれます。そのためメソッド名の命名ルールに慣れると、SQLをほとんど書かずに多くの検索処理を表現できるようになります。

User findByEmail(String email);
List<User> findByNameContaining(String keyword);
  • 名前に応じてSpring DataがSQLを作ってくれる
  • findByEmailwhere email = ?
  • findByNameContainingwhere name like %?%

主な命名キーワード

  • 単純一致: findByEmailwhere email = ?
  • 複数条件: findByNameAndStatuswhere name = ? and status = ?
  • 範囲/比較: findByAgeGreaterThan, findByCreatedAtBetween
  • 文字列検索: findByNameContaining, findByNameStartingWith, findByNameEndingWith
  • Null判定: findByEmailIsNull, findByEmailIsNotNull
  • 真偽値: findByActiveTrue, findByActiveFalse
  • 存在確認/件数: existsByEmail, countByStatus
  • 並び替え: findByStatusOrderByCreatedAtDesc

サンプル:

List<User> findByAgeGreaterThan(int age);
List<User> findByNameContaining(String keyword);
List<User> findByStatusOrderByCreatedAtDesc(Status status);
boolean existsByEmail(String email);
long countByStatus(Status status);

補足 よく使うのは findBy + フィールド名、And/OrContaining(部分一致)です。複雑になってきたら @Query を使うのが実務的です。

CRUD操作の実装例

Create(作成)

// 新規作成
User created = userRepository.save(new User("Taro", "taro@example.com"));

Read(取得)

// 全件取得
List<User> users = userRepository.findAll();

// 主キーで1件取得(見つからなければ例外)
User one = userRepository.findById(1L).orElseThrow();

Update(更新)

User u = userRepository.findById(1L).orElseThrow();
u.setName("Taro Yamada");
u.setEmail("taro.yamada@example.com");
User updated = userRepository.save(u);

Delete(削除)

// 主キーで削除
userRepository.deleteById(1L);

// 取得してから削除
User target = userRepository.findById(2L).orElseThrow();
userRepository.delete(target);

よくある失敗と注意点

1. Entityクラス関連

  • デフォルトコンストラクタを忘れる
    → JPAは内部でリフレクションを使ってインスタンス化するため、引数なしコンストラクタが必須。
  • getter/setter がない
    → フィールドにアクセスできず、DB値が反映されない。
  • equals/hashCode を全フィールドで自動生成
    → 関連エンティティを含むと無限ループやパフォーマンス低下につながる。idのみで比較するのが定番。
  • toString に関連フィールドを含める
    → 双方向関連があると無限ループでスタックオーバーフローが発生する。exclude指定を忘れない。

2. Repository関連

  • JpaRepository を継承し忘れる
    • → ただの interface になり、SpringがリポジトリをBean登録できない。
  • メソッド名を間違える
    • findByname(n小文字)などタイポすると実行時エラー。
  • 戻り値を Optional にせず null を受け取る
    • NullPointerException が頻発。Optionalを活用して安全に扱うべき。

3. アノテーション・制約関連

  • @Column(nullable = false) を書かない
    → DB側でNULLが入ってしまい、後で不整合や例外を生む。
  • Bean Validation (@NotNull など) を付けない
    → 入力チェックがコントローラ層に伝わらず、DBエラーで初めて気づくケースになる。
  • IDの自動採番戦略をDBに合わせていない
    → PostgreSQLなら SEQUENCE を使うことが多いが、IDENTITY のままにして意図せぬ挙動になる。

4. CRUD操作での注意

  • save() = 新規 + 更新 の両方に使えることを知らない
    → IDが存在する場合は更新、null の場合は新規になる。
  • delete の対象が存在しない場合の挙動を想定していない
    deleteById は存在しないIDを指定すると EmptyResultDataAccessException が出る。

まとめ

 この記事では EntityとRepositoryの基礎 を学びました。

  • Entityは @Entity を付けたクラスで、テーブルの行をオブジェクトとして扱える仕組みでした。
  • Lombokを活用すると、getter/setterやコンストラクタを省略できて便利でした。
  • Repositoryは JpaRepository を継承するだけで、SQLを直接書かずにCRUD操作が可能でした。
  • メソッド名の命名ルールに従えば、自動でクエリを生成してくれる仕組みでした。

 これにより、SQLを書く負担を大幅に減らしつつ、安全で保守性の高いデータアクセス ができるようになりました。

 ここまでで「データをオブジェクトとして扱う」準備が整いました。次回は H2データベースを使った開発環境構築 を取り上げます。メモリDBを利用して、SQLを書かずに実際のデータ保存・検索を体験していきます。

最新の投稿

SNSでもご購読できます。

コメントを残す