【问题标题】:Kotlinic pattern for using Spring Data JPA's "query by example"使用 Spring Data JPA“通过示例查询”的 Kotlin 模式
【发布时间】:2017-12-14 01:56:21
【问题描述】:

Spring Data JPA 引入了一个不错的功能,"query by example" (QBE)。您可以通过构造实体的实例来表达您的搜索条件。

您不必编写 JPQL。它比repository query derivation 使用更少的“魔法”。语法很好。它可以防止琐碎的存储库代码爆炸。它可以很好地经受重构。

但有一个问题: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&lt;Product&gt; 的方法——在某个城市的某个商店出售的所有产品:

productRepository.findAll(Example.of(Product(city = "London", shopName="OkayTea")))

或者至少,这是我想要做的。有问题。 不可能构造这个对象:

Product(city = "London", shopName="OkayTea")

这是因为Product 的构造函数要求所有 定义它的字段。确实:这就是我大部分时间想要的。

Java 中通常的折衷方案是:使用无参数构造函数构造实体,使所有内容可变,不保证完整性。

有没有很好的 Kotlin 模式来解决这个问题:

  • 一般要求所有参数都在构造时实例化
  • 提供一些机制来生成部分构造的实例以与示例 API 一起使用

诚然,这些看起来完全是相互矛盾的目标。但也许还有另一种方法可以解决这个问题?

例如:也许我们可以制作一个模拟/代理对象,它看起来是一个产品,但没有相同的构造约束?

【问题讨论】:

  • 数据类不可能,如果你想保留数据类给你的所有好东西。

标签: spring-data-jpa kotlin


【解决方案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)

【讨论】:

  • 在我的项目中,如果为空,我想忽略。例如,如果 shopName 为 null,那么它将忽略此条件。我该如何处理这种情况?
【解决方案2】:

因为主构造函数的参数不是可选的,也不能为空。您可以使参数为空并为每个参数设置一个默认值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()

【讨论】:

  • 谢谢;所以我们放弃所有的完整性保证。但这是一个很大的妥协:我们显着改变了我们的完整性合同,只是为了支持 QBE。我在想也许还有另一种方法——例如使用模拟或代理——来生成一个Product 实例。模拟/代理将满足示例 API 的要求,并且将具有 Product 类的构造函数约束。
  • @Birchlabs 嗨,如果您在数据库中的列不能为空。该列应具有默认值。您可以使用默认值来初始化主构造函数上的属性。例如:val id:Int, val city:String="unknown" 并继续。 id必需的city可选的。我的回答只是一个意见,让你知道 kotlin 有一个特性。
  • 我不担心数据库限制。如果我想强制执行“从不持久为空”,我可以使用 @NotNull 持久性注释来做到这一点。更大的问题是我想阻止我的应用程序的开发人员构造无效实例。构造函数曾经向我保证,我的应用程序中的每个Product 实例的所有字段都以有效方式初始化。而此构造函数移除了该保证,以支持 QBE。
  • 表达约束id必需是复杂的;它可以在构建时、API 边界、持久性边界需要。特别是:id 在持久性边界处是必需,但在查询中是可选。理想情况下,QBE 应该有一组与其他用例不同的约束。
  • 虽然它一个解决方案,但它不能回答问题。 @Birchlabs 明确表示它应该“通常要求所有 args 在构造时实例化”,甚至在他的问题中承认这个解决方案,认为它是不可取的。
猜你喜欢
  • 2015-02-21
  • 2020-09-22
  • 2019-02-09
  • 1970-01-01
  • 2020-01-27
  • 1970-01-01
  • 1970-01-01
  • 2019-01-07
  • 2022-01-01
相关资源
最近更新 更多