Takip et

Hibernate One-to-One Mapping Örneği: Detaylı Bir Annotation Yaklaşımı

Hibernate One-to-One Mapping Örneği: Detaylı Bir Annotation Yaklaşımı Giriş: Hibernate ve İlişkisel Veritabanları Modern yazılım gelişti

Hibernate One-to-One Mapping Örneği: Detaylı Bir Annotation Yaklaşımı

Giriş: Hibernate ve İlişkisel Veritabanları

Modern yazılım geliştirmede, nesne yönelimli programlama (OOP) dilleri ile ilişkisel veritabanları arasında bir köprü kurmak kritik bir öneme sahiptir. Bu köprü, “Nesne-İlişkisel Eşleme” (Object-Relational Mapping – ORM) araçları aracılığıyla sağlanır. Hibernate, Java ekosistemindeki en popüler ve güçlü ORM araçlarından biridir. Geliştiricilerin SQL sorguları yazma zahmetinden kurtulmasını sağlayarak, veritabanı işlemlerini Java nesneleri üzerinden gerçekleştirmelerine olanak tanır. Bu sayede, iş mantığına daha fazla odaklanılabilir ve veritabanı bağımsızlığı artırılır.

İlişkisel veritabanları, tablolar arasındaki ilişkileri tanımlamak için çeşitli mekanizmalar sunar. Bu ilişkiler genellikle üç ana kategoriye ayrılır: Bire-Bir (One-to-One), Bire-Çok (One-to-Many) ve Çoka-Çok (Many-to-Many). Bu makalede, bu ilişkilerden “Bire-Bir” (One-to-One) ilişkisinin Hibernate’de, özellikle de annotation’lar kullanılarak nasıl modellendiğini ve yönetildiğini ayrıntılı bir şekilde inceleyeceğiz. Bire-Bir ilişkiler, genellikle bir varlığın diğer bir varlıkla kesinlikle tekil bir bağlantıya sahip olduğu durumlarda kullanılır. Örneğin, bir “Kullanıcı”nın sadece bir “KullanıcıProfili”ne veya bir “Çalışan”ın sadece bir “Pasaport” bilgisine sahip olması gibi senaryolarda karşımıza çıkar. Hibernate, bu tür karmaşık veritabanı ilişkilerini Java sınıfları arasında basit ve anlaşılır annotation’lar aracılığıyla temsil etme yeteneği sunar.

Hibernate’de Annotation Tabanlı Yapılandırma

Hibernate, başlangıçta XML tabanlı yapılandırma dosyaları (örneğin .hbm.xml) ile popülerlik kazanmış olsa da, günümüzde annotation’lar (Java 5’ten itibaren gelen @ ile başlayan yapılar) aracılığıyla yapılan yapılandırma çok daha yaygın ve tercih edilen bir yöntem haline gelmiştir. Annotation’lar, ilgili kodu ve meta veriyi aynı yerde tutarak okunabilirliği artırır, geliştirme sürecini hızlandırır ve XML dosyalarının yönetim yükünü ortadan kaldırır. Bu yaklaşım, özellikle Spring Data JPA gibi modern framework’lerle entegre edildiğinde, geliştiricilere daha tutarlı ve verimli bir deneyim sunar.

Bir Hibernate projesinde annotation’ları kullanmak için gerekli temel bağımlılıklar genellikle hibernate-core ve veritabanı sürücüsüdür. Bu bağımlılıklar, Maven veya Gradle gibi bağımlılık yönetim araçları aracılığıyla kolayca projenize eklenebilir. Annotation’lar sayesinde, Java sınıflarınızı doğrudan veritabanı tablolarına, sütunlarına ve aralarındaki ilişkilere eşleştirebiliriz.

Bire-Bir (One-to-One) İlişki Türleri ve Uygulama Senaryoları

Bire-Bir ilişkiler, veritabanı tasarımında iki ana şekilde modellenebilir ve Hibernate’de bu iki modelleme yaklaşımı farklı annotation’lar ve stratejiler gerektirir:

1. Paylaşılan Birincil Anahtar (Shared Primary Key)

Bu senaryoda, her iki tablonun da aynı birincil anahtarı (Primary Key) paylaşması esastır. Genellikle bir tablo, diğer bir tablonun “uzantısı” veya “detayı” olduğunda bu yaklaşım tercih edilir. Örneğin, bir Kullanici tablosu ve bu kullanıcının ek bilgilerini tutan KullaniciDetay tablosu. KullaniciDetay tablosunun birincil anahtarı, aynı zamanda Kullanici tablosunun birincil anahtarıdır ve bu iki tablo arasında hem birincil anahtar hem de yabancı anahtar (Foreign Key) olarak işlev görür.

* Örnek Senaryo: Bir Kullanici nesnesi, temel kimlik bilgilerini (ID, kullanıcı adı, şifre) içerirken, KullaniciDetay nesnesi daha az erişilen veya daha özel bilgileri (adres, telefon, doğum tarihi) tutabilir. Bu, veritabanı performansını artırmak ve veri modelini daha modüler hale getirmek için yapılabilir.

2. Yabancı Anahtar (Foreign Key) Tabanlı İlişki

Bu daha geleneksel ve yaygın bir One-to-One ilişki modellemesidir. Bir tablonun birincil anahtarı, diğer tablonun bir sütununda yabancı anahtar olarak tutulur. Bu durumda, yabancı anahtarı içeren tablo “owning side” (sahip taraf) olarak kabul edilir ve ilişkinin yönetiminden sorumludur.

* Örnek Senaryo: Bir Calisan nesnesinin bir Pasaport nesnesine sahip olması. Pasaport tablosu, Calisan tablosunun birincil anahtarını kendi içinde bir yabancı anahtar olarak tutar. Bu sayede her Calisan için tek bir Pasaport kaydı olabilir ve her Pasaport kaydı tek bir Calisan‘a ait olur. Bu modelde, Pasaport tablosunun kendi birincil anahtarı genellikle otomatik artan bir değer olabilir ve Calisan ID’si ile aynı olmak zorunda değildir.

Bu iki farklı modelleme yaklaşımı, Hibernate’de farklı annotation’lar ve stratejilerle ele alınır. Şimdi bu annotation’ları ve kullanımlarını detaylıca inceleyelim.

Hibernate One-to-One Mapping için Temel ve İlişki Annotasyonları

Hibernate’de bir One-to-One ilişkiyi tanımlamak için çeşitli annotation’lar kullanılır. Bunları genel ve ilişkiye özgü olarak ikiye ayırabiliriz:

Temel Varlık Annotasyonları:

* @Entity: Bir Java sınıfının bir veritabanı tablosuna eşlenen bir varlık olduğunu belirtir. Hibernate, bu sınıfları yönetir.
* @Table(name = "tablo_adi"): İsteğe bağlıdır. Eğer sınıf adı ile veritabanı tablo adı farklıysa kullanılır.
* @Id: Birincil anahtar alanını belirtir. Her varlık sınıfında mutlaka bulunmalıdır.
* @GeneratedValue(strategy = GenerationType.IDENTITY): Birincil anahtarın nasıl üretileceğini belirtir. IDENTITY, AUTO, SEQUENCE, TABLE gibi stratejiler mevcuttur. IDENTITY genellikle otomatik artan sütunlar için kullanılır.
* @Column(name = "sutun_adi", nullable = false, unique = true, length = 255): İsteğe bağlıdır. Alan adı ile veritabanı sütun adı farklıysa veya sütun özellikleri (null olamaz, benzersiz, uzunluk vb.) belirtilmek isteniyorsa kullanılır.

One-to-One İlişki için Özel Annotasyonlar:

* @OneToOne: Bu annotation, iki varlık sınıfı arasında bire-bir bir ilişki olduğunu belirtir. Alanın üzerine eklenir.
* mappedBy = "ilişki_alanı": Bu özellik, ilişkinin “non-owning side” (sahip olmayan taraf) tarafından kullanılır. İlişkinin diğer tarafındaki (owning side) alanın adını belirtir. mappedBy kullanan taraf, veritabanında yabancı anahtarı tutmaz; bu anahtar diğer tarafta bulunur. mappedBy olmadan, Hibernate her iki tarafta da bir yabancı anahtar sütunu oluşturmaya çalışır ki bu bire-bir ilişkilerde genellikle istenmez.
* cascade = CascadeType.ALL: İlişkili varlıklar üzerindeki işlemlerin (kaydetme, güncelleme, silme vb.) nasıl yayıldığını yönetir. Örneğin, bir Kullanici silindiğinde, ilişkili KullaniciDetay‘ın da silinmesini istiyorsak CascadeType.REMOVE veya CascadeType.ALL kullanırız. Diğer yaygın değerler PERSIST, MERGE, REFRESH, DETACH‘dir.
* fetch = FetchType.LAZY: İlişkili verilerin ne zaman yükleneceğini belirler. FetchType.LAZY (varsayılan), ilişkili varlığın ancak ihtiyaç duyulduğunda (erişildiğinde) yüklenmesini sağlar. FetchType.EAGER ise ana varlık yüklendiğinde ilişkili varlığın da hemen yüklenmesini sağlar. Performans için genellikle LAZY tercih edilir.
* orphanRemoval = true: Yalnız kalan çocuk varlıkların silinmesini sağlar. Eğer bir çocuk varlık, ebeveyninden ayrılırsa (ilişki koparılırsa), veritabanından otomatik olarak silinir. Bu özellik, cascade = CascadeType.REMOVE veya CascadeType.ALL ile birlikte kullanılmalıdır.
* @JoinColumn(name = "yabanci_anahtar_sutunu_adi", unique = true, nullable = false): Bu annotation, ilişkinin “owning side” (sahip taraf) tarafından kullanılır ve yabancı anahtarın bulunduğu sütunu belirtir. unique = true özelliği, bire-bir ilişkiyi garanti etmek için önemlidir, çünkü bu, bu sütunda her değerin yalnızca bir kez görünebileceği anlamına gelir.
* @PrimaryKeyJoinColumn: Bu annotation, özellikle “paylaşılan birincil anahtar” senaryosunda kullanılır. İlişkili varlığın birincil anahtarının, aynı zamanda yabancı anahtar olarak kullanıldığını belirtir. Yani, iki tablo aynı PK’yı paylaşır.
* @MapsId: Paylaşılan birincil anahtar senaryosunda, alt (child) varlığın birincil anahtarının üst (parent) varlığın birincil anahtarından türetildiğini belirtir. Genellikle @PrimaryKeyJoinColumn ile birlikte kullanılır.

Örnek Uygulama: Bire-Bir İlişkilerin Modellenmesi

Şimdi iki farklı bire-bir ilişki modellemesini kod örnekleriyle inceleyelim.

Yöntem 1: Paylaşılan Birincil Anahtar (Shared Primary Key)

Bu senaryoda, bir Kullanici ve KullaniciDetay varlıklarımız olacak. KullaniciDetay‘ın ID’si, Kullanici‘nın ID’si ile aynı olacak.

Kullanici.java (Owning Side – Ebeveyn)

package com.example.hibernate.onetone.sharedpk.entity;

import javax.persistence.*;

@Entity
@Table(name = "kullanicilar")
public class Kullanici {

    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    @Column(name = "kullanici_id")
    private Long id;

    @Column(name = "kullanici_adi", unique = true, nullable = false)
    private String kullaniciAdi;

    @Column(name = "eposta", unique = true, nullable = false)
    private String eposta;

    // One-to-One ilişki: Kullanici bir KullaniciDetay'a sahiptir.
    // cascade = CascadeType.ALL: Kullanici ile yapılan tüm işlemler KullaniciDetay'a da yansır.
    // orphanRemoval = true: Eğer bir KullaniciDetay, Kullanici'dan ayrılırsa silinir.
    // mappedBy = "kullanici": KullaniciDetay sınıfındaki "kullanici" alanı bu ilişkinin sahibidir.
    // Bu taraf (Kullanici) owning side değildir, çünkü mappedBy kullanılmıştır.
    // Ancak Shared Primary Key'de mantıksal olarak Kullanici ebeveyn gibi davranır.
    @OneToOne(mappedBy = "kullanici", cascade = CascadeType.ALL, orphanRemoval = true, fetch = FetchType.LAZY)
    private KullaniciDetay detay;

    public Kullanici() {
    }

    public Kullanici(String kullaniciAdi, String eposta) {
        this.kullaniciAdi = kullaniciAdi;
        this.eposta = eposta;
    }

    // Getter ve Setter'lar
    public Long getId() {
        return id;
    }

    public void setId(Long id) {
        this.id = id;
    }

    public String getKullaniciAdi() {
        return kullaniciAdi;
    }

    public void setKullaniciAdi(String kullaniciAdi) {
        this.kullaniciAdi = kullaniciAdi;
    }

    public String getEposta() {
        return eposta;
    }

    public void setEposta(String eposta) {
        this.eposta = eposta;
    }

    public KullaniciDetay getDetay() {
        return detay;
    }

    // İlişkiyi iki taraflı kurmak için helper metot
    public void setDetay(KullaniciDetay detay) {
        if (detay == null) {
            if (this.detay != null) {
                this.detay.setKullanici(null);
            }
        } else {
            detay.setKullanici(this);
        }
        this.detay = detay;
    }

    @Override
    public String toString() {
        return "Kullanici{" +
               "id=" + id +
               ", kullaniciAdi='" + kullaniciAdi + '\'' +
               ", eposta='" + eposta + '\'' +
               '}';
    }
}

KullaniciDetay.java (Non-Owning Side – Çocuk)

package com.example.hibernate.onetone.sharedpk.entity;

import javax.persistence.*;

@Entity
@Table(name = "kullanici_detaylari")
public class KullaniciDetay {

    // KullaniciDetay'ın ID'si, Kullanici'nın ID'si ile aynıdır.
    // @MapsId, bu ID'nin ilişkili varlıktan (Kullanici) eşlendiğini belirtir.
    @Id
    @Column(name = "kullanici_id") // Sütun adı, Kullanici tablosundaki PK ile aynı.
    private Long id;

    @Column(name = "adres")
    private String adres;

    @Column(name = "telefon")
    private String telefon;

    // One-to-One ilişki: KullaniciDetay bir Kullanici'ya aittir.
    // @MapsId ile birlikte, @PrimaryKeyJoinColumn kullanılır.
    // name = "kullanici_id": Bu tablodaki yabancı anahtar sütunu.
    // referencedColumnName = "kullanici_id": İlişkili tablodaki (Kullanici) referans alınan sütun.
    @OneToOne(fetch = FetchType.LAZY)
    @MapsId // Bu, birincil anahtarın ilişkili varlıktan alındığını belirtir.
    @JoinColumn(name = "kullanici_id") // Bu tarafta yabancı anahtar yok, sadece ID eşlemesi.
                                        // Aslında bu, bu tablonun PK'sının aynı zamanda FK olduğunu belirtir.
    private Kullanici kullanici;

    public KullaniciDetay() {
    }

    public KullaniciDetay(String adres, String telefon) {
        this.adres = adres;
        this.telefon = telefon;
    }

    // Getter ve Setter'lar
    public Long getId() {
        return id;
    }

    public void setId(Long id) {
        this.id = id;
    }

    public String getAdres() {
        return adres;
    }

    public void setAdres(String adres) {
        this.adres = adres;
    }

    public String getTelefon() {
        return telefon;
    }

    public void setTelefon(String telefon) {
        this.telefon = telefon;
    }

    public Kullanici getKullanici() {
        return kullanici;
    }

    public void setKullanici(Kullanici kullanici) {
        this.kullanici = kullanici;
    }

    @Override
    public String toString() {
        return "KullaniciDetay{" +
               "id=" + id +
               ", adres='" + adres + '\'' +
               ", telefon='" + telefon + '\'' +
               '}';
    }
}

Açıklama:
* Kullanici sınıfı, KullaniciDetay sınıfındaki kullanici alanını mappedBy ile referans gösterir. Bu, Kullanici‘nın “non-owning side” olduğunu ve yabancı anahtarın KullaniciDetay tablosunda tutulduğunu belirtir. Ancak, paylaşılan birincil anahtar durumunda, KullaniciDetay‘ın kendi birincil anahtarı Kullanici‘nın birincil anahtarını referans aldığı için, mantıksal olarak KullaniciDetay “owning side” gibi davranır.
* KullaniciDetay sınıfında, @Id ve @MapsId annotation’ları birlikte kullanılır. @MapsId bu varlığın birincil anahtarının, ilişkili varlığın (bu durumda Kullanici) birincil anahtarından alındığını belirtir.
* @JoinColumn(name = "kullanici_id") burada KullaniciDetay‘ın birincil anahtarının aynı zamanda Kullanici tablosuna bir referans (yabancı anahtar) olduğunu gösterir. Bu, veritabanında kullanici_detaylari tablosunun kullanici_id sütununun hem PK hem de FK olmasını sağlar.

Yöntem 2: Yabancı Anahtar (Foreign Key) Tabanlı İlişki

Bu senaryoda, bir Calisan ve Pasaport varlıklarımız olacak. Pasaport tablosu, Calisan tablosunun ID’sini yabancı anahtar olarak tutacak. Pasaport‘un kendi ayrı birincil anahtarı olacak.

Calisan.java (Owning Side – Sahip Taraf)

package com.example.hibernate.onetone.fk.entity;

import javax.persistence.*;

@Entity
@Table(name = "calisanlar")
public class Calisan {

    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    @Column(name = "calisan_id")
    private Long id;

    @Column(name = "ad", nullable = false)
    private String ad;

    @Column(name = "soyad", nullable = false)
    private String soyad;

    // One-to-One ilişki: Calisan bir Pasaport'a sahiptir.
    // cascade = CascadeType.ALL: Calisan ile yapılan tüm işlemler Pasaport'a da yansır.
    // orphanRemoval = true: Eğer bir Pasaport, Calisan'dan ayrılırsa silinir.
    // @JoinColumn(name = "pasaport_id", unique = true, nullable = false):
    // Bu, "calisanlar" tablosunda "pasaport_id" adında bir yabancı anahtar sütunu oluşturur.
    // unique = true: Bir pasaport sadece bir çalışana ait olabilir, bu bire-bir ilişkiyi garanti eder.
    // nullable = false: Her çalışanın bir pasaportu olmak zorunda.
    @OneToOne(cascade = CascadeType.ALL, orphanRemoval = true, fetch = FetchType.LAZY)
    @JoinColumn(name = "pasaport_id", unique = true, nullable = false)
    private Pasaport pasaport;

    public Calisan() {
    }

    public Calisan(String ad, String soyad) {
        this.ad = ad;
        this.soyad = soyad;
    }

    // Getter ve Setter'lar
    public Long getId() {
        return id;
    }

    public void setId(Long id) {
        this.id = id;
    }

    public String getAd() {
        return ad;
    }

    public void setAd(String ad) {
        this.ad = ad;
    }

    public String getSoyad() {
        return soyad;
    }

    public void setSoyad(String soyad) {
        this.soyad = soyad;
    }

    public Pasaport getPasaport() {
        return pasaport;
    }

    // İlişkiyi iki taraflı kurmak için helper metot
    public void setPasaport(Pasaport pasaport) {
        if (pasaport == null) {
            if (this.pasaport != null) {
                this.pasaport.setCalisan(null);
            }
        } else {
            pasaport.setCalisan(this);
        }
        this.pasaport = pasaport;
    }

    @Override
    public String toString() {
        return "Calisan{" +
               "id=" + id +
               ", ad='" + ad + '\'' +
               ", soyad='" + soyad + '\'' +
               '}';
    }
}

Pasaport.java (Non-Owning Side – Sahip Olmayan Taraf)

package com.example.hibernate.onetone.fk.entity;

import javax.persistence.*;
import java.time.LocalDate;

@Entity
@Table(name = "pasaportlar")
public class Pasaport {

    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    @Column(name = "pasaport_id")
    private Long id;

    @Column(name = "pasaport_numarasi", unique = true, nullable = false)
    private String pasaportNumarasi;

    @Column(name = "verilis_tarihi", nullable = false)
    private LocalDate verilisTarihi;

    @Column(name = "son_gecerlilik_tarihi", nullable = false)
    private LocalDate sonGecerlilikTarihi;

    // One-to-One ilişki: Pasaport bir Calisan'a aittir.
    // mappedBy = "pasaport": Calisan sınıfındaki "pasaport" alanı bu ilişkinin sahibidir.
    // Bu taraf (Pasaport) owning side değildir, yabancı anahtarı tutmaz.
    @OneToOne(mappedBy = "pasaport", fetch = FetchType.LAZY)
    private Calisan calisan;

    public Pasaport() {
    }

    public Pasaport(String pasaportNumarasi, LocalDate verilisTarihi, LocalDate sonGecerlilikTarihi) {
        this.pasaportNumarasi = pasaportNumarasi;
        this.verilisTarihi = verilisTarihi;
        this.sonGecerlilikTarihi = sonGecerlilikTarihi;
    }

    // Getter ve Setter'lar
    public Long getId() {
        return id;
    }

    public void setId(Long id) {
        this.id = id;
    }

    public String getPasaportNumarasi() {
        return pasaportNumarasi;
    }

    public void setPasaportNumarasi(String pasaportNumarasi) {
        this.pasaportNumarasi = pasaportNumarasi;
    }

    public LocalDate getVerilisTarihi() {
        return verilisTarihi;
    }

    public void setVerilisTarihi(LocalDate verilisTarihi) {
        this.verilisTarihi = verilisTarihi;
    }

    public LocalDate getSonGecerlilikTarihi() {
        return sonGecerlilikTarihi;
    }

    public void setSonGecerlilikTarihi(LocalDate sonGecerlilikTarihi) {
        this.sonGecerlilikTarihi = sonGecerlilikTarihi;
    }

    public Calisan getCalisan() {
        return calisan;
    }

    public void setCalisan(Calisan calisan) {
        this.calisan = calisan;
    }

    @Override
    public String toString() {
        return "Pasaport{" +
               "id=" + id +
               ", pasaportNumarasi='" + pasaportNumarasi + '\'' +
               ", verilisTarihi=" + verilisTarihi +
               ", sonGecerlilikTarihi=" + sonGecerlilikTarihi +
               '}';
    }
}

Açıklama:
* Calisan sınıfı “owning side”dır çünkü @JoinColumn annotation’ını kullanır. Bu, calisanlar tablosunda pasaport_id adında bir yabancı anahtar sütunu oluşturulacağını ve bu sütunun pasaportlar tablosundaki pasaport_id‘yi referans alacağını belirtir. unique = true özelliği, bu yabancı anahtarın her zaman benzersiz olmasını sağlayarak bire-bir ilişkiyi zorunlu kılar.
* Pasaport sınıfı “non-owning side”dır ve mappedBy = "pasaport" kullanır. Bu, Pasaport nesnesinin kendisinin veritabanında bir yabancı anahtar sütunu oluşturmayacağını, ilişkinin Calisan sınıfındaki pasaport alanı tarafından yönetildiğini gösterir.

Her iki örnekte de, setDetay ve setPasaport gibi yardımcı metotlar (helper methods) tanımlanmıştır. Bu metotlar, çift yönlü (bidirectional) ilişkilerde her iki tarafın da doğru şekilde ayarlanmasını sağlar. Bu, özellikle bir varlığı kaydederken veya güncellerken ilişki tutarlılığını sağlamak için önemlidir.

Hibernate Yapılandırması

Hibernate’in çalışabilmesi için veritabanı bağlantı bilgilerini ve eşlenen varlık sınıflarını içeren bir yapılandırma dosyasına ihtiyacı vardır. Geleneksel olarak bu hibernate.cfg.xml dosyasıdır.

hibernate.cfg.xml



    
        
        com.mysql.cj.jdbc.Driver
        jdbc:mysql://localhost:3306/hibernate_onetone_db?useSSL=false&serverTimezone=UTC
        root
        your_password

        
        org.hibernate.dialect.MySQL8Dialect

        
        true
        true

        
        
        
        
        
        update

        
        
        
        
        

    

pom.xml (Gerekli Bağımlılıklar)


    4.0.0
    com.example
    hibernate-onetone-example
    1.0-SNAPSHOT
    
        11
        11
    
    
        
        
            org.hibernate
            hibernate-core
            5.6.15.Final
        
        
        
            mysql
            mysql-connector-java
            8.0.33
        
        
        
            javax.persistence
            javax.persistence-api
            2.2
        
    

CRUD İşlemleri ve Test

Şimdi bu varlıklar üzerinde temel CRUD (Create, Read, Update, Delete) işlemlerini nasıl yapacağımıza dair bir örnek uygulama yazalım.

package com.example.hibernate.onetone.main;

import com.example.hibernate.onetone.fk.entity.Calisan;
import com.example.hibernate.onetone.fk.entity.Pasaport;
import com.example.hibernate.onetone.sharedpk.entity.Kullanici;
import com.example.hibernate.onetone.sharedpk.entity.KullaniciDetay;
import org.hibernate.Session;
import org.hibernate.SessionFactory;
import org.hibernate.cfg.Configuration;

import java.time.LocalDate;

public class OneToOneDemo {

    public static void main(String[] args) {
        // SessionFactory oluşturma
        SessionFactory factory = new Configuration()
                .configure("hibernate.cfg.xml")
                .addAnnotatedClass(Kullanici.class)
                .addAnnotatedClass(KullaniciDetay.class)
                .addAnnotatedClass(Calisan.class)
                .addAnnotatedClass(Pasaport.class)
                .buildSessionFactory();

        Session session = null;

        try {
            // Shared Primary Key Örneği (Kullanici - KullaniciDetay)
            System.out.println("--- Kullanici - KullaniciDetay Örneği ---");
            session = factory.getCurrentSession();
            session.beginTransaction();

            // 1. Oluşturma ve Kaydetme
            Kullanici kullanici1 = new Kullanici("ahmet.yilmaz", "ahmet@example.com");
            KullaniciDetay detay1 = new KullaniciDetay("Ankara, Türkiye", "555-1234567");
            kullanici1.setDetay(detay1); // Helper metot ile iki yönlü ilişki kurulur

            System.out.println("Kullanici ve KullaniciDetay kaydediliyor...");
            session.save(kullanici1); // CascadeType.ALL sayesinde detay da kaydedilir
            System.out.println("Kaydedilen Kullanici ID: " + kullanici1.getId());
            System.out.println("Kaydedilen KullaniciDetay ID: " + detay1.getId()); // ID'ler aynı olmalı!

            session.getTransaction().commit();
            System.out.println("Kullanici ve KullaniciDetay başarıyla kaydedildi.");

            // 2. Okuma
            session = factory.getCurrentSession();
            session.beginTransaction();
            System.out.println("\nKullanici ID " + kullanici1.getId() + " okunuyor...");
            Kullanici okunanKullanici = session.get(Kullanici.class, kullanici1.getId());
            System.out.println("Okunan Kullanici: " + okunanKullanici);
            System.out.println("Okunan KullaniciDetay: " + okunanKullanici.getDetay()); // LAZY olduğu için burada yüklenir
            session.getTransaction().commit();
            System.out.println("Kullanici ve KullaniciDetay başarıyla okundu.");

            // 3. Güncelleme
            session = factory.getCurrentSession();
            session.beginTransaction();
            System.out.println("\nKullaniciDetay güncelleniyor...");
            okunanKullanici.getDetay().setAdres("İstanbul, Türkiye");
            session.merge(okunanKullanici); // Değişiklikler merge ile kaydedilir
            session.getTransaction().commit();
            System.out.println("KullaniciDetay başarıyla güncellendi.");

            // 4. Silme
            session = factory.getCurrentSession();
            session.beginTransaction();
            System.out.println("\nKullanici ID " + kullanici1.getId() + " siliniyor...");
            // CascadeType.ALL ve orphanRemoval=true sayesinde detay da silinir
            session.delete(okunanKullanici);
            session.getTransaction().commit();
            System.out.println("Kullanici ve ilişkili KullaniciDetay başarıyla silindi.");


            // Foreign Key Tabanlı Örnek (Calisan - Pasaport)
            System.out.println("\n--- Calisan - Pasaport Örneği ---");
            session = factory.getCurrentSession();
            session.beginTransaction();

            // 1. Oluşturma ve Kaydetme
            Calisan calisan1 = new Calisan("Ayşe", "Demir");
            Pasaport pasaport1 = new Pasaport("TR123456", LocalDate.of(2020, 1, 1), LocalDate.of(2030, 1, 1));
            calisan1.setPasaport(pasaport1); // Helper metot ile iki yönlü ilişki kurulur

            System.out.println("Calisan ve Pasaport kaydediliyor...");
            session.save(calisan1); // CascadeType.ALL sayesinde pasaport da kaydedilir
            System.out.println("Kaydedilen Calisan ID: " + calisan1.getId());
            System.out.println("Kaydedilen Pasaport ID: " + pasaport1.getId()); // ID'ler farklı olmalı!

            session.getTransaction().commit();
            System.out.println("Calisan ve Pasaport başarıyla kaydedildi.");

            // 2. Okuma
            session = factory.getCurrentSession();
            session.beginTransaction();
            System.out.println("\nCalisan ID " + calisan1.getId() + " okunuyor...");
            Calisan okunanCalisan = session.get(Calisan.class, calisan1.getId());
            System.out.println("Okunan Calisan: " + okunanCalisan);
            System.out.println("Okunan Pasaport: " + okunanCalisan.getPasaport()); // LAZY olduğu için burada yüklenir
            session.getTransaction().commit();
            System.out.println("Calisan ve Pasaport başarıyla okundu.");

            // 3. Güncelleme
            session = factory.getCurrentSession();
            session.beginTransaction();
            System.out.println("\nPasaport bilgisi güncelleniyor...");
            okunanCalisan.getPasaport().setSonGecerlilikTarihi(LocalDate.of(2035, 1, 1));
            session.merge(okunanCalisan); // Değişiklikler merge ile kaydedilir
            session.getTransaction().commit();
            System.out.println("Pasaport bilgisi başarıyla güncellendi.");

            // 4. Silme
            session = factory.getCurrentSession();
            session.beginTransaction();
            System.out.println("\nCalisan ID " + calisan1.getId() + " siliniyor...");
            // CascadeType.ALL ve orphanRemoval=true sayesinde pasaport da silinir
            session.delete(okunanCalisan);
            session.getTransaction().commit();
            System.out.println("Calisan ve ilişkili Pasaport başarıyla silindi.");


        } catch (Exception exc) {
            exc.printStackTrace();
        } finally {
            if (session != null && session.isOpen()) {
                session.close();
            }
            factory.close();
        }
    }
}

Bu örnek kod, hem paylaşılan birincil anahtar hem de yabancı anahtar tabanlı One-to-One ilişkiler için oluşturma, okuma, güncelleme ve silme işlemlerini göstermektedir. SessionFactory ve Session yönetimi, transaction’lar ve cascade mekanizmasının nasıl çalıştığı açıkça görülmektedir.

Dikkat Edilmesi Gerekenler ve En İyi Uygulamalar

* Owning Side ve mappedBy: Çift yönlü ilişkilerde, bir taraf “owning side” (sahip taraf) olmalıdır. Yabancı anahtar bu tarafta bulunur. Diğer taraf ise “non-owning side”dır ve mappedBy özelliğini kullanarak owning side’daki alanı referans gösterir. mappedBy kullanmak, Hibernate’in gereksiz yabancı anahtar sütunları oluşturmasını engeller ve ilişki tutarlılığını sağlar.
* cascade Stratejileri: CascadeType.ALL genellikle geliştirme sürecini kolaylaştırsa da, üretim ortamlarında dikkatli kullanılmalıdır. Gereksiz veya istenmeyen veri silmelerini önlemek için daha spesifik CascadeType değerleri (örneğin PERSIST, MERGE, REMOVE) tercih edilebilir.
* fetch Stratejileri (EAGER vs. LAZY): Performans açısından FetchType.LAZY (tembel yükleme) genellikle tercih edilir. EAGER (hevesli yükleme), ana varlıkla birlikte ilişkili varlıkların da hemen yüklenmesine neden olur, bu da çok sayıda ilişkili varlık olduğunda performansı olumsuz etkileyebilir (N+1 problemi). LAZY yükleme, ilişkili varlığa ilk erişildiğinde yüklenmesini sağlar.
* N+1 Problemi: LAZY yükleme kullanılırken, her bir ana varlık için ayrı bir sorgu ile ilişkili varlıkların yüklenmesi “N+1 problemi”ne yol açabilir. Bu durum, özellikle bir listedeki tüm ana varlıkların ilişkili detaylarına erişildiğinde ortaya çıkar. Bu sorunu çözmek için JPQL/HQL’deki JOIN FETCH veya Criteria API’deki fetch gibi mekanizmalar kullanılabilir.
* Bidirectional (Çift Yönlü) ve Unidirectional (Tek Yönlü) İlişkiler: Yukarıdaki örnekler çift yönlü ilişkileri göstermektedir (hem Kullanici‘dan KullaniciDetay‘a hem de tersi). Eğer sadece tek bir yönde ilişkiye ihtiyacınız varsa (örneğin sadece Kullanici‘dan KullaniciDetay‘a erişim), ilişkiyi tek yönlü olarak tanımlamak (sadece owning side’ı belirtmek ve mappedBy kullanmamak) kod karmaşıklığını azaltabilir.
* orphanRemoval = true: Bu özellik, bir ebeveyn varlıktan bir çocuk varlık kaldırıldığında (ilişki koparıldığında), çocuğun veritabanından otomatik olarak silinmesini sağlar. Bu, gereksiz veri birikimini önler ve veri bütünlüğünü korur. CascadeType.REMOVE ile benzer görünse de, orphanRemoval ilişkiden koparılan varlıkları silerken, CascadeType.REMOVE ebeveyn varlık silindiğinde çocuk varlıkları siler.
* Veritabanı Tasarımı: Hibernate mapping’leri, altta yatan veritabanı şemasıyla uyumlu olmalıdır. Özellikle bire-bir ilişkilerde, yabancı anahtar sütunlarının UNIQUE kısıtlamasına sahip olması kritik öneme sahiptir.

Sonuç

Hibernate, Java uygulamalarında ilişkisel veritabanı etkileşimlerini basitleştiren güçlü bir ORM aracıdır. Annotation’lar aracılığıyla, bire-bir ilişkiler gibi karmaşık veritabanı yapılarını Java nesneleri arasında kolayca modelleyebilir ve yönetebiliriz. Bu makalede, paylaşılan birincil anahtar ve yabancı anahtar tabanlı olmak üzere iki ana bire-bir ilişki modellemesi incelendi, gerekli annotation’lar detaylandırıldı ve her iki senaryo için kapsamlı kod örnekleri sunuldu.

Doğru mapping stratejilerini seçmek, cascade ve fetch türlerini bilinçli bir şekilde kullanmak, performans ve veri bütünlüğü açısından hayati önem taşır. Hibernate’in sunduğu bu esneklik ve güç, geliştiricilerin daha temiz, daha sürdürülebilir ve daha verimli uygulamalar geliştirmesine olanak tanır. Unutulmamalıdır ki, her ne kadar Hibernate veritabanı detaylarını soyutlasa da, altta yatan veritabanı prensiplerini ve optimizasyon tekniklerini anlamak, yüksek performanslı ve hatasız uygulamalar geliştirmek için kritik bir beceridir.

Yorumlar
İçeriği beğendiniz mi? Bir tartışma başlatın veya görüşlerinizi paylaşın.
Yorum Yaz

Bir yanıt yazın

E-posta adresiniz yayınlanmayacak. Gerekli alanlar * ile işaretlenmişlerdir

Gönder

E-posta Bülteni
Yazılım Topluluğuna Katılın
En son güncellemeleri, yaratıcı ipuçlarını ve özel kaynakları doğrudan e-posta kutunuza alın. Tasarım ve inovasyonun geleceğini birlikte keşfedelim.
Exit mobile version