Spring Data JPA introduces a nice feature, "query by example" (QBE). You express your search criteria by constructing an instance of the entity.
You do not have to write JPQL. It uses less "magic" than does repository query derivation. The syntax is nice. It prevents explosions of trivial repository code. It survives refactors very well.
There's a problem though: QBE only works if you can partially construct an object.
Here's my entity:
@Entity
@Table(name="product")
data class Product(
@Id val id: String,
val city: String,
val shopName: String,
val productName: String,
val productVersion: Short
)
Here's my repository (empty! this is a nice thing about QBE):
@Repository
interface ProductRepository : JpaRepository<Product, String>
And here's how you would fetch a List<Product>
— all the products that are sold in some shop, in some city:
productRepository.findAll(Example.of(Product(city = "London", shopName="OkayTea")))
Or at least, that's what I want to do. There's a problem. It's not possible to construct this object:
Product(city = "London", shopName="OkayTea")
This is because Product
's constructor requires that all its fields be defined. And indeed: that's what I want most of the time.
The usual compromise in Java would be: construct entities using no-args constructor, make everything mutable, have no guarantees about completedness.
Is there a nice Kotlin pattern to solve this problem:
Admittedly these look like totally conflicting goals. But maybe there's another way to approach this?
For example: maybe we can make a mock/proxy object, which appears to be a Product, but doesn't have the same construction constraints?
You can query by example using kotlin data classes with non-null fields, however it will not look as good as java code.
val matcher = ExampleMatcher.matching()
.withMatcher("city", ExampleMatcher.GenericPropertyMatcher().exact())
.withMatcher("shopName", ExampleMatcher.GenericPropertyMatcher().exact())
.withIgnorePaths("id", "productName", "productVersion")
val product = Product(
id = "",
city = "London",
shopName = "OkayTea",
productName = "",
productVersion = 0
)
productRepository.findAll(Example.of(product, matcher))
If you are using it for integration tests and you don't want to pollute your Repository
interface with methods which are only used in said tests and also
you have a lot of fields in the database entity class, you can create an extension function that extracts fields which will be ignored in the query.
private fun <T : Any> KClass<T>.ignoredProperties(vararg exclusions: String): Array<String> {
return declaredMemberProperties
.filterNot { exclusions.contains(it.name) }
.map { it.name }
.toTypedArray()
}
and use it like this:
val ignoredFields = Product::class.ignoredProperties("city", "shopName")
val matcher = ExampleMatcher.matching()
.withMatcher("city", ExampleMatcher.GenericPropertyMatcher().exact())
.withMatcher("shopName", ExampleMatcher.GenericPropertyMatcher().exact())
.withIgnorePaths(*ignoredFields)
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With