Java Persistence API (JPA) ile Programlama – II

Giriş

Bir önceki yazımızda NİE (nesne-ilişkisel eşleştirme) kavramını açıklamış ve JPA’ye giriş yapmış, temel JPA ayarlarını ve nesnelerini de öğrenmiştik. Daha önceki yazıyı okuyup, örnek projedeki kodları çalıştırdıysanız JPA üzerinde ilerlemeye hazırsınız demektir. Bu yazıda temel olarak eşleştirme (mapping) bilgimizi arttıracağız. Önce kimlik bilgisi atamaları için JPA’in sağladığı stratejileri inceleyeceğiz. Sonra da entityler arasındaki 1-1, 1-N ve M-N türdeki ilişkilerinde veri tabanındaki tablolarla eşleştirmeyi ve bu bağlamda birden fazla nesneyi veri tabanından getirme (fetching) ya da uygulama yükleme (loading) ve iletilen işlem (cascading) stratejilerini yerimiz elverdiğince inceleyeceğiz. Yazı içerisinde Eclipse projesinden kod örnekleri yer alacaktır. Anlatılan konularla ilgili kodlar projede farklı paketlerde yer almaktadır. Her örnek farklı bir persistence-unit ile ifade edilmiş ve ilgili kısımda (personIdentityAuto) gibi belirtilmiştir.

Kimlik Bilgisi

İlk yazımızda Customer sınıfındaki kimlik bilgisini ifade eden id alanını @Id ile notlandırmış ve bu alana p1 paketinde long tipinde bir değer atamış (personIdentityAssigned), p2 paketinde ise @GeneratedValue(strategy = GenerationType.AUTO) notu ile, kullandığımız Hibernate JPA ürünün otomatik olarak değer atamasını sağlamıştık (personIdentityAuto). Dolayısıyla ilk durumda id ataması için bir strateji verilmediğinden, oluşturduğumuz Customer nesnelerine kimlik bilgilerini biz vermiştik (assigned). Bu durumun sıkıntısı, kimlik bilgilerinin tekilliğini sağlamanın programcıya düşüyor olmasıdır. İkinci durumda ise kimlik bilgisinin yönetimini, kullanılan JPA ürününe bıraktık ki o da ilgili veri tabanı için uygun bir strateji seçecektir. Bu durumda, örneklerimizde kullandığımız JPA ürünü olan Hibernate, Oracle XE veri tabanı için bir dizi (sequence) oluşturmuş ve ardışıl tamsayıları bu diziden üretmişti. İlk yazıda GenerationType’ın, AUTO dışında IDENTITY, SEQUENCE ve TABLE olmak üzere üç tane daha stratejisi olduğunu da belirtmiştik. Şimdi bu üç kimlik bilgisi stratejilerini görelim.

GenerationType.SEQUENCE, veri tabanında oluşturulacak bir dizi (sequence) nesnesini kullanarak, veri tabanına yazma anında, nesneye kimlik bilgisinin atanmasını sağlar. Aşağıda, bundan sonraki örneklerimizde kullanacağımız Person sınıfının kimlik bilgisi olan id alanı, veri tabanında oluşturulan CUSTOMERIDSEQUENCE isimi bir dizi ile atanıyor.

@Id
@SequenceGenerator(name="CustomerIDSequence", sequenceName="CUSTOMERIDSEQUENCE",
allocationSize=2)
@GeneratedValue(strategy=GenerationType.SEQUENCE, generator="CustomerIDSequence")
private int id;

Kod 1

Yukarıdaki gibi bir notlandırma yapınca Hibernate, XE’de CUSTOMERIDSEQUENCE isminde bir dizi oluşturmakta ve test sınıfını her çalıştırmada id’ye atadığı değerleri 1 ileriden başlatmaktadır. İsterseniz persistence.xml dosyasındaki hibernate.hbm2ddl.auto özelliğininin değerinin create olmasını önler ve aynı isimdeki diziyi siz kendiniz XE’de oluşturabilirsiniz ki bu durumda Hibernate sizin oluşturduğunuz diziyi kullanacaktır. Entityler için tamsayılardan oluşan ve tekillik dışında bir anlamı olmayan anahtar alanlarını kullanmanın her zaman daha avantajlı olduğu gerçeğini göz önüne alırsak, dizi yapısını destekleyen Oracle ve DB2 gibi veri tabanlarında yukarıdaki gibi diziden kimlik bilgisini almak son derece avantajlıdır.

Dizi yapısını desteklemeyen Microsoft SQL Server gibi veri tabanları için dizi yerine kimlik sütunu kullanılabilir. Bu durum, JPA’in GenerationType.IDENTITY tipine karşılık gelmektedir. Bunun için Person entitysinin id alanı aşağıdaki gibi notlandırılmış olmalıdır:

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

Kod 2

Bu durumda persistence.xml dosyasındaki hibernate.hbm2ddl.auto özelliği kullanıldığında Hibernate SQL Server’da, Identity özelliği olan ve varsayılan halde 1’er artan bir kimlik sütununa sahip bir tablo oluşturacaktır (personIdentityIdentity).

Arzu ederseniz, GenerationType.TABLE kullanarak bir tablodan da tekil kimlik bilgisi üretebilirsiniz. Bunun için aşağıdaki gibi bir notlandırma yeterli olacaktır:

@Id
@TableGenerator(
name="PersonIDGenerator",
table="PersonIDGeneratorTable",
pkColumnName="PersonIDGenerator",
valueColumnName="PersonIDValue",
pkColumnValue="PersonID",
allocationSize=1)
@GeneratedValue(strategy=GenerationType.TABLE, generator="PersonIDGenerator")

Kod 3

Bu durumda Oracle XE üzerinde, içinde PersonIDGenerator ve PersonIDValue sütunları bulunan PersonIDGeneratorTable isimli bir tablo oluşturmanız gerekecektir (ya da artık biliyoruz ki Hibernate bütün bunları bizim için oluşturabilir) (personIdentityTable). Veri tabanına her yeni Person nesnesinin kaydı sırasında bu tablodaki PersonID değerine karşılık gelen PersonIDValue değeri, bu nesnenin anahtar alanı olarak atanacaktır.

Birleşik Kimlik

Bazen, özellikle de eski verilerle çalışırken, tablolardaki anahtar alanların birden fazla sütunu içermesi söz konusu olabilir. Bu tip anahtar alanlara birleşik anahtar (composite key) denir ve sınıf tanımlarımızda bu durum birleşik kimlik (composite id) bilgisiyle halledilmelidir. JPA, birleşik kimlik alanların eşleştirilmesi için üç yol sağlar: İlki birleşik kimliği oluşturan her bir alanın @Id ile notlandırılmasıdır (Kod 4). Bu şekilde Hibernate veri tabanında FISTNAME ve LASTNAME sütunlarının ikisini birden anahtar alan olarak işaretleyecektir. Böyle bir birleşik alan halinde entitynin Serializable olması gerekmektedir. Bu yaklaşımı, birleşik anahtarı oluşturan kimlik bileşenlerinin hep birlikte bir sınıf olarak anlam ifade etmediği ve tekrar kullanım ihtiyacının gerekmediği durumlarda kullanırız (personIdentityComposite1).

@Entity(name = "Person")
@Table(name="PersonIdentityComposite1")
public class Person implements Serializable{

@Id //composite key
private String firstName;

@Id //composite key
private String lastName;

. . .
}

Kod 4

Birleşik anahtarı oluşturmanın ikinci yolu ise kimlik bilgisinin tamamen bir başka entity olması durumudur ki bu hal, nesne-merkezli programlama açısından daha anlamlıdır. Örneğimizdeki Person sınıfının firstName ve lastName alanlarının Name isminde bir sınıfın alanları olduğunu ve Name tipinden bir nesnenin de Person sınıfının kimlik bilgisini oluşturduğunu düşünün. Bu durumda Name sınıfının JPA’in @Embeddable notu ile Person sınıfındaki Name nesnesinin ise @EmbeddedId ile notlandırılması gereklidir (personIdentityComposite2). (Kod 5)

@Entity(name = "Name")
@Embeddable
public class Name implements Serializable{

private String firstName;
private String lastName;
. . .
}

@Entity(name = "Person")
@Table(name="PersonIdentityComposite2")
public class Person implements Serializable{

@EmbeddedId
private Name name;
. . .
}

Kod 5

Birleşik anahtar alanlarını ilk halde olduğu gibi ayrı ayrı @Id ile notlandırıp, bu alanları ayrı bir sınıf olarak ifade etmek de en son ve en az kullanılan durumdur. Kod 6’dan da görüldüğü gibi Name sınıfına kimlik bilgisi ile ilgili ayrı bir notlandırma yapmaya gerek yoktur sadece @IdClass sınıfının alanlarının isim ve tipleri, entitynin @Id ile notlandırılmış alanlarının isim ve tipleri ile aynı olmalıdır (personIdentityComposite3).

@Entity(name = "Person")
@Table(name="PersonIdentityComposite3")
@IdClass(Name.class)
public class Person implements Serializable{

@Id
private String firstName;
@Id
private String lastName;
. . .
}

@Entity(name = "Name")
public class Name implements Serializable{

private String firstName;
private String lastName;
. . .
}

Kod 6

Birleşik kimlik ve alan konusunda tavsiyemiz öncelikle mümkünse @Embeddable sınıf kullanılması ya da alternatif olarak ilk şeklin tercih edilmesidir.

Eşleştirme

JPA kullanımında en temel şey, uygulamadaki sınıfların veri tabanındaki tablolarla eşleştirilmesidir. Mapping dediğimiz bu işlem, uygulamadaki sınıfları ve aralarındaki ilişkileri (association), veri tabanında tablolar ve aralarındaki ilişkilere (relation) yansıtmaktan başka birşey değildir. Şimdi sırayla eşleştirme yaparken göz önünde bulundurmamız gereken bazı konuları ele alalım.

@Transient

Eşleştirmede ilk göz önüne alınacak konu entitynin veri tabanında kalıcı hale getirilmeyecek alanlarının olup olmadığıdır. JPA varsayılan durumda, bütün static olmayan ve @Transient ile notlandırılmamış alanları veri tabanında kalıcı kılar. Yani eğer entitimizde veri tabanında kalıcı olması gerekmeyen alanlar varsa, bu alanları @Transient ile notlandırırız. Aşağıdaki örneği @Transient notu ile ve bu notsuz iki defa çalıştırın ve oluşan tabloları karşılaştırın (personTransient).

@Entity(name = "Person")
@Table(name="PersonTransient")
public class Person {

@Id //signifies the primary key
@GeneratedValue(strategy = GenerationType.AUTO)
private int id;
private String firstName;
private String lastName;
@Transient
private String fullName;
. . .
}

Kod 7

Eşleştirilebilecek Tipler

Eşleştirirme yaparken entitinin içeriği bizim için en önemli şeydir. Entititilerde yukarıda da ifade ettiğimiz gibi static olmayan ve @Transient ile notlandırılmamış her alan veri tabanında kalıcı hale gelecektir. Alanlara erişimin, bean özellikleri (property) şeklinde, yani private alanlar ve set/get erişim metotları olması durumundan ilk yazımızda bahsetmiştik. Bu şekilde oluşturulmuş bir Java sınıfının özellikleri, şu tiplerden birisi olarak tanımlanmış olabilir: Bütün Java basit tipleri (primitives), bunları sarmalayan nesne tipleri (wrappers), java.lang.String, java.math.BigInteger, java.math.BigDecimal, java.util.Date, java.util.Calendar, java.sql.Date, java.sql.Time, java.sql.Timestamp, byte[], Byte[], char[] ve Character[] tipleri, kullanıcıların tanımladığı bütün Serializable sınıflar, enum tipleri, entityler ve entity torbaları (collections) ve gömülü (embedded) sınıflar. Bu tiplerden olan herhangi bir değişken, bir entitynin özelliği olarak veri tabanında kalıcı kılınabilir. Biraz daha açıklarsak, JPA, bir entitynin, dört farklı türde eşleştirilen dolayısıyla da kalıcı kılınan alana sahip olabileceğini söyler: İlki, JPA’in nasıl eşleştirildiğini bildiği 8 tane basit tip ile java.lang.String, java.math.BigInteger, java.math.BigDecimal, java.util.Date, java.util.Calendar, java.sql.Date, java.sql.Time, java.sql.Timestamp, byte[], Byte[], char[] ve Character[] tipleridir. Bir alan bu tiplerden birisinden değişken olarak tanımlandığında JPA veri tabanına varsayılan durumda eşleştirmesini yapar. Yok eğer entitynin bir alanı bu tipten değilse, örneğin Person içinde Car tipinden bir değişen olarak bulunuyorsa, bu değişkenin eşleştirmesi için geriye üç durum söz konusu olabilir: Serializable nesne, gömülü (embedded) nesne ve entity.

Eğer Car tipinde olan değişken bir entity değilse, yani @Entity ile notlandırılmamış ama Car sınıfı Serializable arayüzünü (interface) gerçekleştirmekteyse Car nesnesi, Person entitysinin eşleştirildiği tabloya byte dizisi olarak yazılır. Car sınıfı @Entity ile notlandırılmadığından, Car nesnesinin alanlarının ayrı ayrı sütunlarla eşleştirilmesi mümkün değildir. İlgili örneği çalıştırdığınızda PersonWithSerializedCar tablosunda Car isminde ve RAW tipinde bir alan olduğunu göreceksiniz (personSerializable).

Bir sınıf içinde, diğer kullanıcı tanımlı sınıfından oluşturulmuş bir nesne varsa, bu nesnenin yukarıdaki örnekte olduğu gibi serialized olarak yazılması çoğunlukla tercih edeceğimiz bir durum değildir. Yani yukarıdaki örnekte Car nesnesinin de Person gibi veri tabanıyla tablo ve sütun temelli eşleştirilmesi için Car sınıfının ya @Entity ya da @Embeddable olarak notlandırılması gereklidir. Car sınıfı @Embeddable ile notlandırıldığında, nesnesi de Person sınıfında @Embedded olarak notlandırılır. Gömülü nesne olarak ifade edebileceğimiz @Embeddable ile notlandırılmış sınıflar, anlam açısından gömüldüğü entitynin bir parçası olurlar, yani esas entity nesnesi yoksa, içinde gömülü olan nesne de zaten hiç olmamalıdır. Böyle bir yapıyı Customer-Address ilişkisinde daha rahat görebiliriz. Address nesnesi, Customer nesnesi olmadan ve ondan bağımsız oluşturulamayacaksa, aradaki ilişkiyi ifade etmenin en iyi yolu gömülü nesne yaklaşımıdır. Bu durumda kodumuz şöyle olur (customerEmbedded):

@Entity(name = "Customer")
@Table(name="CustomerEmbedded")
public class Customer{
. . .
@Embedded
private Address address = new Address();
. . .
}

@Embeddable
public class Address{
private String street;

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

. . .
}

Kod 8

@Embeddable sınıflar, içinde gömülü bulundukları sınıfla çok yakın bir ilişki içinde bulunduklarından @Entity ile notlandırılmaz, kendi kimlik bilgileri olmaz ve alanları, @Table ile notlandırılamazlar çünkü alanları, içinde gömülü olduğu entitynin tablosundaki sütunlara yazılır. İlgili kod örneğini çalıştırdığınızda CustomerWithEmbeddedAddress isimli tabloya bakarak bunu teyit edebilirsiniz.

Entitiler arasındaki İlişkiler

JPA’in otomatik olarak eşleştirdiği tipler, serializable ve gömülü tipler dışında kalan bütün sınıflar @Entity ile notlandırılmalıdır. Bu durumda birden fazla entity arasındaki ilişkiden (association) bahsediyoruz demektir ki böyle bir ilişkinin bazı özellikleri söz konusudur: (1) İlişkinin yönü: Yani ilişkinin iki tarafındaki entityler birbirlerini bilecekler mi (direction of association) sorusu cevaplandırılmalıdır. (2) İlişkiye giren entity sayısı (cardinality): Yani 1-1, 1-N ve M-N’den hangisi? (3) Entityleri yükleme stratejisi: Yani ilişkinin bir tarafındaki entity veri tabanından yüklenince diğer tarafındaki(ler) de yüklenmeli mi? (4) İlişkinin bir tarafındaki entitye bir işlem yapıldığında örneğin entity silindiğinde ilişkinin diğer tarafındaki entity(ler)e ne olacak?

İlk sorunun cevabı basittir. Sonuçta ilişki ya tek yönlüdür ya da iki yönlüdür. Sahip ve arabası yani Person-Car ilişkisinde örneğin, ilişki tek yönlü olduğunda Person, Car’ı arabası olarak bilir ama tersi doğru değildir. (Ya da Car, Person’u sahibi olarak bilir ama Person Car’ı arabası olarak bilmez. Bu durum aradaki ilişkide neyi ana nesne olarak aldığınızla ilgili bir farklılıktır.) Eğer Car sahibini bilecekse Person tipinde bir nesneyi Car sınıfında tanımlarız ve gerekli notlandırmayı yaparız, bilmeyecekse buna gerek yoktur.

İkinci sorunun cevabı bir kişinin kaç tane arabası olacağı ve bir arabanın kaç tane sahibi olacağı ile ilgilidir. Örneklerle 1-1, 1-N/N-1 ve M-N ilişkilerini işleyeceğiz. Bu durumlariçin @OneToOne, @OneToMany, @ManyToOne ve @ManyToMany notları mevcuttur.

Üçüncü sorunun cevabı iki mümkün durumdan birisidir ve javax.persistence.FetchType enum tipiyle ifade edilir: Sahip veri tabanından getirildiğinde, arabası/arabaları da gelecekse bu durum “önce yükleme” (eager loading) olarak adlandırılır. Aksi taktirde bir sahip veri tabanından uygulamaya yüklendiğinde arabası/arabaları gelmez, ihtiyaç duyulursa daha sonra getirilir ki bu durum da “sonradan/tembel yükleme” (lazy loading) olarak adlandırılır. JPA varsayılan durumda önce yüklemeyi kullanır. Bu durum her alanın eşleştirmesinde @Basic(fetch=FetchType.EAGER) yazılmasına eş bir haldir. Fakat böyle bir davranışın, uygulamanın birbirleriyle ilintili nesneler ağından oluştuğu gerçeği düşünüldüğünde, pratikte ciddi sonuçlar doğuracağı açıktır. Yani bu şekilde davranışla veri tabanından bir entity uygulamaya yüklendiğinde bütün veri tabanının binlerce ya da milyonlarca entity olarak uygulamaya yüklenmesi ihtimal dahilindedir. Dolayısıyla her yerde önce yüklemeyi kullanmak pek de mümkün değildir. Bu yüzden JPA varsayılan durumda, @OneToOne ve @ManyToOne ilişkilerde karşıdaki “One” tarafını önce yükler yani fecth=FetchType.EAGER söz konusudur, @OneToMany ve @ManyToMany ilişkilerde ise karşıdaki “Many” tarafını, sonradan, isteğe bağlı olarak yükler yani fecth=FetchType.LAZY söz konusudur.

Dördüncü sorunun cevabı ise altı mümkün durumdan birisidir ve varsayılan halde JPA bir entity üzerinde gerçekleştirilen hiç bir EntityManager eylemini, ilişkinin diğer tarafındaki entity(ler)e iletmez. Diğer beş durum javax.persistence.CascadeType enum tipiyle ifade edilir. Bu beş durum sırasıyla ALL, MERGE, PERSIST, REFRESH ve REMOVE’dur.

1-1 İlişki

Person ile Car arasında iki yönlü 1-1 bir ilişki düşünelim. Bu ilişkiyi eşleştirmenin yolu Person’da Car nesnesi için @OneToOne(mappedBy=”owner”), Car’da sahibi olan Person nesnesi için @OneToOne notlarını kullanmaktır.

@Entity(name = "Person")
@Table(name="PersonOne2OneBi")
public class Person {
. . .
@OneToOne(mappedBy="owner")
private Car car;
. . .
}

@Entity(name = "Car")
@Table(name="CarOne2OneBi")
public class Car {
. . .
@OneToOne
@JoinColumn(name="OwnerId")
private Person owner;
. . .
}

Kod 9

Person sınıfındaki @OneToOne notu ilişkinin 1-1 olduğunu ve ve bu nottaki mappedBy özelliği ise ilişkide esas nesnenin Person olduğunu ve ilişki iki yönlü olduğundan, diğer tarafta yani Car sınıfında Person nesnesinin “owner” referansıyla ifade edildiğini gösterir. Car sınıfındaki @JoinColumn ise ilişkinin sahibi olan Person’ın eşleştiği tablonun anahtar alanının Car’ın eşleştiği tabloda hangi sütun ismiyle yabancı anahtar (foreign key) olarak bulunacağını ifade etmektedir. @JoinColumn kullanılmazsa JPA bir varsayılan sütun ismi belirleyecektir (personOne2OneBi).

Test kodunu yazdığımız EntityManagerTest sınıfında Person ve Car nesnelerini şöyle kalıcı kılarız:

tx.begin();
Person owner = new Person("Mihrimah", "Kaldiroglu");
Car car = new Car("Mercedes", "C200", "2010");
owner.setCar(car);
car.setOwner(owner);
em.persist(owner);
// em.persist(car);
tx.commit();
em.close();

Kod 10

Yukarıdaki kodu çalıştırdığınızda Person nesnesini veri tabanına yazarken, içinde veri tabanına yazılmayan bir başka nesneye yani Car nesnesine referans olduğunu belirten bir hata alacaksınız. Yani JPA veri tabanında Person için PersonOne2OneBiDir isminde bir tablo yaratıp bu tablonun içine Person nesnesini yeni bir satır olarak yazarken, bu satırdaki anahtar alanı, Car nesnesinin yazıldığı CarOne2OneBiDir tablosunun OwnerId isimli sütununa yabancı anahtar olarak yazmak istiyor ama Car nesnesi yazılmadığından hata veriyor. Çünkü JPA varsayılan halde bir nesneyi veri tabanına yazarken içindeki nesneleri de otomatik olarak yazmaz, yani işlemleri cascade etmez. Bunu çözmenin iki yolu vardır: İlki Person nesnesini veri tabanına yazarken içindeki Car nesnesini de yazmasını @OneToOne(mappedBy=”owner”, cascade=CascadeType.PERSIST) notu ile söylemek. Bu notlandırma ile ne zaman yeni bir Person nesnesi veri tabanında kalıcı kılınsa (persist) içindeki Car nesnesine de bu işlemin yapılması gerektiğini ifade eder. Bu durumun her zaman geçerli olması istenen bir hal değilse o zaman cascade işlemini elle yapmak gereklidir ki bu da yukarıdaki kod parçasında “//” ile geçersiz kılınan kodun da çalıştığı hale karşılık gelir.

Cascade ile ilgili benzer durum Person nesnesini silerken de ortaya çıkar. Person sınıfında Car nesnesi CascadeType.REMOVE ile de notlandırılmazsa, Car nesnesi silinmeden Person nesnesi de silinemeyecektir. Bu durumda Car nesnesi @OneToOne(mappedBy=”owner”, cascade={CascadeType.PERSIST,CascadeType.REMOVE}) ile notlandırmak gereklidir. Bu durumda cascade kullanmak dışında diğer iki seçeneğimiz önce ilgili Car nesnesini silmek sonra Person nesnesini silmek ya da Person-Car ilişkisini belki Person sınıfına removeCar(Car car) gibi bir metot ekleyerek, çalışma zamanında istendiğinde ortadan kaldırmaktır.

Cascade ile ilgili iki tip daha vardır. Bunlar birden fazla yönetilen (managed) nesneyi veri tabanından tekrar yüklemek (refresh) için CascadeType.REFRESH ve birden fazla koparılmış (detached) nesneyi veri tabanında güncellemek (merge) için CascadeType.MERGE. İki entity arasında her türlü cascade istediğimzide ise CascadeType.ALL notu kullanırız ki, tüm bu dört cascade halini kapsar.

Eğer ilişki tek yönlü olsaydı, yani Person arabasını bildiği halde Car sahibini bilmeseydi Person sınıfında @OneToOne notu ile yetinip mappedBy kullanmayacaktık. Cascade için yukarıda söylediklerimiz tabi olarak burada da geçerlidir.

Bundan Sonrası

Buraya kadar, önce kimlik bilgisi oluşturma stratejilerini ele aldık; sonra da JPA kullanarak 1-1 eşleştirmenin tek ve iki yönlü olarak nasıl yapıldığını gördük. 1-1 eşleştirmeyi kullanarak cascade etkisinden bahsettik. Bir sonraki yazımızda ise 1-N ve M-N gibi çoklu eşleştirmeleri ve veri tabanından nesne getirme stratejilerini ele alacağız.

Toplam görüntülenme sayısı: 3281