You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

ZIO Quill:如何为不同实体通用化共享操作?

实现Scala查询中通用的changedAfter方法

问题背景

我们有两个包含时间戳字段的Scala case class:

import java.time.LocalDate

case class Person(name: String, age: Int, createdAt: LocalDate, lastModified: LocalDate)
case class Address(street: String, zip: Int, createdAt: LocalDate, lastModified: LocalDate)

希望写出如下风格的查询代码,其中.changedAfter()方法能通用作用于任何包含createdAt和lastModified字段的实体:

for {
    p <- query[Person]
           .changedAfter(lift(LocalDate.of(2022, 1, 1)))
    a <- query[Address].join(a => a.ownerId == p.id)
            .changedAfter(lift(LocalDate.of(2022, 1, 1)))
} yield (p, a)

实现方案

1. 定义类型类约束实体

用Scala的类型类HasTimestamps来抽象所有带有createdAt和lastModified字段的实体,避免强制继承父类的侵入式设计:

trait HasTimestamps[T] {
  def createdAt(entity: T): LocalDate
  def lastModified(entity: T): LocalDate
}

2. 为实体提供类型类实例

手动实现(Scala 2/3通用)

给Person和Address分别实现类型类实例:

// Person的类型类实例
implicit val personTimestamps: HasTimestamps[Person] = new HasTimestamps[Person] {
  override def createdAt(p: Person): LocalDate = p.createdAt
  override def lastModified(p: Person): LocalDate = p.lastModified
}

// Address的类型类实例
implicit val addressTimestamps: HasTimestamps[Address] = new HasTimestamps[Address] {
  override def createdAt(a: Address): LocalDate = a.createdAt
  override def lastModified(a: Address): LocalDate = a.lastModified
}

自动派生(Scala 3简化版)

利用Scala 3的镜像(Mirror)功能自动生成实例,无需手动编写每个实体的实现:

object HasTimestamps {
  inline given [T <: Product](using mirror: Mirror.ProductOf[T]): HasTimestamps[T] = 
    new HasTimestamps[T] {
      override def createdAt(entity: T): LocalDate = 
        mirror.productElement(entity, mirror.productElementNames.indexOf("createdAt")).asInstanceOf[LocalDate]
      override def lastModified(entity: T): LocalDate = 
        mirror.productElement(entity, mirror.productElementNames.indexOf("lastModified")).asInstanceOf[LocalDate]
    }
}

只要case class包含createdAt和lastModified字段,就能自动获得HasTimestamps实例。

3. 给查询类型扩展changedAfter方法

假设你的query[T]返回的是Query[T]类型(比如Slick等ORM的查询对象),通过扩展方法为其添加通用的changedAfter:

// 适配带lift的查询DSL场景(比如Slick)
extension [T](query: Query[T])(using ts: HasTimestamps[T])
  def changedAfter(dateExpr: Rep[LocalDate]): Query[T] = 
    query.filter(entity => 
      ts.createdAt(entity) > dateExpr || ts.lastModified(entity) > dateExpr
    )

这里的Rep[LocalDate]是查询DSL中代表表达式的类型,lift方法负责将普通LocalDate转换为查询表达式类型。

如果是简单的内存查询场景,扩展方法可以简化为:

extension [T](query: Iterable[T])(using ts: HasTimestamps[T])
  def changedAfter(date: LocalDate): Iterable[T] = 
    query.filter(entity => 
      ts.createdAt(entity).isAfter(date) || ts.lastModified(entity).isAfter(date)
    )

效果验证

完成上述实现后,就能直接使用你期望的查询语法,changedAfter会自动作用于所有符合条件的实体类型。

内容的提问来源于stack exchange,提问作者Askar Bozcan

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.08.17 00:05:33