← Kursa Dön
📄 Text · 15 min

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ı

ParametreVarsayılanAçıklamaÖrnek
namefield adı (snake_case)Veritabanı sütun adıname = "product_name"
nullabletrueNULL olabilir mi?nullable = false → NOT NULL
uniquefalseBenzersiz mi?unique = true → UNIQUE
length255VARCHAR uzunluğulength = 500 → VARCHAR(500)
precision0Toplam basamak sayısı (BigDecimal)precision = 10
scale0Ondalık basamak sayısıscale = 2
insertabletrueINSERT'e dahil mi?insertable = false → oluşturmada yoksay
updatabletrueUPDATE'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 TipiSQL TipiNot
StringVARCHAR(255)@Column(length) ile ayarlanır
int / IntegerINTEGER
long / LongBIGINT
short / ShortSMALLINT
byte / ByteTINYINT
float / FloatFLOATHassasiyet sorunu — para için kullanmayın
double / DoubleDOUBLEHassasiyet sorunu — para için kullanmayın
BigDecimalDECIMAL(p,s)precision/scale belirtilmeli, para için ideal
BigIntegerNUMERIC
boolean / BooleanBOOLEAN / BITDB'ye göre değişir
char / CharacterCHAR(1)
LocalDateDATE
LocalTimeTIME
LocalDateTimeTIMESTAMP
InstantTIMESTAMPUTC bazlı, uluslararası uygulamalar için
ZonedDateTimeTIMESTAMP WITH TZ
UUIDUUID / BINARY(16)PostgreSQL native UUID, MySQL binary
byte[]BLOB@Lob ile büyük binary
String + @LobCLOB / TEXTBüyük metin
EnumVARCHAR / 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.3

Hata 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 kaydedilir

Hata 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) — @Temporal gerekmez

  • `@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.