Spring JPA Specification API kullanımı

Düşündüğümden uzun hale gelen bu yazının konusu Spring data JPA 'nın Specification API 'si. Bu api bugünlerde o kadar popüler değil gibi ama bana sorarsanız yazılımcılar için veri odaklı uygulamalar yazmayı epeyce kolaylaştıracak potansiyeli var. Specification api ile karmaşık sql sorguları yazmaya veya Hibernate Criteria API ile uğraşmaya gerek kalmadan java kodları ile sql sorguları oluşturabiliyoruz. Bu konuda bir örnek verebilmek adına küçük çaplı bir projeyi ayağa kaldıracağım. Bu sayede daha sonra siz de istediğiniz eklemeleri yapabilirsiniz. Kodlamaya eriniyorsanız projenin son halini buradan indirebilirsiniz. Yine de bu projeyi anlayabilmek ve çalıştırabilmek için bazı ön gereklilikler var. Öncelikle gereksinimler şunlar:

  • Spring boot bilgisi (Bu yazıdan destek alabilirsiniz)
  • Java 8 veya java 11 'in bilgisayarınızda kuru olması
  • Eclipse veya Intellij tercih ederseniz bir IDE
  • Postgresql veya istediğiniz başka bir RDBMS teknolojisi

Projenin oluşturulması

İlk olarak projeyi Spring Initializr 'da oluşturarak başlıyoruz. Proje her ne kadar basit olsa da atlamamamız gereken detaylar var. Aşağıdaki dependency 'ler (bağımlılıklar) ile projeyi oluşturabilirsiniz.

Bu bağımlılıkları özetlersek, devtools ile uygulamanız otomatik olarak arka planda çalışan tomcat 'e yüklenir ve reload edilir. Restful servis projesi olarak tasarlayacağım için spring wen gereksinimimiz var. Lombok ile getter setter constructor kodlarımızı kısaltmış olacağız. En önemlisi son ikisi burada. Spring Data JPA arka planda Hibernate kullanarak veritabanımıza bağlanacak. Hibernate ise Postgresql driver 'ı ile bağlantı implementasyonu yapacak.

Veritabanı hazırlığı

Sıra geldi uygulamanın kullanacağı veritabanını hazırlamaya. Veritabanını yönetebilmek için bilgisayarınıza postgresql ile beraber PgAdmin aracını kurmanız gerekiyor. PgAdmin aracılığı ile aşağıdaki gibi bir database ve şema oluşturabilirsiniz.

Burada postgresql 'in postgres isimli default kullanıcısını kullanıyoruz. Bütün yetkileri ve ayrıcalıkları da bu kullanıcıya veriyoruz. Bu aşama tamamsa spring projesini eclipse 'de maven projesi import işlemi ile açabilirsiniz. Kod yazma zamanı geldi.

Model ve repository sınıfları

Muhtemelen birçoğunuzun bildiği gibi veritabanı ile çalışan projelerimiz genelde modeller ve repository 'ler ile çalışmaktadır. Model sınıfları veritabanındaki tabloları ifade eder. Repository sınıfları ise veriye erişip CRUD işlemlerini gerçekleştirir. Bu küçük projede çok basit bir ürün satış yapısı hazırlayacağız. Person ve Product bilgilerini veritabanına atıp bu ikisini bir araya getirip satış yapmış olacağız. Veya alışveriş de diyebiliriz bunda. Kişiler ürünleri satın alacak. Hibernate yardımı ile 3 model sınıfını aşağıdaki gibi oluşturabilirsiniz. Aradaki yorum satırları ile açıklamaları yapayım.

			
import javax.persistence.Column;
import javax.persistence.Entity;
import javax.persistence.GeneratedValue;
import javax.persistence.GenerationType;
import javax.persistence.Id;
import javax.persistence.Table;

import lombok.AllArgsConstructor;
import lombok.Getter;
import lombok.NoArgsConstructor;
import lombok.Setter;

// Bu sınıf bir entity sınıfıdır ve hibernate 'yin persistance context 'inde yönetilir
@Entity
// Bu sınıf veritabanındaki tablo karşılığı olarak "sale" şemasında "person" isimli tablo olacak
@Table(schema = "sale", name = "person")
// Lombok ile getter, setter, all argument and no argument constructor metodlar otomatik oluşur
@Getter
@Setter
@AllArgsConstructor
@NoArgsConstructor
public class Person
{
	// ID sütunu primary key yani birincil anahtardır
	@Id
	// Her insert işleminde 1 artacak şekilde
	@GeneratedValue(strategy = GenerationType.IDENTITY)
	private long id;

	// Örneklerde sale nesnesi oluşturmak için tek değişken alan constructor 'a ihtiyacımız olacak
	public Person(long id)
	{
		this.id = id;
	}

	@Column
	private String name;

	// Burada 1-n ilişki kurulabilir ve kişinin altından sale yani satış bilgilerine ulaşılabilir
	// Fakat ben specification api 'yi daha iyi gözlemleyebilmek adına bu ilişkiyi kurmayacağım
	// @OneToMany(mappedBy = "person", fetch = FetchType.EAGER)
	// private List<Sale> sales = new ArrayList<Sale>();
}
			
		

Aynı yapı product sınıfı için de geçerli olacak. Bu yüzden burada notlar yok :)

			
import javax.persistence.Column;
import javax.persistence.Entity;
import javax.persistence.GeneratedValue;
import javax.persistence.GenerationType;
import javax.persistence.Id;
import javax.persistence.Table;

import lombok.AllArgsConstructor;
import lombok.Getter;
import lombok.NoArgsConstructor;
import lombok.Setter;

@Entity
@Table(schema = "sale", name = "product")
@Getter
@Setter
@AllArgsConstructor
@NoArgsConstructor
public class Product
{
	@Id
	@GeneratedValue(strategy = GenerationType.IDENTITY)
	private long id;

	public Product(long id)
	{
		this.id = id;
	}

	@Column
	private String name;

	@Column
	private int price;

	// @OneToMany(mappedBy = "product", fetch = FetchType.EAGER)
	// private List<Sale> sales = new ArrayList<Sale>();
}
			
		

Sale yani satış tablosu bir join tablosu olacak. Person ve product id bilgilerini alıp yanına kendi id 'sini ekleyecek.

			
import java.util.Date;

import javax.persistence.Column;
import javax.persistence.Entity;
import javax.persistence.GeneratedValue;
import javax.persistence.GenerationType;
import javax.persistence.Id;
import javax.persistence.JoinColumn;
import javax.persistence.ManyToOne;
import javax.persistence.Table;
import javax.persistence.Temporal;
import javax.persistence.TemporalType;

import org.hibernate.annotations.OnDelete;
import org.hibernate.annotations.OnDeleteAction;

import lombok.AllArgsConstructor;
import lombok.Getter;
import lombok.NoArgsConstructor;
import lombok.Setter;

@Entity
@Table(schema = "sale", name = "sale")
@Getter
@Setter
@AllArgsConstructor
@NoArgsConstructor
public class Sale
{
	@Id
	@GeneratedValue(strategy = GenerationType.IDENTITY)
	private long id;

	// Bir ürün birden fazla kere satılabilir
	@ManyToOne
	@JoinColumn
	@OnDelete(action = OnDeleteAction.CASCADE)
	private Product product;

	// Bir kişinin birden fazla satış kaydı olabilir
	@ManyToOne
	@JoinColumn
	@OnDelete(action = OnDeleteAction.CASCADE)
	private Person person;

	@Column
	// Satış tarihi veritabanında timestamp olarak tutulur, tarih ve saat ile
	@Temporal(TemporalType.TIMESTAMP)
	private Date saledate = new Date();
}
			
		

Bu model sınıflarını oluşturduktan sonra yapmamız gereken bir ayar daha var. Bu da application.properties içerisinde veritabanı bağlantı ayarları. Set etmemiz gereken ayarlar aşağıdaki gibi.

			
# 'hibernate' bizim veritabanı adımızdı
spring.datasource.url = jdbc:postgresql://localhost:5432/hibernate
# 'postgres' bizim kullanıcımızdı
spring.datasource.username = postgres
spring.datasource.password = <sifreniz>
# Buradaki dialect kullandığınız postgresql versiyonu ile uyumlu olmalıdır ve önemlidir
spring.jpa.properties.hibernate.dialect = org.hibernate.dialect.PostgreSQL10Dialect
# Uygulama yeniden başlatıldığında model sınıflarındaki değişikliklerin veritabanını güncellemesi için 'update' modunu kullanıyoruz
# 'create' modunu kullanarak bütün veritabanının silinip baştan oluşturulmasını sağlayabilirsiniz
spring.jpa.hibernate.ddl-auto = update
			
		

Bu ayarları yaptıktan sonra uygulamayı spring boot uygulaması olarak başlatabilirsiniz. Uygulama açıldığında aşağıdaki resimde göreceğiniz gibi tablolarınızın oluşmuş olacaktır.

Sıradaki adım ise bu modeller ile CRUD işlemleri yapacak olan repository sınıflarını yazmak olacak. Burada spring data jpa 'nın JpaRepository interface 'inden destek alacağız. Bu interface arka planda org.springframework.data.jpa.repository.support.SimpleJpaRepository sınıfında implement edilmiş ve spring boot bu sınıfı kullanacak. Bu interface içerisinde yeni metod tanımlamaları da yapacağız. Burada JpaSpecificationExecutor interface 'ini de kullanıyoruz. Bu sayede specification bilgisini işlemlerimizde kullanabileceğiz. Doğal olarak bu repository 'leri kullananan restful endpoint 'lerini de yazacağım ve metodları orada açıklayacağım. 3 model için yazdığım 3 repository aşağıdaki gibi.

			
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.data.jpa.repository.JpaSpecificationExecutor;

import com.numankaraaslan.springbootjpa.model.Product;

public interface ProductRepo extends JpaRepository<Product, Long>, JpaSpecificationExecutor<Product>
{
}
			
		

			
import java.util.List;

import org.springframework.data.domain.Sort;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.data.jpa.repository.JpaSpecificationExecutor;

import com.numankaraaslan.springbootjpa.model.Person;

public interface PersonRepo extends JpaRepository<Person, Long>, JpaSpecificationExecutor<Person>
{
	public Person findByname(String name);

	public List<Person> findBynameLike(String name, Sort sort);
}
			
		

			
import java.util.List;

import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.data.jpa.repository.JpaSpecificationExecutor;

import com.numankaraaslan.springbootjpa.model.Sale;

public interface SaleRepo extends JpaRepository<Sale, Long>, JpaSpecificationExecutor<Sale>
{
	public List<Sale> findAllByperson_name(String name);

	public List<Sale> findAllByperson_nameLike(String name);
}
			
		

Rest Controller sınıfları

Model ve repository sınıfları tamamlandıktan sonra restful servis tarafını yazmaya başlayabiliriz. Burada yazdığım kodlarımı yine yorum satırları ile açıklayacağım. 3 repository için 3 restful controller sıfını oluşturacağım. Buradaki findById, findAll ve save metodları ile ilgili özel bir durum bulunmuyor. Bu metodlar zaten spring jpa metodlarında hazırda var ve entity manager ile üzerlerinde işlemler yapılıyor. Bu metodlar generic çalıştığı için istediğimiz sınıf türünde de çalışabiliyor. Bu endpoint 'ler path parametrelerini ve response body anotasyonlarını kullanıyor. Jackson ise arka planda request ve response body 'lerinde nesnelerimizi json 'a veya json 'ları nesnelerimize çevirme işlemini (object mapping) yapıyor. Bütün bunlar da spring boot sayesinde oluyor. Person servis endpoint 'leri ile başlayayım.

			
import java.util.List;

import org.springframework.data.domain.Sort;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

import com.numankaraaslan.springbootjpa.model.Person;
import com.numankaraaslan.springbootjpa.repository.PersonRepo;

import lombok.AllArgsConstructor;

// Bu controller özel bir controller 'dır ve response body 'si üretir, web sayfası döndürmek yerine
@RestController
@AllArgsConstructor
// Bu controller 'a /person yolu (mapping) ile ulaşılacak
@RequestMapping(path = "person")
public class PersonController
{
	// Yukarıdaki all args constructor yardımı ile person repository sınıfı buraya inject edilecek
	private PersonRepo personRepo;

	// http://localhost:8080/person/findById/1
	@GetMapping(path = "findById/{id}")
	public ResponseEntity<Person> findById(@PathVariable(name = "id") long id)
	{
		return ResponseEntity.ok(personRepo.findById(id).get());
	}

	// http://localhost:8080/person/findAll
	@GetMapping(path = "findAll")
	public ResponseEntity<List<Person>> findAll()
	{
		return ResponseEntity.ok(personRepo.findAll());
	}

	// http://localhost:8080/person/save
	@PostMapping(path = "save")
	public ResponseEntity<Person> save(@RequestBody Person person)
	{
		// {"name":"Adam"}, {"name":"Jack"}, {"name":"Edward"}
		return ResponseEntity.ok(personRepo.save(person));
	}

	// http://localhost:8080/person/findByName/Jack
	@GetMapping(path = "findByName/{name}")
	public ResponseEntity<Person> findByName(@PathVariable(name = "name") String name)
	{
		return ResponseEntity.ok(personRepo.findByname(name));
	}

	// http://localhost:8080/person/findByNameLike/a
	@GetMapping(path = "findByNameLike/{name}")
	public ResponseEntity<List<Person>> findByNameLike(@PathVariable(name = "name") String name)
	{
		return ResponseEntity.ok(personRepo.findBynameLike("%" + name + "%", Sort.by("name").descending()));
	}
}
			
		

Product endpoint 'lerimiz daha basit olacak. Test amaçlı sadece 3 işlem yeterlidir.

			
import java.util.List;

import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

import com.numankaraaslan.springbootjpa.model.Product;
import com.numankaraaslan.springbootjpa.repository.ProductRepo;

import lombok.AllArgsConstructor;

@RestController
@AllArgsConstructor
@RequestMapping(path = "product")
public class ProductController
{
	private ProductRepo productRepo;

	// http://localhost:8080/product/findById/1
	@GetMapping(path = "findById/{id}")
	public ResponseEntity<Product> findById(@PathVariable(name = "id") long id)
	{
		return ResponseEntity.ok(productRepo.findById(id).get());
	}

	// http://localhost:8080/product/findAll
	@GetMapping(path = "findAll")
	public ResponseEntity<List<Product>> findAll()
	{
		return ResponseEntity.ok(productRepo.findAll());
	}

	// http://localhost:8080/product/save
	@PostMapping(path = "save")
	public ResponseEntity<Product> save(@RequestBody Product product)
	{
		// {"name":"Mouse", "price":100}, {"name":"Keyboard", "price":200}, {"name":"Monitor", "price":300}
		return ResponseEntity.ok(productRepo.save(product));
	}
}
			
		

Sale controller 'ında eğlence başlıyor :) Buradaki kodları yorum satırlarında değil aşağıda açıklayacağım.

			
import java.util.List;

import javax.persistence.criteria.CriteriaBuilder;
import javax.persistence.criteria.CriteriaQuery;
import javax.persistence.criteria.Predicate;
import javax.persistence.criteria.Root;

import org.springframework.data.domain.Sort;
import org.springframework.data.jpa.domain.Specification;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

import com.numankaraaslan.springbootjpa.model.Sale;
import com.numankaraaslan.springbootjpa.repository.SaleRepo;

import lombok.AllArgsConstructor;

@RestController
@AllArgsConstructor
@RequestMapping(path = "sale")
public class SaleController
{
	private SaleRepo saleRepo;

	// http://localhost:8080/sale/findById/1
	@GetMapping(path = "findById/{id}")
	public ResponseEntity<Sale> findById(@PathVariable(name = "id") long id)
	{
		return ResponseEntity.ok(saleRepo.findById(id).get());
	}

	// http://localhost:8080/sale/findAll
	@GetMapping(path = "findAll")
	public ResponseEntity<List<Sale>> findAll()
	{
		return ResponseEntity.ok(saleRepo.findAll());
	}

	// http://localhost:8080/sale/save
	@PostMapping(path = "save")
	public ResponseEntity<Sale> save(@RequestBody Sale sale)
	{
		// {"product":1,"person":1}, {"product":1,"person":2}, {"product":2,"person":1}, {"product":2,"person":2}, {"product":3,"person":1}, {"product":3,"person":3}
		return ResponseEntity.ok(saleRepo.save(sale));
	}

	// http://localhost:8080/sale/findAllByPersonName/ward
	@GetMapping(path = "findAllByPersonName/{personname}")
	public ResponseEntity<List<Sale>> findAllByPersonName(@PathVariable(name = "personname") String name)
	{
		return ResponseEntity.ok(saleRepo.findAllByperson_nameLike("%" + name + "%"));
	}

	// http://localhost:8080/sale/findByPersonAndPrice/Adam/150
	@GetMapping(path = "findByPersonAndPrice/{personname}/{price}")
	public ResponseEntity<List<Sale>> findByPersonAndPrice(@PathVariable(name = "personname") String name, @PathVariable(name = "price") int price)
	{
		Specification<Sale> personSpec = new Specification<Sale>()
		{
			private static final long serialVersionUID = -8354621229900183586L;

			@Override
			public Predicate toPredicate(Root<Sale> root, CriteriaQuery<?> query, CriteriaBuilder criteriaBuilder)
			{
				return criteriaBuilder.equal(root.get("person").get("name"), name);
			}
		};
		Specification<Sale> priceSpec = new Specification<Sale>()
		{
			private static final long serialVersionUID = 4964898933388153362L;

			@Override
			public Predicate toPredicate(Root<Sale> root, CriteriaQuery<?> query, CriteriaBuilder criteriaBuilder)
			{
				return criteriaBuilder.greaterThanOrEqualTo(root.get("product").get("price"), price);
			}
		};
		Specification<Sale> spec = Specification.where(personSpec.and(priceSpec));
		Sort sort = Sort.by("product.name").ascending();
		List<Sale> result = saleRepo.findAll(spec, sort);
		return ResponseEntity.ok(result);
	}
}
			
		

Türemiş sorgular (derived query)

Öncelikle PersonRepository interface 'indeki aşağıdaki iki metoda bakalım. Bu metodlar tabloda belli bir kolona bakarak arama yapıyor.

			
public Person findByname(String name);

public List<Person> findBynameLike(String name, Sort sort);
			
		

İlk metod bildiğimiz şekliyle "select * from sale.person where name = :name" sorgusunu üretir arka planda. Bunu spring data jpa yapıyor. Buradaki "name" ifadesi person nesnesi içerisindeki değişkenin adıdır, veritabanındaki kolon ismi olmak zorunda değildir.

Bu metoddan sonra bir metod daha ekledim ve bu metodun like sorgusu şeklinde çalışmasını istedim. Bu metod ise "select * from sale.person where name like :name order by :sort" şeklinde bir sorgu oluşturacak. Buradaki sort parametresi ile veriler alınırken database tarafında sıralanmasını (order by) sağlayacak. Bu metodu çağırmak için restful tarafında "personRepo.findBynameLike("%" + name + "%", Sort.by("name").descending())" kodunu yazdım. Sort içindeki "name" parametresi person içerisindeki "name" değişkenini ifade ediyor yine, DB 'deki kolon ismi değil.

Bundan daha karmaşık olan sale repository metodlarına bakalım. Buradaki metodlar da türemiş sorgular fakat ekstra bir özellikleri var. Yukarıda hatırlarsanız 1-n ilişkilerini person ve product için tanımlamamıştım. Bu yüzden person veya product nesneleri üzerinden direkt olarak sale bilgilerine erişemem, veritabanına ayrı bir sorgu atmam gerekir. Aşağıdaki iki metod ile bir kişinin ismi ile sale tablosunda arama yapabiliyorum. İsmi "Jack" olan veya isminin içinde "ack" geçen kişilerin yaptığı satın alma işlemlerini bulmak istersem bu iki metodu kullanabilirim. _ (alt tire) ifadesi ile person içerisindeki name değişkenine bak şeklinde bir sorgu oluşuyor. Yani _ ile bağlı olduğu nesnenin field 'larına geçiyoruz. Hatta o field da bir nesne olsaydı onun da _ ile field 'larına geçebilirdik.

			
public List<Sale> findAllByperson_name(String name);

public List<Sale> findAllByperson_nameLike(String name);
			
		

İşin güzel tarafı da bu sorgular sale tablosunda olduğu halde arka planda join işlemi yapar ve person 'la birleştirerek yani join yaparak sql ifadesini yazar. Bu sorguda da person içindeki name sütununa bakacak hale geririr. "Select s.* from sale s inner join person p on s.person_id = p.id where p.name like '%ack%'" gibi bir sorgu oluşacaktır. Bu sorguyu yazmak veya criteria api ile uğraşmak zorunda kalmadık. Restful servis taraında bu metodu "saleRepo.findAllByperson_nameLike("%" + name + "%")" şeklinde çağırdım. Buradaki %% işaretlerine dikkat. Bunlar like sorgusu için gerekli.

Specification API

Geldik yazının esas konusuna. Türemiş sorgular varken specification api 'ye neden ihtiyaç olsun? Bazı durumlarda hem sıralama hem sayfalama hem de birden fazla tabloyu ilgilendiren where koşulları bir araya gelirse yukarıdaki mantıkla sorgu yazmak mümkün olmayabilir. Dahası yukarıdaki formatı ezberlemeye çalışmaktansa java kodları ile yazılımcıların anlayabileceği şekilde sql ifadeleri üretmek daha kolay olacaktır. Benim örneğimde "bana ismi Jack olan müşterilerin 150 tl 'den yüksek ücret ödediği alışverişlerin listesini getir ve ürün ismine göre sırala" şeklinde bir sorgu yazmak istediğimizde 3 sorgu birleşir ve 2 where koşulu (person name ve product price) ile beraber 1 de sort işin içine girer. Bunun için türemiş sorgu yazmak belki mümkün bile olmayabilir veya olsa da tren gibi bir metoda dönüşebilir. Esnetmesi ise o kadar zor olacaktır. Criteria api ile yapmak için de çok karmaşık ve hiç doğal görünmeyen kodlar yazmak zorunda kalırsınız. Bu aşamada specification api yardımımıza koşabilir. Aşağıda yorum satırları ile açıklayayım.

			
Specification<Sale> personSpec = new Specification<Sale>()
{
	// Burada 2 koşul (condition) implement edeceğiz, birisi person name ve diğeri product price için
	// 1 koşul = 1 specification
	@Override
	public Predicate toPredicate(Root<Sale> root, CriteriaQuery<?> query, CriteriaBuilder criteriaBuilder)
	{
		// Bu satır "sale" entity 'sinin içindeki "person" nesnesinin "name" özelliğinin
		// Yukarıdaki name parametresine eşit olması gerektiğini söyler (like ifadesi de yazılabilir)
		// Bu kadar basit :)
		return criteriaBuilder.equal(root.get("person").get("name"), name);
	}
};
Specification<Sale> priceSpec = new Specification<Sale>()
{
	// 2. koşul için 2. specification
	@Override
	public Predicate toPredicate(Root<Sale> root, CriteriaQuery<?> query, CriteriaBuilder criteriaBuilder)
	{
		// Bu satır "sale" entity 'sinin içindeki "product" nesnesinin "price" özelliğinin
		// Yukarıdaki price parametresinden büyük veya ona eşit olması gerektiğini söyler
		// Bu kadar basit :)
		return criteriaBuilder.greaterThanOrEqualTo(root.get("product").get("price"), price);
	}
};
			
		

Bu iki specification 'ı AND koşulu ile bir araya getirmem lazım. Bunu da 1 satırda yapabilirim ama istediğimiz kadar specification 'ı da birleştirebiliriz bu yöntemle tabi ki.

			
Specification<Sale> spec = Specification.where(personSpec.and(priceSpec));
			
		

Bu şekilde where koşulu tamamlanıyor ve geriye sort işlemi kalıyor. Burada sıralamayı ürün adına yani product içindeki name değişkenine göre yapmak istiyorum ki bu veri daha sorguya dahil bile olmadı farkındaysanız. Spring boot ve hibernate iş birliği ile benim istediğim Order By ifadesini oluşturacak. Product içindeki name ile sorgulamak için aşağıdaki gibi sort oluşturdum.

			
Sort sort = Sort.by("product.name").ascending();
			
		

Kodlama kısmı bitiyor bu şekilde. Bu metodu çalıştırması için sale repository 'sinde yeni bir metod yazmak zorunda değilim. Çünkü JpaSpecificationExecutor interface 'ini de extend etmiştim. Yazdığım specification 'ı ve sort nesnesini findAll metoduna aşağıdaki gibi verebilirim.

			
List<Sale> result = saleRepo.findAll(spec, sort);
			
		

Bu yazdığımız controller, repository ve model sınıflarından sonra uygulamayı controller sınıflarında belirttiğim linkler ile test edebilirsiniz. Aynı metodların içerisinde save işlemleri için de post body 'sinde kullanabileceğiniz json formatları bulunuyor. Benim yaptığım postman testlerinin sonucunu aşağıda paylaşayım.

Specification api 'sini neden tercih ediyorum

Bu api ile karmaşık sql sorgularını java kodları ile oluşturmuş oluyoruz. Join yapılarını veya criteria api 'i bilmek zorunda kalmıyoruz. Persistance context mantığı ile uğraşmıyoruz. Birden fazla koşul ifadesini kolayca ve tek satırda bir araya getirebiliyoruz. Umarım faydalı olmuştur. Bir sonraki yazıda görüşmek üzere :)


3 Yorum

  • Baha

    22 Mart 2023

    Harika anlatim

  • Numan Karaaslan

    24 Mart 2023

    Teşekkür ederim :)

  • BT

    22 Ağustos 2023

    Güzel anlatım elinize sağlık

Bir yorum yazabilirsiniz