← Kursa Dön
📄 Text · 20 min

Entity Sınıfları: @Entity, @Table, @Id

Giriş

Veritabanında bir users tablonuz var: id, name, email, age sütunları. Java tarafında ise User sınıfınız var: id, name, email, age field'ları. Bu ikisi arasında bir köprü kurmanız gerekiyor — "Bu Java sınıfı şu veritabanı tablosuna karşılık gelir, bu field şu sütuna eşlensin" demeniz lazım. İşte Entity annotation'ları tam olarak bu köprüyü kurar.

Gerçek Dünya Analojisi

Bir çevirmen düşünün. İngilizce bir belgeniz var ve Türkçe'ye çevrilmesi gerekiyor. Çevirmen, her İngilizce kelimenin Türkçe karşılığını bilir. JPA'daki entity annotation'ları da bu çevirmen rolündedir: Java nesnelerini veritabanı kayıtlarına, field'ları sütunlara "çevirir". Bu çeviri kurallarını siz annotation'larla tanımlarsınız.

Entity Nedir?

JPA'da Entity, veritabanındaki bir tabloya karşılık gelen Java sınıfıdır. Her entity nesnesi, tablodaki bir satırı temsil eder. Entity sınıfları @Entity annotation'ı ile işaretlenir ve JPA provider (Hibernate) tarafından yönetilir.

Java Dünyası              Veritabanı Dünyası
─────────────             ──────────────────
User sınıfı         →     users tablosu
User nesnesi         →     users tablosundaki bir satır
id field'ı           →     id sütunu (PRIMARY KEY)
name field'ı         →     name sütunu (VARCHAR)
email field'ı        →     email sütunu (VARCHAR, UNIQUE)

@Entity — "Bu Sınıf Bir Entity'dir"

@Entity annotation'ı, bir Java sınıfını JPA entity'si olarak işaretler. Bu annotation olmadan JPA, sınıfınızı tanımaz ve veritabanıyla eşleştirmez.

import jakarta.persistence.Entity;
import jakarta.persistence.Id;
import jakarta.persistence.GeneratedValue;
import jakarta.persistence.GenerationType;

@Entity // Bu sınıf bir veritabanı tablosuna karşılık gelir
public class User {

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

    private String name;
    private String email;
    private int age;

    // JPA zorunlu: Parametresiz constructor
    protected User() {}

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

    // Getter/Setter'lar...
}

Entity Sınıfı Kuralları

Bir sınıfın geçerli bir JPA entity'si olabilmesi için şu kuralları sağlaması gerekir:

KuralAçıklamaNeden?
@Entity annotation'ıZorunluJPA'nın sınıfı entity olarak tanıması için
Parametresiz constructorpublic veya protectedHibernate, reflection ile nesne oluşturur
@Id alanıHer entity'de zorunluPrimary key olmadan tablo olmaz
final sınıf olmamalıSınıf final olamazHibernate proxy oluşturmak için extend eder
final field olmamalıField'lar final olamazHibernate setter veya reflection ile değer atar
Serializable (opsiyonel)Composite key'lerde zorunluDetached entity'ler için önerilir
// ❌ YANLIŞ — final sınıf
@Entity
public final class User { } // Hibernate proxy oluşturamaz!

// ❌ YANLIŞ — parametresiz constructor yok
@Entity
public class User {
    public User(String name) { this.name = name; }
    // protected User() {} → Bu eksik!
}

// ❌ YANLIŞ — @Id yok
@Entity
public class User {
    private String name; // Primary key nerede?
}

// ✅ DOĞRU
@Entity
public class User {
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;
    private String name;
    protected User() {} // JPA zorunlu
}

⚠️ Dikkat: Parametresiz constructor'ı protected yapın, public değil. Böylece dışarıdan boş nesne oluşturulmasını engellersiniz, ama Hibernate yine de kullanabilir. Bu küçük detay, kodunuzun robustness'ını artırır.


@Table — Tablo Detaylarını Özelleştirme

@Entity tek başına kullanıldığında, tablo adı sınıf adı olur (Useruser tablosu). @Table ile tablo adını, şemayı ve constraint'leri özelleştirebilirsiniz:

@Entity
@Table(
    name = "products",              // Tablo adı (varsayılan: "Product")
    schema = "ecommerce",           // Şema adı (opsiyonel)
    catalog = "mydb",               // Katalog adı (opsiyonel, nadir kullanılır)
    uniqueConstraints = {
        @UniqueConstraint(
            name = "uk_product_sku",      // Constraint adı (debug kolaylığı)
            columnNames = {"sku"}
        ),
        @UniqueConstraint(
            name = "uk_product_name_brand",
            columnNames = {"name", "brand"} // Birleşik unique constraint
        )
    },
    indexes = {
        @Index(name = "idx_product_category", columnList = "category"),
        @Index(name = "idx_product_price", columnList = "price DESC"),
        @Index(name = "idx_product_name_price", columnList = "name, price") // Birleşik index
    }
)
public class Product {
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;

    @Column(nullable = false, unique = true, length = 50)
    private String sku;

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

    @Column(nullable = false, length = 100)
    private String brand;

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

    @Column(nullable = false, precision = 10, scale = 2)
    private BigDecimal price;

    protected Product() {}
    // ...
}

Naming Strategy — İsimlendirme Kuralları

Hibernate varsayılan olarak ImplicitNamingStrategy ve PhysicalNamingStrategy kullanır:

Java Field Adı    →    Veritabanı Sütun Adı
─────────────          ─────────────────────
firstName         →    first_name        (camelCase → snake_case)
orderStatus       →    order_status
createdAt         →    created_at
isActive          →    is_active

Spring Boot bu dönüşümü otomatik yapar (SpringPhysicalNamingStrategy). Yani @Column(name = "first_name") yazmak gerekmez, firstName otomatik olarak first_name olur.

# Naming strategy'yi değiştirmek isterseniz (varsayılan genellikle idealdir)
spring.jpa.hibernate.naming.physical-strategy=org.hibernate.boot.model.naming.CamelCaseToUnderscoresNamingStrategy

💡 İpucu: Spring Boot'un varsayılan naming strategy'si camelCase → snake_case dönüşümü yapar. Bu, Java ve SQL kurallarının doğal uyumunu sağlar. Bozuk bir sebep yoksa değiştirmeyin.


@Id ve @GeneratedValue — Primary Key Stratejileri

Her entity'nin benzersiz bir kimliği (primary key) olmalıdır. @Id primary key alanını, @GeneratedValue ise ID'nin nasıl üretileceğini belirler.

Strateji 1: IDENTITY — Veritabanı Auto-Increment

@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
  • Nasıl çalışır: Veritabanı auto-increment kullanır (MySQL: AUTO_INCREMENT, PostgreSQL: SERIAL)

  • Avantaj: Basit, yaygın, anlaşılır

  • Dezavantaj: Batch insert'te yavaş — Hibernate her insert'ten sonra ID'yi almak için ek sorgu yapar

  • Kullanım: MySQL, MariaDB, SQL Server

Strateji 2: SEQUENCE — Veritabanı Sequence

@Id
@GeneratedValue(strategy = GenerationType.SEQUENCE, generator = "user_seq")
@SequenceGenerator(
    name = "user_seq",
    sequenceName = "user_sequence",
    initialValue = 1,
    allocationSize = 50  // Performans için batch allocation
)
private Long id;
  • Nasıl çalışır: Veritabanı sequence'inden ID alır

  • Avantaj: Batch insert'te çok hızlı — allocationSize kadar ID'yi bir kerede alır

  • Dezavantaj: MySQL'de sequence desteği sınırlı

  • Kullanım: PostgreSQL, Oracle, H2

allocationSize Neden Önemli? allocationSize = 50 demek, Hibernate tek bir sequence çağrısıyla 50 ID'yi bir kerede ayırır. 50 entity kaydederken tek bir SELECT nextval('user_sequence') yerine her seferinde gitmez. Bu, toplu kayıtlarda dramatik performans artışı sağlar.

Strateji 3: UUID — Evrensel Benzersiz Kimlik

@Id
@GeneratedValue(strategy = GenerationType.UUID)
private UUID id;
  • Nasıl çalışır: Java'da UUID oluşturulur

  • Avantaj: Dağıtık sistemlerde çakışma riski yok, veritabanı bağımsız

  • Dezavantaj: 128-bit → Long'a göre daha fazla yer kaplar, index performansı daha düşük

  • Kullanım: Mikroservisler, dağıtık sistemler, public API'ler (ID tahmin edilemez)

Strateji 4: TABLE — Tablo Tabanlı (Kullanmayın!)

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

⚠️ KULLANMAYIN! Ayrı bir tablo oluşturup oradan ID üretir. Pessimistic lock kullanır, çok yavaştır ve ölçeklenmez.

Strateji 5: AUTO — Otomatik (Dikkatli Kullanın!)

@Id
@GeneratedValue(strategy = GenerationType.AUTO) // veya @GeneratedValue sadece
private Long id;

⚠️ Dikkat: AUTO stratejisi JPA provider'ına (Hibernate) kararı bırakır. Hibernate, veritabanına göre TABLE veya SEQUENCE seçebilir — bu da beklenmeyen davranışlara yol açabilir. Stratejiyi açıkça belirtin.

Hangi Stratejiyi Seçmeli?

VeritabanıÖnerilen StratejiNeden
MySQL / MariaDBIDENTITYNative auto-increment, basit
PostgreSQLSEQUENCENative sequence desteği, batch-friendly
OracleSEQUENCENative sequence desteği
H2 (test)IDENTITY veya SEQUENCEİkisi de çalışır
Dağıtık sistemUUIDMerkezi ID üretici gerekmez

@EmbeddedId — Bileşik Anahtar (Composite Key)

Bazen birden fazla sütun birlikte primary key oluşturur. Örneğin bir "öğrenci-ders" kayıt tablosu: primary key = (student_id, course_id).

Adım 1: Bileşik Anahtar Sınıfı

import jakarta.persistence.Embeddable;
import java.io.Serializable;
import java.util.Objects;

@Embeddable // Bu sınıf başka bir entity'ye gömülebilir
public class EnrollmentId implements Serializable { // Serializable ZORUNLU!

    private Long studentId;
    private Long courseId;

    // Parametresiz constructor (JPA zorunlu)
    protected EnrollmentId() {}

    public EnrollmentId(Long studentId, Long courseId) {
        this.studentId = studentId;
        this.courseId = courseId;
    }

    // equals() ve hashCode() ZORUNLU — JPA bileşik anahtarları karşılaştırırken kullanır
    @Override
    public boolean equals(Object o) {
        if (this == o) return true;
        if (o == null || getClass() != o.getClass()) return false;
        EnrollmentId that = (EnrollmentId) o;
        return Objects.equals(studentId, that.studentId)
            && Objects.equals(courseId, that.courseId);
    }

    @Override
    public int hashCode() {
        return Objects.hash(studentId, courseId);
    }

    // Getter'lar
    public Long getStudentId() { return studentId; }
    public Long getCourseId() { return courseId; }
}

Adım 2: Entity'de Kullanım

@Entity
@Table(name = "enrollments")
public class Enrollment {

    @EmbeddedId
    private EnrollmentId id;

    @Column(nullable = false)
    private LocalDate enrollmentDate;

    @Column(length = 2)
    private String grade; // AA, BA, BB, CB...

    protected Enrollment() {}

    public Enrollment(Long studentId, Long courseId) {
        this.id = new EnrollmentId(studentId, courseId);
        this.enrollmentDate = LocalDate.now();
    }

    // Getter/Setter'lar
}
// Kullanım
EnrollmentId key = new EnrollmentId(studentId, courseId);
Optional<Enrollment> enrollment = enrollmentRepository.findById(key);

⚠️ Dikkat: Composite key sınıfında equals() ve hashCode() override etmezseniz, findById() çalışmaz! JPA, iki composite key'in eşit olup olmadığını bu metotlarla kontrol eder.

@IdClass Alternatifi

@EmbeddedId yerine @IdClass de kullanılabilir:

@Entity
@Table(name = "enrollments")
@IdClass(EnrollmentId.class) // @EmbeddedId yerine @IdClass
public class Enrollment {

    @Id
    private Long studentId; // Ayrı ayrı @Id

    @Id
    private Long courseId;  // Ayrı ayrı @Id

    private LocalDate enrollmentDate;
}

@EmbeddedId vs @IdClass:

Özellik@EmbeddedId@IdClass
JPQL'de erişime.id.studentIde.studentId
Entity'deki tanımTek alan (id)Ayrı alanlar
Partial key sorgusuDaha karmaşıkDaha kolay
Tercih✅ Daha yaygınEski tarz

Tam Bir Entity Örneği — E-Ticaret Ürünü

Tüm annotation'ları bir araya getiren kapsamlı bir örnek:

@Entity
@Table(
    name = "products",
    uniqueConstraints = @UniqueConstraint(name = "uk_product_sku", columnNames = "sku"),
    indexes = {
        @Index(name = "idx_product_category", columnList = "category"),
        @Index(name = "idx_product_price", columnList = "price")
    }
)
public class Product {

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

    @Column(nullable = false, unique = true, length = 50)
    private String sku; // Stock Keeping Unit

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

    @Column(length = 2000)
    private String description;

    @Column(nullable = false, precision = 10, scale = 2)
    private BigDecimal price;

    @Column(nullable = false)
    private int stockQuantity;

    @Column(nullable = false, length = 100)
    private String category;

    @Column(length = 100)
    private String brand;

    @Enumerated(EnumType.STRING)
    @Column(nullable = false, length = 20)
    private ProductStatus status = ProductStatus.ACTIVE;

    @Column(nullable = false, updatable = false)
    private LocalDateTime createdAt;

    private LocalDateTime updatedAt;

    // === Constructors ===
    protected Product() {} // JPA zorunlu

    public Product(String sku, String name, BigDecimal price, int stockQuantity, String category) {
        this.sku = sku;
        this.name = name;
        this.price = price;
        this.stockQuantity = stockQuantity;
        this.category = category;
        this.createdAt = LocalDateTime.now();
    }

    // === Business Methods ===
    public void decreaseStock(int quantity) {
        if (this.stockQuantity < quantity) {
            throw new InsufficientStockException(this.sku, this.stockQuantity, quantity);
        }
        this.stockQuantity -= quantity;
        this.updatedAt = LocalDateTime.now();
    }

    public boolean isInStock() {
        return this.stockQuantity > 0 && this.status == ProductStatus.ACTIVE;
    }

    // === Getter/Setter ===
    public Long getId() { return id; }
    public String getSku() { return sku; }
    public String getName() { return name; }
    public void setName(String name) { this.name = name; }
    public BigDecimal getPrice() { return price; }
    public void setPrice(BigDecimal price) { this.price = price; }
    public int getStockQuantity() { return stockQuantity; }
    public String getCategory() { return category; }
    public ProductStatus getStatus() { return status; }
    public void setStatus(ProductStatus status) { this.status = status; }
    public LocalDateTime getCreatedAt() { return createdAt; }
    public LocalDateTime getUpdatedAt() { return updatedAt; }

    // === toString (loglama için) ===
    @Override
    public String toString() {
        return "Product{id=%d, sku='%s', name='%s', price=%s}"
            .formatted(id, sku, name, price);
    }
}

public enum ProductStatus {
    ACTIVE,      // Satışta
    INACTIVE,    // Satışta değil
    DRAFT,       // Taslak
    DISCONTINUED // Üretimden kalktı
}

Yaygın Hatalar ve Çözümleri

Hata 1: Parametresiz Constructor Unutmak

// ❌ YANLIŞ — Parametresiz constructor yok
@Entity
public class User {
    @Id @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;
    private String name;

    public User(String name) { this.name = name; }
    // Hibernate: "No default constructor for entity: User"
}

// ✅ DOĞRU
@Entity
public class User {
    @Id @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;
    private String name;

    protected User() {} // Hibernate için
    public User(String name) { this.name = name; }
}

Hata 2: @GeneratedValue Olmadan ID Yönetimi

// ❌ YANLIŞ — @GeneratedValue yok, ID manuel set edilmeli
@Entity
public class User {
    @Id
    private Long id; // Kim set edecek?
}

User user = new User();
userRepository.save(user); // id = null → Hata veya sorunlu davranış!

// ✅ DOĞRU — Ya @GeneratedValue kullanın ya da manuel set edin
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id; // Veritabanı otomatik üretir

Hata 3: AUTO Stratejisinin Beklenmeyen Davranışı

// ⚠️ DİKKAT — AUTO, veritabanına göre farklı davranır
@GeneratedValue(strategy = GenerationType.AUTO)
private Long id;
// MySQL'de TABLE stratejisi seçebilir → hibernate_sequences tablosu oluşur
// PostgreSQL'de SEQUENCE seçer
// H2'de SEQUENCE seçer

// ✅ DOĞRU — Stratejiyi açıkça belirtin
@GeneratedValue(strategy = GenerationType.IDENTITY) // MySQL için
@GeneratedValue(strategy = GenerationType.SEQUENCE) // PostgreSQL için

Hata 4: equals/hashCode Composite Key'de Unutmak

// ❌ YANLIŞ — equals/hashCode yok
@Embeddable
public class OrderItemId implements Serializable {
    private Long orderId;
    private Long productId;
    // equals ve hashCode YOK → findById() düzgün çalışmaz!
}

// ✅ DOĞRU — equals ve hashCode override edilmeli
@Override
public boolean equals(Object o) { ... }
@Override
public int hashCode() { ... }

Hata 5: Entity'de Lombok @Data Kullanmak

// ❌ DİKKAT — @Data, equals/hashCode'u tüm alanlara göre üretir
@Entity
@Data // toString, equals, hashCode, getter, setter hepsi
public class User {
    @Id @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;
    private String name;
}
// Sorun: equals, lazy-loaded alanları tetikleyebilir
// Sorun: toString, tüm ilişkileri yükleyebilir

// ✅ DOĞRU — Sadece ihtiyaç duyulanları kullanın
@Entity
@Getter @Setter
@NoArgsConstructor(access = AccessLevel.PROTECTED)
public class User {
    @Id @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;
    private String name;

    // ID bazlı equals/hashCode (Hibernate best practice)
    @Override
    public boolean equals(Object o) {
        if (this == o) return true;
        if (!(o instanceof User other)) return false;
        return id != null && id.equals(other.getId());
    }

    @Override
    public int hashCode() {
        return getClass().hashCode(); // Sabit değer — proxy'ler için güvenli
    }
}

Özet

  • `@Entity` sınıfı veritabanı tablosuna, her entity nesnesi bir satıra karşılık gelir

  • `@Table` ile tablo adı, şema, unique constraint'ler ve index'ler tanımlanır

  • `@Id` primary key alanını, `@GeneratedValue` ID üretim stratejisini belirler

  • IDENTITY MySQL için, SEQUENCE PostgreSQL için, UUID dağıtık sistemler için tercih edilir

  • Composite key için @EmbeddedId + @Embeddable kullanın — equals()/hashCode() zorunlu

  • Entity'de parametresiz constructor (protected), final olmayan sınıf ve field'lar zorunlu

  • Lombok kullanıyorsanız @Data yerine @Getter @Setter tercih edin — equals/hashCode'u ID bazlı kendiniz yazın