author-pic

Ferry S

An ISTJ, Type 5, Engineer, Gamer, and Thriller-Movies-Lover
Dynamic Query pada Java, Part I: JPA
Thu. Jun 30th, 2022 11:20 PM8 mins read
Dynamic Query pada Java, Part I: JPA
Source: Bing Image Creator - sql query

Java Persistence Query Language (JPQL) adalah bahasa untuk men-generalisasi SQL pada Java yang terdapat pada JPA (Jakarta Persistence API). JPQL jadi standard tersendiri pada environment Java, karena apapun Database SQL yang kita gunakan, kita bisa menggunakan JPQL sebagai pengganti native SQL yang beragam pada tiap-tiap database. Jadi misalkan kita gonta-ganti database, kita tidak perlu khawatir query yang telah kita gunakan tidak support oleh database lainnya, karena semuanya diterjemahkan oleh JPA. JPA itu sendiri sebenarnya adalah abstraksi dari framework ORM (Object Relational Mapping) di Java. Implementasi JPA yang paling populer hingga saat ini adalah Hibernate. Salah satu fitur keren dari ORM adalah mereka dapat generate table dan query yang kita butuhkan hanya dengan Persistence Data Class yang kita buat pada aplikasi😎. Mungkin masih ada yang bingung mengenai ORM ini karena semuanya jadi serba otomatis, terutama ketika ingin menerapkan dynamic query. Langsung saja kita lakukan prakteknya.

Kita coba bikin System Informasi Kampus yang sederhana ya, kurang lebih seperti ini.

ferry.vercel.app - System Informasi Kampus Entity Relationship Diagram
Entity Relationship Diagram System Informasi Kampus
Gw ga menampilkan Entity code-nya, melainkan hanya gambar relasinya aja biar ga kepanjangan tulisannya😁. Ini cukup simple sih, bisa kelihatan strukturnya dari ER Diagram di atas.

Maven dependencies pom.xml

<parent>
	<groupId>org.springframework.boot</groupId>
	<artifactId>spring-boot-starter-parent</artifactId>
	<version>2.7.1</version>
	<relativePath/>
</parent>
<dependencies>
	<dependency>
		<groupId>org.springframework.boot</groupId>
		<artifactId>spring-boot-starter-data-jpa</artifactId>
	</dependency>
	<dependency>
		<groupId>org.postgresql</groupId>
		<artifactId>postgresql</artifactId>
		<scope>runtime</scope>
	</dependency>
	<dependency>
		<groupId>org.springframework.boot</groupId>
		<artifactId>spring-boot-configuration-processor</artifactId>
		<optional>true</optional>
	</dependency>
	<dependency>
		<groupId>org.projectlombok</groupId>
		<artifactId>lombok</artifactId>
		<optional>true</optional>
		<scope>provided</scope>
	</dependency>
	<dependency>
		<groupId>org.hibernate</groupId>
		<artifactId>hibernate-jpamodelgen</artifactId>
		<scope>provided</scope>
	</dependency>
</dependencies>

Disini kita ga menggunakan pure JPA-Hibernate, tapi menggunakan Spring Data JPA karena fiturnya lebih lengkap. Di dalam Spring Data JPA udah dilengkapi Hibernate, JPA, dan integrasi framework bawaan Spring. Kita juga menggunakan hibernate-jpamodelgen untuk generate constant dari class Entity. Versionnya ga gw tulis karena gw ngikut versi dari spring-boot-starter-parent, bisa disesuaikan aja ya😁.

Kita mulai dari hal yang ringan dulu, yaitu bikin code untuk mendapatkan semua list dari table. Untuk melakukannya bisa lewat JPA Entity Manager maupun langsung dari Hibernate Session. Best practice-nya sih, better lewat abstraksi JPA Entity Manager karena sangat umum dan ga framework dependant.

TypedQuery<Student> query = entityManager.createQuery("select s from Student s", Student.class);
List<Student> list = query.getResultList();
for(Student student : list){
	System.out.println("student.getStudentName() = " + student.getStudentName());
}

Kita menggunakan JPQL untuk mendapatkan semua list dari table Student. Berbeda dengan native Query yang mengharuskan kita menulis nama sesuai table, sedangkan JPQL menggunakan nama Entity Class sebagai pengganti table.

Selanjutnya kita coba melakukan hal yang sama menggunakan Hibernate. Kita juga bisa menggunakan Hibernate Session untuk melakukan query.

Query<Student> hibernateQuery = session.createQuery("select s from Student s", Student.class);
List<Student> resultList = hibernateQuery.getResultList();
for(Student student : resultList){
	System.out.println("student.getStudentName() = " + student.getStudentName());
}

Hasilnya sama, hanya beda syntax saja😀.

Sekarang kita akan buat yang lebih canggih menggunakan Spring Data JPA.

Interface StudentSpringJpaRepository

public interface StudentSpringJpaRepository extends JpaRepository<Student, Long>{ }

Contoh penggunaan

List<Student> all = studentRepository.findAll();
for(Student student : all){
	System.out.println("student.getStudentName() = " + student.getStudentName());
}

Dengan Spring Data JPA, kita malah tinggal bikin repository aja dan extend JpaRepository, implementasinya akan di-generate oleh Spring Data secara runtime melalui proxy😎. Ga perlu capek-capek bikin query dari awal. By default, method findAll dan beberapa method lainnya udah disediakan oleh interface JpaRepository.

Section sebelumnya hanyalah untuk menampilkan semua data tanpa filter. Jika menggunakan filter pada Spring Data JPA, kita tinggal menambahkan method dengan suffix “ByColumnName” dan parameter sesuai kolom tersebut. Implementasinya juga akan di-generate oleh Spring Data JPA. Contohnya kita ingin mendapatkan data berdasarkan murid yang masih aktif.

public interface StudentSpringJpaRepository extends JpaRepository<Student, Long>{
	List<Student> findByActive(boolean active);
}

Pada bagian sebelumnya, filternya baku sehingga ketika ingin menambah filter baru kita wajib bikin method baru lagi dengan nama method yang akan makin panjang. Lumayan repot🙄. Salah satu cara menggunakan dynamic query pada Spring Data JPA adalah menggunakan annotasi @Query. Kita membutuhkan sebuah class yang berguna sebagai parameter where clause nantinya.

Class StudentFilter

@Builder
@Value
public class StudentFilter{
	String npm;
	String nik;
	String studentName;
	Boolean active;
	LocalDate birthDateRangeStart;
	LocalDate birthDateRangeEnd;
	Integer batchYear;
}

Interface StudentSpringJpaRepository

public interface StudentSpringJpaRepository extends JpaRepository<Student, Long>{

	@Query("" +
			"select s " +
			"from Student s " +
			"where " +
			"(:#{#filter?.active} is null or s.active = :#{#filter?.active}) AND " +
			"(:#{#filter?.nik} is null or s.nik = :#{#filter?.nik}) AND " +
			"(:#{#filter?.birthDateRangeStart?.toString()} is null or :#{#filter?.birthDateRangeEnd?.toString()} is null or " +
			"s.birthDate BETWEEN :#{#filter?.birthDateRangeStart} AND :#{#filter.birthDateRangeEnd}) AND " +
			"(:#{#filter?.studentName} is null or s.studentName LIKE %:#{#filter?.studentName}%) AND " +
			"(:#{#filter?.npm} is null or s.npm = :#{#filter?.npm}) " +
			"")
	List<Student> findWithManualQuery(@Param("filter") StudentFilter filter);
}

Pada query di atas, kita menggunakan logic if parameter is null untuk mengeliminasi filter value yang berisi null. Namun, cara di atas memiliki kelemahan, yaitu tidak bisa untuk dynamic Join. Misalkan kalau ada value pada filter batchYear, maka kita butuh join ke table StudentBatch, tentu saja kita harus bikin method baru seperti berikut.

public interface StudentSpringJpaRepository extends JpaRepository<Student, Long>{

	@Query("" +
			"select " +
			"s " +
			"from Student s " +
			"join fetch s.studentBatch " +
			"where " +
			"(:#{#filter?.active} is null or s.active = :#{#filter?.active}) AND " +
			"(:#{#filter?.nik} is null or s.nik = :#{#filter?.nik}) AND " +
			"(:#{#filter?.batchYear} is null or s.studentBatch.batchYear = :#{#filter?.batchYear}) AND " +
			"(:#{#filter?.birthDateRangeStart?.toString()} is null or :#{#filter?.birthDateRangeEnd?.toString()} is null or " +
			"s.birthDate BETWEEN :#{#filter?.birthDateRangeStart} AND :#{#filter.birthDateRangeEnd}) AND " +
			"(:#{#filter?.studentName} is null or s.studentName LIKE %:#{#filter?.studentName}%) AND " +
			"(:#{#filter?.npm} is null or s.npm = :#{#filter?.npm}) " +
			"")
	List<Student> findWithManualQueryJoinBatch(@Param("filter") StudentFilter filter);
}

Ribet juga🤨.

Selain menggunakan @Query kita juga bisa menggunakan JPA Specification. Kali ini kita tidak membuat query menggunakan constant String seperti di atas.

Interface StudentSpringJpaRepository

public interface StudentSpringJpaRepository extends JpaRepository<Student, Long>, JpaSpecificationExecutor<Student>{ }

Class StudentSpecification

@RequiredArgsConstructor
public class StudentSpecification implements Specification<Student>{
	private final StudentFilter studentFilter;

	@Override
	public Predicate toPredicate(Root<Student> root, CriteriaQuery<?> query, CriteriaBuilder criteriaBuilder){
		List<Predicate> predicates = new ArrayList<>();
		if(studentFilter.getActive() != null){
			predicates.add(criteriaBuilder.equal(root.get(Student_.active), studentFilter.getActive()));
		}
		if(studentFilter.getStudentName() != null){
			predicates.add(criteriaBuilder.like(root.get(Student_.studentName),
					'%' + studentFilter.getStudentName() + '%'));
		}
		if(studentFilter.getBirthDateRangeEnd() != null && studentFilter.getBirthDateRangeStart() != null){
			predicates.add(criteriaBuilder.between(root.get(Student_.birthDate),
					studentFilter.getBirthDateRangeStart(), studentFilter.getBirthDateRangeEnd()));
		}
		if(studentFilter.getNik() != null){
			predicates.add(criteriaBuilder.equal(root.get(Student_.nik), studentFilter.getNik()));
		}
		if(studentFilter.getNpm() != null){
			predicates.add(criteriaBuilder.equal(root.get(Student_.npm), studentFilter.getNpm()));
		}
		if(studentFilter.getBatchYear() != null){
			Join<Student, StudentBatch> join = root.join(Student_.studentBatch);
			predicates.add(criteriaBuilder.equal(join.get(StudentBatch_.batchYear),
					studentFilter.getBatchYear()));
		}
		return criteriaBuilder.and(predicates.toArray(new Predicate[0]));
	}

}

Contoh Penggunaan

StudentFilter studentFilter = StudentFilter.builder()
		.active(true)
		.studentName("ferry")
		.nik("1313727230300101")
		.birthDateRangeEnd(LocalDate.now())
		.birthDateRangeStart(LocalDate.of(2019, Month.JANUARY, 1))
		.npm("1133080")
		.batchYear(2013)
		.build();
StudentSpecification spec = new StudentSpecification(studentFilter);
List<Student> students = studentRepository.findAll(spec);
for(Student student : students){
	System.out.println("student.getStudentName() = " + student.getStudentName());
}

Pada interface StudentSpringJpaRepository kita meng-extend JpaSpecificationExecutor untuk bisa menggunakan fitur ini pada Spring Data JPA. Dari interface tersebut sudah disediakan method seperti findAll dengan parameter Specification, sehingga kita tidak perlu bikin method baru lagi pada interface, cukup extend aja. Pada class StudentSpecification kita mengimplementasi interface Specification, lalu override method toPredicate dan menerapkan filter logic-nya di sana. Kali ini code-nya ga hanya dynamic filter, tapi juga dynamic join. Ketika value filter batchYear ada isinya, dia akan join table StudentBatch dan akan filter berdasarkan kolom batchYear pada table StudentBatch. Jika filter batchYear kosong, maka tidak akan join sama sekali. Yang agak ribet, filter batchYear sangat dependant terhadap Join Type dan ga bisa dipisah🤨. Oh ya, biar ga bingung, class dengan suffix _ seperti Student_ adalah class yang di-generate oleh library hibernate-jpamodelgen saat compile untuk mendapatkan constant dari class Entity.

Section selanjutnya adalah Dynamic Selection, dimana selection value yang di-generate dinamis sesuai kebutuhan. Pada Spring Data JPA kita bisa melakukannya melalui fitur bernama Projection. Projection juga merupakan salah satu cara terbaik untuk menghindari N+1 Problem yang terkenal pada JPA. Misalkan kita ingin mendapatkan list Student yang masih aktif dengan selection clause yang dinamis.

Interface StudentDefaultProjection

public interface StudentDefaultProjection{
	String getNpm();
	String getStudentName();
	boolean isActive();
	LocalDate getBirthDate();
}

Interface StudentNikAndNameProjection

public interface StudentNikAndNameProjection{
	String getNik();
	String getStudentName();
}

Interface StudentSpringJpaRepository

public interface StudentSpringJpaRepository extends JpaRepository<Student, Long>{
	<C> List<C> findByActive(boolean active, Class<C> clazz);
}

Contoh penggunaan

List<StudentDefaultProjection> projections = studentRepository.findByActive(true, StudentDefaultProjection.class);
for(StudentDefaultProjection projection : projections){
	System.out.println("projection.getStudentName() = " + projection.getStudentName());
}

Kita hanya perlu membuat interface projection yang berisi method getter dari property selection yang kita inginkan sesuai nama alias atau nama pada entity class. Pada StudentSpringJpaRepository kita hanya perlu menambah method dengan return value berupa Generic Class dan sebuah parameter Generic Class di paling akhir. Penggunaannya, kita hanya perlu menambahkan argument Projection Class yang diinginkan, lalu Spring akan men-generate otomatis selection clause berdasarkan getter method yang ada pada Projection tersebut😎. Sayangnya, Projection di atas hingga saat ini belum bisa dipersatukan menggunakan Specification. Jadi kita tidak bisa memaksimalkan fitur Dynamic Selection dan Dynamic Filter secara bersamaan. Kabar baiknya, fitur ini katanya sedang dikembangkan oleh Team Spring Data untuk Spring Boot 3.0 akhir tahun nanti. Semoga saja🤗.

Kita telah mempraktekkan berbagai cara melakukan query value menggunakan JPA, Hibernate, & Spring Data JPA mulai dari standard query, dynamic query, dynamic Join, hingga dynamic selection. Spring Data JPA memberikan engineer ‘kenyamanan’ karena beberapa hal dapat dilakukan dengan effort lebih sedikit. Tapi, karena semua serba otomatis, engineer jadi ga punya full control yang dinamis terhadap query. Contohnya dynamic query dan dynamic selection yang belum bisa digunakan secara bersamaan. API dari JPA ini menurut gw juga ga user-friendly seperti Specification di atas. Untuk itu, kita butuh library pihak ketiga untuk melakukan itu semua sehingga kita punya full control terhadap query yang akan kita eksekusi. Next part, gw akan bahas dynamic query menggunakan library QueryDSL😎. Untuk source code aplikasi di atas, nanti akan gw share di github.