Spring Data ve Hibernate ile Java Persistence’e Giriş
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
veCategory
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ı iseSEQUENCE_NAME
veNEXT_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
veMonetaryAmount
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[]
veyaCharacter[]
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)
…