Column Mapping: @Column, @Enumerated, @Lob
Giriş
Önceki derste entity'nin ne olduğunu, @Entity ve @Id annotation'larını öğrendik. Şimdi bir adım daha ileri giderek, entity'nin alanlarını (field) veritabanı sütunlarına nasıl eşleyeceğimizi detaylıca inceleyeceğiz. Her Java tipinin veritabanında bir karşılığı var — ama bu eşleştirme her zaman otomatik ve istediğiniz gibi olmaz.
Gerçek Dünya Analojisi
Bir uluslararası kargo gönderiyorsunuz. Paketin boyutunu, ağırlığını, içeriğini ve kırılganlık durumunu belirtmeniz gerekiyor. Her bilgi, kargo formundaki farklı bir alana yazılır ve farklı kurallar uygulanır: ağırlık kilogram cinsinden, boyut santimetre cinsinden, içerik metin olarak, kırılganlık evet/hayır olarak. @Column ve diğer mapping annotation'ları, Java field'larınızın veritabanına "nasıl paketleneceğini" tanımlar — hangi sütuna, hangi boyutta, hangi kısıtlamayla.
@Column — Sütun Detaylarını Kontrol Etme
@Column annotation'ı, bir field'ın veritabanındaki sütunuyla ilgili tüm detayları belirlemenizi sağlar:
@Entity
@Table(name = "products")
public class Product {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
@Column(
name = "product_name", // Sütun adı (varsayılan: field adı → snake_case)
nullable = false, // NOT NULL kısıtı
unique = false, // UNIQUE kısıtı
length = 255, // VARCHAR uzunluğu (sadece String için)
insertable = true, // INSERT SQL'ine dahil edilsin mi?
updatable = true, // UPDATE SQL'ine dahil edilsin mi?
columnDefinition = "" // Özel SQL tanım (son çare)
)
private String name;
@Column(nullable = false, precision = 10, scale = 2)
private BigDecimal price;
// → DECIMAL(10,2): 10 basamak toplam, 2 ondalık
// Örn: 99999999.99 (max)
@Column(nullable = false, columnDefinition = "TEXT")
private String description;
// columnDefinition = "TEXT" → VARCHAR(255) yerine TEXT tipi kullanılır
}@Column Parametreleri Detaylı
| Parametre | Varsayılan | Açıklama | Örnek |
|---|---|---|---|
name | field adı (snake_case) | Veritabanı sütun adı | name = "product_name" |
nullable | true | NULL olabilir mi? | nullable = false → NOT NULL |
unique | false | Benzersiz mi? | unique = true → UNIQUE |
length | 255 | VARCHAR uzunluğu | length = 500 → VARCHAR(500) |
precision | 0 | Toplam basamak sayısı (BigDecimal) | precision = 10 |
scale | 0 | Ondalık basamak sayısı | scale = 2 |
insertable | true | INSERT'e dahil mi? | insertable = false → oluşturmada yoksay |
updatable | true | UPDATE'e dahil mi? | updatable = false → güncellemede yoksay |
columnDefinition | "" | Ham SQL sütun tanımı | columnDefinition = "TEXT" |
insertable ve updatable Ne İşe Yarar?
@Entity
public class AuditableEntity {
// Oluşturulma tarihi: Sadece INSERT'te yazılır, asla güncellenmez
@Column(nullable = false, updatable = false)
private LocalDateTime createdAt;
// Hesaplanmış alan: Java tarafında hesaplanır, veritabanına yazılmaz
@Column(insertable = false, updatable = false)
private String computedField;
// Sadece okunur (veritabanı trigger'ı ile doldurulan alan)
@Column(insertable = false, updatable = false)
private String dbGeneratedField;
}columnDefinition — Son Çare
columnDefinition parametresi, veritabanına özel SQL tipi belirtmek için kullanılır. Taşınabilirliği bozar, bu yüzden sadece gerçekten gerektiğinde kullanın:
// ✅ Gerekli kullanım örnekleri
@Column(columnDefinition = "TEXT") // VARCHAR(255) yerine TEXT
@Column(columnDefinition = "JSONB") // PostgreSQL JSON tipi
@Column(columnDefinition = "TINYINT(1)") // MySQL boolean
@Column(columnDefinition = "TIMESTAMP DEFAULT CURRENT_TIMESTAMP")
// ❌ Gereksiz kullanım — @Column parametreleri yeterli
@Column(columnDefinition = "VARCHAR(100) NOT NULL") // nullable=false, length=100 kullanın⚠️ Dikkat: columnDefinition kullandığınızda, nullable, length, precision gibi diğer parametreler yoksayılır. Tüm sütun tanımını columnDefinition içinde yapmanız gerekir.
@Enumerated — Enum Eşleştirme
Java enum'larını veritabanına kaydetmenin iki yolu vardır — ama birini asla kullanmamalısınız:
public enum OrderStatus {
PENDING, // 0
CONFIRMED, // 1
SHIPPED, // 2
DELIVERED, // 3
CANCELLED // 4
}
@Entity
public class Order {
// ✅ STRING — Enum adını string olarak kaydeder
@Enumerated(EnumType.STRING)
@Column(nullable = false, length = 20)
private OrderStatus status;
// Veritabanında: "PENDING", "CONFIRMED", "SHIPPED"...
// ❌ ORDINAL — Enum sıra numarasını kaydeder
@Enumerated(EnumType.ORDINAL) // ASLA KULLANMAYIN!
private OrderStatus status;
// Veritabanında: 0, 1, 2, 3, 4
}Neden ORDINAL Tehlikeli?
Düşünün ki OrderStatus enum'una yeni bir değer eklediniz:
// ÖNCEKİ versiyon
public enum OrderStatus {
PENDING, // 0
CONFIRMED, // 1
SHIPPED, // 2
DELIVERED, // 3
CANCELLED // 4
}
// YENİ versiyon — PROCESSING eklendi
public enum OrderStatus {
PENDING, // 0
CONFIRMED, // 1
PROCESSING, // 2 ← YENİ!
SHIPPED, // 3 ← KAYDI! (eskiden 2 idi)
DELIVERED, // 4 ← KAYDI!
CANCELLED // 5 ← KAYDI!
}
// FELAKET: Veritabanındaki 2 değeri eskiden SHIPPED idi, şimdi PROCESSING!
// Tüm gönderilmiş siparişler "işleniyor" statüsüne döndü!EnumType.STRING ile bu sorun asla yaşanmaz çünkü veritabanında "SHIPPED" yazısı saklanır, sıra numarası değil.
⚠️ Dikkat: @Enumerated belirtmezseniz varsayılan ORDINAL'dır! Her zaman açıkça @Enumerated(EnumType.STRING) yazın.
Enum'larda @Column(length) Ayarı
// Enum değerlerinin en uzunundan biraz fazla ayarlayın
@Enumerated(EnumType.STRING)
@Column(nullable = false, length = 20) // En uzun: "CANCELLED" = 9 karakter
private OrderStatus status;💡 İpucu: İleride daha uzun enum değerleri ekleyebilirsiniz. length değerini biraz cömert tutun — VARCHAR depolama maliyeti sadece gerçek uzunluk kadardır.
@Temporal — Tarih/Saat Eşleştirme
Java 8+ Tipleri (Modern — @Temporal Gerekmez)
@Entity
public class Event {
// LocalDate → DATE sütunu (sadece tarih: 2025-03-15)
@Column(nullable = false)
private LocalDate eventDate;
// LocalTime → TIME sütunu (sadece saat: 14:30:00)
private LocalTime startTime;
// LocalDateTime → TIMESTAMP sütunu (tarih + saat: 2025-03-15 14:30:00)
@Column(nullable = false)
private LocalDateTime createdAt;
// Instant → TIMESTAMP (UTC, timezone-safe)
private Instant lastModifiedAt;
// ZonedDateTime → TIMESTAMP WITH TIMEZONE
private ZonedDateTime scheduledAt;
// OffsetDateTime → TIMESTAMP WITH TIMEZONE
private OffsetDateTime publishedAt;
// Duration → BIGINT (nanosaniye olarak)
private Duration duration;
}Eski java.util.Date (Legacy — @Temporal Zorunlu)
@Entity
public class LegacyEvent {
// @Temporal OLMADAN → Hata veya beklenmeyen davranış!
@Temporal(TemporalType.DATE)
private Date eventDate; // → DATE (sadece tarih)
@Temporal(TemporalType.TIME)
private Date eventTime; // → TIME (sadece saat)
@Temporal(TemporalType.TIMESTAMP)
private Date eventTimestamp; // → TIMESTAMP (tarih + saat)
}💡 İpucu: Yeni projelerde asla java.util.Date kullanmayın. Java 8+ java.time API'sini (LocalDate, LocalDateTime, Instant) kullanın. Daha güvenli, daha okunabilir ve @Temporal gibi ekstra annotation gerektirmez.
@Lob — Büyük Nesneler (Large Objects)
@Entity
public class Document {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
@Column(nullable = false)
private String title;
// CLOB — Büyük metin (Character Large Object)
@Lob
private String content;
// MySQL: LONGTEXT, PostgreSQL: TEXT, H2: CLOB
// BLOB — Binary veri (Binary Large Object)
@Lob
private byte[] attachment;
// MySQL: LONGBLOB, PostgreSQL: BYTEA, H2: BLOB
// Dosya boyutu
private Long fileSize;
// MIME type
@Column(length = 100)
private String contentType; // "application/pdf", "image/png"
}@Lob vs @Column(columnDefinition = "TEXT")
// Yöntem 1: @Lob
@Lob
private String content; // JPA standardı, taşınabilir
// Yöntem 2: columnDefinition
@Column(columnDefinition = "TEXT")
private String content; // Veritabanına özel, daha açık
// Yöntem 3: @Column(length) ile büyük VARCHAR
@Column(length = 10000)
private String content; // VARCHAR(10000) — bazı veritabanlarında sınırlı⚠️ Dikkat: Büyük dosyaları (resim, video, PDF) veritabanında saklamak genellikle kötü bir pratiktir. Dosyayı disk veya object storage'a (S3) kaydedin, veritabanında sadece yolunu/URL'ini tutun. @Lob küçük belgeler, kısa metinler için uygundur.
@Transient — Kalıcı Olmayan Alan
@Transient ile işaretlenen alanlar veritabanına yazılmaz ve okunmaz. Sadece Java tarafında kullanılır:
@Entity
public class User {
@Column(nullable = false, length = 50)
private String firstName;
@Column(nullable = false, length = 50)
private String lastName;
@Transient // Veritabanına yazılmaz
private String fullName;
public String getFullName() {
return firstName + " " + lastName;
}
@Transient
private int loginAttempts; // Sadece runtime'da izlenir
@Transient
private boolean passwordChanged; // İş mantığı flag'i
}@Transient vs Java transient keyword:
@Transient // JPA annotation — sadece JPA'yı etkiler
private String temp; // Java serialization'da serialized olur
transient // Java keyword — hem JPA hem serialization'ı etkiler
private String temp; // Hiçbir yerde persist olmaz💡 İpucu: @Transient kullanın, Java transient keyword'ünü değil. @Transient sadece JPA persistence'ı etkiler, serialization'a dokunmaz.
Java Tipleri → SQL Tipleri Eşleştirme Tablosu
| Java Tipi | SQL Tipi | Not |
|---|---|---|
String | VARCHAR(255) | @Column(length) ile ayarlanır |
int / Integer | INTEGER | |
long / Long | BIGINT | |
short / Short | SMALLINT | |
byte / Byte | TINYINT | |
float / Float | FLOAT | Hassasiyet sorunu — para için kullanmayın |
double / Double | DOUBLE | Hassasiyet sorunu — para için kullanmayın |
BigDecimal | DECIMAL(p,s) | precision/scale belirtilmeli, para için ideal |
BigInteger | NUMERIC | |
boolean / Boolean | BOOLEAN / BIT | DB'ye göre değişir |
char / Character | CHAR(1) | |
LocalDate | DATE | |
LocalTime | TIME | |
LocalDateTime | TIMESTAMP | |
Instant | TIMESTAMP | UTC bazlı, uluslararası uygulamalar için |
ZonedDateTime | TIMESTAMP WITH TZ | |
UUID | UUID / BINARY(16) | PostgreSQL native UUID, MySQL binary |
byte[] | BLOB | @Lob ile büyük binary |
String + @Lob | CLOB / TEXT | Büyük metin |
Enum | VARCHAR / INTEGER | @Enumerated ile kontrol |
Para Birimi İçin BigDecimal
// ❌ YANLIŞ — float/double para için kullanılmaz
@Column
private double price; // 0.1 + 0.2 = 0.30000000000000004 😱
// ✅ DOĞRU — BigDecimal kullanın
@Column(nullable = false, precision = 10, scale = 2)
private BigDecimal price; // 0.1 + 0.2 = 0.3 ✅Bütünleşik Gerçek Dünya Örneği: Blog Sistemi
@Entity
@Table(name = "articles", indexes = {
@Index(name = "idx_article_slug", columnList = "slug"),
@Index(name = "idx_article_status_published", columnList = "status, publishedAt")
})
public class Article {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
@Column(nullable = false, length = 300)
private String title;
@Column(nullable = false, unique = true, length = 300)
private String slug; // URL-friendly başlık: "spring-boot-nedir"
@Column(length = 500)
private String summary; // Kısa özet
@Lob // Büyük metin — makale içeriği
@Column(nullable = false)
private String content;
@Column(length = 500)
private String coverImageUrl; // Kapak görseli URL
@Enumerated(EnumType.STRING)
@Column(nullable = false, length = 20)
private ArticleStatus status = ArticleStatus.DRAFT;
@Column(nullable = false)
private int viewCount = 0;
@Column(nullable = false)
private int likeCount = 0;
@Column(nullable = false)
private int readTimeMinutes; // Tahmini okuma süresi
@Column(nullable = false, updatable = false)
private LocalDateTime createdAt = LocalDateTime.now();
private LocalDateTime updatedAt;
private LocalDateTime publishedAt; // Yayınlanma tarihi (null = henüz yayınlanmadı)
@Transient // Veritabanına yazılmaz — runtime'da hesaplanır
private boolean isNew;
public boolean getIsNew() {
return publishedAt != null
&& publishedAt.isAfter(LocalDateTime.now().minusDays(7));
}
// === Business Methods ===
public void publish() {
this.status = ArticleStatus.PUBLISHED;
this.publishedAt = LocalDateTime.now();
this.updatedAt = LocalDateTime.now();
}
public void incrementViewCount() {
this.viewCount++;
}
// Constructor, Getter/Setter...
protected Article() {}
public Article(String title, String slug, String content, int readTimeMinutes) {
this.title = title;
this.slug = slug;
this.content = content;
this.readTimeMinutes = readTimeMinutes;
}
}
public enum ArticleStatus {
DRAFT, REVIEW, PUBLISHED, ARCHIVED
}Yaygın Hatalar ve Çözümleri
Hata 1: Para İçin double Kullanmak
// ❌ double ile para hesabı
double price = 0.1 + 0.2; // = 0.30000000000000004
// Müşteri 0.30 TL ödeyecekken 0.30000000000000004 TL hesaplanır!
// ✅ BigDecimal ile
BigDecimal price = new BigDecimal("0.1").add(new BigDecimal("0.2")); // = 0.3Hata 2: @Enumerated Belirtmemek
// ❌ @Enumerated yok → varsayılan ORDINAL!
private OrderStatus status; // 0, 1, 2 olarak kaydedilir
// ✅ Her zaman STRING belirtin
@Enumerated(EnumType.STRING)
private OrderStatus status; // "PENDING", "CONFIRMED" olarak kaydedilirHata 3: @Column(length) BigDecimal'de Kullanmak
// ❌ YANLIŞ — length, String içindir
@Column(length = 10) // BigDecimal için anlamsız
private BigDecimal price;
// ✅ DOĞRU — precision ve scale kullanın
@Column(precision = 10, scale = 2)
private BigDecimal price;Hata 4: Boolean Mapping Sorunları
// ⚠️ DİKKAT — MySQL'de boolean → TINYINT(1)
@Column(nullable = false)
private boolean active; // 0 veya 1
// primitive boolean → varsayılan false (NULL olamaz)
// Boolean (wrapper) → NULL olabilir ama dikkatli olun
@Column
private Boolean verified; // null = bilinmiyor, true = doğrulanmış, false = reddedilmişÖzet
`@Column` ile sütun adı, boyutu, null kısıtı ve unique kısıtı belirlenir
`@Enumerated(EnumType.STRING)` her zaman kullanın — ORDINAL tehlikelidir
Java 8+ tarih tipleri kullanın (LocalDate, LocalDateTime, Instant) —
@Temporalgerekmez`@Lob` büyük metin (CLOB) ve binary veri (BLOB) için kullanılır
`@Transient` veritabanına yazılmaması gereken alanları işaretler
`BigDecimal` para ve hassas sayısal değerler için, `double` asla para için kullanılmaz
`columnDefinition` son çare olarak kullanılır — taşınabilirliği bozar
İleri Seviye: @Convert ile Custom Type Mapping
Bazen standart mapping yetmez. @Convert ile kendi dönüşüm mantığınızı yazabilirsiniz:
// JSON string'i Java nesnesine çevirme
@Converter(autoApply = false)
public class AddressConverter implements AttributeConverter<Address, String> {
private final ObjectMapper mapper = new ObjectMapper();
@Override
public String convertToDatabaseColumn(Address address) {
try {
return address == null ? null : mapper.writeValueAsString(address);
} catch (JsonProcessingException e) {
throw new IllegalArgumentException("Cannot convert Address to JSON", e);
}
}
@Override
public Address convertToEntityAttribute(String json) {
try {
return json == null ? null : mapper.readValue(json, Address.class);
} catch (JsonProcessingException e) {
throw new IllegalArgumentException("Cannot convert JSON to Address", e);
}
}
}
// Entity'de kullanım
@Entity
public class Customer {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
private String name;
@Convert(converter = AddressConverter.class)
@Column(columnDefinition = "TEXT") // JSON string uzun olabilir
private Address address; // Java nesnesi ↔ JSON string
}Başka bir kullanışlı örnek — boolean yerine Y/N kullanan legacy veritabanları:
@Converter(autoApply = false)
public class YesNoConverter implements AttributeConverter<Boolean, String> {
@Override
public String convertToDatabaseColumn(Boolean value) {
return Boolean.TRUE.equals(value) ? "Y" : "N";
}
@Override
public Boolean convertToEntityAttribute(String value) {
return "Y".equalsIgnoreCase(value);
}
}
// Kullanım
@Convert(converter = YesNoConverter.class)
@Column(nullable = false, length = 1)
private boolean active; // Veritabanında 'Y' veya 'N'@Convert ile Şifreleme
Hassas verileri veritabanına şifreli kaydetmek için:
@Converter
public class EncryptionConverter implements AttributeConverter<String, String> {
private static final String SECRET_KEY = System.getenv("ENCRYPTION_KEY");
@Override
public String convertToDatabaseColumn(String plainText) {
// AES şifreleme mantığı
return encrypt(plainText, SECRET_KEY);
}
@Override
public String convertToEntityAttribute(String encrypted) {
// AES çözme mantığı
return decrypt(encrypted, SECRET_KEY);
}
}
@Entity
public class User {
@Convert(converter = EncryptionConverter.class)
@Column(length = 500) // Şifreli metin daha uzun olabilir
private String socialSecurityNumber; // Veritabanında şifreli, Java'da düz metin
}💡 İpucu: @Converter(autoApply = true) yaparsanız, o tipin tüm field'larına converter otomatik uygulanır. Ama bu genellikle çok agresiftir — sadece gerçekten her yerde aynı dönüşümü istiyorsanız kullanın.
AI Asistan
Sorularını yanıtlamaya hazır