Sitemap

Spring Data ve Hibernate ile Java Persistence’e Giriş

19 min readDec 31, 2023

Selamlar, güzel bulduğum bir kitabın bazı kısımlarını aktarmak istiyorum. Faydalı olması dileğiyle.

Kitabın orjinaline şuradan erişebilirsiniz.

Öncelikli olarak, bazı kısaltmaların anlamlarına bakalım.

ORM(Object/Relational Mapping, Nesne İlişkisel Eşlemesi), Java(veya herhangi bir dil) uygulamasındaki nesnelerin, ilişkisel bir veritabanındaki tablolara otomatik olarak kalıcı hale(persistence) getirilmesidir. Bunun için metadata’dan(XML olarak veya anotasyon ile) yararlanılır.

JPA(Jakarta Persistence API, eski adıyla Java Persistence API) ise, nesnelerin kalıcılığını ve nesne/ilişkisel eşlemeleri yöneten bir API’yi tanımlayan bir şartnamedir(spesifikasyondur). Hibernate bu spesifikasyonun en popüler uygulamasıdır. Yani JPA, nesnelerin kalıcı olması için ne yapılması gerektiğini belirlerken, Hibernate ise bunun nasıl yapılacağını belirler.

Spring Data JPA, Spring Data ailesinin diğer bir projesidir. JPA uygulamalarının(örneğin Hibernate) üstüne ilave bir katmandır. JPA’nın tüm yeteneklerini kullanmakla kalmaz, üstüne ekstra yetenekler ekler. Detayına ileride değineceğiz.

Hibernate

ORM, nesne-yönelimli sistemler ile ilişkisel veritabanları arasındaki bağlantıyı sağlayan programlama tekniğidir. Hibernate, en ünlü ORM örneklerinden biridir(Eclipselink, MyBatis gibi alternatifler).

Hibernate projesi içinde şunlar bulunur;

  • Hibernate ORM: SQL veritabanları ile persistence için gerekli temel servistir. Birçok diğer projenin temelidir.
  • Hibernate EntityManager: Standart JPA’daki EntityManager’in Hibernate uygulamasıdır.
  • Hibernate Validator: Domain model sınıfları için bildirimsel validasyon sağlar.
  • Hibernate Envers: Sistemi incelemek(audit) için kullanılır. Git versiyon yönetim sisteminin yaptığına benzer bir iş yapar.
  • Hibernate Search: Domain modeli verilerinin indeksini Apache Lucene veritabanında güncel tutar.
  • Hibernate OGM: Object/grid mapper. Key/value, döküman veya graf-tabanlı veri saklanmasını sağlar.
  • Hibernate Reactive: Hibernate ORM için reactive API.

Spring Data

İlişkisel veya NoSQL veritabanlara erişimi kolaylaştıran Spring Framework’üne ait proje ailesidir.

  • Spring Data Commons: Spring Data projelerinin şemsiye projesidir. Java sınıflarının kalıcı hale getirilmesi için gerekli metadata modelini sağlar.
  • Spring Data JPA: JPA tabanlı repository’lerin uygulanması ile uğraşır.
  • Spring Data JDBC: JDBC tabanlı repository’lerin uygulanması ile uğraşır.
  • Spring Data REST: Spring Data repository’lerinin RESTful kaynaklar olarak dışarı açılmasıyla uğraşır.
  • Spring Data MongoDB: MongoDB döküman veritabanına erişim ile uğraşır.
  • Spring Data Redis: Redis key/value (anahtar/değer) veritabanına erişim ile uğraşır.

JPA ile İlk Örnek

MySQL veritabanına bağlanan ve bir mesajı veritabanına kaydedip, sonra geri okuyan bir JPA örneğine bakacağız.

hibernate-entitymanager modülü ile geçişli(transitive) olarak hibernate-core modülü çekilir. junit-jupiter-engine kütüphanesini test için kullanacağız. Ve mysql’e bağlanmak için driver gerekiyor.

JPA’daki başlanğıç noktamız, persistence unit’tir(kalıcılık birimi). Persistence unit, veritabanı bağlantısı ile domain model sınıflarının bir araya getirilmesinden sorumludur. Her uygulamanın, en azından bir tane persistence unit’i olur. Bunu oluşturmak için META-INF/persistence.xml dosyasını kullanırız.

Her persistence unit’in tekil bir ismi olur. Burada ch02 verdik.

JPA sadece bir şartname(spesifikasyon) sunduğundan, sağlayıcı(provider) olarak Hibernate kullanacağımızı söyleriz.

Sonra, hangi veritabanı sürücüsünü(driver) kullanacağımızı, URL adresinin ne olduğunu, kullanıcı adı ve şifresini belirtiriz.

Son özellikler olarak ise, MySQL’e hangi sürümü ile bağlanacağımızı (hibernate.dialect), JPA’da SQL’ler çalıştırılırken SQL kodu gösterilsin mi (hibernate.show_sql), formatlı olarak mı gösterilsin (hibernate.format_sql), her seferinde uygulama çalıştırılırken veritabanı baştan mı oluşturulsun (hibernate.hbm2ddl.auto) gibi özellikler setlenir.

Şimdi persistence sınıfını ekleyelim.

Her persistence entity(varlık) sınıfı, en azından @Entity anotasyonuna sahip olmalıdır. Hibernate bu sınıfı, MESSAGE adında tabloyla eşleştirir.

Her persistence sınıfı, @Id ile nitelenmiş bir alana sahip olmalıdır. Hibernate bu alanı, id isimli sütuna eşleştirir.

@GeneratedValue ile id’lerin otomatik üretilmesini sağlarız. İlerde daha geniş değineceğiz.

Message sınıfına ait 2 farklı örnek, aynı id’ye(birincil anahtar, primary key) sahipse, veritabanında aynı satıra denk geldiğiniz anlarız.

Veritabanına Kaydetme ve Okuma

Örnek testimize bakalım.

İlk olarak veritabanı ile konuşacak EntityManagerFactory nesnesine ihtiyaç duyarız. Bu nesne, çoklu threadli erişim için thread-safe’tir. Uygulamadaki veritabanına erişim için gerekli tüm kod, bu nesneyi paylaşır.

createEntityManager() metodu ile veritabanı ile yeni bir oturum başlatılır.

getTransaction().begin() ile yeni bir transaction başlatılır.

Sonra Message nesnesi oluşturulur ve veritabanına kaydedilir. Daha sonra bu işlem commit edilir. Aynı transaction altındaki işlemler commit edilene kadar veritabanına işlenmez.

Daha sonra kayıt tekrar veritabanından okunur ve düzgün kaydedilmiş mi diye kontrol edilir.

assertAll ile içlerinde bazı testler fail olsa bile tüm testler çalıştırılır.

EntityManager ve EntityManagerFactory nesneleri son aşamada kapatmamız gerekiyor.

select sorgusunda (select m from Message m) yazdığımız kod SQL değildir, JPQL(Jakarta Persistence Query Language) sorgusudur. Dikkat ederseniz Message ismi veritabanında tablo ismine değil, persistence sınıfının ismine işaret eder.

Ayrıca, ikinci commit işlemi sırasında, Hibernate text alanının değiştiğini otomatik olarak anlar ve veritabanında güncelleme yapar, ekleme yerine. Bu, JPA’nın otomatik dirty-checking(kirlilik-kontrolü) özelliğidir.

Native Hibernate Konfigürasyonu ile İlk Örnek

JPA örneğinin Hibernate ile yapmak istediğimizdeki kodlara bakalım. Öncelikli olarak JPA bağımlılıkları yerine Hibernate bağımlılıklarını kullanacağız.

JPA’daki EntityManagerFactory sınıfının, Hibernate’ki karşılığı org.hibernate.SessionFactory sınıfıdır. Uygulama başına genelde bir tane oluştururuz.

Native olarak Hibernate’i konfigüre etmek için, hibernate.properties veya hibernate.cfg.xml’den yararlanırız. İkinci yöntemle örnek vereceğiz. src/main/resource veya src/test/resource altına hibernate.cfg.xml dosyasını oluştururuz.

Daha önce bahsettiğimiz benzer özellikleri, burada da setleriz.

Örnek kodumuza bakacak olursak,

SessionFactory nesnesi oluşturabilmemiz için, configuration oluşturulmasına ihtiyaç duyarız. configure() metodunun çağrılması, hibernate.cfg.xml dosyasının yüklenmesini sağlar.

createSessionFactory() metodu ve openSession() ile bir Session nesnesi oluştururuz. Bundan sonraki kodlar, önceki örneğe çok benziyor.

Ekstra olarak, CriteriaQuery sınıfı ile select sorguları yazabiliyoruz.

JPA ve Hibernate Arasında Değişim

JPA ile çalıştığınızı farzedin ve Hibernate API’ye erişmek ihtyacı olabilir veya bunun tam tersi de olabilir. EntityManagerFactory nesnesinden SessionFactory nesneni oluşturabiliriz. JPA 2.0 ile beraber unwrap metoduyla bunu yapabiliriz.

JPA’dan Hibernate erişim.

private static SessionFactory getSessionFactory(EntityManagerFactory entityManagerFactory) {
return entityManagerFactory.unwrap(SessionFactory.class);
}

İşlemin tersini yapmak istersek şöyle olur.

Hibernate’ten JPA’ya erişim.

private static EntityManagerFactory createEntityManagerFactory() {
Configuration configuration = new Configuration();
configuration.configure().addAnnotatedClass(Message.class);

Map<String, String> properties = new HashMap<>();
Enumeration<?> propertyNames = configuration.getProperties().propertyNames();
while (propertyNames.hasMoreElements()) {
String element = (String) propertyNames.nextElement();
properties.put(element, configuration.getProperties().getProperty(element));
}
return Persistence.createEntityManagerFactory("ch02", properties);
}

Spring Data JPA ile İlk Örnek

Spring Data JPA ile bir mesajı veritabanına kaydedelim ve oradan geri okuyalım. Maven’de gerekli konfigürasyonu yapalım.

spring-data-jpa kütüphanesi sayesinde geçişli olarak spring-core ve spring-context gelir.

Testler için de spring-test kütüphanesini ekledik.

Anotasyon tabanlı konfigürasyon işlemine bakalım.

datasource bean nesnesi ile gerekli veri kaynağı oluşturuluyor. Hangi veritabanı driver’ı kullanılacak, adresi nedir, kullanıcı adı ve şifre gibi bilgiler veriliyor.

Her bir veritabanı işlemi kendi transaction sınırları içinde olduğu için 2. metodda transactionManager nesnesine ihtiyaç duyarız.

Ne tür bir JPA çeşidi kullanacağımızı jpaVendorAdapter metodu ile belirtiriz.

entityManagerFactory metodu ile JPA’nın standart container yapısını takiben bir EntityManagerFactory nesnesi üretir.

Spring Data JPA ile yazılacak kod miktarı daha az olur. Sadece, kendi repository arayüzünü(interface) tanımlamamız ve bunu Spring Data arayüzlerinden türetmemiz gerekir.

public interface MessageRepository extends CrudRepository<Message, Long> {
}

Message sınıfının @Id ile etiketlenmiş alanının tipi Long olduğu için, jenerik tip tanımında Long tanımlarız. Spring Data JPA, MessageRepository arayüzü için bir proxy sınıfı yaratır ve onun metodlarını implement eder.

Spring Data JPA ile Message sınıfını kaydedip sonra geri okuyacak kod örneğine bakalım.

JUnit 5'te Spring text contextini entegre etmek için SpringExtension kullanırız.

SpringDataConfiguration sınıfındaki bean’leri kullanmak için ContextConfiguration kullanırız.

MessageRepository nesnesi, field injection ile(normalde bu injection yöntemi tavsiye edilmez ama test için kullanılmasında sakınca yok) Spring tarafından testimize inject edilir. Spring burada, proxy nesneni döner.

Yeni bir Message nesneni oluşturulup, save metodu ile kaydedilir.

Daha sonra veritabanından okumak için findAll metodundan yararlanabiliriz. Bu yardımcı metodlar, CrudRepository arayüzünden gelirler.

Dikkat ederseniz, Spring Data JPA testi, JPA veya native Hibernate’e göre çok daha kısa oldu. Yük daha çok konfigürasyon tarafına kaydı ama uygulama kodu çok daha az tuttu.

Bu 3 yöntemin performansına bakacak olursak, insert/update/select/delete için benzer bir tablo ortaya çıkar.

Hibernate ve JPA’nın performansı birbirine yakın seyrederken, Spring Data JPA bunlara göre daha yavaş kalır. Tercih ederken, göz önünde bulundurmakta fayda var.

Spring Data JPA ile Çalışma

Spring Data, farklı veritabanlarına özgü birçok projeyi barındıran şemsiye projesidir. Spring Data’nın amacı, farklı veri depolarına yönelik veri erişimi için soyutlama sağlamaktır.

Spring Data JPA, Spring Data Commons ve JPA Provider(bizim durumda Hibernate) üzerine kuruludur.

Spring Data JPA, veritabanı ile iletişimi kolaylaştırmak için bir çok şey sağlar.

  • Data source nesnesini konfigüre eder,
  • Entity Manager Factory nesnesini konfigüre eder,
  • Transaction Manager nesnesini konfigüre eder,
  • Anotasyon ile transaction’ları yönetir.

Yeni bir Spring Data JPA Projesi

Spring Data JPA ile CRUD operasyonlarını yapabilecek ve sorgular çalıştıracak bir uygulama yapacağız.

Projenin bağımlılıkları için maven ile yönettiğimiz örnek bir pom.xml dosyasında şunlar olur.

spring-boot-starter-parent ile bazı varsayılan konfigürasyonlar kalıtım ile alınır.

spring-boot-starter-data-jpa, Spring Boot tarafından kullanılan başlangıç bağımlılığıdır. Kendi içinde geçişli olarak Hibernate kullanır.

mysql-connector-java, MySQL veritabanı için JDBC sürücüsüdür.

spring-boot-starter-test, test için kullanacağımız başlangıç bağımlılığıdır.

spring-boot-maven-plugin ise Spring Boot projesini derleyip, çalıştırmak için gerekli bir yardımcı eklentidir.

User(Kullanıcı) Entity Sınıfını Oluşturma

User sınıfını oluşturalım.

USER birçok veritabanında saklı bir anahtar kelime olduğu için tablomuz için USERS ismini kullandık.

id alan adıyla, birincil anahtar(primary key) oluşturduk ve diğer alanları ekledik.

JPA, argümansız yapılandırıcıya(no-argument constructor) ihtiyaç duyar. JPA, Java Reflection API ile argümansız yapılandırıcı üzerinden nesneyi oluşturur.

Sonra UserRepository sınıfını oluştururuz.

public interface UserRepository extends CrudRepository<User, Long> {
}

Yukarda bahsettiğimiz CrudRepository arayüzünden bazı metodlar UserRepository arayüzüne aktarılır.

Spring Boot application.properties dosyası ile konfigürasyonlarımızı söyleriz.

spring.datasource.url=jdbc:mysql://localhost:3306/CH04_SPRINGDATAJPA 
?serverTimezone=UTC
spring.datasource.username=root
spring.datasource.password=
spring.jpa.properties.hibernate.dialect=org.hibernate.dialect.MySQL8Dialect
spring.jpa.show-sql=true
spring.jpa.hibernate.ddl-auto=create

Burada, bağlanacağımız veritabanın URL adresi, kullanıcı adı, şifre, hangi dialect ile bağlanacağımız gibi bilgileri setleriz.

Şimdi, örnek bir Spring Boot uygulaması ile 2 kullanıcı kaydedelim.

configure(UserRepository) metodu için Spring Boot tarafından UserRepository nesnesi inject edilir ve bunun sayesinde User nesnesini kaydederiz. Kaydettiğimiz kullanıcıları sonradan findAll() ile ekrana yazdırırız.

User{id=1, username='beth', registrationDate=2020-08-03}
User{id=2, username='mike', registrationDate=2020-01-18}

Spring Data JPA ile Sorgulama Metodları

User sınıfına email, level ve active diye 3 alan daha ekleyelim. User sıfının alanları şu hale gelir.

@Entity
@Table(name = "USERS")
public class User {
@Id
@GeneratedValue
private Long id;
private String username;
private LocalDate registrationDate;
private String email;
private int level;
private boolean active;

UserRepository arayüzünü de, JpaRepository arayüzünden türeteceğiz. JpaRepository, PagingAndSortingRepository’den türer, o da CrudRepository’den türer. JpaRepository arayüzünde diğerlerine göre daha farklı metodlar barındırır.

Bazı sorgulama metodlarını da eklersek, UserRepository arayüzünün son hali şöyle olur.

public interface UserRepository extends JpaRepository<User, Long> {
User findByUsername(String username);
List<User> findAllByOrderByUsernameAsc();
List<User> findByRegistrationDateBetween(LocalDate start, LocalDate end);
List<User> findByUsernameAndEmail(String username, String email);
List<User> findByUsernameOrEmail(String username, String email);
List<User> findByUsernameIgnoreCase(String username);
List<User> findByLevelOrderByUsernameDesc(int level);
List<User> findByLevelGreaterThanEqual(int level);
List<User> findByUsernameContaining(String text);
List<User> findByUsernameLike(String text);
List<User> findByUsernameStartingWith(String start);
List<User> findByUsernameEndingWith(String end);
List<User> findByActive(boolean active);
List<User> findByRegistrationDateIn(Collection<LocalDate> dates);
List<User> findByRegistrationDateNotIn(Collection<LocalDate> dates);
}

Bu sorgulama metodlarının amacı, veritabanından bilgi getirmektir. Spring Data JPA, metod isimlerine dayanarak sorgu yapmamızı sağlayan bir yapı sunar. Gördüğünüz gibi veritabanında ihtiyaç duyulan hemen hemen her işlemi bu metodlarla sağlayabiliyoruz.

Bazı örnek kullanımları şu şekilde örneklendirebiliriz. Diğer kullanımlar da benzer mantıkta çalışır.

Sıralama(Sorting), Sayfalama(Paging) ve Sonuçları Sınırlandırma(Limiting Results)

Bu tür işlemler için Pageable arayüzü ve onun uygulaması PageRequest sınıfından, Sort sıfından yararlanırız.

UserRepository arayüzü için bazı örnekleri şöyle verebiliriz.

User findFirstByOrderByUsernameAsc();
User findTopByOrderByRegistrationDateDesc(); // Tek kayıt getir
Page<User> findAll(Pageable pageable);
List<User> findFirst2ByLevel(int level, Sort sort); // Sadece 2 kayıt getir
List<User> findByLevel(int level, Sort sort);
List<User> findByActive(boolean active, Pageable pageable);

@Query Anotasyonu

@Query anotasyonu ile özelleşmiş sorgular yazabiliriz. @Query anotasyonu kullanıldığı zaman, metod isminin herhangi bir isimlendirme kuralını takip etmesi beklenmez.

@Query anotasyonu içinde Spring Expression Language(SpEL) de kullanabiliriz. Örneğin, @Entity anotasyonu ile etiketlenmiş bir sınıfın içinde #{#entityName} ile o sınıfın ismine ulaşabiliriz.

Örneğin;

@Query("select count(u) from User u where u.active = ?1") 
int findNumberOfUsersByActivity(boolean active);

@Query("select u from User u where u.level = :level and u.active = :active")
List<User> findByLevelAndActive(@Param("level") int level,
@Param("active") boolean active);

@Query(value = "SELECT COUNT(*) FROM USERS WHERE ACTIVE = ?1",
nativeQuery = true)
int findNumberOfUsersByActivityNative(boolean active);

@Query("select u.username, LENGTH(u.email) as email_length from "
+ "#{#entityName} u where u.username like %?1%")
List<Object[]> findByAsArrayAndSort(String text, Sort sort);

İlk sorguda, kullanıcının aktiflik durumuna göre kullanıcı sayısını getirir. Parametrenin set edilmesi, parametrenin pozisyonuna göre olur.

İkinci sorguda, bazı kriterlere göre kullanıcı listesi getirilir. Parametrelerin set edilmesi, parametre ismine göre @Param anotasyonu ile yapılır.

Üçüncü sorguda, nativeQuery bayrağı set edilerek, doğrudan SQL yazılması sağlanır.

Dörgüncü sorguda, diğer kriterle birlikte, #{#entityName} ile User ismine ulaşıldı.

Projeksiyonlar (Projections)

Entity’nin her zaman bütün alanları gerekmez. Frontend tarafında ihtiyacı olduğu kadar alanları dönmek, performan açısından doğru bir hamle olur. Bu yüzden kök entity’i doğrudan dönmek yerine, onun bir projeksiyonunu döneriz. Yine, burada SpEL’den yararlanabiliriz.

public class Projection {
public interface UserSummary {
String getUsername();

@Value("#{target.username} #{target.email}")
String getInfo();
}
}

getUserName() metodu ile doğrudan userName alanını döneriz. Buna, closed(kapalı) projeksiyon denir. Spring Data JPA, bu tür sorguları optimize eder.

getInfo() metodu ile SpEL yardımıyla 2 alanı birleştirebiliriz. Buna, open(açık) projeksiyon denir. SpEL çalışma zamanında hesaplandığı için, Spring Data JPA bu tür sorguları optimize edemez.

Benzer olarak, projeksiyonu, interface-tabanlı yerine sınıf-tabanlı da oluşturabiliriz.

public class Projection {
// . . .
public static class UsernameOnly {
private String username;

public UsernameOnly(String username) {
this.username = username;
}

public String getUsername() {
return username;
}
}
}

Entity sınıflarını UserRepository içinde kullandığımız gibi projeksiyonları da kullanırız.

List<Projection.UserSummary> findByRegistrationDateAfter(LocalDate date);
List<Projection.UsernameOnly> findByEmail(String username);

Değer Değiştiren Sorgular

INSERT, UPDATE, ve DELETE sorgularında, hatta DDL sorgularında(tablo manipülasyonları) @Modifying anotasyonunu kullanırız. @Query anotasyonuna argümanlar için yine de ihtiyaç olabilir. Bu tarz metodlar @Transactional olarak da etiketlenmelidir ya da programlamatik olarak yönetilen transaction içinden çağrılmalıdır.

Bazı örnekleri inceleyelim.

@Modifying 
@Transactional
@Query("update User u set u.level = ?2 where u.level = ?1")
int updateLevel(int oldLevel, int newLevel);

@Transactional
int deleteByLevel(int level);

@Transactional
@Modifying
@Query("delete from User u where u.level = ?1")
int deleteBulkByLevel(int level);

updateLevel metodunda, level alanı değiştirildiği için @Modifying ve @Transactional olarak etiketlenir.

deleteByLevel metodu, metod isminden sorgusu üretileceği için @Modifying kullanmaya gerek kalmaz.

deleteBulkByLevel metodu da, updateLevel mantığında çalışır.

deleteByLevel ile deleteBulkByLevel arasında şöyle bir fark vardır. deleteByLevel kullanıcıları tek tek silerken, deleteBulkByLevel ise topluca, tek seferde siler.

Bu işlemlerde, @Modifying anotasyonunu kullanmadığımız zaman, InvalidDataAccessApiUsage hatası alırız.

Example ile Sorgulama (Query by Example, QBE)

QBE, klasik sorgulamalardan farklı bir sorgulama tekniğidir. Probe, ExampleMatcher ve Example olarak 3 kısımdan oluşur. Basit sorgulamalarda işimize yarar.

“J” karakteri ile başlayan kullanıcıları sorgulayalım.

@Test
void testUsernameWithQueryByExample() {
User user = new User();
user.setUsername("J");

ExampleMatcher matcher = ExampleMatcher.matching()
.withIgnorePaths("level", "active")
.withStringMatcher(ExampleMatcher.StringMatcher.STARTING)
.withIgnoreCase();

Example<User> example = Example.of(user, matcher);

List<User> users = userRepository.findAll(example);
assertEquals(3, users.size());
}

User nesnesi oluşturarak, probe oluştururuz.

null referans alan tüm tipler otomatik olarak matcher tarafından yoksayılır. level ve active alanları primitif tipler olduğu için, kendimiz yoksaymalıyız.

Daha sonra Example nesnesi oluştururuz ve bu nesne ile userRepository üzerinden sorgularımızı çalıştırırız.

Kalıcı Sınıfları Eşleme(Mapping Persistence Classes)

Biraz da, entity sınıflarının SQL tablolarıyla nasıl eşleştiğine bakalım.

Spring Data JPA, bir veri erişimi soyutlaması olarak, JPA sağlayıcıları(provider) üzerine kurulur. Bu sağlayıcılarından en çok kullanılanı Hibernate idir. Spring Data JPA, veritabanıyla etkileşim için gerekli kod miktarını en aza indirir. Bu yüzden, Persistent sınıfların eşlenmesi yapıldığı zaman, bu sınıflar hem Hibernate tarafından hem de Spring Data JPA tarafından kullanılabilir. Biz burada, örnekler için JPA anotasyonlarını kullanacağız.

  • Entity tipi: User, Item ve Category gibi örnekler, entity tipidir. Entity sınıfının örneği, veritabanında kalıcı olarak saklanır. Kendine ait bir yaşam döngüsü vardır, bağımsız olarak varolabilir.
  • Value tipi: Bağımsız olarak varolamaz, bir Entity tipine bağlıdır. JPA şartnamesinde, temel özellik tipleri veya embeddable(gömülebilir) sınıflar olarak bilinir. İlerde buna daha detaylı değineceğiz.

Java’da Kimlik(Identity) ve Eşitliği Anlama

Çift eşittir işareti(==) ile referans eşitliğine bakarız. İki referansın, bellekteki aynı lokasyona işaret ettiği anlamına gelir. Yani, aynı kimliğe sahiptir deriz.

Nesne eşitliğine ise, equals() metodu ile bakarız. Eşitlik, iki ayrı nesne örneğinin aynı değerlere sahip olması demektir, yani aynı duruma(state).

Veritabanı kimliğinde ise, aynı tabloda aynı birincil anahtara(primary key) sahipse, aynı kimliğe sahiptir.

Entity Sınıfı ve Mapping(Eşleme)

Bir entity sınıfı oluşturmak için, @Entity anotasyonu tek başına yeterli değildir, @Id anotasyonu da gerekir. Item örneğine bakalım.

@Entity
public class Item {
@Id
@GeneratedValue(generator = "ID_GENERATOR")
private Long id;

public Long getId() {
return id;
}
}

Bu, en temel entity sınıfıdır. Veritabanında ITEM tablosuna eşlenir. id alanının değerinin üretilme kısmına daha detaylı değineceğiz.

Hibernate ve Spring Data JPA, alanlarda değer saklamak ve okumak için, jenerik yapılar üzerinden, o alana(field) doğrudan erişir. Setter ve getter metodlarını kullanmaz. Setter ve getter metodların olması, uygulama akışı açısından işimize yarar (encapsulation/kapsüllemede işimize yarar).

Tablonun birincil anahtarları, burada id alanı, değişmez. Entity üzerinde public olarak setlenmesine izin vermemeliyiz.

Birincil Anahtar Üretimini Konfigüre Etme

Entity sınıfı içinde @Id anotasyonunun gerekli olduğunu söylemiştik. Onunla beraber, @GeneratedValue anotasyonu kullanmaz isek, JPA sağlayıcısı id alanının değerinin yönetilmesini bizim yapacağımızı farzeder.

JPA standardında, farklı adette id üretim stratejisi bulunur. @GeneratedValue(strategy = …) ile bunları seçebiliriz. Sırayla bakalım.

  • GenerationType.AUTO — Hibernate ya da Hibernate sağlayıcı kullanan Spring Data JPA, uygun bir strateji seçer. Bunun için de konfigüre ettiğin SQL dialect’e sorar. Parametresiz olarak @GeneratedValue() kullanmakla aynı anlama gelir.
  • GenerationType.SEQUENCE — Hibernate ya da Hibernate sağlayıcı kullanan Spring Data JPA, veritabanında HIBERNATE_SEQUENCE adında sequence(sıra) oluşturmanı bekler. Bu sequence, her INSERT işleminden önce çalıştırılır.
  • GenerationType.IDENTITY — Hibernate ya da Hibernate sağlayıcı kullanan Spring Data JPA, özel otomatik olarak artırılmış bir birincil anahtar sütunun olmasını bekler. Bu alan, her INSERT öncesi otomatik olarak artırılır.
  • GenerationType.TABLE — Hibernate ya da Hibernate sağlayıcı kullanan Spring Data JPA, birincil anahtarın sonraki değerini tutan kendi veritabanı şemasında ekstra bir tablo tutar. Her entity sınıfı için bir satır tutar. Bu tablo, her INSERT öncesi okunur ve güncellenir. Varsayılan tablo ismi HIBERNATE_SEQUENCES olur ve onun sütunları ise SEQUENCE_NAME ve NEXT_VALUE idir.

AUTO kullanımı uygun gibi gözükse de, ID’lerin oluşturulmasında daha fazla kontrole sahip olmak istersiniz. Birçok uygulama, bu gibi esnekliğinden dolayı veritabanı sequence’leri ile çalışır. Veritabanı sequence’lerinin isim ve diğer ayarlarını değiştirmek isteyebilirsiniz. Varolanlardan bir tanesini seçmek yerine kendinize özgü @GeneratedValue (generator = “ID_GENERATOR”) gibi bir tane oluşturabilirsiniz. Buna isimli tanımlayıcı jeneratör denir.

@org.hibernate.annotations.GenericGenerator(
name = "ID_GENERATOR",
strategy = "enhanced-sequence",
parameters = {
@org.hibernate.annotations.Parameter(
name = "sequence_name",
value = "JPWHSD_SEQUENCE"
),
@org.hibernate.annotations.Parameter(
name = "initial_value",
value = "1000"
)
}
)

enhanced-sequence stratejisi ile sıralı sayısal değerler üretiriz. Hibernate ve Spring Data JPA gerçek veritabanı sequence’lerini kullanır.

sequence_name gibi istediğimiz bir ismi sequence’imize verebiliriz.

initial_value gibi bir parametre ile de sequence’imizi istediğimiz değerden başlatabiliriz.

Dinamik SQL Oluşturma

Varsayılan olarak, Hibernate ve Spring Data JPA uygulama ayağa kalkıp persistence unit’i(kalıcılık birimi) oluşturduğu zaman, her persistent sınıfı için SQL ifadelerini oluşturur. Bu ifadeler, basit ekleme(create), okuma(read), güncelleme(update) ve silme(delete) operasyonlarıdır(CRUD). Her seferinde bu SQL stringleri çalışma zamanında oluşturmak yerine, bunları oluşturmak ve önbelleğe(cache) almak daha ucuzdur.

Peki, uygulama ayağa kalkarken Hibernate UPDATE ifadesini nasıl oluşturur? Ne de olsa bu aşamada, hangi sütunların güncelleneceği bilinmiyor. Cevap ise, üretilmiş SQL ifadesinde tüm sütunların yer alıyor olmasında. Bir sütunun değeri değişmediyse, onun eski değeri tekrar set edilir.

Yüzlerce sütunun yer aldığı eski tablolarda, bütün sütunların güncellenmesi istemeyiz. Başlangıçtaki SQL oluşturmayı iptal edip, çalışma zamanında dinamik ifadelerin oluşturulmasını isteriz.

INSERT ve UPDATE SQL ifadesini başta oluşturulmasını pasifleştirmek için, native Hibernate anotasyonlarını kullanırız.

@Entity
@org.hibernate.annotations.DynamicInsert
@org.hibernate.annotations.DynamicUpdate
public class Item {
// . . .
}

Bu sayede, UPDATE işlemi sadece değişen sütunları kapsayacak şekilde SQL oluşturur.

Değişmez(Immutable) Entity’ler

Bazı durumlarda, entity sınıfımızda UPDATE işlemine ihtiyaç duymayız. Sadece INSERT işlemine ihtiyaç duyarız. Hibernate bu durumda, bazı optimizasyonlar yapabilir. Örneğin, dirty-checking(kirlilik kontrolü) baypas edebilir. @Immutable anotasyonu ile bunu sağlarız.

@Entity
@org.hibernate.annotations.Immutable
public class Bid {
// . . .
}

Veritabanı şemasında view nesneleri(read-only tablolar) oluşturamadığın zamanlarda, immutable entity sınıfları oluşturabilirsiniz.

Subselect Sorgusuna Entity Eşleme

Bazen, veritabanı yöneticileri, veritabanı şemasında bir değişiklik yapmana izin vermez. Yeni bir view eklemek bile mümkün olmayabilir. Bunu uygulama seviyesinde, @SubSelect ile simüle edebiliriz.

@Entity
@org.hibernate.annotations.Immutable
@org.hibernate.annotations.Subselect(
value = "select i.ID as ITEMID, i.NAME as NAME, " +
"count(b.ID) as NUMBEROFBIDS " +
"from ITEM i left outer join BID b on i.ID = b.ITEM_ID " +
"group by i.ID, i.NAME"
)
@org.hibernate.annotations.Synchronize({"ITEM", "BID"})
public class ItemBidSummary {
@Id
private Long itemId;
private String name;
private long numberOfBids;

public ItemBidSummary() {
}
// Getter methods . . .
// . . .
}

@Synchronize anotasyonu içinde referans verdiğimiz tüm tabloları listelemeliyiz. Bu sayede, framework hangi tabloları sorgulama öncesi flush(güncel değerlerini almak) edeceğini bilir. ItemBidSummary üzerinde @Table anotasyonunun olmadığına dikkat edin. Bu yüzden, framework sorgulamayı çalıştırmadan önce auto-flush edeceğini bilmez. @Synchronize ile biz söylemeliyiz.

Sonrasında diğer tablolara benzer şekilde repository sınıfını oluşturup, sorgularımızda kullanabiliriz.

public interface ItemBidSummaryRepository extends
CrudRepository<ItemBidSummary, Long> {
}
Optional<ItemBidSummary> itemBidSummary = 
itemBidSummaryRepository.findById(1000L);

Değer Tiplerini Eşleme(Mapping Value Types)

Değer tipleri 2 kategoriye ayrılır;

  • Temel değer-tipli(value-typed) sınıfları: Örneğin, String, Date, primitifler ve onların sarmalayıcı sınıfları
  • Kullanıcı-tanımlı değer-tip sınıfları: Örneğin, Address ve MonetaryAmount

JPA 2'deki önemli yeni özellikler: JPA 2.2, Java 8 Date ve Time API’sini destekler. Daha önce java.util.Date türündeki alanlara eklemek için gereken @Temporal gibi anotasyonları kullanmaya artık gerek kalmıyor.

Şu tür alanlar otomatik olarak JPA tarafından veritabanına persistent hale getirilir.

  • Primitif tipler ve onların sarmalayıcı(wrapper) sınıfları. String, BigInteger, BigDecimal, java.time.LocalDateTime, java.time.LocalDate, java.time .LocalDate, java.util.Date, java.util.Calendar, java.sql.Date, java.sql .Time, java.sql.Timestamp, byte[], Byte[], char[] veya Character[] olan alanlar otomatik olarak persistent(kalıcı) hale getirilir
  • @Embeddable ile bir sınıfı etiketlersek veya bir alanı @Embedded etiketlersek. İlerde değineceğiz.
  • Bir tipin özelliği java.io.Serializable ise, onun değeri serilize edilmiş bir halde saklanır. (Çok tavsiye edilen bir yöntem değildir.)
  • Diğer türlü, açılışta bir hata fırlatılır.

Temel Özelliklerin Varsayılanları

Veritabanında kalıcı hale getirilmesini istemediğimiz alanı @javax.persistence.Transient anotasyonu ile veya Java’nın transient anahtar sözcüğü ile etiketleriz. transient anahtar sözcüğü alanları hem serialization işleminin hem de persistence(kalıcılık) işleminin dışında tutar. @javax.persistence.Transient ise sadece persistence işleminin dışında tutar.

Varsayılan harici ayarlar kullanmak istediğimizde, @Basic kullanabiliriz.

@Basic(optional = false)
BigDecimal initialPrice;

Kısıtlı bir anotasyondur, fazla bir seçenek sunmaz. optional = false dediğimiz için SQL scriptleri oluştururken, NOT NULL eklenir.

Bunun yerine daha kabiliyetli olan @Column kullanırız. Aynı işi @Column ile yapmak için,

@Column(nullable = false)
BigDecimal initialPrice;

Sonuç olarak, aynı işi yapmak için @Basic, @Column ve Bean Validation sınıfından @NotNull kullanabiliriz.

@Column ile veritabanı sütunun ismini de değiştirebiliriz.

@Column(name = "START_PRICE", nullable = false)
BigDecimal initialPrice;

@Column anotasyonunun başka özellikleri de vardır. Yeri gelince değineceğiz.

Property(Özellik, Alan) Erişimi Değiştirmek

Spring Data JPA varsayılan olarak alanlara doğrudan Genericlerle erişir. Bunu @Access ile değiştirebiliriz. AccessType.FIELD ile doğrudan(default davranış) veya AccessType.PROPERTY ile getter/setter metodlarıyla erişebiliriz.

@Entity
public class Item {
@Id
@GeneratedValue(generator = "ID_GENERATOR")
private Long id;

@Access(AccessType.PROPERTY)
@Column(name = "ITEM_NAME")
private String name;

public String getName() {
return name;
}
public void setName(String name) {
this.name = !name.startsWith("AUCTION: ") ? "AUCTION: " + name : name;
}
}

@Access ile sınıf seviyesinde kullanırsak tüm alanların erişimini değiştiririz.

Formula(Formül) Anotasyonu

@org.hibernate.annotations.Formula anotasyonu ile çalışma zamanında bazı değerleri hesaplayabiliriz.

@Formula(
"CONCAT(SUBSTR(DESCRIPTION, 1, 12), '...')"
)
private String shortDescription;

@Formula(
"(SELECT AVG(B.AMOUNT) FROM BID B WHERE B.ITEM_ID = ID)"
)
private BigDecimal averageBidAmount;

SQL formülleri, her veritabanı sorgulamasında çalışır.

Sütün Değerlerini Değiştirmek

Örneğin, kilogramdan libreye çevirmek gibi bir işlem yapmak istediğimizde @org.hibernate.annotations.ColumnTransformer anotasyonu işimize yarar.

@Column(name = "IMPERIALWEIGHT")
@ColumnTransformer(
read = "IMPERIALWEIGHT / 2.20462",
write = "? * 2.20462"
)
private double metricWeight;

Üretilmiş veya Varsayılan Değerler

Örnek üzerinden inceleyelim.

@CreationTimestamp
private LocalDate createdOn;

@UpdateTimestamp
private LocalDateTime lastModified;

@Column(insertable = false)
@ColumnDefault("1.00")
@Generated(
org.hibernate.annotations.GenerationTime.INSERT
)
private BigDecimal initialPrice;

@CreationTimestamp ile create işlemi sırasında tarih değeri setlenir.

@UpdateTimestamp ile de update işlemi sırasında tarih alanı setlenir.

@Generated ile ne zaman bu işlemin yapılacağını söyleriz. insertable=false olduğu için GenerationTime.ALWAYS ile hem INSERT hem UPDATE işlemlerinden sonra güncelleme yaparken, GenerationTime.INSERT ile INSERT işleminden sonra varsayılan değeri alması için güncelleme yapar. @ColumnDefault ile SQL oluşturulurken default değerleri de oluşturulur.

Tarih alanları için, JPA 2.2 ve Java 8'den sonra @Temporal anotasyonunu kullanmaya gerek kalmaz.

Enum’ları Eşleştirmek

Örnek bir enum tanımlayalım.

public enum AuctionType {
HIGHEST_BID,
LOWEST_BID,
FIXED_PRICE
}

Bunu alan map’lemesinde kullanmak istersek,

@NotNull
@Enumerated(EnumType.STRING)
private AuctionType auctionType = AuctionType.HIGHEST_BID;

@Enumerated anotasyonunu kullanmazsak, Hibernate ve JPA enum değerinin ORDINAL pozisyonunu tutar. HIGHEST_BID için 1, LOWEST_BID için 2, FIXED_PRICE için 3 gibi devam eder. Bu varsayılan davranıştır ama enum’a yeni bir değer eklemek istediğimizde bu ORDINAL değerleri değişebilir. EnumType.STRING ile enum’ın string değeri yani adını almasını sağlarız.

Gömülebilir(Embeddable) Bileşenleri Eşleştirme

Address ve User nesnelerimizin olduğunu düşünelim ve aralarında composition ilişkisi olsun. Yani Adress nesnesi User entity’si olmadan yaşayamaz. Bunu görselleştirelim.

Normalde yukardaki gibi ayrı tablolar olarak da yönetilebilirken, biz aşağıdaki gibi User entity’sinin alanları olarak yönetmek istiyoruz.

Address nesnesini değer tipi(value type) gibi düşünürüz. String veya BigDecimal gibi.

Adress nesnesini embeddable yapalım.

@Embeddable
public class Address {
@NotNull
@Column(nullable = false)
private String street;

@NotNull
@Column(nullable = false, length = 5)
private String zipcode;

@NotNull
@Column(nullable = false)
private String city;

public Address() {}
public Address(String street, String zipcode, String city) {
this.street = street;
this.zipcode = zipcode;
this.city = city;
}
//getters and setters
}

Dikkat ederseniz, @Entity anotasyonu yerine @Embeddable anotasyonu kullandık. @NotNull anotasyonu Hibernate’teki bir hatadan dolayı DDL SQL’lerini oluşturmuyor ama @Column(nullable = false) aynı işi yapıyor.

city alanına length ile herhangi bir uzunluk vermediğimiz için default 255 uzunlukta tanımlanır.

Address sınıfını User içinde kullanalım.

@Entity
@Table(name = "USERS")
public class User {
@Id
@GeneratedValue(generator = Constants.ID_GENERATOR)
private Long id;

private Address homeAddress;
// . . .
}

Hibernate ve Spring Data JPA otomatik olarak Address sınıfını algılar ve User entity’sindeki alanlarla bunları eşler.

Gömülü(Embedded) Özellikleri Değiştirme

billingAddress, User sınıfının diğer bir gömülü bileşenidir. Yani, diğer bir Address sınıfının USERS tablosunda saklanması demektir. Bu da çatışma anlamına gelir. Varsayılan olan isimleri değiştirerek bunun önüne geçeriz.

@Entity
@Table(name = "USERS")
public class User {
@Embedded
@AttributeOverride(name = "street",
column = @Column(name = "BILLING_STREET"))
@AttributeOverride(name = "zipcode",
column = @Column(name = "BILLING_ZIPCODE", length = 5))
@AttributeOverride(name = "city",
column = @Column(name = "BILLING_CITY"))
private Address billingAddress;

public Address getBillingAddress() {
return billingAddress;
}
// ...
}

@Embedded anotasyonunun kullanılması zorunlu olmasa da, niyetimizin belli olması açısından kullanılması faydalıdır.

@AttributeOverride ile de, değiştirmek istediğimiz alanları belirtiriz. Örneğin, Address sınıfındaki street alanı artık varsayılan değerdeki gibi değil, tablodaki BILLING_STREET alanında saklamak istediğimizi söyleriz. @AttributeOverride kullandığımız için Address sınıfındaki @Column anotasyonları görmezden gelinir.

Java Tiplerini ve SQL Tiplerini Eşleştirmek

Herhangi bir JPA sağlayıcı, minimum kümedeki Java-to-SQL tip çevrimlerini desteklemelidir. Java primitif tiplerine ve onların sarmalayıcı(wrapper) sınıflarına bakalım.

Bu SQL tipleri, standart ANSI tip isimleridir. Bazı veritabanı satıcıları, bu tipleri görmezden gelebilir. Veritabanı farklılıklarına ve onların dialect’lerine göre bu tip eşleştirmeleri değişebilir.

Karakter tiplerine bakalım.

java.lang.String için Hibernate’in default uzunluğu 255'dir.

Tarih ve zaman tiplerine bakalım.

Tarih ve zaman tipleri için Java 8 ile gelen java.time paketini kullanmak en doğrusudur.

Binary ve Büyük Veri Tiplerine bakalım.

Veri tiplerinin uzunluklarına göre de SQL tipleri değişebilir.

JPA şartnamesinde @Lob olarak kısa yol bulunur.

@Entity
public class Item {
@Lob
private byte[] image;

@Lob
private String description;
}

byte[] alanı için SQL BLOB(Binary Large Object) veri tipiyle eşleme sağlarken, String için CLOB(Character Large Object) ile eşleme sağlar.

Özel JPA Dönüştürücüler Tanımlamak

Bazı durumlarda, özel dönüştürü sınıflara ihtiyacımız olabilir. Örneğin, parasal tutarı(11.23 USD veya 99 EUR gibi) tutmak için bir MonetaryAmount sınıfına ihtiyaç duyduğumuzu düşünelim.

public class MonetaryAmount implements Serializable {
private final BigDecimal value;
private final Currency currency;

public MonetaryAmount(BigDecimal value, Currency currency) {
this.value = value;
this.currency = currency;
}

public BigDecimal getValue() {
return value;
}

public Currency getCurrency() {
return currency;
}

@Override
public boolean equals(Object o) {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
MonetaryAmount that = (MonetaryAmount) o;
return Objects.equals(value, that.value) &&
Objects.equals(currency, that.currency);
}

public int hashCode() {
return Objects.hash(value, currency);
}

public String toString() {
return value + " " + currency;
}

public static MonetaryAmount fromString(String s) {
String[] split = s.split(" ");
return new MonetaryAmount(
new BigDecimal(split[0]),
Currency.getInstance(split[1])
);
}
}

Bu değer-tipli(value-types) sınıf java.io.Serializable’dan türemesi gerekiyor. Hibernate, cache ile ilgili işlemlerde bunu kullanacak.

equals() ve hashCode() metodlarını değer ile karşılaştırma için override ederiz.

Bunu kullanmak istediğimizde şu şekilde olur.

@NotNull
@Convert(converter = MonetaryAmountConverter.class)
@Column(name = "PRICE", length = 63)
private MonetaryAmount buyNowPrice;

Örneğin, 11.23 USD veya 99 EUR gibi değerleri PRICE alanında String olarak tutarız.

Son olarak dönüştürücü sınıfımızı ekleriz.

@Converter
public class MonetaryAmountConverter
implements AttributeConverter < MonetaryAmount, String > {
@Override
public String convertToDatabaseColumn(MonetaryAmount monetaryAmount) {
return monetaryAmount.toString();
}

@Override
public MonetaryAmount convertToEntityAttribute(String s) {
return MonetaryAmount.fromString(s);
}
}

Özel dönüştürücü sınıfımız AttributeConverter arayüzünden türemesi gerekiyor ve @Converter ile etiketlenmesi gerekiyor. 2 metodunu override ederek, kendimize uygun şekilde değiştirelim. Bu şekilde kendi dönüştürücümüzü eklemiş olduk.

Kalıtımın Eşlenmesi(Mapping Inheritance)

--

--

No responses yet