Шаблон Kotlinic для использования Spring Data JPA "запрос по примеру"
В Spring Data JPA появилась замечательная функция "запрос по примеру" (QBE). Вы выражаете свои критерии поиска, создавая экземпляр объекта.
Вам не нужно писать JPQL. Он использует меньше "магии", чем вывод запросов к репозиторию. Синтаксис хороший. Это предотвращает взрывы тривиального кода репозитория. Он хорошо переживает рефакторы.
Однако есть проблема: QBE работает, только если вы можете частично построить объект.
Вот моя сущность:
@Entity
@Table(name="product")
data class Product(
@Id val id: String,
val city: String,
val shopName: String,
val productName: String,
val productVersion: Short
)
Вот мой репозиторий (пусто! Это хорошая вещь в QBE):
@Repository
interface ProductRepository : JpaRepository<Product, String>
А вот как вы могли бы получить List<Product>
- все товары, которые продаются в каком-то магазине, в каком-то городе:
productRepository.findAll(Example.of(Product(city = "London", shopName="OkayTea")))
Или, по крайней мере, это то, что я хочу сделать. Есть проблема. Невозможно построить этот объект:
Product(city = "London", shopName="OkayTea")
Это потому что Product
Конструктор требует, чтобы все его поля были определены. И действительно: это то, чего я хочу большую часть времени.
Обычный компромисс в Java: создавать объекты с помощью конструктора no-args, делать все изменчивым, не иметь гарантий относительно завершенности.
Есть хороший шаблон Kotlin для решения этой проблемы:
- как правило, требуют, чтобы все аргументы были созданы на строительстве
- предоставить также некоторый механизм для создания частично сконструированных экземпляров для использования с примером API
По общему признанию это похоже на полностью противоречивые цели. Но, может быть, есть другой способ приблизиться к этому?
Например: может быть, мы можем создать фиктивный / прокси-объект, который выглядит как Продукт, но не имеет тех же конструктивных ограничений?
1 ответ
Вы можете запросить, например, используя классы данных kotlin с ненулевыми полями, однако это будет выглядеть не так хорошо, как Java-код.
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))
Если вы используете его для интеграционных тестов и не хотите загрязнять свой Repository
интерфейс с методами, которые используются только в указанных тестах, а также у вас есть много полей в классе сущности базы данных, вы можете создать функцию расширения, которая извлекает поля, которые будут игнорироваться в запросе.
private fun <T : Any> KClass<T>.ignoredProperties(vararg exclusions: String): Array<String> {
return declaredMemberProperties
.filterNot { exclusions.contains(it.name) }
.map { it.name }
.toTypedArray()
}
и используйте это так:
val ignoredFields = Product::class.ignoredProperties("city", "shopName")
val matcher = ExampleMatcher.matching()
.withMatcher("city", ExampleMatcher.GenericPropertyMatcher().exact())
.withMatcher("shopName", ExampleMatcher.GenericPropertyMatcher().exact())
.withIgnorePaths(*ignoredFields)
Потому что параметры первичного конструктора не являются обязательными и не могут быть обнуляемыми. Вы можете сделать параметры обнуляемыми и установить значение по умолчанию null
для каждого, например:
@Entity
@Table(name = "product")
data class Product(
@Id val id: String? = null,
val city: String? = null,
val shopName: String? = null,
val productName: String? = null,
val productVersion: Short? = null
)
Тем не менее, вы должны действовать Product
свойства с безопасным вызовом ?.
, например:
val product = Product()
// safe-call ---v
val cityToLowerCase = product.city?.toLowerCase()