10.

【Spring】@Entityアノテーションとは

編集
この記事の要点
  • @Entity は JPA でDB テーブルと対応する Java クラスを示す
  • デフォルトでクラス名 = テーブル名(snake_case で)。明示する場合は @Table(name="...")
  • 必須: 引数なしコンストラクタ + @Id 付き主キーフィールド
  • final クラス・final フィールド・enum は不可
  • Hibernate のプロキシ生成のため、private にすると遅延読み込み不可

 

@Entity の基本

import jakarta.persistence.*;  // Jakarta EE 9+ (Spring Boot 3+)
// または javax.persistence.* (Jakarta EE 8 以前)

@Entity
@Table(name = "users")
public class User {

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

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

    @Column(unique = true)
    private String email;

    @Column(name = "created_at", updatable = false)
    private LocalDateTime createdAt;

    // 引数なしコンストラクタ (必須)
    public User() {}

    // ビジネス用コンストラクタ
    public User(String name, String email) {
        this.name = name;
        this.email = email;
    }

    // getter / setter (省略可: Lombok @Data)
}

必須要件

  1. クラスに @Entity を付与
  2. 主キーが必要: 少なくとも 1 つの @Id 付きフィールド
  3. 引数なしコンストラクタ: Hibernate がリフレクションで生成するため
  4. final クラスにしない: プロキシ生成不可
  5. Serializable 実装推奨(必須ではないが、キャッシュ・分散環境で便利)

テーブル名のマッピング

// クラス名から自動推測 (snake_case)
@Entity
public class UserProfile { }
// → DB テーブル "user_profile"

// @Table で明示
@Entity
@Table(name = "user_profiles")
public class UserProfile { }
// → DB テーブル "user_profiles"

// スキーマ指定
@Entity
@Table(name = "users", schema = "auth")
public class User { }
// → "auth.users"

// インデックス・UNIQUE 制約も定義
@Entity
@Table(name = "users",
    indexes = {
        @Index(name = "idx_users_email", columnList = "email", unique = true),
        @Index(name = "idx_users_status", columnList = "status")
    },
    uniqueConstraints = {
        @UniqueConstraint(columnNames = {"tenant_id", "username"})
    }
)
public class User { }

主キーの定義

// 単一主キー
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;

// UUID 主キー (Hibernate 6+)
@Id
@GeneratedValue(strategy = GenerationType.UUID)
private UUID id;

// 複合主キー (@EmbeddedId)
@Embeddable
public class OrderItemId implements Serializable {
    private Long orderId;
    private Long productId;
    // equals/hashCode 必須
}

@Entity
public class OrderItem {
    @EmbeddedId
    private OrderItemId id;
}

カラムマッピング

@Entity
public class User {

    @Id @GeneratedValue
    private Long id;

    // デフォルト: フィールド名 = カラム名 (snake_case 変換あり)
    private String name;          // → name カラム
    private String firstName;     // → first_name カラム (Spring Boot 標準)

    // カラム名明示
    @Column(name = "email_address", length = 255, nullable = false, unique = true)
    private String email;

    // 不変 (INSERT/UPDATE 含めない)
    @Column(insertable = false, updatable = false)
    private LocalDateTime serverGenerated;

    // BigDecimal (金額)
    @Column(precision = 10, scale = 2)
    private BigDecimal price;

    // LOB (Large Object: TEXT, BLOB)
    @Lob
    @Column(columnDefinition = "TEXT")
    private String bio;

    @Lob
    private byte[] image;

    // Enum
    @Enumerated(EnumType.STRING)
    private UserStatus status;  // 文字列で保存

    // 日時
    @Column(name = "created_at")
    @CreationTimestamp
    private LocalDateTime createdAt;

    @Column(name = "updated_at")
    @UpdateTimestamp
    private LocalDateTime updatedAt;

    // 一時的フィールド (DB に保存しない)
    @Transient
    private String displayName;  // 計算用 / メモ用

    // バージョン (楽観ロック)
    @Version
    private Long version;
}

関連 (リレーションシップ)

@OneToMany / @ManyToOne

// 1:N の親
@Entity
public class User {
    @Id @GeneratedValue
    private Long id;

    @OneToMany(mappedBy = "user", cascade = CascadeType.ALL, orphanRemoval = true)
    private List orders = new ArrayList<>();
}

// N:1 の子
@Entity
public class Order {
    @Id @GeneratedValue
    private Long id;

    @ManyToOne(fetch = FetchType.LAZY)  // ← LAZY 推奨
    @JoinColumn(name = "user_id", nullable = false)
    private User user;
}

@OneToOne

@Entity
public class User {
    @Id @GeneratedValue
    private Long id;

    @OneToOne(mappedBy = "user", cascade = CascadeType.ALL)
    private UserProfile profile;
}

@Entity
public class UserProfile {
    @Id
    private Long id;

    @OneToOne(fetch = FetchType.LAZY)
    @MapsId
    @JoinColumn(name = "id")
    private User user;

    private String bio;
}

@ManyToMany(中間テーブル)

@Entity
public class User {
    @Id @GeneratedValue
    private Long id;

    @ManyToMany
    @JoinTable(name = "user_roles",
        joinColumns = @JoinColumn(name = "user_id"),
        inverseJoinColumns = @JoinColumn(name = "role_id"))
    private Set roles = new HashSet<>();
}

@Entity
public class Role {
    @Id @GeneratedValue
    private Long id;

    private String name;

    @ManyToMany(mappedBy = "roles")
    private Set users = new HashSet<>();
}

// → user_roles 中間テーブルが自動生成される

継承戦略

// 戦略 1: 単一テーブル (デフォルト)
@Entity
@Inheritance(strategy = InheritanceType.SINGLE_TABLE)
@DiscriminatorColumn(name = "type")
public abstract class Animal {
    @Id @GeneratedValue
    private Long id;
    private String name;
}

@Entity
@DiscriminatorValue("DOG")
public class Dog extends Animal { }

@Entity
@DiscriminatorValue("CAT")
public class Cat extends Animal { }

// → 1 つの animal テーブルに type カラムで区別

// 戦略 2: テーブル分割 (JOINED)
@Inheritance(strategy = InheritanceType.JOINED)
// → animal + dog + cat テーブル

// 戦略 3: クラスごとテーブル (TABLE_PER_CLASS)
@Inheritance(strategy = InheritanceType.TABLE_PER_CLASS)

ライフサイクルイベント

@Entity
@EntityListeners(AuditListener.class)  // 別クラスでリスナー
public class User {

    @PrePersist
    public void onCreate() {
        this.createdAt = LocalDateTime.now();
    }

    @PreUpdate
    public void onUpdate() {
        this.updatedAt = LocalDateTime.now();
    }

    @PostLoad
    public void afterLoad() {
        // DB から読み込み後
    }

    @PreRemove
    public void beforeDelete() {
        // 削除前
    }
}

Lombok との併用

@Entity
@Table(name = "users")
@Getter @Setter
@NoArgsConstructor
@AllArgsConstructor
@Builder
@EqualsAndHashCode(of = "id")  // id だけで equals
@ToString(exclude = "orders")    // orders 除外 (LAZY のため)
public class User {
    @Id @GeneratedValue
    private Long id;

    @Column(nullable = false)
    private String name;

    @OneToMany(mappedBy = "user")
    private List orders;
}

// 利用
User user = User.builder()
    .name("Alice")
    .build();

注意点

  • 引数なしコンストラクタ必須: @NoArgsConstructor または手動
  • FetchType.LAZY 推奨: デフォルトの EAGER は N+1 問題の元
  • equals/hashCode: id だけで判定(オブジェクトの内容で比較するとリレーション読込で無限ループ)
  • toString で関連を含めない: LAZY 読込時に余分なクエリ発行
  • FetchType.EAGER は慎重に: 大量データで爆発

関連記事

編集
Post Share
子ページ

子ページはありません

同階層のページ
  1. @After
  2. @Autowired
  3. @Bean
  4. @Before
  5. @Column
  6. @Component
  7. @Configuration
  8. @Controller
  9. @Data
  10. @Entity
  11. @GeneratedValue
  12. @Id
  13. @Modifying
  14. @PathVariable
  15. @PropertySource
  16. @Repository
  17. @RequestBody
  18. @RequestMapping
  19. @ResponseBody
  20. @RestController
  21. @Service
  22. @SpringBootApplication
  23. @Table
  24. @Transactional
  25. @Value