Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Kotlinic pattern for using Spring Data JPA's "query by example"

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:

  • generally require that all args are instantiated on construction
  • provide also some mechanism to produce partially-constructed instances for use with Example API

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?

like image 393
Birchlabs Avatar asked Jul 10 '17 10:07

Birchlabs


1 Answers

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)

like image 93
kdev Avatar answered Oct 23 '22 02:10

kdev