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:
| Kural | Açıklama | Neden? |
|---|---|---|
@Entity annotation'ı | Zorunlu | JPA'nın sınıfı entity olarak tanıması için |
| Parametresiz constructor | public veya protected | Hibernate, reflection ile nesne oluşturur |
@Id alanı | Her entity'de zorunlu | Primary key olmadan tablo olmaz |
final sınıf olmamalı | Sınıf final olamaz | Hibernate proxy oluşturmak için extend eder |
final field olmamalı | Field'lar final olamaz | Hibernate setter veya reflection ile değer atar |
| Serializable (opsiyonel) | Composite key'lerde zorunlu | Detached 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 (User → user 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_activeSpring 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ı —
allocationSizekadar ID'yi bir kerede alırDezavantaj: 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 Strateji | Neden |
|---|---|---|
| MySQL / MariaDB | IDENTITY | Native auto-increment, basit |
| PostgreSQL | SEQUENCE | Native sequence desteği, batch-friendly |
| Oracle | SEQUENCE | Native sequence desteği |
| H2 (test) | IDENTITY veya SEQUENCE | İkisi de çalışır |
| Dağıtık sistem | UUID | Merkezi 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şim | e.id.studentId | e.studentId |
| Entity'deki tanım | Tek alan (id) | Ayrı alanlar |
| Partial key sorgusu | Daha karmaşık | Daha kolay |
| Tercih | ✅ Daha yaygın | Eski 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 üretirHata 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çinHata 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+@Embeddablekullanın —equals()/hashCode()zorunluEntity'de parametresiz constructor (protected), final olmayan sınıf ve field'lar zorunlu
Lombok kullanıyorsanız
@Datayerine@Getter @Settertercih edin — equals/hashCode'u ID bazlı kendiniz yazın
AI Asistan
Sorularını yanıtlamaya hazır